hippo-memory 1.60.0 → 1.62.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 (593) hide show
  1. package/README.md +29 -54
  2. package/dist/ablation.js +9 -27
  3. package/dist/agent-memories/apply.d.ts +1 -1
  4. package/dist/agent-memories/apply.js +4 -1
  5. package/dist/agent-memories/claude-code.d.ts +3 -1
  6. package/dist/agent-memories/claude-code.js +54 -10
  7. package/dist/agent-memories/codex.js +1 -1
  8. package/dist/agent-memories/gemini.js +1 -1
  9. package/dist/agent-memories/legacy.js +1 -1
  10. package/dist/agent-memories/source.js +1 -1
  11. package/dist/agent-memories/sync.d.ts +3 -3
  12. package/dist/agent-memories/sync.js +22 -9
  13. package/dist/agent-memories/types.d.ts +0 -2
  14. package/dist/ambient-store.d.ts +14 -0
  15. package/dist/ambient-store.js +90 -0
  16. package/dist/ambient.d.ts +23 -0
  17. package/dist/ambient.js +72 -50
  18. package/dist/api/assemble.d.ts +93 -0
  19. package/dist/api/assemble.js +153 -0
  20. package/dist/api/audit.d.ts +17 -0
  21. package/dist/api/audit.js +23 -0
  22. package/dist/api/auth.d.ts +79 -0
  23. package/dist/api/auth.js +178 -0
  24. package/dist/api/context-select.d.ts +49 -0
  25. package/dist/api/context-select.js +344 -0
  26. package/dist/api/context-types.d.ts +105 -0
  27. package/dist/api/context-types.js +3 -0
  28. package/dist/api/context.d.ts +31 -0
  29. package/dist/api/context.js +378 -0
  30. package/dist/api/dormant.d.ts +30 -0
  31. package/dist/api/dormant.js +140 -0
  32. package/dist/api/drill-down.d.ts +84 -0
  33. package/dist/api/drill-down.js +126 -0
  34. package/dist/api/forget.d.ts +57 -0
  35. package/dist/api/forget.js +87 -0
  36. package/dist/api/goals.d.ts +18 -0
  37. package/dist/api/goals.js +33 -0
  38. package/dist/api/learn.d.ts +31 -0
  39. package/dist/api/learn.js +88 -0
  40. package/dist/api/outcome.d.ts +61 -0
  41. package/dist/api/outcome.js +66 -0
  42. package/dist/api/promote.d.ts +54 -0
  43. package/dist/api/promote.js +192 -0
  44. package/dist/api/quarantine.d.ts +24 -0
  45. package/dist/api/quarantine.js +121 -0
  46. package/dist/api/recall-types.d.ts +390 -0
  47. package/dist/api/recall-types.js +3 -0
  48. package/dist/api/recall.d.ts +36 -0
  49. package/dist/api/recall.js +499 -0
  50. package/dist/api/remember.d.ts +35 -0
  51. package/dist/api/remember.js +43 -0
  52. package/dist/api/sleep.d.ts +136 -0
  53. package/dist/api/sleep.js +256 -0
  54. package/dist/api/tokens.d.ts +26 -0
  55. package/dist/api/tokens.js +60 -0
  56. package/dist/api/types.d.ts +56 -0
  57. package/dist/api/types.js +38 -0
  58. package/dist/api.d.ts +20 -1262
  59. package/dist/api.js +25 -2727
  60. package/dist/audit.d.ts +3 -0
  61. package/dist/audit.js +6 -3
  62. package/dist/auth.d.ts +45 -4
  63. package/dist/auth.js +125 -48
  64. package/dist/autolearn.js +2 -2
  65. package/dist/capture/command.d.ts +33 -0
  66. package/dist/capture/command.js +264 -0
  67. package/dist/capture/compact.d.ts +44 -0
  68. package/dist/capture/compact.js +354 -0
  69. package/dist/capture/extract.d.ts +21 -0
  70. package/dist/capture/extract.js +464 -0
  71. package/dist/capture/transcript.d.ts +40 -0
  72. package/dist/capture/transcript.js +193 -0
  73. package/dist/capture-error.js +2 -1
  74. package/dist/churn-git.js +4 -2
  75. package/dist/cli/audit.d.ts +3 -0
  76. package/dist/cli/audit.js +159 -0
  77. package/dist/cli/auth.d.ts +2 -0
  78. package/dist/cli/auth.js +171 -0
  79. package/dist/cli/briefs.d.ts +4 -0
  80. package/dist/cli/briefs.js +453 -0
  81. package/dist/cli/card.d.ts +3 -0
  82. package/dist/cli/card.js +333 -0
  83. package/dist/cli/context.d.ts +15 -0
  84. package/dist/cli/context.js +376 -0
  85. package/dist/cli/continuity.d.ts +6 -0
  86. package/dist/cli/continuity.js +457 -0
  87. package/dist/cli/curate.d.ts +19 -0
  88. package/dist/cli/curate.js +556 -0
  89. package/dist/cli/dag.d.ts +5 -0
  90. package/dist/cli/dag.js +177 -0
  91. package/dist/cli/decisions.d.ts +4 -0
  92. package/dist/cli/decisions.js +528 -0
  93. package/dist/cli/eval.d.ts +5 -0
  94. package/dist/cli/eval.js +213 -0
  95. package/dist/cli/explain.d.ts +4 -0
  96. package/dist/cli/explain.js +156 -0
  97. package/dist/cli/goals.d.ts +2 -0
  98. package/dist/cli/goals.js +196 -0
  99. package/dist/cli/hook-blocks.d.ts +18 -0
  100. package/dist/cli/hook-blocks.js +233 -0
  101. package/dist/cli/init.d.ts +2 -0
  102. package/dist/cli/init.js +305 -0
  103. package/dist/cli/maintenance.d.ts +4 -0
  104. package/dist/cli/maintenance.js +190 -0
  105. package/dist/cli/playbooks.d.ts +4 -0
  106. package/dist/cli/playbooks.js +573 -0
  107. package/dist/cli/projects.js +9 -6
  108. package/dist/cli/recall.d.ts +7 -0
  109. package/dist/cli/recall.js +582 -0
  110. package/dist/cli/remember.d.ts +5 -0
  111. package/dist/cli/remember.js +485 -0
  112. package/dist/cli/serve.d.ts +5 -0
  113. package/dist/cli/serve.js +40 -0
  114. package/dist/cli/session-hooks.d.ts +29 -0
  115. package/dist/cli/session-hooks.js +647 -0
  116. package/dist/cli/setup.d.ts +4 -0
  117. package/dist/cli/setup.js +397 -0
  118. package/dist/cli/shared.d.ts +15 -19
  119. package/dist/cli/shared.js +50 -341
  120. package/dist/cli/slack.d.ts +2 -0
  121. package/dist/cli/slack.js +171 -0
  122. package/dist/cli/status.d.ts +18 -0
  123. package/dist/cli/status.js +409 -0
  124. package/dist/cli/transfer.d.ts +10 -0
  125. package/dist/cli/transfer.js +424 -0
  126. package/dist/cli/usage.d.ts +85 -0
  127. package/dist/cli/usage.js +741 -0
  128. package/dist/cli.d.ts +71 -120
  129. package/dist/cli.js +170 -8469
  130. package/dist/client.js +15 -8
  131. package/dist/compaction-record.d.ts +0 -2
  132. package/dist/compaction-record.js +7 -5
  133. package/dist/connectors/github/backfill.js +94 -87
  134. package/dist/connectors/github/cli-impl.js +4 -3
  135. package/dist/connectors/github/dlq.js +67 -54
  136. package/dist/connectors/github/ingest.js +34 -35
  137. package/dist/connectors/github/tenant-routing.js +3 -2
  138. package/dist/connectors/github/webhook.js +135 -216
  139. package/dist/connectors/slack/dlq.js +49 -61
  140. package/dist/connectors/slack/ingest.js +55 -48
  141. package/dist/connectors/slack/tenant-routing.js +4 -3
  142. package/dist/connectors/slack/webhook.js +72 -74
  143. package/dist/consolidate/conflicts.d.ts +10 -0
  144. package/dist/consolidate/conflicts.js +178 -0
  145. package/dist/consolidate/decay.d.ts +12 -0
  146. package/dist/consolidate/decay.js +145 -0
  147. package/dist/consolidate/llm-passes.d.ts +3 -0
  148. package/dist/consolidate/llm-passes.js +141 -0
  149. package/dist/consolidate/merge.d.ts +7 -0
  150. package/dist/consolidate/merge.js +251 -0
  151. package/dist/consolidate/physics-pass.d.ts +3 -0
  152. package/dist/consolidate/physics-pass.js +60 -0
  153. package/dist/consolidate/run.d.ts +69 -0
  154. package/dist/consolidate/run.js +76 -0
  155. package/dist/consolidate/sleep.d.ts +18 -0
  156. package/dist/consolidate/sleep.js +209 -0
  157. package/dist/consolidate/traces.d.ts +4 -0
  158. package/dist/consolidate/traces.js +178 -0
  159. package/dist/context-auto.js +7 -11
  160. package/dist/context-render.d.ts +1 -1
  161. package/dist/context-render.js +1 -1
  162. package/dist/customer-notes.d.ts +3 -0
  163. package/dist/customer-notes.js +83 -72
  164. package/dist/dag.js +225 -188
  165. package/dist/dashboard-actions.d.ts +20 -0
  166. package/dist/dashboard-actions.js +88 -0
  167. package/dist/dashboard-params.d.ts +45 -0
  168. package/dist/dashboard-params.js +127 -0
  169. package/dist/dashboard-queries.d.ts +15 -0
  170. package/dist/dashboard-queries.js +355 -0
  171. package/dist/dashboard-snapshot.d.ts +138 -0
  172. package/dist/dashboard-snapshot.js +308 -0
  173. package/dist/dashboard-types.d.ts +163 -0
  174. package/dist/dashboard-types.js +3 -0
  175. package/dist/dashboard.d.ts +6 -7
  176. package/dist/dashboard.js +228 -202
  177. package/dist/db/busy.d.ts +5 -0
  178. package/dist/db/busy.js +24 -0
  179. package/dist/db/continuity.d.ts +5 -0
  180. package/dist/db/continuity.js +145 -0
  181. package/dist/db/meta.d.ts +8 -0
  182. package/dist/db/meta.js +35 -0
  183. package/dist/db/migrate.d.ts +9 -0
  184. package/dist/db/migrate.js +138 -0
  185. package/dist/db/migrations/index.d.ts +5 -0
  186. package/dist/db/migrations/index.js +109 -0
  187. package/dist/db/migrations/types.d.ts +14 -0
  188. package/dist/db/migrations/types.js +2 -0
  189. package/dist/db/migrations/v01.d.ts +3 -0
  190. package/dist/db/migrations/v01.js +38 -0
  191. package/dist/db/migrations/v02.d.ts +3 -0
  192. package/dist/db/migrations/v02.js +21 -0
  193. package/dist/db/migrations/v03.d.ts +3 -0
  194. package/dist/db/migrations/v03.js +22 -0
  195. package/dist/db/migrations/v04.d.ts +3 -0
  196. package/dist/db/migrations/v04.js +28 -0
  197. package/dist/db/migrations/v05.d.ts +3 -0
  198. package/dist/db/migrations/v05.js +21 -0
  199. package/dist/db/migrations/v06.d.ts +3 -0
  200. package/dist/db/migrations/v06.js +25 -0
  201. package/dist/db/migrations/v07.d.ts +3 -0
  202. package/dist/db/migrations/v07.js +13 -0
  203. package/dist/db/migrations/v08.d.ts +3 -0
  204. package/dist/db/migrations/v08.js +8 -0
  205. package/dist/db/migrations/v09.d.ts +3 -0
  206. package/dist/db/migrations/v09.js +13 -0
  207. package/dist/db/migrations/v10.d.ts +3 -0
  208. package/dist/db/migrations/v10.js +17 -0
  209. package/dist/db/migrations/v11.d.ts +3 -0
  210. package/dist/db/migrations/v11.js +15 -0
  211. package/dist/db/migrations/v12.d.ts +3 -0
  212. package/dist/db/migrations/v12.js +11 -0
  213. package/dist/db/migrations/v13.d.ts +3 -0
  214. package/dist/db/migrations/v13.js +15 -0
  215. package/dist/db/migrations/v14.d.ts +3 -0
  216. package/dist/db/migrations/v14.js +66 -0
  217. package/dist/db/migrations/v15.d.ts +3 -0
  218. package/dist/db/migrations/v15.js +42 -0
  219. package/dist/db/migrations/v16.d.ts +3 -0
  220. package/dist/db/migrations/v16.js +61 -0
  221. package/dist/db/migrations/v17.d.ts +3 -0
  222. package/dist/db/migrations/v17.js +46 -0
  223. package/dist/db/migrations/v18.d.ts +3 -0
  224. package/dist/db/migrations/v18.js +60 -0
  225. package/dist/db/migrations/v19.d.ts +3 -0
  226. package/dist/db/migrations/v19.js +28 -0
  227. package/dist/db/migrations/v20.d.ts +3 -0
  228. package/dist/db/migrations/v20.js +42 -0
  229. package/dist/db/migrations/v21.d.ts +3 -0
  230. package/dist/db/migrations/v21.js +16 -0
  231. package/dist/db/migrations/v22.d.ts +3 -0
  232. package/dist/db/migrations/v22.js +82 -0
  233. package/dist/db/migrations/v23.d.ts +3 -0
  234. package/dist/db/migrations/v23.js +48 -0
  235. package/dist/db/migrations/v24.d.ts +3 -0
  236. package/dist/db/migrations/v24.js +72 -0
  237. package/dist/db/migrations/v25.d.ts +3 -0
  238. package/dist/db/migrations/v25.js +46 -0
  239. package/dist/db/migrations/v26.d.ts +3 -0
  240. package/dist/db/migrations/v26.js +23 -0
  241. package/dist/db/migrations/v27.d.ts +3 -0
  242. package/dist/db/migrations/v27.js +57 -0
  243. package/dist/db/migrations/v28.d.ts +3 -0
  244. package/dist/db/migrations/v28.js +38 -0
  245. package/dist/db/migrations/v29.d.ts +3 -0
  246. package/dist/db/migrations/v29.js +78 -0
  247. package/dist/db/migrations/v30.d.ts +3 -0
  248. package/dist/db/migrations/v30.js +92 -0
  249. package/dist/db/migrations/v31.d.ts +3 -0
  250. package/dist/db/migrations/v31.js +74 -0
  251. package/dist/db/migrations/v32.d.ts +3 -0
  252. package/dist/db/migrations/v32.js +102 -0
  253. package/dist/db/migrations/v33.d.ts +3 -0
  254. package/dist/db/migrations/v33.js +104 -0
  255. package/dist/db/migrations/v34.d.ts +3 -0
  256. package/dist/db/migrations/v34.js +93 -0
  257. package/dist/db/migrations/v35.d.ts +3 -0
  258. package/dist/db/migrations/v35.js +98 -0
  259. package/dist/db/migrations/v36.d.ts +3 -0
  260. package/dist/db/migrations/v36.js +98 -0
  261. package/dist/db/migrations/v37.d.ts +3 -0
  262. package/dist/db/migrations/v37.js +219 -0
  263. package/dist/db/migrations/v38.d.ts +3 -0
  264. package/dist/db/migrations/v38.js +277 -0
  265. package/dist/db/migrations/v39.d.ts +3 -0
  266. package/dist/db/migrations/v39.js +59 -0
  267. package/dist/db/migrations/v40.d.ts +3 -0
  268. package/dist/db/migrations/v40.js +74 -0
  269. package/dist/db/migrations/v41.d.ts +3 -0
  270. package/dist/db/migrations/v41.js +43 -0
  271. package/dist/db/migrations/v42.d.ts +3 -0
  272. package/dist/db/migrations/v42.js +41 -0
  273. package/dist/db/migrations/v43.d.ts +3 -0
  274. package/dist/db/migrations/v43.js +67 -0
  275. package/dist/db/migrations/v44.d.ts +3 -0
  276. package/dist/db/migrations/v44.js +28 -0
  277. package/dist/db/migrations/v45.d.ts +3 -0
  278. package/dist/db/migrations/v45.js +30 -0
  279. package/dist/db/migrations/v46.d.ts +3 -0
  280. package/dist/db/migrations/v46.js +25 -0
  281. package/dist/db/migrations/v47.d.ts +3 -0
  282. package/dist/db/migrations/v47.js +17 -0
  283. package/dist/db/migrations/v48.d.ts +3 -0
  284. package/dist/db/migrations/v48.js +10 -0
  285. package/dist/db/migrations/v49.d.ts +3 -0
  286. package/dist/db/migrations/v49.js +31 -0
  287. package/dist/db/migrations/v50.d.ts +3 -0
  288. package/dist/db/migrations/v50.js +67 -0
  289. package/dist/db/migrations/v51.d.ts +3 -0
  290. package/dist/db/migrations/v51.js +14 -0
  291. package/dist/db/migrations/v52.d.ts +3 -0
  292. package/dist/db/migrations/v52.js +7 -0
  293. package/dist/db/open.d.ts +23 -0
  294. package/dist/db/open.js +146 -0
  295. package/dist/db/sqlite.d.ts +21 -0
  296. package/dist/db/sqlite.js +8 -0
  297. package/dist/db/tables.d.ts +7 -0
  298. package/dist/db/tables.js +38 -0
  299. package/dist/db.d.ts +6 -46
  300. package/dist/db.js +5 -3049
  301. package/dist/decisions.d.ts +4 -1
  302. package/dist/decisions.js +102 -83
  303. package/dist/dedupe.js +3 -2
  304. package/dist/delivery-recorder.js +4 -1
  305. package/dist/doctor.js +14 -10
  306. package/dist/embedding-provider.d.ts +1 -1
  307. package/dist/embedding-provider.js +4 -3
  308. package/dist/embeddings.d.ts +9 -52
  309. package/dist/embeddings.js +43 -297
  310. package/dist/env.d.ts +75 -0
  311. package/dist/env.js +119 -0
  312. package/dist/eval-suite.js +1 -1
  313. package/dist/eval.js +2 -2
  314. package/dist/extract.js +1 -1
  315. package/dist/gated-write.js +3 -1
  316. package/dist/goals.d.ts +3 -1
  317. package/dist/goals.js +117 -86
  318. package/dist/graph/read.d.ts +73 -0
  319. package/dist/graph/read.js +325 -0
  320. package/dist/graph/rows.d.ts +45 -0
  321. package/dist/graph/rows.js +51 -0
  322. package/dist/graph/types.d.ts +83 -0
  323. package/dist/graph/types.js +11 -0
  324. package/dist/graph/write.d.ts +93 -0
  325. package/dist/{graph.js → graph/write.js} +6 -384
  326. package/dist/graph-extract.js +2 -1
  327. package/dist/graph-recall.d.ts +1 -1
  328. package/dist/graph-recall.js +2 -2
  329. package/dist/graph-stream.js +1 -1
  330. package/dist/graph-view.d.ts +1 -1
  331. package/dist/graph-view.js +1 -1
  332. package/dist/half-life-migration.d.ts +1 -1
  333. package/dist/half-life-migration.js +2 -1
  334. package/dist/hooks/codex-session.d.ts +8 -0
  335. package/dist/hooks/codex-session.js +76 -0
  336. package/dist/hooks/codex-wrapper.d.ts +55 -0
  337. package/dist/hooks/codex-wrapper.js +288 -0
  338. package/dist/hooks/json-hooks.d.ts +63 -0
  339. package/dist/hooks/json-hooks.js +356 -0
  340. package/dist/hooks/opencode.d.ts +50 -0
  341. package/dist/hooks/opencode.js +202 -0
  342. package/dist/hooks/shared.d.ts +54 -0
  343. package/dist/hooks/shared.js +77 -0
  344. package/dist/http-retry.d.ts +2 -0
  345. package/dist/http-retry.js +4 -3
  346. package/dist/http-util.d.ts +3 -0
  347. package/dist/http-util.js +10 -0
  348. package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
  349. package/dist/importers/core.js +141 -0
  350. package/dist/importers/markdown-parse.d.ts +41 -0
  351. package/dist/importers/markdown-parse.js +132 -0
  352. package/dist/importers/markdown.d.ts +3 -0
  353. package/dist/importers/markdown.js +92 -0
  354. package/dist/importers/sources.d.ts +7 -0
  355. package/dist/importers/sources.js +229 -0
  356. package/dist/importers/vault.d.ts +11 -0
  357. package/dist/importers/vault.js +352 -0
  358. package/dist/incidents.d.ts +3 -0
  359. package/dist/incidents.js +52 -43
  360. package/dist/index.d.ts +25 -6
  361. package/dist/index.js +23 -6
  362. package/dist/invalidation.js +2 -1
  363. package/dist/judgment.js +2 -1
  364. package/dist/keyset.d.ts +13 -0
  365. package/dist/keyset.js +8 -0
  366. package/dist/local-embedding.d.ts +13 -0
  367. package/dist/local-embedding.js +165 -0
  368. package/dist/log.d.ts +7 -0
  369. package/dist/log.js +19 -1
  370. package/dist/mcp/admin-tools.d.ts +8 -0
  371. package/dist/mcp/admin-tools.js +116 -0
  372. package/dist/mcp/format.d.ts +27 -0
  373. package/dist/mcp/format.js +135 -0
  374. package/dist/mcp/memory-tools.d.ts +5 -0
  375. package/dist/mcp/memory-tools.js +83 -0
  376. package/dist/mcp/protocol.d.ts +83 -0
  377. package/dist/mcp/protocol.js +55 -0
  378. package/dist/mcp/recall-tools.d.ts +6 -0
  379. package/dist/mcp/recall-tools.js +320 -0
  380. package/dist/mcp/request.d.ts +10 -0
  381. package/dist/mcp/request.js +163 -0
  382. package/dist/mcp/server.d.ts +4 -75
  383. package/dist/mcp/server.js +7 -1190
  384. package/dist/mcp/session-state.d.ts +11 -0
  385. package/dist/mcp/session-state.js +28 -0
  386. package/dist/mcp/stdio.d.ts +8 -0
  387. package/dist/mcp/stdio.js +79 -0
  388. package/dist/mcp/tools.d.ts +11 -0
  389. package/dist/mcp/tools.js +247 -0
  390. package/dist/memory.d.ts +3 -0
  391. package/dist/memory.js +27 -1
  392. package/dist/multihop.d.ts +1 -1
  393. package/dist/multihop.js +2 -1
  394. package/dist/owner-validation.js +2 -1
  395. package/dist/physics-state.js +10 -7
  396. package/dist/policies.d.ts +3 -0
  397. package/dist/policies.js +93 -74
  398. package/dist/postinstall.js +3 -2
  399. package/dist/predictions/planning-fallacy.d.ts +100 -0
  400. package/dist/predictions/planning-fallacy.js +190 -0
  401. package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
  402. package/dist/predictions/store.js +434 -0
  403. package/dist/processes.d.ts +3 -0
  404. package/dist/processes.js +94 -76
  405. package/dist/project-briefs.d.ts +3 -0
  406. package/dist/project-briefs.js +142 -113
  407. package/dist/project-identity.js +3 -2
  408. package/dist/project-merge.d.ts +12 -4
  409. package/dist/project-merge.js +132 -45
  410. package/dist/quarantine.d.ts +2 -1
  411. package/dist/quarantine.js +8 -5
  412. package/dist/raw-archive.js +1 -1
  413. package/dist/recall-history.js +3 -2
  414. package/dist/recall-pipeline.d.ts +2 -2
  415. package/dist/recall-pipeline.js +16 -8
  416. package/dist/recall-scope.js +1 -1
  417. package/dist/recall-trace.d.ts +1 -1
  418. package/dist/refine-llm.js +2 -1
  419. package/dist/reject-flow.js +6 -1
  420. package/dist/rerankers/clef.js +6 -5
  421. package/dist/rerankers/jev.d.ts +1 -1
  422. package/dist/rerankers/jev.js +4 -4
  423. package/dist/rerankers/llm.d.ts +4 -2
  424. package/dist/rerankers/llm.js +58 -38
  425. package/dist/rerankers/types.d.ts +1 -1
  426. package/dist/salience.js +1 -1
  427. package/dist/scheduler.d.ts +1 -0
  428. package/dist/scheduler.js +26 -4
  429. package/dist/scope.js +4 -3
  430. package/dist/search/as-of.d.ts +10 -0
  431. package/dist/search/as-of.js +22 -0
  432. package/dist/search/bm25-search.d.ts +14 -0
  433. package/dist/search/bm25-search.js +43 -0
  434. package/dist/search/bm25.d.ts +15 -0
  435. package/dist/search/bm25.js +54 -0
  436. package/dist/search/boosts.d.ts +54 -0
  437. package/dist/search/boosts.js +94 -0
  438. package/dist/search/breakdown.d.ts +7 -0
  439. package/dist/search/breakdown.js +20 -0
  440. package/dist/search/explain.d.ts +25 -0
  441. package/dist/search/explain.js +31 -0
  442. package/dist/search/finalize.d.ts +8 -0
  443. package/dist/search/finalize.js +52 -0
  444. package/dist/search/fusion.d.ts +27 -0
  445. package/dist/search/fusion.js +42 -0
  446. package/dist/search/hybrid-score.d.ts +20 -0
  447. package/dist/search/hybrid-score.js +73 -0
  448. package/dist/search/hybrid.d.ts +46 -0
  449. package/dist/search/hybrid.js +64 -0
  450. package/dist/search/physics-search.d.ts +29 -0
  451. package/dist/search/physics-search.js +162 -0
  452. package/dist/search/rerank.d.ts +10 -0
  453. package/dist/search/rerank.js +72 -0
  454. package/dist/search/temporal.d.ts +15 -0
  455. package/dist/search/temporal.js +45 -0
  456. package/dist/search/types.d.ts +91 -0
  457. package/dist/search/types.js +2 -0
  458. package/dist/search/vector.d.ts +30 -0
  459. package/dist/search/vector.js +71 -0
  460. package/dist/secret-detect.d.ts +2 -0
  461. package/dist/secret-detect.js +2 -1
  462. package/dist/server/auth.d.ts +52 -0
  463. package/dist/server/auth.js +221 -0
  464. package/dist/server/client-ip.d.ts +25 -0
  465. package/dist/server/client-ip.js +92 -0
  466. package/dist/server/cursor.d.ts +23 -0
  467. package/dist/server/cursor.js +58 -0
  468. package/dist/server/lifecycle.d.ts +7 -0
  469. package/dist/server/lifecycle.js +29 -0
  470. package/dist/server/mcp-http.d.ts +5 -0
  471. package/dist/server/mcp-http.js +199 -0
  472. package/dist/server/request.d.ts +33 -0
  473. package/dist/server/request.js +103 -0
  474. package/dist/server/routes/admin.d.ts +9 -0
  475. package/dist/server/routes/admin.js +158 -0
  476. package/dist/server/routes/customer-notes.d.ts +7 -0
  477. package/dist/server/routes/customer-notes.js +112 -0
  478. package/dist/server/routes/decisions.d.ts +7 -0
  479. package/dist/server/routes/decisions.js +133 -0
  480. package/dist/server/routes/incidents.d.ts +7 -0
  481. package/dist/server/routes/incidents.js +126 -0
  482. package/dist/server/routes/memories.d.ts +10 -0
  483. package/dist/server/routes/memories.js +179 -0
  484. package/dist/server/routes/policies.d.ts +8 -0
  485. package/dist/server/routes/policies.js +151 -0
  486. package/dist/server/routes/predictions.d.ts +7 -0
  487. package/dist/server/routes/predictions.js +164 -0
  488. package/dist/server/routes/processes.d.ts +7 -0
  489. package/dist/server/routes/processes.js +161 -0
  490. package/dist/server/routes/project-briefs.d.ts +8 -0
  491. package/dist/server/routes/project-briefs.js +136 -0
  492. package/dist/server/routes/recall.d.ts +8 -0
  493. package/dist/server/routes/recall.js +340 -0
  494. package/dist/server/routes/skills.d.ts +8 -0
  495. package/dist/server/routes/skills.js +150 -0
  496. package/dist/server/types.d.ts +56 -0
  497. package/dist/server/types.js +2 -0
  498. package/dist/server/validation.d.ts +14 -0
  499. package/dist/server/validation.js +91 -0
  500. package/dist/server.d.ts +7 -61
  501. package/dist/server.js +69 -2362
  502. package/dist/session-digest.d.ts +1 -1
  503. package/dist/session-digest.js +9 -2
  504. package/dist/shared.d.ts +12 -1
  505. package/dist/shared.js +33 -27
  506. package/dist/skills.d.ts +3 -0
  507. package/dist/skills.js +88 -70
  508. package/dist/stdin.js +2 -2
  509. package/dist/store/audit-event.d.ts +19 -0
  510. package/dist/store/audit-event.js +33 -0
  511. package/dist/store/candidates.d.ts +42 -0
  512. package/dist/store/candidates.js +152 -0
  513. package/dist/store/conflicts.d.ts +44 -0
  514. package/dist/store/conflicts.js +444 -0
  515. package/dist/store/delete-and-batch.d.ts +63 -0
  516. package/dist/store/delete-and-batch.js +310 -0
  517. package/dist/store/entry-reads.d.ts +92 -0
  518. package/dist/store/entry-reads.js +255 -0
  519. package/dist/store/entry-row.d.ts +67 -0
  520. package/dist/store/entry-row.js +208 -0
  521. package/dist/store/entry-writes.d.ts +46 -0
  522. package/dist/store/entry-writes.js +148 -0
  523. package/dist/store/handoffs.d.ts +26 -0
  524. package/dist/store/handoffs.js +184 -0
  525. package/dist/store/index-and-stats.d.ts +52 -0
  526. package/dist/store/index-and-stats.js +214 -0
  527. package/dist/store/markdown.d.ts +10 -0
  528. package/dist/store/markdown.js +108 -0
  529. package/dist/store/mirrors.d.ts +49 -0
  530. package/dist/store/mirrors.js +312 -0
  531. package/dist/store/open.d.ts +15 -0
  532. package/dist/store/open.js +223 -0
  533. package/dist/store/rows.d.ts +178 -0
  534. package/dist/store/rows.js +158 -0
  535. package/dist/store/search-rows.d.ts +86 -0
  536. package/dist/store/search-rows.js +260 -0
  537. package/dist/store/sessions.d.ts +83 -0
  538. package/dist/store/sessions.js +272 -0
  539. package/dist/store/summaries.d.ts +94 -0
  540. package/dist/store/summaries.js +377 -0
  541. package/dist/store/tenant-lookup.d.ts +12 -0
  542. package/dist/store/tenant-lookup.js +16 -0
  543. package/dist/store-cards.js +2 -1
  544. package/dist/summary-dirty.d.ts +4 -0
  545. package/dist/summary-dirty.js +32 -0
  546. package/dist/support-bundle.js +4 -3
  547. package/dist/tenant.js +2 -2
  548. package/dist/tokenize.d.ts +2 -0
  549. package/dist/tokenize.js +16 -0
  550. package/dist/transcript-tail.d.ts +7 -0
  551. package/dist/transcript-tail.js +48 -0
  552. package/dist/vector-store.d.ts +27 -0
  553. package/dist/vector-store.js +210 -0
  554. package/dist/version.d.ts +2 -2
  555. package/dist/version.js +2 -2
  556. package/dist/working-memory.js +1 -1
  557. package/dist/yaml.js +36 -11
  558. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  559. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  560. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  561. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  562. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  563. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  564. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  565. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  566. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  567. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  568. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  569. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  570. package/dist-ui/assets/index-DPN7cP19.js +33 -0
  571. package/dist-ui/assets/index-dFloKRVr.css +1 -0
  572. package/dist-ui/index.html +3 -25
  573. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  574. package/extensions/openclaw-plugin/package.json +1 -1
  575. package/openclaw.plugin.json +1 -1
  576. package/package.json +1 -1
  577. package/dist/capture.d.ts +0 -155
  578. package/dist/capture.js +0 -1295
  579. package/dist/consolidate.d.ts +0 -58
  580. package/dist/consolidate.js +0 -1124
  581. package/dist/graph.d.ts +0 -245
  582. package/dist/hooks.d.ts +0 -208
  583. package/dist/hooks.js +0 -1076
  584. package/dist/importers.js +0 -900
  585. package/dist/predictions.js +0 -620
  586. package/dist/search.d.ts +0 -320
  587. package/dist/search.js +0 -970
  588. package/dist/store.d.ts +0 -776
  589. package/dist/store.js +0 -3473
  590. package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
  591. package/dist-ui/assets/index-BhT8RvO6.js +0 -61
  592. package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
  593. package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
package/dist/capture.js DELETED
@@ -1,1295 +0,0 @@
1
- /**
2
- * Capture actionable items from conversation text.
3
- *
4
- * Uses heuristic pattern matching (no LLM) to extract:
5
- * - Decisions ("we decided", "let's do", "going with")
6
- * - Specs / requirements (bullet lists after spec/feature/plan headings)
7
- * - Rules / constraints ("never", "always", "the rule is", "must")
8
- * - Errors / gotchas ("error:", "bug:", "gotcha:", "watch out")
9
- * - Preferences ("prefer", "use X instead of Y", "don't use")
10
- */
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { createMemory, Layer } from './memory.js';
14
- import { duplicateKey, storedTextKeys } from './same-text.js';
15
- import { isContentWorthStoring } from './audit.js';
16
- import { isInitialized, openStore, stampOriginProject, writeEntryMirrors, loadAllEntries, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, } from './store.js';
17
- import { gatedWrite } from './gated-write.js';
18
- import { PRE_COMPACT_INSTRUCTION, compactionOrigin, parsePostCompactPayload, postCompactLine, recordCompactionStart, recordSnapshotSaved, saveCompaction, replayCompactionsAt, COMPACTION_DB_WAIT_MS, } from './compaction-record.js';
19
- import { getGlobalRoot, initGlobal } from './shared.js';
20
- import { embedMemory } from './embeddings.js';
21
- import { resolveTenantId } from './tenant.js';
22
- import { defaultPreCompactLogPath } from './hooks.js';
23
- import { maskEmails, redactSecretsStrict } from './secret-detect.js';
24
- import { RejectedValueError, checkRejectionGuard } from './rejection.js';
25
- import { openHippoDb, closeHippoDb } from './db.js';
26
- import { loadConfig } from './config.js';
27
- import { classifyOriginProject } from './project-identity.js';
28
- import { isObjectLike, isStringValue, readClaudeCodePreCompact } from './capture-contract.js';
29
- import { log } from './log.js';
30
- // Sentence-level patterns
31
- //
32
- // T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
33
- // discriminating keyword (plus its trailing separator, verbatim), group 2 is
34
- // the content that follows it. Previously only the after-keyword content was
35
- // captured, so a negation like "never" / "must not" was discarded and a
36
- // prohibition inverted into an instruction ("Never use X" stored as "use X").
37
- // `extractFromPatterns` reassembles group1 + a clause-bounded group2 (T2, via
38
- // `boundToClause` below) rather than reading a single fixed-width group —
39
- // group 2's own reach is widened to {1,500} because the true stopping point
40
- // is now found by content, not counted characters. Keeping the keyword in
41
- // its own group (rather than folding it into one bigger capture) matters:
42
- // `boundToClause` must scan for a clause boundary only in group 2, never in
43
- // group 1 — several keywords end in their own colon ("error:", "rule:",
44
- // "decision:") which is not a clause boundary in the prose sense and would
45
- // wrongly truncate the capture down to just the keyword if scanned.
46
- // `PREFERENCE_PATTERNS[0]` is the one exception — its two-capture-group
47
- // shape means something different (two content spans either side of
48
- // "instead of"/"over"/"not") and is a separate, backlogged defect (see
49
- // docs/plans/2026-08-23-df2-capture-anchoring.md); left as-is.
50
- const DECISION_PATTERNS = [
51
- /(?:we(?:'ve| have)?|i(?:'ve| have)?|let's)\s+(decid(?:ed|e)\s+(?:to\s+)?)(.{1,500})/i,
52
- /(?:let's|we(?:'ll| will| should)?)\s+((?:go with|do|use|try|build|implement|switch to)\s+)(.{1,500})/i,
53
- /((?:going|went)\s+with\s+)(.{1,500})/i,
54
- /((?:the plan is|plan:)\s+)(.{1,500})/i,
55
- /(decision:\s*)(.{1,500})/i,
56
- ];
57
- const RULE_PATTERNS = [
58
- /\b((?:never|always|must(?:\s+not)?|do(?:n't| not)\s+ever)\s+)(.{1,500})/i,
59
- /((?:the rule is|rule:)\s*)(.{1,500})/i,
60
- /((?:important|critical|remember):\s*)(.{1,500})/i,
61
- /((?:make sure|ensure)\s+(?:to\s+)?)(.{1,500})/i,
62
- ];
63
- const ERROR_PATTERNS = [
64
- /((?:error|bug|gotcha|watch out|careful|warning|caveat|trap):\s*)(.{1,500})/i,
65
- /((?:this broke|this breaks|this will break|broke because)\s+)(.{1,500})/i,
66
- /((?:the (?:issue|problem|fix) (?:is|was))\s+)(.{1,500})/i,
67
- /((?:don't forget|easy to miss):\s*)(.{1,500})/i,
68
- ];
69
- // PREFERENCE_PATTERNS[0] keeps its pre-DF2 two-capture-group shape
70
- // (match[1]-only, unbounded) — out of scope here, backlogged. See
71
- // extractFromPatterns' reference check against this exact array element.
72
- const PREFERENCE_PATTERNS = [
73
- /(?:prefer|use)\s+(.{5,100})\s+(?:instead of|over|not)\s+(.{3,100})/i,
74
- /((?:don't use|avoid|skip)\s+)(.{1,500})/i,
75
- /((?:we(?:'re| are)\s+using|the stack is|we use)\s+)(.{1,500})/i,
76
- ];
77
- // Heading patterns that signal a following list of specs/requirements
78
- const SPEC_HEADING_PATTERNS = [
79
- /^#+\s*(?:features?|requirements?|specs?|specifications?|plan|design|architecture|interface|api|todo|tasks?|implementation|notes?)(?:\s|:|$)/i,
80
- /^(?:features?|requirements?|specs?|specifications?|plan|design|tasks?|implementation)(?:\s*:|$)/i,
81
- ];
82
- // ---------------------------------------------------------------------------
83
- // Extraction engine
84
- // ---------------------------------------------------------------------------
85
- export function splitSentences(text) {
86
- // Split on sentence boundaries, keeping reasonable chunks
87
- return text
88
- .split(/(?<=[.!?])\s+|\n/)
89
- .map((s) => s.trim())
90
- .filter((s) => s.length > 5);
91
- }
92
- /**
93
- * T2 (DF2): bound a keyword+content capture to its clause instead of a fixed
94
- * character count. `full` is `keywordPrefix + content` (already concatenated
95
- * so the 200-char ceiling below applies to the whole stored string, not just
96
- * the part after the keyword); `searchFrom` is `keywordPrefix.length`, so the
97
- * clause/terminator scan only ever looks INSIDE `content` — several keywords
98
- * end in their own colon ("error:", "rule:", "decision:") which must never
99
- * be mistaken for a clause boundary in the prose that follows.
100
- *
101
- * Stops at the first `,`/`;`/`:` followed by whitespace, or at a sentence
102
- * terminator `[.!?]` followed by whitespace or end-of-string — whichever
103
- * comes first — and never past `maxLen` chars total.
104
- *
105
- * The whitespace requirement on the terminator is load-bearing: a bare
106
- * `[.!?]` would split inside a token like `.env` / `capture.ts` / `v1.35.0`,
107
- * turning a full clause into a fragment that then fails the write gate and
108
- * is silently dropped (measured in the plan). The `maxLen` ceiling is also
109
- * load-bearing: without it, a clause-free span can run past the 500-char
110
- * gate in `extractFromPatterns` and the whole match is dropped, where today
111
- * it is truncated and stored.
112
- */
113
- /**
114
- * Does a plausible CLOSING single quote appear after `from`? Mirror of the
115
- * opener rule in `boundToClause`: a closer sits tight against the literal it
116
- * ends (non-whitespace before) and is followed by whitespace, punctuation, or
117
- * end-of-string - never by a letter, which is what makes "user's" an
118
- * apostrophe rather than a partner.
119
- */
120
- function isLetterOrDigit(ch) {
121
- return ch !== undefined && /[\p{L}\p{N}]/u.test(ch);
122
- }
123
- function lastCloserIndex(full) {
124
- // ONE pass, not one per apostrophe. The previous shape rescanned the whole
125
- // remaining suffix at every boundary apostrophe, so a transcript full of
126
- // elisions ("keep 'em", "wait 'til", ...) made extraction quadratic and
127
- // could stall a moderately sized capture. Codex P2, r7.
128
- //
129
- // A quote CLOSES THE SIDE IT IS TIGHT AGAINST. The test needs both
130
- // neighbours, and one round proved that empirically: these two have the
131
- // same following character and opposite roles -
132
- // "preserve 'a, b'-style" next '-' -> CLOSER, content on the left
133
- // "then run '--force," next '-' -> OPENER, content on the right
134
- // so no forward-only rule can separate them. An earlier revision tried
135
- // exactly that and broke in both directions at once (codex P1+P2, r10):
136
- // it missed closers before token-joining punctuation and accepted
137
- // "'.env" as a closer because a dot happened to be in its class.
138
- //
139
- // never a closer next is a letter/digit "user's", "keep 'em"
140
- // closer prev is non-whitespace "'a, b' to", "'a, b'-style"
141
- // closer next is whitespace or end "preserve 'a, b ' exactly"
142
- // closer next is clause punctuation "preserve 'a, b ', then"
143
- // that itself ends the token
144
- //
145
- // The last clause is what separates ", " from ".env": a dot followed by a
146
- // letter joins a filename, a dot followed by space ends a sentence.
147
- for (let j = full.length - 1; j >= 0; j--) {
148
- if (full[j] !== "'")
149
- continue;
150
- const prev = full[j - 1];
151
- const next = full[j + 1];
152
- if (isLetterOrDigit(next))
153
- continue;
154
- // "tight against content" excludes an OPENING delimiter: in
155
- // "call parse('--force" the paren is non-whitespace but the quote after
156
- // it is an opener, and treating it as a closer let an earlier elision
157
- // pair across the clause boundary. Codex P2, r11.
158
- const tightBefore = prev !== undefined && !/[\s([{]/.test(prev);
159
- const endsAfter = next === undefined || /\s/.test(next);
160
- // Punctuation splits in two, and this is the fact the last three
161
- // revisions kept rediscovering piecemeal:
162
- // , ; : ! ? ) ] } never join tokens - they end the literal whatever
163
- // follows, so "'echo a, b ';then" closes at the ;
164
- // . - _ / @ # DO join tokens - "'.env" and "'--force" continue a
165
- // filename or flag, so these only close when trailed
166
- // by whitespace
167
- const afterNext = full[j + 2];
168
- const closesRegardless = next !== undefined && /[,;:!?)\]}]/.test(next);
169
- const joinerThenSpace = next !== undefined && /[.\-_/@#]/.test(next) &&
170
- (afterNext === undefined || /\s/.test(afterNext));
171
- if (tightBefore || endsAfter || closesRegardless || joinerThenSpace)
172
- return j;
173
- }
174
- return -1;
175
- }
176
- function boundToClause(full, searchFrom, maxLen = 200) {
177
- // Scan for the first PROSE clause boundary after `searchFrom`.
178
- //
179
- // Depth-awareness is not a nicety here: hippo memories are full of code, and
180
- // a naive scan reintroduces the exact fragment defect this whole change
181
- // exists to remove. Measured before this guard existed:
182
- // "Always call build(x, y) before deploy." -> "Always call build(x"
183
- // "Never pass {a: 1, b: 2} to the writer." -> "Never pass {a"
184
- // Both then PASS the write gate, because they contain code punctuation and
185
- // so read as "specific" — a malformed fragment stored with high confidence.
186
- // Codex review finding (P1) on this branch.
187
- //
188
- // So: a separator only ends the clause at bracket depth zero and outside
189
- // quotes. Unbalanced closers are tolerated (depth floors at 0) because
190
- // captured text often starts mid-expression.
191
- const lastCloser = lastCloserIndex(full);
192
- let depth = 0;
193
- let quote = null;
194
- let cutEnd = full.length;
195
- for (let i = searchFrom; i < full.length; i++) {
196
- const ch = full[i];
197
- if (quote) {
198
- // Closing uses the SAME shape test as opening. This was asymmetric:
199
- // twelve rounds went into deciding when a quote OPENS a literal, while
200
- // the close accepted any bare "'" - so the apostrophe in a possessive
201
- // INSIDE a literal closed it early:
202
- // "Always pass 'user's a, b list' to the parser."
203
- // -> "Always pass 'user's a" (master kept the whole literal)
204
- // a mid-literal fragment that then PASSES the write gate, which is
205
- // precisely the defect this branch exists to remove. Found by the
206
- // ship-gate review after every earlier gate missed it.
207
- if (ch === quote && (quote !== "'" || !isLetterOrDigit(full[i + 1]))) {
208
- quote = null;
209
- }
210
- continue;
211
- }
212
- // Quote handling has to tell an APOSTROPHE from a single-quoted
213
- // LITERAL, because getting either wrong reintroduces the fragment
214
- // defect this change exists to remove, and both fragments PASS the
215
- // write gate (code punctuation reads as "specific"):
216
- // treat every ' as a quote -> "Always ensure it's enabled, then
217
- // restart..." never leaves quote mode, bounding disabled entirely
218
- // treat no ' as a quote -> "Always pass 'a, b' to the parser."
219
- // cuts at the comma and stores "Always pass 'a"
220
- // Both were codex P1s on this branch, in consecutive rounds.
221
- //
222
- // Discriminator: a ' OPENS a literal only at a word boundary - preceded
223
- // by start/whitespace/open-bracket AND followed by non-whitespace. An
224
- // in-word apostrophe ("it's", "user's") has letters on both sides and is
225
- // just a character.
226
- if (ch === '"' || ch === '`') {
227
- quote = ch;
228
- continue;
229
- }
230
- if (ch === "'") {
231
- const prev = i > 0 ? full[i - 1] : undefined;
232
- const next = full[i + 1];
233
- const atBoundary = (prev === undefined || /[\s([{]/.test(prev)) &&
234
- next !== undefined && !/\s/.test(next);
235
- // Pairing is VERIFIED, not assumed. A word-boundary test alone still
236
- // opens quote mode on elided forms ("keep 'em", "wait 'til"), which
237
- // have no closer, so the scanner never leaves quote mode and bounding
238
- // is disabled for the rest of the capture. Requiring an actual closing
239
- // quote later in the string replaces a guess with a checkable fact -
240
- // an elided form simply has no partner. Codex P2, this round.
241
- // ...and the partner must LOOK like a closer, not merely be another
242
- // apostrophe. Distance cannot separate the two cases - both put a `'`
243
- // far downstream:
244
- // "Always pass '<600-char literal>' to the parser" -> the far ' IS
245
- // the closer, and pairing must succeed
246
- // "keep 'em enabled, then <520 chars> check user's config" -> the far
247
- // ' is in-word, and pairing against it re-opens quote mode on the
248
- // elision, disabling bounding for the rest of the capture
249
- // So apply the SAME shape rule already used to open a literal, mirrored:
250
- // a closer has non-whitespace before it and whitespace/punctuation (not
251
- // a letter) after. "user's" fails it on both counts. Codex P2, r6.
252
- if (atBoundary && lastCloser > i) {
253
- quote = ch;
254
- }
255
- continue;
256
- }
257
- if (ch === '(' || ch === '[' || ch === '{') {
258
- depth++;
259
- continue;
260
- }
261
- if (ch === ')' || ch === ']' || ch === '}') {
262
- if (depth > 0)
263
- depth--;
264
- continue;
265
- }
266
- if (depth > 0)
267
- continue;
268
- const next = full[i + 1];
269
- // Prose clause separator: , ; : followed by whitespace.
270
- if ((ch === ',' || ch === ';' || ch === ':') && next !== undefined && /\s/.test(next)) {
271
- cutEnd = i;
272
- break;
273
- }
274
- // Sentence terminator, but only when followed by whitespace or end — a
275
- // bare [.!?] splits inside .env, capture.ts, v1.35.0.
276
- if ((ch === '.' || ch === '!' || ch === '?') && (next === undefined || /\s/.test(next))) {
277
- cutEnd = i + 1;
278
- break;
279
- }
280
- }
281
- let bounded = full.slice(0, cutEnd);
282
- if (bounded.length > maxLen)
283
- bounded = bounded.slice(0, maxLen);
284
- return bounded;
285
- }
286
- function cleanExtract(raw) {
287
- let content = raw
288
- .replace(/^[:\s-]+/, '')
289
- .replace(/[.!?,;:\s]+$/, '')
290
- .trim();
291
- // T2 trailing cleanup: clause-bounding can cut inside a parenthetical and
292
- // leave an unmatched trailing ')' (e.g. "...(two had never got entries),"
293
- // bounds to "never got entries)"). Strip a trailing ')' ONLY while closes
294
- // outnumber opens in the string so far — a balanced parenthetical like
295
- // "always run the suite (twice)" must be left intact.
296
- while (content.endsWith(')')) {
297
- const opens = (content.match(/\(/g) ?? []).length;
298
- const closes = (content.match(/\)/g) ?? []).length;
299
- if (closes <= opens)
300
- break;
301
- content = content
302
- .slice(0, -1)
303
- .replace(/[.!?,;:\s]+$/, '')
304
- .trim();
305
- }
306
- return content;
307
- }
308
- /** The 500-char write gate minus the 200-char bound counted from the keyword. */
309
- const RULE_LEAD_CAP = 300;
310
- /** A modal rule keeps its subject ("We must never ..."); null keeps the keyword-start form when the lead is long or the keyword sits inside a bracket. */
311
- function ruleLead(sentence, keywordStart, contentStart) {
312
- if (keywordStart < 0 || contentStart < 0 || contentStart > RULE_LEAD_CAP)
313
- return null;
314
- let depth = 0;
315
- for (const ch of sentence.slice(0, keywordStart)) {
316
- if (ch === '(' || ch === '[' || ch === '{')
317
- depth++;
318
- else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
319
- depth--;
320
- }
321
- return depth > 0 ? null : sentence.slice(0, contentStart);
322
- }
323
- function extractFromPatterns(sentence, patterns, category, tag) {
324
- for (const pat of patterns) {
325
- // `d` gives per-group offsets; see the contentStart comment below.
326
- const dpat = pat.flags.includes('d') ? pat : new RegExp(pat.source, pat.flags + 'd');
327
- const match = dpat.exec(sentence);
328
- if (match) {
329
- let bounded;
330
- if (pat === PREFERENCE_PATTERNS[0]) {
331
- // PREFERENCE_PATTERNS[0] is the one pattern left out of T1/T2 (see
332
- // comment at its definition) — its match[1] keeps its pre-DF2,
333
- // unbounded shape rather than going through clause-bounding.
334
- bounded = match[1] ?? match[0];
335
- }
336
- else {
337
- // group1 = keyword + separator (verbatim, never clause-bounded);
338
- // group2 = the content that follows it (clause-bounded below).
339
- //
340
- // T1 preserves the keyword only when it carries SEMANTIC SIGN — a
341
- // negation or modality ("never", "must not", "do not ever",
342
- // "always"). Dropping those inverts the meaning, which is the whole
343
- // point of T1. A LABEL keyword ("decision:", "rule:", "error:",
344
- // "important:") carries no sign: it only names the category, which
345
- // is already recorded in `category`/`tags`, so prefixing it onto the
346
- // content is duplication that also breaks value-keyed matching
347
- // (AT1's rejected-value digest hashes the bare content).
348
- // Discriminator: a label ends in its own colon, or is a "the X is"
349
- // phrase.
350
- const rawPrefix = match[1] ?? '';
351
- // ONLY colon-terminated labels are dropped. Dropping "the X is/was"
352
- // too left the residue starting with "to ", which isFragment then
353
- // rejected outright - so "The plan is to ship on Friday" and "The fix
354
- // was to bump the pool timeout" stored NOTHING, where every prior
355
- // version stored them. Silent loss on two of the highest-traffic
356
- // patterns, and invisible: an absent memory leaves no trace. The AT1
357
- // rejected-value evidence only ever involved colon labels
358
- // ("decision: "), so the narrower rule keeps that guarantee.
359
- const isLabelPrefix = /:\s*$/.test(rawPrefix);
360
- const keywordPrefix = isLabelPrefix ? '' : rawPrefix;
361
- // Scan the UNTRUNCATED remainder, not match[2]. The patterns cap
362
- // their content group at 500 chars, so a quoted literal whose closer
363
- // sits past that point had no visible partner and the pairing check
364
- // read the opener as prose - cutting inside the literal. That was a
365
- // blindness built into the scanner's INPUT, not a bad predicate, so
366
- // no further predicate could have fixed it. boundToClause already
367
- // caps its OUTPUT at maxLen, so widening the input costs nothing and
368
- // makes pairing decidable on the whole sentence. Codex P2, round 5.
369
- // Group 2's REAL offset, read from the regex engine. Deriving it as
370
- // `match.index + rawPrefix.length` assumes group 1 starts the match,
371
- // but DECISION_PATTERNS carry an uncaptured subject ("we ", "let's ")
372
- // ahead of it - so the offset landed inside the keyword and the
373
- // widened slice duplicated text: "We decided to pin..." stored as
374
- // "decided to to pin...". Real corruption of the commonest decision
375
- // capture, shipped in the previous commit. Codex P1, r6.
376
- const contentStart = match.indices?.[2]?.[0] ?? -1;
377
- const afterKeyword = contentStart >= 0 ? sentence.slice(contentStart) : (match[2] ?? match[0]);
378
- const keywordStart = match.indices?.[1]?.[0] ?? -1;
379
- const lead = pat === RULE_PATTERNS[0] ? ruleLead(sentence, keywordStart, contentStart) : null;
380
- bounded = lead !== null
381
- ? boundToClause(lead + afterKeyword, lead.length, keywordStart + 200)
382
- : boundToClause(keywordPrefix + afterKeyword, keywordPrefix.length);
383
- }
384
- const content = cleanExtract(bounded);
385
- if (content.length >= 8 && content.length <= 500) {
386
- return { content, category, tags: [tag, 'captured'] };
387
- }
388
- }
389
- }
390
- return null;
391
- }
392
- /** Extract spec items from bullet lists that follow spec-like headings. */
393
- function extractSpecSections(text) {
394
- const items = [];
395
- const lines = text.split('\n');
396
- let inSpecSection = false;
397
- for (const line of lines) {
398
- const trimmed = line.trim();
399
- // Check if this line is a spec heading
400
- if (SPEC_HEADING_PATTERNS.some((p) => p.test(trimmed))) {
401
- inSpecSection = true;
402
- continue;
403
- }
404
- // Another heading resets the section
405
- if (/^#+\s/.test(trimmed) || /^[A-Z][a-z]+:$/.test(trimmed)) {
406
- inSpecSection = false;
407
- continue;
408
- }
409
- // Blank line after non-bullet content ends section
410
- if (!trimmed && inSpecSection) {
411
- // Keep going, blank lines within spec sections are ok
412
- continue;
413
- }
414
- if (inSpecSection) {
415
- const bulletMatch = trimmed.match(/^[-*]\s+(.+)/) || trimmed.match(/^\d+\.\s+(.+)/);
416
- if (bulletMatch) {
417
- const content = bulletMatch[1].trim();
418
- if (content.length >= 8 && content.length <= 500) {
419
- items.push({
420
- content,
421
- category: 'spec',
422
- tags: ['spec', 'captured'],
423
- });
424
- }
425
- }
426
- }
427
- }
428
- return items;
429
- }
430
- /**
431
- * Main extraction function. Scans text for actionable items using heuristics.
432
- */
433
- export function extractFromText(text) {
434
- const items = [];
435
- const seen = new Set();
436
- const addIfNew = (item) => {
437
- const norm = duplicateKey(item.content);
438
- if (seen.has(norm))
439
- return;
440
- if (!isContentWorthStoring(item.content))
441
- return;
442
- seen.add(norm);
443
- items.push(item);
444
- };
445
- // 1. Extract spec sections (bullet lists under spec headings)
446
- for (const item of extractSpecSections(text)) {
447
- addIfNew(item);
448
- }
449
- // 2. Pattern-match on individual sentences
450
- const sentences = splitSentences(text);
451
- for (const sentence of sentences) {
452
- // Try each category in priority order
453
- const decision = extractFromPatterns(sentence, DECISION_PATTERNS, 'decision', 'decision');
454
- if (decision) {
455
- addIfNew(decision);
456
- continue;
457
- }
458
- const rule = extractFromPatterns(sentence, RULE_PATTERNS, 'rule', 'rule');
459
- if (rule) {
460
- addIfNew(rule);
461
- continue;
462
- }
463
- const error = extractFromPatterns(sentence, ERROR_PATTERNS, 'error', 'error');
464
- if (error) {
465
- addIfNew(error);
466
- continue;
467
- }
468
- const preference = extractFromPatterns(sentence, PREFERENCE_PATTERNS, 'preference', 'preference');
469
- if (preference) {
470
- addIfNew(preference);
471
- continue;
472
- }
473
- }
474
- return items;
475
- }
476
- /** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
477
- export function errorMessage(cause) {
478
- return cause instanceof Error ? cause.message : String(cause);
479
- }
480
- /**
481
- * Build a compact text summary from a Claude Code / OpenCode JSONL transcript.
482
- * Keeps plain user messages and the final chunk of assistant text, drops
483
- * thinking blocks, tool_use, and tool_result noise. Output is fed to the
484
- * existing `extractFromText` pipeline.
485
- *
486
- * Exported for tests.
487
- */
488
- /** Leading markers of the command lines Claude Code writes with type 'user'. */
489
- const CLAUDE_CODE_COMMAND_PREFIXES = ['<local-command-', '<command-name>', '<command-message>', '<command-args>'];
490
- function isNonHumanUserLine(entry, content) {
491
- if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true || entry.promptSource === 'system')
492
- return true;
493
- const head = content.trimStart();
494
- return CLAUDE_CODE_COMMAND_PREFIXES.some((p) => head.startsWith(p));
495
- }
496
- const NON_HUMAN_BLOCK_PREFIXES = ['<ide_', '[Request interrupted by user'];
497
- /** The human's words on a Claude Code user line, '' when none; VS Code stores prompts as text blocks, beside its own open-file, selection and interrupt blocks. */
498
- function humanUserText(entry, message) {
499
- const content = message.content;
500
- let text = '';
501
- if (isStringValue(content)) {
502
- text = content;
503
- }
504
- else if (Array.isArray(content) && !content.some((b) => isObjectLike(b) && 'type' in b && b.type === 'tool_result')) {
505
- const parts = [];
506
- for (const block of content) {
507
- const blockText = isObjectLike(block) && 'type' in block && block.type === 'text' && 'text' in block ? block.text : undefined;
508
- if (isStringValue(blockText) && !NON_HUMAN_BLOCK_PREFIXES.some((p) => blockText.trimStart().startsWith(p)))
509
- parts.push(blockText);
510
- }
511
- text = parts.join('\n');
512
- }
513
- text = text.trim();
514
- return text && !isNonHumanUserLine(entry, text) ? text : '';
515
- }
516
- export function collectSessionTurns(jsonl, visit) {
517
- const lines = jsonl.split('\n').filter((l) => l.trim());
518
- const turns = [];
519
- for (const line of lines) {
520
- let entry;
521
- try {
522
- entry = JSON.parse(line);
523
- }
524
- catch {
525
- continue; // a torn or partial transcript line carries no turn
526
- }
527
- if (!isObjectLike(entry) || !('type' in entry))
528
- continue;
529
- visit?.(entry);
530
- if (entry.type === 'user' || entry.type === 'assistant') {
531
- const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
532
- if (!message)
533
- continue;
534
- const content = 'content' in message ? message.content : undefined;
535
- if (entry.type === 'user') {
536
- const text = humanUserText(entry, message);
537
- if (text)
538
- turns.push({ role: 'user', text });
539
- }
540
- else if (Array.isArray(content)) {
541
- // Keep assistant text blocks; drop thinking + tool_use
542
- const chunks = [];
543
- for (const block of content) {
544
- if (isObjectLike(block)) {
545
- const blockText = 'type' in block && block.type === 'text' && 'text' in block ? block.text : undefined;
546
- if (isStringValue(blockText) && blockText.trim()) {
547
- chunks.push(blockText.trim());
548
- }
549
- }
550
- }
551
- if (chunks.length > 0) {
552
- turns.push({ role: 'assistant', text: chunks.join('\n') });
553
- }
554
- }
555
- continue;
556
- }
557
- // Codex rollout transcript shape: response_item -> payload.message
558
- if (entry.type === 'response_item') {
559
- const payload = 'payload' in entry && isObjectLike(entry.payload) ? entry.payload : undefined;
560
- if (!payload || !('type' in payload) || payload.type !== 'message')
561
- continue;
562
- const role = 'role' in payload ? payload.role : undefined;
563
- const content = 'content' in payload ? payload.content : undefined;
564
- if (!Array.isArray(content))
565
- continue;
566
- const chunks = [];
567
- for (const block of content) {
568
- if (!isObjectLike(block))
569
- continue;
570
- const blockType = 'type' in block ? block.type : undefined;
571
- const blockText = 'text' in block ? block.text : undefined;
572
- if (role === 'user' && blockType === 'input_text' && isStringValue(blockText) && blockText.trim()) {
573
- chunks.push(blockText.trim());
574
- }
575
- if (role === 'assistant' && blockType === 'output_text' && isStringValue(blockText) && blockText.trim()) {
576
- chunks.push(blockText.trim());
577
- }
578
- }
579
- if (chunks.length === 0)
580
- continue;
581
- if (role === 'user')
582
- turns.push({ role: 'user', text: chunks.join('\n') });
583
- if (role === 'assistant')
584
- turns.push({ role: 'assistant', text: chunks.join('\n') });
585
- }
586
- }
587
- return turns;
588
- }
589
- export function summariseTranscript(jsonl) {
590
- return summariseSessionTurns(collectSessionTurns(jsonl));
591
- }
592
- function summariseSessionTurns(turns) {
593
- const userMessages = turns.filter((t) => t.role === 'user').map((t) => t.text);
594
- const assistantTexts = turns.filter((t) => t.role === 'assistant').map((t) => t.text);
595
- if (userMessages.length === 0 && assistantTexts.length === 0)
596
- return '';
597
- // Keep the tail: last ~20 user turns and last ~10 assistant replies.
598
- // Session-end is about what was decided near the end, not at the start.
599
- const tailUsers = userMessages.slice(-20);
600
- const tailAssistants = assistantTexts.slice(-10);
601
- return [
602
- '# Session Summary',
603
- '',
604
- '## User Messages',
605
- ...tailUsers.map((m) => `- ${m.replace(/\s+/g, ' ').slice(0, 500)}`),
606
- '',
607
- '## Assistant Responses',
608
- ...tailAssistants.map((t) => t.slice(0, 2000)),
609
- ].join('\n');
610
- }
611
- /**
612
- * Resolve a transcript path for `--last-session`.
613
- *
614
- * Priority, where the first source present is the only one tried:
615
- * 1. Explicit `transcriptPath` option (from `--transcript <path>`)
616
- * 2. Stdin JSON payload (Claude Code / OpenCode SessionEnd hook shape)
617
- * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`, only when the caller passes `mayScan` (only the caller knows it is not a hook) and there is no path and no stdin text, because this scan spans every project on the box
618
- *
619
- * Returns null when nothing resolves, a named transcript or payload whose file is missing included. Never throws.
620
- */
621
- export function resolveLastSessionTranscript(explicit, stdinText, opts) {
622
- if (explicit)
623
- return fs.existsSync(explicit) ? explicit : null;
624
- if (stdinText && stdinText.trim() !== '') {
625
- try {
626
- const payload = JSON.parse(stdinText);
627
- if (isObjectLike(payload) && 'transcript_path' in payload) {
628
- const tp = payload.transcript_path;
629
- if (isStringValue(tp) && fs.existsSync(tp))
630
- return tp;
631
- }
632
- }
633
- catch {
634
- // not JSON, but still a payload, so no scan
635
- }
636
- return null;
637
- }
638
- if (!opts.mayScan)
639
- return null;
640
- const home = process.env.HOME || process.env.USERPROFILE;
641
- if (!home)
642
- return null;
643
- const projectsDir = path.join(home, '.claude', 'projects');
644
- if (!fs.existsSync(projectsDir))
645
- return null;
646
- let newest = null;
647
- try {
648
- for (const entry of fs.readdirSync(projectsDir)) {
649
- const subDir = path.join(projectsDir, entry);
650
- const stat = fs.statSync(subDir);
651
- if (!stat.isDirectory())
652
- continue;
653
- for (const file of fs.readdirSync(subDir)) {
654
- if (!file.endsWith('.jsonl'))
655
- continue;
656
- const full = path.join(subDir, file);
657
- const m = fs.statSync(full).mtimeMs;
658
- if (!newest || m > newest.mtime)
659
- newest = { path: full, mtime: m };
660
- }
661
- }
662
- }
663
- catch {
664
- return null; // an unlistable projects dir means no transcript to find; the caller prints that
665
- }
666
- return newest?.path ?? null;
667
- }
668
- export function cmdCapture(hippoRoot, options) {
669
- // Tee stdout/stderr to a log file when --log-file is set. Used by the
670
- // SessionEnd hook so output (otherwise swallowed by TUI teardown) surfaces
671
- // on the next session start via `hippo last-sleep`. Runs second in the
672
- // SessionEnd sequence after `hippo sleep`, so we APPEND rather than
673
- // truncate — sleep already wrote its own header + body to this file.
674
- const restoreStdio = options.logFile ? beginLogTee(options.logFile) : null;
675
- try {
676
- cmdCaptureCore(hippoRoot, options);
677
- if (options.logFile)
678
- console.log('[hippo] capture complete');
679
- }
680
- catch (err) {
681
- if (options.logFile)
682
- console.log(`[hippo] capture failed: ${errorMessage(err)}`);
683
- throw err;
684
- }
685
- finally {
686
- if (restoreStdio)
687
- restoreStdio();
688
- }
689
- }
690
- /**
691
- * Append-mode tee: writes a banner line then mirrors every stdout/stderr
692
- * chunk to `logFile` until the returned restore function is called.
693
- * Failures to write the log are non-fatal; the real streams still get
694
- * the data.
695
- */
696
- function beginLogTee(logFile) {
697
- try {
698
- fs.mkdirSync(path.dirname(logFile), { recursive: true });
699
- fs.appendFileSync(logFile, `[hippo] ${new Date().toISOString()} capturing session...\n`, 'utf8');
700
- }
701
- catch (err) {
702
- log.warn(`could not open log file ${logFile}: ${errorMessage(err)}`);
703
- return () => { };
704
- }
705
- const origStdoutWrite = process.stdout.write.bind(process.stdout);
706
- const origStderrWrite = process.stderr.write.bind(process.stderr);
707
- const tee = (chunk) => {
708
- try {
709
- const buf = isStringValue(chunk) ? chunk : Buffer.from(chunk).toString('utf8');
710
- fs.appendFileSync(logFile, buf, 'utf8');
711
- }
712
- catch {
713
- // log failures are non-fatal
714
- }
715
- };
716
- const wrapWrite = (origWrite) => {
717
- const wrapped = (...args) => {
718
- tee(args[0]);
719
- // SAFETY: forwarding the exact arguments Node's real overloaded
720
- // `write` received is safe regardless of which overload the call
721
- // site used — Node dispatches on the actual argument shapes at
722
- // runtime, and `StreamWriteArgs` is the union of both overloads'
723
- // parameter lists.
724
- return origWrite(...args);
725
- };
726
- // SAFETY: `wrapped` matches both real `write` overload shapes it's
727
- // assigned to; TS can't verify a single implementation covers an
728
- // overloaded type, but this one forwards to the real stream method.
729
- return wrapped;
730
- };
731
- process.stdout.write = wrapWrite(origStdoutWrite);
732
- process.stderr.write = wrapWrite(origStderrWrite);
733
- return () => {
734
- process.stdout.write = origStdoutWrite;
735
- process.stderr.write = origStderrWrite;
736
- };
737
- }
738
- // Console lines here are the `hippo capture` command's printed result, so they stay off the logger.
739
- function cmdCaptureCore(hippoRoot, options) {
740
- const useGlobal = options.global;
741
- const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
742
- if (useGlobal) {
743
- initGlobal();
744
- }
745
- else {
746
- if (!isInitialized(hippoRoot)) {
747
- console.error(`No hippo store at ${hippoRoot} (searched ${process.cwd()} and its parents up to your home directory). Run \`hippo init\` first.`);
748
- process.exit(1);
749
- }
750
- }
751
- // Read input text
752
- let text;
753
- switch (options.source) {
754
- case 'stdin': {
755
- try {
756
- text = fs.readFileSync(0, 'utf8');
757
- }
758
- catch {
759
- console.error('No input on stdin. Pipe text in or use --file <path>.');
760
- process.exit(1);
761
- }
762
- break;
763
- }
764
- case 'file': {
765
- if (!options.filePath) {
766
- console.error('Missing file path. Usage: hippo capture --file <path>');
767
- process.exit(1);
768
- }
769
- if (!fs.existsSync(options.filePath)) {
770
- console.error(`File not found: ${options.filePath}`);
771
- process.exit(1);
772
- }
773
- text = fs.readFileSync(options.filePath, 'utf8');
774
- break;
775
- }
776
- case 'last-session': {
777
- let turns = options.sessionTurns;
778
- if (!turns) {
779
- const resolved = resolveLastSessionTranscript(options.transcriptPath, options.stdinText, { mayScan: !options.stdinTimedOut });
780
- if (!resolved) {
781
- console.log('No transcript found. Pass --transcript <path> or run from a SessionEnd hook.');
782
- return;
783
- }
784
- turns = collectSessionTurns(fs.readFileSync(resolved, 'utf8'));
785
- }
786
- text = summariseSessionTurns(turns);
787
- if (!text) {
788
- console.log('Transcript had no user/assistant messages to summarise.');
789
- return;
790
- }
791
- break;
792
- }
793
- }
794
- if (!text || text.trim().length === 0) {
795
- console.log('No text to capture from.');
796
- return;
797
- }
798
- // Scrub once here, as the snapshot fields are: every source can carry a pasted token (AGENTS.md: no secrets in memories).
799
- const extracted = extractFromText(maskEmails(redactSecretsStrict(text)));
800
- if (extracted.length === 0) {
801
- console.log('No actionable items found in the input.');
802
- return;
803
- }
804
- // Dedup only against rows this capture's reader sees: another tenant's rows (L9), or another
805
- // project's, are hidden from it, so they must not stop its own copy.
806
- const stored = loadAllEntries(targetRoot, useGlobal ? undefined : options.tenantId);
807
- const origin = options.originProject;
808
- const keys = storedTextKeys(origin === undefined
809
- ? stored
810
- : stored.filter((e) => classifyOriginProject(e.origin_project, origin) !== 'cross-project'));
811
- let captured = 0;
812
- let skipped = 0;
813
- let rejected = 0;
814
- const baseHalfLifeDays = loadConfig(targetRoot).defaultHalfLifeDays;
815
- // AT1 P2 fix (dry-run parity, docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
816
- // dry-run used to skip the guarded write branch ENTIRELY, so a tombstoned
817
- // extraction printed as `[capture]` and counted toward `captured` — the
818
- // preview lied about what a real run would do. Mirrors importers.ts's
819
- // importEntries dry-run probe (commit 6146e82): open a read-only handle
820
- // once, run the same checkRejectionGuard the real write path uses via
821
- // writeEntry, never write anything.
822
- const dryRunDb = options.dryRun ? openHippoDb(targetRoot) : null;
823
- const writeDb = options.dryRun ? null : openStore(targetRoot);
824
- try {
825
- for (const item of extracted) {
826
- if (keys.has(duplicateKey(item.content))) {
827
- skipped++;
828
- if (options.dryRun) {
829
- console.log(` [skip] (${item.category}) ${item.content.slice(0, 80)}`);
830
- }
831
- continue;
832
- }
833
- // A3: kind defaults to 'distilled'. capture.ts extracts curated items from
834
- // session output (not raw transcript chunks), so distilled is correct. If a
835
- // future variant captures full raw session text, it MUST set kind: 'raw'
836
- // and route deletions through archiveRawMemory(). See MEMORY_ENVELOPE.md.
837
- // L9: the dedup read above is scoped by options.tenantId — the WRITE
838
- // must match, or scoped-dedup-passes-then-default-tenant-write breaks
839
- // the per-tenant contract. Mirror the dedup-read guard: when
840
- // global: true, the global store is host-wide and tenant is irrelevant
841
- // (createMemory's default 'default' applies). When global: false,
842
- // options.tenantId scopes the write to the same tenant as the dedup.
843
- const created = createMemory(item.content, {
844
- layer: Layer.Episodic,
845
- tags: item.tags,
846
- source: 'capture',
847
- confidence: 'observed',
848
- tenantId: useGlobal ? undefined : options.tenantId,
849
- baseHalfLifeDays,
850
- });
851
- const entry = options.originProject === undefined ? created : { ...created, origin_project: options.originProject };
852
- if (options.dryRun) {
853
- if (dryRunDb) {
854
- try {
855
- checkRejectionGuard(dryRunDb, entry.tenantId ?? 'default', entry.id, entry.content);
856
- }
857
- catch (err) {
858
- if (err instanceof RejectedValueError) {
859
- rejected++;
860
- console.log(` [reject] (${item.category}) ${item.content.slice(0, 80)} - matches a rejected value`);
861
- continue;
862
- }
863
- throw err;
864
- }
865
- }
866
- console.log(` [capture] (${item.category}) ${item.content}`);
867
- }
868
- else if (writeDb !== null) {
869
- // AT1 (plan §3 containment): one rejected item must not abort the
870
- // rest of this capture's items.
871
- const stamped = stampOriginProject(targetRoot, entry);
872
- const outcome = gatedWrite(writeDb, targetRoot, stamped);
873
- if (outcome === 'skipped:rejected') {
874
- rejected++;
875
- continue;
876
- }
877
- if (outcome !== 'written') {
878
- skipped++;
879
- continue;
880
- }
881
- writeEntryMirrors(targetRoot, stamped);
882
- updateStats(targetRoot, { remembered: 1 });
883
- keys.add(duplicateKey(item.content)); // within-batch dedup
884
- void embedMemory(targetRoot, entry);
885
- }
886
- captured++;
887
- }
888
- }
889
- finally {
890
- if (dryRunDb)
891
- closeHippoDb(dryRunDb);
892
- if (writeDb)
893
- closeHippoDb(writeDb);
894
- }
895
- const prefix = options.dryRun ? '[dry-run] ' : '';
896
- const globalPrefix = useGlobal ? '[global] ' : '';
897
- console.log(`\n${prefix}${globalPrefix}Captured ${captured} items (${skipped} skipped as duplicates` +
898
- (rejected > 0 ? `, ${rejected} rejected` : '') +
899
- ')');
900
- }
901
- // ---------------------------------------------------------------------------
902
- // `hippo pre-compact` — PreCompact hook producer
903
- // ---------------------------------------------------------------------------
904
- /** Never read the whole transcript — PreCompact fires exactly when it's largest. */
905
- export const PRE_COMPACT_TAIL_BYTES = 256 * 1024;
906
- export const PRE_COMPACT_TASK_CAP = 200;
907
- export const PRE_COMPACT_SUMMARY_CAP = 2000;
908
- export const PRE_COMPACT_NEXT_STEP_CAP = 500;
909
- /**
910
- * Truncate `text` to at most `maxChars` UTF-16 code units without splitting
911
- * a surrogate pair at the boundary. A plain `slice(0, n)` can land between a
912
- * high and low surrogate, leaving an unpaired surrogate in a stored/
913
- * re-injected snapshot field. If the code unit at the cut point is a high
914
- * surrogate (0xD800-0xDBFF), back off one unit so the pair stays whole.
915
- */
916
- export function truncateCodePointSafe(text, maxChars) {
917
- if (text.length <= maxChars)
918
- return text;
919
- let end = maxChars;
920
- if (end > 0) {
921
- const code = text.charCodeAt(end - 1);
922
- if (code >= 0xd800 && code <= 0xdbff)
923
- end -= 1;
924
- }
925
- return text.slice(0, end);
926
- }
927
- /**
928
- * Cap from the RECENT end: summariseTranscript emits user turns oldest-to-
929
- * newest with assistant responses after them, so a head-first cap keeps
930
- * stale context and drops exactly the newest working state this feature
931
- * exists to preserve. Keep the LAST maxChars instead, aligned forward to a
932
- * nearby line start, with a trim marker (codex round 3).
933
- */
934
- export function truncateKeepNewest(text, maxChars) {
935
- if (text.length <= maxChars)
936
- return text;
937
- let start = text.length - maxChars;
938
- const code = text.charCodeAt(start);
939
- if (code >= 0xdc00 && code <= 0xdfff)
940
- start += 1; // never start on a low surrogate
941
- const nl = text.indexOf('\n', start);
942
- if (nl !== -1 && nl + 1 < text.length && nl - start < 200)
943
- start = nl + 1;
944
- return '[...earlier turns trimmed]\n' + text.slice(start);
945
- }
946
- /**
947
- * Positional read of the last `capBytes` of `transcriptPath`, aligned
948
- * forward to the first complete JSONL line. Uses fs.openSync/readSync at a
949
- * byte offset rather than reading the whole file and slicing — PreCompact
950
- * fires exactly when transcripts are largest, so a whole-file read is the
951
- * one thing this path cannot do.
952
- *
953
- * Boundary handling (X14): a seek that lands mid-line must drop that
954
- * partial first line so every remaining line parses as complete JSON. A
955
- * seek that lands exactly after a '\n' already starts on a complete line
956
- * and must NOT drop it — doing so would silently discard one whole line on
957
- * every tail read whose start offset happens to align with a line break.
958
- * Distinguished by peeking at the single byte immediately before `start`.
959
- */
960
- export function readTranscriptTail(transcriptPath, capBytes = PRE_COMPACT_TAIL_BYTES) {
961
- const size = fs.statSync(transcriptPath).size;
962
- const start = Math.max(0, size - capBytes);
963
- const length = size - start;
964
- if (length <= 0)
965
- return '';
966
- const fd = fs.openSync(transcriptPath, 'r');
967
- try {
968
- let onLineBoundary = start === 0;
969
- if (!onLineBoundary) {
970
- const prevByte = Buffer.alloc(1);
971
- fs.readSync(fd, prevByte, 0, 1, start - 1);
972
- onLineBoundary = prevByte[0] === 0x0a; // '\n'
973
- }
974
- const buf = Buffer.alloc(length);
975
- fs.readSync(fd, buf, 0, length, start);
976
- let text = buf.toString('utf8');
977
- if (!onLineBoundary) {
978
- // Landed mid-line — drop the partial first line so every remaining
979
- // line parses as complete JSON.
980
- const nl = text.indexOf('\n');
981
- text = nl === -1 ? '' : text.slice(nl + 1);
982
- }
983
- return text;
984
- }
985
- finally {
986
- fs.closeSync(fd);
987
- }
988
- }
989
- /** Most recent plain-text user message in a JSONL tail. Claude Code transcript shape only (PreCompact is claude-code-only). */
990
- function lastPlainUserMessage(jsonl) {
991
- const lines = jsonl.split('\n').filter((l) => l.trim());
992
- for (let i = lines.length - 1; i >= 0; i--) {
993
- let entry;
994
- try {
995
- entry = JSON.parse(lines[i]);
996
- }
997
- catch {
998
- continue; // the tail read can start mid-line; skip the torn fragment
999
- }
1000
- if (!isObjectLike(entry))
1001
- continue;
1002
- if (!('type' in entry) || entry.type !== 'user')
1003
- continue;
1004
- // Meta/sidechain lines carry type:'user' but are not the human: after a
1005
- // FIRST compaction the transcript holds the compact summary as an isMeta
1006
- // user line, and sub-agent turns are isSidechain — deriving "task" from
1007
- // either yields junk on every later compaction.
1008
- if (('isMeta' in entry && entry.isMeta === true) || ('isSidechain' in entry && entry.isSidechain === true))
1009
- continue;
1010
- const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
1011
- if (!message)
1012
- continue;
1013
- const text = humanUserText(entry, message);
1014
- if (text)
1015
- return text;
1016
- }
1017
- return '';
1018
- }
1019
- /** Last assistant text block in a JSONL tail (skips thinking + tool_use, same as summariseTranscript). */
1020
- function lastAssistantTextBlock(jsonl) {
1021
- const lines = jsonl.split('\n').filter((l) => l.trim());
1022
- for (let i = lines.length - 1; i >= 0; i--) {
1023
- let entry;
1024
- try {
1025
- entry = JSON.parse(lines[i]);
1026
- }
1027
- catch {
1028
- continue; // the tail read can start mid-line; skip the torn fragment
1029
- }
1030
- if (!isObjectLike(entry))
1031
- continue;
1032
- if (!('type' in entry) || entry.type !== 'assistant')
1033
- continue;
1034
- // Same meta/sidechain guard as lastPlainUserMessage: sub-agent turns
1035
- // (isSidechain) are not this session's next step.
1036
- if (('isMeta' in entry && entry.isMeta === true) || ('isSidechain' in entry && entry.isSidechain === true))
1037
- continue;
1038
- const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
1039
- if (!message)
1040
- continue;
1041
- const content = 'content' in message ? message.content : undefined;
1042
- if (!Array.isArray(content))
1043
- continue;
1044
- for (let j = content.length - 1; j >= 0; j--) {
1045
- const block = content[j];
1046
- if (isObjectLike(block)) {
1047
- const blockText = 'type' in block && block.type === 'text' && 'text' in block ? block.text : undefined;
1048
- if (isStringValue(blockText) && blockText.trim()) {
1049
- return blockText.trim();
1050
- }
1051
- }
1052
- }
1053
- }
1054
- return '';
1055
- }
1056
- // Diagnostic-only log; a long-lived install must not grow it unbounded.
1057
- const PRE_COMPACT_LOG_MAX_BYTES = 256 * 1024;
1058
- /**
1059
- * Log-forgery guard: messages here interpolate payload-controlled values
1060
- * (transcript paths, session ids). Strip C0 control chars — newlines above
1061
- * all — so a crafted value can't inject fake `[hippo] ...` log lines.
1062
- * Exported for every `[hippo]`-prefixed log writer that interpolates
1063
- * payload-controlled values (cli.ts appendSessionEndCloseLog) — one shared
1064
- * guard, not per-file copies.
1065
- */
1066
- export function sanitizeLogMessage(message) {
1067
- // eslint-disable-next-line no-control-regex
1068
- return message.replace(/[\x00-\x1f]/g, '');
1069
- }
1070
- function appendPreCompactLog(logFile, message) {
1071
- try {
1072
- fs.mkdirSync(path.dirname(logFile), { recursive: true });
1073
- const stat = fs.existsSync(logFile) ? fs.statSync(logFile) : null;
1074
- if (stat && stat.size > PRE_COMPACT_LOG_MAX_BYTES) {
1075
- fs.writeFileSync(logFile, '', 'utf8'); // start fresh — dumb cap, no rotation
1076
- }
1077
- fs.appendFileSync(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
1078
- }
1079
- catch {
1080
- // Diagnostic-only; a log write failure must never affect the exit-0 contract.
1081
- }
1082
- }
1083
- /** True iff `filePath` exists and is readable — checks both in one call. */
1084
- function isReadableFile(filePath) {
1085
- try {
1086
- fs.accessSync(filePath, fs.constants.R_OK);
1087
- return true;
1088
- }
1089
- catch {
1090
- return false; // missing and unreadable both mean "no file" to the caller
1091
- }
1092
- }
1093
- /** PreCompact stdout is the summariser's instructions; sent before the snapshot work because a locked store can run the hook past its 30 s limit, and via writeSync because process.exit drops buffered pipe output. */
1094
- function printPreCompactInstruction(logFile) {
1095
- try {
1096
- fs.writeSync(1, `${PRE_COMPACT_INSTRUCTION}\n`);
1097
- }
1098
- catch (err) {
1099
- appendPreCompactLog(logFile, `instruction not printed: ${errorMessage(err)}`);
1100
- }
1101
- }
1102
- /** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
1103
- export function transcriptWorkingState(transcriptPath, log) {
1104
- let tail = '';
1105
- let rawTask = '';
1106
- let rawNextStep = '';
1107
- try {
1108
- // Compaction fires when a big tool_result lands (CX7), so the last human and assistant turns can sit
1109
- // megabytes back: grow the window a bounded number of times until both turns are in it.
1110
- const size = fs.statSync(transcriptPath).size;
1111
- for (const cap of [PRE_COMPACT_TAIL_BYTES, PRE_COMPACT_TAIL_BYTES * 4, PRE_COMPACT_TAIL_BYTES * 16]) {
1112
- if (cap > PRE_COMPACT_TAIL_BYTES)
1113
- log(`tail window grown to ${cap} bytes (last ${rawTask ? 'assistant' : 'user'} turn is further back)`);
1114
- tail = readTranscriptTail(transcriptPath, cap);
1115
- rawTask = lastPlainUserMessage(tail);
1116
- rawNextStep = lastAssistantTextBlock(tail);
1117
- if ((rawTask && rawNextStep) || size <= cap)
1118
- break;
1119
- }
1120
- }
1121
- catch (err) {
1122
- log(`skip: could not read transcript tail: ${errorMessage(err)}`);
1123
- return null;
1124
- }
1125
- const rawSummary = summariseTranscript(tail);
1126
- if (!rawTask.trim() && !rawSummary.trim() && !rawNextStep.trim()) {
1127
- log('skip: empty summary');
1128
- return null;
1129
- }
1130
- // X9: these fields skip the capture content gate and reach a prompt, so the strict scrub runs. The caps protect the
1131
- // re-injection token budget and never split a surrogate pair (X2); `hippo snapshot save` stays uncapped.
1132
- const task = maskEmails(redactSecretsStrict(rawTask));
1133
- const summary = maskEmails(redactSecretsStrict(rawSummary));
1134
- const nextStep = maskEmails(redactSecretsStrict(rawNextStep));
1135
- return {
1136
- task: task.trim() ? truncateCodePointSafe(task, PRE_COMPACT_TASK_CAP) : '',
1137
- summary: summary.trim() ? truncateKeepNewest(summary, PRE_COMPACT_SUMMARY_CAP) : '',
1138
- next_step: nextStep.trim() ? truncateCodePointSafe(nextStep, PRE_COMPACT_NEXT_STEP_CAP) : '',
1139
- };
1140
- }
1141
- /** Runs the PreCompact producer: records the compaction, asks the summariser for memories, saves a working-state snapshot. Never extracts memories itself; SessionEnd capture owns that. */
1142
- function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
1143
- // X3: the PreCompact hook fires in every Claude Code project, including
1144
- // ones that never ran `hippo init`, so gate before any store-opening call
1145
- // (saveActiveTaskSnapshot etc. call initStore, which would create one).
1146
- if (!isInitialized(hippoRoot)) {
1147
- appendPreCompactLog(logFile, 'skip: store not initialized');
1148
- return;
1149
- }
1150
- const receipt = readClaudeCodePreCompact(stdinText, stdinTimedOut);
1151
- if (receipt.status !== 'received') {
1152
- appendPreCompactLog(logFile, `skip: ${receipt.reason}`);
1153
- return;
1154
- }
1155
- const { sessionId, transcriptPath: payloadTranscriptPath, cwd: payloadCwd, trigger: payloadTrigger } = receipt.input;
1156
- // The record is the "something saved before every compaction", so it lands even when no snapshot is derivable below.
1157
- let recordId = null;
1158
- if (sessionId !== null && sessionId !== '') {
1159
- recordId = recordCompactionStart(hippoRoot, { sessionId, trigger: payloadTrigger, cwd: payloadCwd, transcriptPath: payloadTranscriptPath }, (message) => appendPreCompactLog(logFile, message));
1160
- printPreCompactInstruction(logFile);
1161
- }
1162
- // A payload transcript_path is EXCLUSIVE: never fall back to
1163
- // newest-transcript auto-discovery when it's missing/unreadable. That
1164
- // fallback would snapshot a DIFFERENT session's transcript under THIS
1165
- // payload's session_id — cross-session contamination with wrong linkage
1166
- // (verify-stage E2E finding, 2026-08-03). Auto-discovery only applies
1167
- // on a true manual invocation (no payload at all).
1168
- let transcriptPath;
1169
- if (payloadTranscriptPath !== null) {
1170
- if (isReadableFile(payloadTranscriptPath)) {
1171
- transcriptPath = payloadTranscriptPath;
1172
- }
1173
- else {
1174
- appendPreCompactLog(logFile, `skip: payload transcript_path unreadable: ${payloadTranscriptPath}`);
1175
- return;
1176
- }
1177
- }
1178
- else {
1179
- transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: true });
1180
- }
1181
- if (!transcriptPath) {
1182
- appendPreCompactLog(logFile, 'skip: no transcript resolved');
1183
- return;
1184
- }
1185
- // Nothing derivable skips the write, so a user-authored active snapshot is never clobbered with junk.
1186
- const derived = transcriptWorkingState(transcriptPath, (message) => appendPreCompactLog(logFile, message));
1187
- if (!derived)
1188
- return;
1189
- const tenantId = resolveTenantId({});
1190
- // Per-field merge (X1): a tool-heavy tail whose only user turns are
1191
- // tool_result arrays derives an empty task even though the summary is
1192
- // non-empty. Loading the existing snapshot first lets each field fall
1193
- // back independently instead of the whole write clobbering a
1194
- // user-authored field with blank text.
1195
- let existing = null;
1196
- try {
1197
- existing = loadActiveTaskSnapshot(hippoRoot, tenantId);
1198
- }
1199
- catch {
1200
- // No existing snapshot to merge against — proceed with derived-only.
1201
- }
1202
- // CX6 (codex round 2): field fallback must never move content across
1203
- // sessions — session A's task carried into a snapshot saved under session
1204
- // B's id would pass compact-resume's session gate wearing the wrong
1205
- // badge. Fall back only when the existing snapshot has no session, this
1206
- // payload has none, or they match.
1207
- const fallback = existing !== null &&
1208
- (existing.session_id === null || sessionId === null || existing.session_id === sessionId)
1209
- ? existing
1210
- : null;
1211
- // Carried-over fields are not re-capped, as `hippo snapshot save` stays uncapped; saveActiveTaskSnapshot scrubs every field.
1212
- const task = derived.task || (fallback?.task ?? '');
1213
- const summary = derived.summary || (fallback?.summary ?? '');
1214
- const nextStep = derived.next_step || (fallback?.next_step ?? '');
1215
- // All-empty fields (cross-session tail with nothing derivable) skip the
1216
- // write so a foreign session's junk never displaces the owning snapshot.
1217
- if (!task && !summary && !nextStep) {
1218
- appendPreCompactLog(logFile, 'skip: no snapshot content for this session (nothing derivable; fallback blocked or empty)');
1219
- }
1220
- else {
1221
- try {
1222
- saveActiveTaskSnapshot(hippoRoot, tenantId, {
1223
- task,
1224
- summary,
1225
- next_step: nextStep,
1226
- source: 'pre-compact',
1227
- session_id: sessionId,
1228
- });
1229
- appendPreCompactLog(logFile, 'snapshot saved');
1230
- if (recordId !== null)
1231
- recordSnapshotSaved(hippoRoot, recordId, (message) => appendPreCompactLog(logFile, message));
1232
- }
1233
- catch (err) {
1234
- appendPreCompactLog(logFile, `snapshot save failed: ${errorMessage(err)}`);
1235
- }
1236
- }
1237
- }
1238
- /**
1239
- * PreCompact hook entry point. Exit code 2 on PreCompact BLOCKS compaction,
1240
- * so this verb must exit 0 on every path — malformed stdin, missing
1241
- * transcript, and store errors all degrade to a logged no-op rather than a
1242
- * thrown error. Callers (src/cli.ts) must not wrap this in anything that
1243
- * could turn a caught-and-logged failure back into a non-zero exit.
1244
- */
1245
- export async function cmdPreCompact(hippoRoot, options) {
1246
- const logFile = options.logFile ?? defaultPreCompactLogPath();
1247
- try {
1248
- runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile);
1249
- }
1250
- catch (err) {
1251
- appendPreCompactLog(logFile, `pre-compact failed: ${errorMessage(err)}`);
1252
- }
1253
- process.exit(0);
1254
- }
1255
- /** A PostCompact hook has 10 s in all; replay stops starting new records after this. */
1256
- const POST_COMPACT_REPLAY_BUDGET_MS = 6000;
1257
- /** PostCompact entry point: saves the summary and its items, replays earlier leftovers, returns the one line to show. Never throws, so the hook exits 0. */
1258
- export function cmdPostCompact(hippoRoot, options) {
1259
- const logFile = options.logFile ?? defaultPreCompactLogPath();
1260
- const log = (message) => appendPreCompactLog(logFile, `post-compact: ${message}`);
1261
- const deadline = Date.now() + POST_COMPACT_REPLAY_BUDGET_MS;
1262
- try {
1263
- if (!isInitialized(hippoRoot)) {
1264
- log('skip: no hippo store');
1265
- return null;
1266
- }
1267
- const payload = parsePostCompactPayload(options.stdinText);
1268
- let line = null;
1269
- let storeBusy = false;
1270
- if (payload === null) {
1271
- log('skip: no PostCompact payload naming a session');
1272
- }
1273
- else {
1274
- const saved = saveCompaction(hippoRoot, payload, log);
1275
- line = postCompactLine(saved);
1276
- storeBusy = saved.deferred;
1277
- if (!storeBusy && payload.transcriptPath !== null && options.afterSave) {
1278
- try {
1279
- options.afterSave(payload.transcriptPath, compactionOrigin(hippoRoot, payload.cwd), log);
1280
- }
1281
- catch (err) {
1282
- log(`agent memory import failed: ${errorMessage(err)}`);
1283
- }
1284
- }
1285
- }
1286
- if (!storeBusy)
1287
- replayCompactionsAt(hippoRoot, log, { busyWaitMs: COMPACTION_DB_WAIT_MS, deadline });
1288
- return line;
1289
- }
1290
- catch (err) {
1291
- log(`failed: ${errorMessage(err)}`);
1292
- return null;
1293
- }
1294
- }
1295
- //# sourceMappingURL=capture.js.map