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