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
@@ -1,6 +1,6 @@
1
1
  /**
2
- * E3 graph observability + visualization — READ-ONLY over the entity/relation
3
- * graph (docs/plans/2026-06-02-graph-observability.md).
2
+ * Graph observability and visualization, READ-ONLY over the entity/relation
3
+ * graph.
4
4
  *
5
5
  * This module only READS the graph (`loadEntities` / `loadRelations`) and renders
6
6
  * view-models; it issues no INSERT/UPDATE/DELETE, so `scripts/check-graph-writes.mjs`
@@ -1,6 +1,6 @@
1
1
  /**
2
- * E3 graph observability + visualization — READ-ONLY over the entity/relation
3
- * graph (docs/plans/2026-06-02-graph-observability.md).
2
+ * Graph observability and visualization, READ-ONLY over the entity/relation
3
+ * graph.
4
4
  *
5
5
  * This module only READS the graph (`loadEntities` / `loadRelations`) and renders
6
6
  * view-models; it issues no INSERT/UPDATE/DELETE, so `scripts/check-graph-writes.mjs`
@@ -20,7 +20,7 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
20
20
  const limit = opts.limit ?? DEFAULT_VIEW_LIMIT;
21
21
  // All reads run inside ONE read snapshot so a concurrent `graph extract` /
22
22
  // sleep-drain rebuild can't make the model mix old entity ids with new relation
23
- // ids (codex P2). Every load* call below is passed the snapshot connection `db`.
23
+ // ids. Every load* call below is passed the snapshot connection `db`.
24
24
  return withGraphReadSnapshot(hippoRoot, (db) => {
25
25
  let nodeEntities;
26
26
  let relations;
@@ -31,7 +31,7 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
31
31
  // (2) A name can map to MANY entities (e.g. many notes for one customer), so
32
32
  // cap the focus matches. (3) Discover 1-hop neighbours and cap the UNION to
33
33
  // `limit` nodes. (4) Load ALL edges AMONG the union so neighbour-to-neighbour
34
- // edges that don't touch the focus are included too. (codex P2s.)
34
+ // edges that don't touch the focus are included too.
35
35
  const focus = loadEntitiesByName(hippoRoot, tenantId, opts.entity, { limit }, db);
36
36
  if (focus.length === 0)
37
37
  return { nodes: [], edges: [], truncated: false };
@@ -51,12 +51,12 @@ export function buildGraphModel(hippoRoot, tenantId, opts = {}) {
51
51
  nodeEntities = loadEntitiesByIds(hippoRoot, tenantId, unionIds, db);
52
52
  // Edges AMONG the union (BOTH endpoints in the set): includes
53
53
  // neighbour-to-neighbour edges, and the LIMIT can never drop a valid in-union
54
- // edge in favour of out-of-union rows (codex P2).
54
+ // edge in favour of out-of-union rows.
55
55
  relations = loadRelationsAmong(hippoRoot, tenantId, unionIds, { limit }, db);
56
56
  truncated =
57
57
  focus.length >= limit ||
58
- hop.length >= limit || // neighbour scan capped -> a 1-hop neighbour may be omitted (codex P2)
59
- neighboursCapped || // node cap filled before all neighbours were consumed (codex P2)
58
+ hop.length >= limit || // neighbour scan capped -> a 1-hop neighbour may be omitted
59
+ neighboursCapped || // node cap filled before all neighbours were consumed
60
60
  union.size > unionIds.length ||
61
61
  relations.length >= limit;
62
62
  }
@@ -5,8 +5,7 @@
5
5
  * default base and a few write-time multipliers (`deriveHalfLife`). Changing
6
6
  * the default therefore reaches only new memories; without this migration a
7
7
  * store would mix old-base and new-base memories, a state the decay
8
- * evaluation never tested (docs/evals/2026-09-24-decay-default-prereg.md,
9
- * Migration). The rule, declared there before any run:
8
+ * evaluation never tested. The rule, declared before any run:
10
9
  *
11
10
  * - only a memory still on the old base is rescaled: its half-life is
12
11
  * `deriveHalfLife(from, entry)` plus its recall bonus. A memory hippo shortened since
@@ -5,8 +5,7 @@
5
5
  * default base and a few write-time multipliers (`deriveHalfLife`). Changing
6
6
  * the default therefore reaches only new memories; without this migration a
7
7
  * store would mix old-base and new-base memories, a state the decay
8
- * evaluation never tested (docs/evals/2026-09-24-decay-default-prereg.md,
9
- * Migration). The rule, declared there before any run:
8
+ * evaluation never tested. The rule, declared before any run:
10
9
  *
11
10
  * - only a memory still on the old base is rescaled: its half-life is
12
11
  * `deriveHalfLife(from, entry)` plus its recall bonus. A memory hippo shortened since
@@ -151,7 +150,7 @@ function objectMemoryIds(db) {
151
150
  }
152
151
  return { all, retired };
153
152
  }
154
- /** Memories that lost a conflict, which resolveConflict halved untagged. A resolved conflict with no audit row (before v1.31.0, or found stale) names no winner, so both sides count. */
153
+ /** Memories that lost a conflict, which resolveConflict halved untagged. A resolved conflict with no audit row (resolved before resolves were audited, or found stale) names no winner, so both sides count. */
155
154
  function conflictLosers(db) {
156
155
  // SAFETY: SELECT of two fields every conflict_resolve audit row carries (ConflictResolveMeta in store.ts).
157
156
  const audited = db.prepare(`SELECT json_extract(metadata_json, '$.conflictId') AS conflictId, json_extract(metadata_json, '$.loserId') AS loserId FROM audit_log WHERE op = 'conflict_resolve'`).all();
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { isJsonString } from './shared.js';
3
+ import { isJsonString } from '../json.js';
4
4
  function collectFiles(dir) {
5
5
  if (!fs.existsSync(dir))
6
6
  return [];
@@ -49,7 +49,7 @@ export declare function isCodexWrapperInstalled(): boolean;
49
49
  * opt-in record. Never performs a first install. Replacing another vendor's
50
50
  * binary must stay behind the explicit `hippo hook install codex` command;
51
51
  * doing it from postinstall or routine commands is a consent violation and
52
- * reads as binary hijacking to security scanners (issue #133).
52
+ * reads as binary hijacking to security scanners.
53
53
  */
54
54
  export declare function repairCodexWrapperIfInstalled(hippoCliPath?: string): EnsureCodexWrapperResult;
55
55
  //# sourceMappingURL=codex-wrapper.d.ts.map
@@ -2,7 +2,8 @@ import { envPath } from '../env.js';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { isJsonString, HIPPO_CODEX_WRAPPER_MARKER, homeDir, codexHomeDir, ensureDir } from './shared.js';
5
+ import { HIPPO_CODEX_WRAPPER_MARKER, homeDir, codexHomeDir, ensureDir } from './shared.js';
6
+ import { isJsonString } from '../json.js';
6
7
  export function resolveCodexWrapperPaths() {
7
8
  const home = homeDir();
8
9
  const codexHome = codexHomeDir(home);
@@ -271,7 +272,7 @@ export function isCodexWrapperInstalled() {
271
272
  * opt-in record. Never performs a first install. Replacing another vendor's
272
273
  * binary must stay behind the explicit `hippo hook install codex` command;
273
274
  * doing it from postinstall or routine commands is a consent violation and
274
- * reads as binary hijacking to security scanners (issue #133).
275
+ * reads as binary hijacking to security scanners.
275
276
  */
276
277
  export function repairCodexWrapperIfInstalled(hippoCliPath = resolveHippoCliPath()) {
277
278
  if (readCodexWrapperMetadata() === null) {
@@ -23,8 +23,8 @@
23
23
  *
24
24
  * 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
25
25
  * JSON-hook schema — its config has `additionalProperties: false` and no
26
- * `hooks` key, so v1.10.x-v1.11.1's JSON-hook installer broke opencode
27
- * launch (issue #24). Hippo now installs a TypeScript plugin at
26
+ * `hooks` key, so a JSON-hook install breaks opencode launch. Hippo
27
+ * installs a TypeScript plugin at
28
28
  * `~/.config/opencode/plugins/hippo.ts` subscribing to opencode's
29
29
  * `session.idle` (→ `hippo session-end`) and `session.created` (→
30
30
  * `hippo last-sleep`) events. See OPENCODE_PLUGIN_SOURCE below for the
@@ -23,8 +23,8 @@
23
23
  *
24
24
  * 2. Plugin install (OpenCode only). OpenCode does NOT share Claude Code's
25
25
  * JSON-hook schema — its config has `additionalProperties: false` and no
26
- * `hooks` key, so v1.10.x-v1.11.1's JSON-hook installer broke opencode
27
- * launch (issue #24). Hippo now installs a TypeScript plugin at
26
+ * `hooks` key, so a JSON-hook install breaks opencode launch. Hippo
27
+ * installs a TypeScript plugin at
28
28
  * `~/.config/opencode/plugins/hippo.ts` subscribing to opencode's
29
29
  * `session.idle` (→ `hippo session-end`) and `session.created` (→
30
30
  * `hippo last-sleep`) events. See OPENCODE_PLUGIN_SOURCE below for the
@@ -34,7 +34,8 @@
34
34
  */
35
35
  import * as fs from 'fs';
36
36
  import * as path from 'path';
37
- import { isJsonString, isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, codexHomeDir, defaultPreCompactLogPath } from './shared.js';
37
+ import { isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, codexHomeDir, defaultPreCompactLogPath } from './shared.js';
38
+ import { isJsonString } from '../json.js';
38
39
  export function resolveJsonHookPaths(target) {
39
40
  const home = homeDir();
40
41
  const logsDir = path.join(home, '.hippo', 'logs');
@@ -91,7 +92,7 @@ function migratePinnedInjectRecentCommands(hookArray) {
91
92
  }
92
93
  /**
93
94
  * Returns true when `hooks.SessionEnd` still contains either of the legacy
94
- * v0.22.x split entries (bare `hippo sleep` / `hippo capture --last-session`)
95
+ * split entries (bare `hippo sleep` / `hippo capture --last-session`)
95
96
  * without the current consolidated `hippo session-end` entry.
96
97
  */
97
98
  function hasLegacySplitSessionEnd(hookArray) {
@@ -7,7 +7,7 @@ declare const HIPPO_OPENCODE_PLUGIN_MARKER = "HIPPO_OPENCODE_PLUGIN_V1";
7
7
  * session.idle → `hippo session-end` (Claude Code's SessionEnd equiv)
8
8
  * session.created → `hippo last-sleep` (Claude Code's SessionStart equiv)
9
9
  *
10
- * Design choices forced by plan-eng-critic Rev 0 review (2026-05-23):
10
+ * Design choices:
11
11
  *
12
12
  * 1. No `import type { Plugin } from "@opencode-ai/plugin"`. The package's npm
13
13
  * publication status was unverifiable from the build sandbox (npmjs.com
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { isJsonString, isJsonObject, homeDir } from './shared.js';
3
+ import { isJsonObject, homeDir } from './shared.js';
4
+ import { isJsonString } from '../json.js';
4
5
  const HIPPO_OPENCODE_PLUGIN_MARKER = 'HIPPO_OPENCODE_PLUGIN_V1';
5
6
  /**
6
7
  * The opencode plugin file we install at ~/.config/opencode/plugins/hippo.ts.
@@ -10,7 +11,7 @@ const HIPPO_OPENCODE_PLUGIN_MARKER = 'HIPPO_OPENCODE_PLUGIN_V1';
10
11
  * session.idle → `hippo session-end` (Claude Code's SessionEnd equiv)
11
12
  * session.created → `hippo last-sleep` (Claude Code's SessionStart equiv)
12
13
  *
13
- * Design choices forced by plan-eng-critic Rev 0 review (2026-05-23):
14
+ * Design choices:
14
15
  *
15
16
  * 1. No `import type { Plugin } from "@opencode-ai/plugin"`. The package's npm
16
17
  * publication status was unverifiable from the build sandbox (npmjs.com
@@ -79,11 +80,11 @@ function resolveOpencodeConfigPath() {
79
80
  * hippo itself installs (session-end, last-sleep, sleep, capture, context),
80
81
  * not any third-party tool that happens to be named `hippo`.
81
82
  *
82
- * Critic-mandated structural check (Rev 0): substring matching against
83
+ * Structural check: substring matching against
83
84
  * arbitrary user content is unsafe — a user's
84
85
  * `echo "remember to hippo sleep your laptop"` is not a hippo-owned hook.
85
86
  *
86
- * Per-hook (not per-entry) granularity (Rev 1 review): an entry whose inner
87
+ * Per-hook (not per-entry) granularity: an entry whose inner
87
88
  * hooks array mixes hippo-installed commands with user-authored commands
88
89
  * must NOT lose the user-authored commands. The migration filters the inner
89
90
  * array per-hook, then drops the entry only when its inner array is empty.
@@ -1,10 +1,6 @@
1
- import type { JsonValue, JsonObject } from '../working-memory.js';
2
- /** JSON-value string check. `constructor` narrows a primitive via its boxed wrapper
3
- * instead of inspecting the `typeof` tag (anti-slop/no-runtime-typeof); equivalent to
4
- * `typeof value === 'string'` for every value JSON.parse can ever produce. */
5
- export declare function isJsonString(value: JsonValue | undefined): value is string;
6
- /** JSON-value plain-object check (excludes arrays and null), typeof-free for the same
7
- * reason as isJsonString above. */
1
+ import type { JsonObject } from '../working-memory.js';
2
+ import type { JsonValue } from '../json.js';
3
+ /** JSON-value plain-object check (excludes arrays and null); `constructor` avoids the `typeof` tag banned by anti-slop/no-runtime-typeof. */
8
4
  export declare function isJsonObject(value: JsonValue | undefined): value is JsonObject;
9
5
  export type JsonHookTarget = 'claude-code' | 'codex';
10
6
  export interface ToolDetection {
@@ -2,14 +2,7 @@ import { envHomeDir, processEnv } from '../env.js';
2
2
  import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
- /** JSON-value string check. `constructor` narrows a primitive via its boxed wrapper
6
- * instead of inspecting the `typeof` tag (anti-slop/no-runtime-typeof); equivalent to
7
- * `typeof value === 'string'` for every value JSON.parse can ever produce. */
8
- export function isJsonString(value) {
9
- return value !== undefined && value !== null && value.constructor === String;
10
- }
11
- /** JSON-value plain-object check (excludes arrays and null), typeof-free for the same
12
- * reason as isJsonString above. */
5
+ /** JSON-value plain-object check (excludes arrays and null); `constructor` avoids the `typeof` tag banned by anti-slop/no-runtime-typeof. */
13
6
  export function isJsonObject(value) {
14
7
  return value !== undefined && value !== null && !Array.isArray(value) && value.constructor === Object;
15
8
  }
@@ -1,9 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
- [key: string]: JsonValue;
4
- };
2
+ import type { JsonValue } from './json.js';
5
3
  export declare function isJsonObjectRecord(value: JsonValue | undefined): value is Record<string, JsonValue>;
6
4
  export declare function isHeaderString(value: string | string[] | undefined): value is string;
5
+ export declare const MAX_ID_LEN = 256;
7
6
  export declare const JSON_HEADERS: {
8
7
  readonly 'content-type': "application/json";
9
8
  };
package/dist/http-util.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from './api-errors.js';
2
+ // Leaf module shared by server.ts and the connector webhook receivers; it must not import either.
2
3
  export function isJsonObjectRecord(value) {
3
4
  return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
4
5
  }
@@ -10,6 +11,8 @@ export function isHeaderString(value) {
10
11
  // 1 MB body cap. The CLI never sends payloads near this; anything bigger is
11
12
  // almost certainly a misconfigured client or a deliberate memory-blowup attempt.
12
13
  const MAX_BODY_BYTES = 1024 * 1024;
14
+ // Cap for id-shaped request fields (ids, tenant, session, scope, class): far above real values, small enough to bound logs and indexes.
15
+ export const MAX_ID_LEN = 256;
13
16
  export const JSON_HEADERS = { 'content-type': 'application/json' };
14
17
  export class HttpError extends Error {
15
18
  status;
@@ -3,6 +3,7 @@
3
3
  * Imports memories from ChatGPT, Claude, Cursor, generic files, and structured markdown.
4
4
  */
5
5
  import { MemoryEntry } from '../memory.js';
6
+ import type { JsonValue } from '../json.js';
6
7
  export interface ImportResult {
7
8
  total: number;
8
9
  imported: number;
@@ -44,7 +45,7 @@ export interface ImportOptions {
44
45
  * sharing a basename would collide and clobber each other). importVault throws
45
46
  * if it is missing or blank. Optional in this shared type only because the
46
47
  * other importers ignore it. Operator-supplied, so the loader query LIKE-escapes
47
- * it (see `escapeLike` below).
48
+ * it (`escapeLike` in src/escape.ts).
48
49
  */
49
50
  name?: string;
50
51
  /**
@@ -58,14 +59,6 @@ export interface ImportOptions {
58
59
  * create MemoryEntry objects, write them (unless dry-run), and return a result.
59
60
  */
60
61
  export declare function importEntries(chunks: string[], source: string, tags: string[], options: ImportOptions): ImportResult;
61
- /** The full value space `JSON.parse` can produce. Boundary-guard functions in
62
- * this file accept `JsonValue` (never `unknown`) so a value's origin as
63
- * unparsed external JSON stays visible in its type, then narrow it via the
64
- * `isJson*` predicates below. */
65
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
66
- [key: string]: JsonValue;
67
- };
68
- export declare function isJsonString(x: JsonValue): x is string;
69
62
  export declare function isJsonPlainObject(x: JsonValue): x is {
70
63
  [key: string]: JsonValue;
71
64
  };
@@ -3,7 +3,8 @@
3
3
  * Imports memories from ChatGPT, Claude, Cursor, generic files, and structured markdown.
4
4
  */
5
5
  import { createMemory, Layer } from '../memory.js';
6
- import { writeEntry } from '../store/entry-writes.js';
6
+ import { writeEntryOn } from '../store/entry-writes.js';
7
+ import { openStore } from '../store/open.js';
7
8
  import { loadAllEntries } from '../store/entry-reads.js';
8
9
  import { duplicateKey, storedTextKeys } from '../same-text.js';
9
10
  import { getGlobalRoot, initGlobal } from '../shared.js';
@@ -34,12 +35,8 @@ export function importEntries(chunks, source, tags, options) {
34
35
  let rejected = 0;
35
36
  let redacted = 0;
36
37
  const entries = [];
37
- // AT1 P2 fix: a dry-run preview never called writeEntry, so it never
38
- // checked tombstones either — every non-duplicate chunk counted as
39
- // `imported` even when a real run would refuse it, making the preview's
40
- // `rejected` count silently 0. Probe (read-only) via the same guard
41
- // writeEntry uses internally, without ever writing.
42
- const dryRunDb = options.dryRun ? openHippoDb(targetRoot) : null;
38
+ // A dry run probes the rejection guard read-only; a real run writes every chunk on this one handle.
39
+ const db = options.dryRun ? openHippoDb(targetRoot) : openStore(targetRoot);
43
40
  try {
44
41
  for (const raw of chunks) {
45
42
  const { chunk, wasRedacted } = prepareImportChunk(raw, allTags);
@@ -55,7 +52,7 @@ export function importEntries(chunks, source, tags, options) {
55
52
  continue;
56
53
  }
57
54
  const entry = createImportEntry(chunk, source, allTags, options, baseHalfLifeDays);
58
- if (!writeOrProbeImport(targetRoot, entry, options, dryRunDb)) {
55
+ if (!writeOrProbeImport(db, targetRoot, entry, options)) {
59
56
  rejected++;
60
57
  continue;
61
58
  }
@@ -70,8 +67,7 @@ export function importEntries(chunks, source, tags, options) {
70
67
  return { total, imported, skipped, rejected, redacted, entries };
71
68
  }
72
69
  finally {
73
- if (dryRunDb)
74
- closeHippoDb(dryRunDb);
70
+ closeHippoDb(db);
75
71
  }
76
72
  }
77
73
  /** The secret-vetted chunk capped at 1000 chars, and whether vetting changed it. */
@@ -106,24 +102,13 @@ function createImportEntry(chunk, source, allTags, options, baseHalfLifeDays) {
106
102
  });
107
103
  }
108
104
  /** Writes the entry, or on a dry run only probes the guard; false when a rejected value refuses it. */
109
- function writeOrProbeImport(targetRoot, entry, options, dryRunDb) {
110
- if (options.dryRun) {
111
- if (dryRunDb) {
112
- try {
113
- checkRejectionGuard(dryRunDb, entry.tenantId ?? 'default', entry.id, entry.content);
114
- }
115
- catch (err) {
116
- if (err instanceof RejectedValueError)
117
- return false;
118
- throw err;
119
- }
120
- }
121
- return true;
122
- }
123
- // AT1 (plan §3 containment): a rejection refuses one CHUNK, not the
124
- // whole import batch. Caught per-item so siblings still land.
105
+ function writeOrProbeImport(db, targetRoot, entry, options) {
106
+ // A rejection refuses one chunk, not the whole import, so siblings still land.
125
107
  try {
126
- writeEntry(targetRoot, entry);
108
+ if (options.dryRun)
109
+ checkRejectionGuard(db, entry.tenantId ?? 'default', entry.id, entry.content);
110
+ else
111
+ writeEntryOn(db, targetRoot, entry);
127
112
  }
128
113
  catch (err) {
129
114
  if (err instanceof RejectedValueError)
@@ -132,9 +117,9 @@ function writeOrProbeImport(targetRoot, entry, options, dryRunDb) {
132
117
  }
133
118
  return true;
134
119
  }
135
- export function isJsonString(x) {
136
- return typeof x === 'string';
137
- }
120
+ // ---------------------------------------------------------------------------
121
+ // ChatGPT importer
122
+ // ---------------------------------------------------------------------------
138
123
  export function isJsonPlainObject(x) {
139
124
  return x !== null && !Array.isArray(x) && typeof x === 'object';
140
125
  }
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { importEntries, isJsonString, isJsonPlainObject } from './core.js';
3
+ import { importEntries, isJsonPlainObject } from './core.js';
4
4
  import { parseFrontmatter, collectMarkdownFiles } from './markdown-parse.js';
5
+ import { isJsonString } from '../json.js';
5
6
  /** Coerce one imported record (string, `{content|text: ...}` object, or
6
7
  * anything else) into the plain-text memory chunk it represents. */
7
8
  function extractMemoryText(candidate) {
@@ -9,27 +9,9 @@ import { RejectedValueError, checkRejectionGuard } from '../rejection.js';
9
9
  import { loadConfig } from '../config.js';
10
10
  import { vetSecrets } from '../secret-detect.js';
11
11
  import { log } from '../log.js';
12
- import { isJsonString } from './core.js';
13
12
  import { parseFrontmatter, frontmatterList, parseWikilinks, collectMarkdownFiles, realpathOrResolve } from './markdown-parse.js';
14
- // ---------------------------------------------------------------------------
15
- // K1 vault importer (markdown-vault FOLDER → kind='raw' memories)
16
- //
17
- // MIRRORS THE CONNECTOR PATTERN (src/connectors/slack|github), NOT the
18
- // single-file importers above. Each note becomes a single kind='raw' row with
19
- // provenance in TAGS (`source:vault` + `vault:<name>`), an artifactRef cursor
20
- // key, and a content-hash tag. Changes APPEND a new raw row after archiveRaw of
21
- // the old one; deletions archiveRaw the orphaned rows. We NEVER `supersede` a
22
- // raw row (supersede yields kind='distilled', losing raw-append-only protection
23
- // and escaping the kind='raw' deletion rescan) — all raw deletions route through
24
- // `archiveRaw` (the only trigger-legit raw delete).
25
- // ---------------------------------------------------------------------------
26
- /** Escape LIKE wildcards in operator-supplied text (mirror of
27
- * src/project-briefs.ts:477 / src/store/search-rows.ts, kept local since neither is
28
- * exported). Used so a `%`/`_`/`\` in the vault name cannot over-match the
29
- * loader prefix and archive another vault's rows. */
30
- function escapeLike(term) {
31
- return term.replace(/[%_\\]/g, '\\$&');
32
- }
13
+ import { isJsonString } from '../json.js';
14
+ import { escapeLike } from '../escape.js';
33
15
  /**
34
16
  * Import a markdown vault FOLDER as `kind='raw'` memories.
35
17
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
2
+ * Incident first-class object.
3
3
  *
4
4
  * An incident is a postmortem capsule: a recorded operational event with a
5
5
  * lifecycle and optional linked receipts (the memories that are its evidence).
package/dist/incidents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
2
+ * Incident first-class object.
3
3
  *
4
4
  * An incident is a postmortem capsule: a recorded operational event with a
5
5
  * lifecycle and optional linked receipts (the memories that are its evidence).
@@ -77,6 +77,50 @@ const INCIDENT_COLS = `
77
77
  // ---------------------------------------------------------------------------
78
78
  // Public API
79
79
  // ---------------------------------------------------------------------------
80
+ // Validate every linked receipt BEFORE inserting the row. Each must be a
81
+ // memory in the SAME tenant; a cross-tenant or nonexistent id rejects the
82
+ // whole write rather than recording an unverifiable receipt.
83
+ function validateLinkedMemoryIds(db, tenantId, linkInput) {
84
+ const validated = [];
85
+ for (const linkId of linkInput) {
86
+ // SAFETY: row shape matches the single `id` column named in the SELECT above.
87
+ const exists = db.prepare(`SELECT id FROM memories WHERE id = ? AND tenant_id = ?`).get(linkId, tenantId);
88
+ if (!exists) {
89
+ throw new NotFoundError(`saveIncident: linked memory ${linkId} not found for tenant ${tenantId}`);
90
+ }
91
+ validated.push(linkId);
92
+ }
93
+ return validated;
94
+ }
95
+ /** The afterWrite body: link validation, INSERT, reload, open audit, all in one SAVEPOINT. */
96
+ function writeIncidentRow(db, memoryId, tenantId, opts, actor, now) {
97
+ const validated = validateLinkedMemoryIds(db, tenantId, opts.linkedMemoryIds ?? []);
98
+ const result = db.prepare(`
99
+ INSERT INTO incidents(
100
+ memory_id, tenant_id, incident_text, context,
101
+ status, resolution_text, resolved_at, closed_at, linked_memory_ids, created_at
102
+ ) VALUES (?, ?, ?, ?, 'open', NULL, NULL, NULL, ?, ?)
103
+ `).run(memoryId, tenantId, opts.incidentText, opts.context ?? null, JSON.stringify(validated), now);
104
+ const incidentId = Number(result.lastInsertRowid ?? 0);
105
+ // SAFETY: row's shape matches the columns named in INCIDENT_COLS above.
106
+ const row = db.prepare(`SELECT ${INCIDENT_COLS} FROM incidents WHERE id = ?`)
107
+ .get(incidentId);
108
+ if (!row)
109
+ throw new Error('saveIncident: failed to reload saved incident row');
110
+ // GDPR-light metadata: id + flag only, no incident_text.
111
+ appendAuditEvent(db, {
112
+ tenantId,
113
+ actor,
114
+ op: 'incident_open',
115
+ targetId: String(incidentId),
116
+ metadata: {
117
+ incident_id: incidentId,
118
+ has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
119
+ linked_memory_count: validated.length,
120
+ },
121
+ });
122
+ return row;
123
+ }
80
124
  /**
81
125
  * Create an incident. Writes the memory mirror + the incidents row atomically
82
126
  * inside writeEntry's SAVEPOINT 'write_entry'.
@@ -106,50 +150,13 @@ export function saveIncident(hippoRoot, tenantId, opts, actor = 'cli') {
106
150
  baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
107
151
  tenantId,
108
152
  });
109
- const linkInput = opts.linkedMemoryIds ?? [];
110
153
  // Populated inside afterWrite so the linked-id validation, the INSERT, and the
111
154
  // memory write all share one SAVEPOINT.
112
155
  let savedRow;
113
156
  writeEntry(hippoRoot, mem, {
114
157
  actor,
115
158
  afterWrite: (db, memoryId) => {
116
- // Validate every linked receipt BEFORE inserting the row. Each must be a
117
- // memory in the SAME tenant; a cross-tenant or nonexistent id rejects the
118
- // whole write rather than recording an unverifiable receipt.
119
- const validated = [];
120
- for (const linkId of linkInput) {
121
- // SAFETY: row shape matches the single `id` column named in the SELECT above.
122
- const exists = db.prepare(`SELECT id FROM memories WHERE id = ? AND tenant_id = ?`).get(linkId, tenantId);
123
- if (!exists) {
124
- throw new NotFoundError(`saveIncident: linked memory ${linkId} not found for tenant ${tenantId}`);
125
- }
126
- validated.push(linkId);
127
- }
128
- const result = db.prepare(`
129
- INSERT INTO incidents(
130
- memory_id, tenant_id, incident_text, context,
131
- status, resolution_text, resolved_at, closed_at, linked_memory_ids, created_at
132
- ) VALUES (?, ?, ?, ?, 'open', NULL, NULL, NULL, ?, ?)
133
- `).run(memoryId, tenantId, opts.incidentText, opts.context ?? null, JSON.stringify(validated), now);
134
- const incidentId = Number(result.lastInsertRowid ?? 0);
135
- // SAFETY: row's shape matches the columns named in INCIDENT_COLS above.
136
- const row = db.prepare(`SELECT ${INCIDENT_COLS} FROM incidents WHERE id = ?`)
137
- .get(incidentId);
138
- if (!row)
139
- throw new Error('saveIncident: failed to reload saved incident row');
140
- savedRow = row;
141
- // GDPR-light metadata: id + flag only, no incident_text.
142
- appendAuditEvent(db, {
143
- tenantId,
144
- actor,
145
- op: 'incident_open',
146
- targetId: String(incidentId),
147
- metadata: {
148
- incident_id: incidentId,
149
- has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
150
- linked_memory_count: validated.length,
151
- },
152
- });
159
+ savedRow = writeIncidentRow(db, memoryId, tenantId, opts, actor, now);
153
160
  },
154
161
  });
155
162
  if (!savedRow) {
@@ -1,4 +1,4 @@
1
- /** Prompt-injection detection for untrusted memory content (CD5). Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
1
+ /** Prompt-injection detection for untrusted memory content. Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
2
2
  export interface InstructionDetection {
3
3
  flagged: boolean;
4
4
  reason: string | null;
@@ -1,4 +1,4 @@
1
- /** Prompt-injection detection for untrusted memory content (CD5). Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
1
+ /** Prompt-injection detection for untrusted memory content. Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
2
2
  const INSTRUCTION_PATTERNS = [
3
3
  { name: 'override-instructions', re: /\b(?:ignore|disregard|forget)\b[^.\n]{0,40}\b(?:previous|prior|above)\b[^.\n]{0,40}\binstructions?\b/i },
4
4
  { name: 'role-reassignment', re: /\b(?:you are now|act as|pretend to be)\b[^.\n]{0,40}\b(?:an?\s+)?(?:ai|assistant|agent|system)\b/i },
@@ -47,7 +47,10 @@ export interface ChurnRefs {
47
47
  export declare function extractChurnRefs(content: string): ChurnRefs;
48
48
  export interface DetectChurnStaleOptions {
49
49
  tenantId: string;
50
+ /** The project id; rows under it or legacyName are checked. */
50
51
  projectName: string;
52
+ /** The folder name older rows carry, and the repo folder a memory's paths may start with. */
53
+ legacyName?: string;
51
54
  /** Evaluate matches but write nothing. */
52
55
  dryRun?: boolean;
53
56
  }