failproofai 1.0.0-beta.2 → 1.0.0-beta.4

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 (234) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  39. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  41. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/index.html +1 -1
  44. package/.next/standalone/.next/server/app/index.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  48. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  56. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  57. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  60. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  71. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  72. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  89. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  90. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  91. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_1q9xxca.js} +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
  121. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
  124. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
  128. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  129. package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
  130. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
  135. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  137. package/.next/standalone/Cargo.lock +1992 -198
  138. package/.next/standalone/Cargo.toml +1 -1
  139. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  140. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  141. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  142. package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
  143. package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
  144. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
  145. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
  146. package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
  147. package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
  148. package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
  149. package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
  150. package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
  151. package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
  152. package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
  153. package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
  154. package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
  155. package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
  156. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
  157. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
  158. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
  159. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
  160. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
  161. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
  162. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
  163. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
  164. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
  165. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
  166. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
  167. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
  168. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
  169. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
  170. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
  171. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
  172. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
  173. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
  174. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
  175. package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
  176. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
  177. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
  178. package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
  179. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
  180. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
  181. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
  182. package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
  183. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
  184. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
  185. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
  186. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
  187. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
  188. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
  189. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
  190. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
  191. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
  192. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
  193. package/.next/standalone/package.json +15 -9
  194. package/.next/standalone/server.js +1 -1
  195. package/bin/failproofai.mjs +129 -0
  196. package/dist/cli.mjs +2255 -779
  197. package/dist/worker.mjs +365 -52
  198. package/package.json +15 -9
  199. package/scripts/build-daemon-packages.mjs +210 -0
  200. package/scripts/daemon-platforms.mjs +42 -0
  201. package/scripts/prune-standalone.mjs +6 -0
  202. package/scripts/publish-aliases.mjs +62 -41
  203. package/src/hooks/builtin-policies.ts +139 -0
  204. package/src/hooks/cloud-connection.ts +205 -0
  205. package/src/hooks/cloud-enrollment-cli.ts +227 -0
  206. package/src/hooks/cloud-enrollment.ts +173 -0
  207. package/src/hooks/cloud-managed-policies.ts +138 -0
  208. package/src/hooks/collector-config.ts +226 -0
  209. package/src/hooks/configure-wizard.ts +221 -5
  210. package/src/hooks/custom-hooks-loader.ts +69 -25
  211. package/src/hooks/daemon-client.ts +18 -0
  212. package/src/hooks/daemon-download.ts +137 -27
  213. package/src/hooks/daemon-service.ts +23 -8
  214. package/src/hooks/handler.ts +144 -6
  215. package/src/hooks/hook-activity-store.ts +50 -0
  216. package/src/hooks/loader-utils.ts +6 -4
  217. package/src/hooks/policy-types.ts +35 -0
  218. package/src/hooks/session-pause-cli.ts +157 -0
  219. package/src/hooks/session-pause.ts +236 -0
  220. package/src/hooks/tui.ts +115 -0
  221. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  222. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  225. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  226. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  227. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  228. package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
  229. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  230. package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
  231. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  232. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
  233. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
  234. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
@@ -0,0 +1,318 @@
1
+ //! Hermes session capture — a [`sqlitepoll`](crate::sqlitepoll) adapter.
2
+ //!
3
+ //! Hermes (Nous Research's Slack/Telegram/CLI gateway) keeps everything in one
4
+ //! SQLite database at `~/.hermes/state.db`. We open it read-only through
5
+ //! [`crate::sqlitepoll::open_readonly`] and read two tables — `sessions` and
6
+ //! `messages`. Hermes's own files are never written, moved or deleted.
7
+ //!
8
+ //! # `~/.hermes/sessions/` must never be read, despite its name
9
+ //!
10
+ //! That directory holds no transcripts. It holds
11
+ //! `request_dump_<sessionId>_<ts>.json` error dumps — ~60 KB each on the probe
12
+ //! capture — and every one of them **embeds a partially-redacted Authorization
13
+ //! bearer token** for the model provider, because a request dump is the whole
14
+ //! outbound HTTP request. Uploading one would exfiltrate a credential to the
15
+ //! ingest endpoint. The directory name actively invites the wrong glob, which
16
+ //! is why this warning is here and not only in a commit message: nothing in
17
+ //! this module walks a directory at all, and nothing should start.
18
+ //!
19
+ //! Four more things in the Hermes home are out of scope for their own reasons:
20
+ //!
21
+ //! * `messages_fts*` / `messages_fts_trigram*` — FTS5 shadow tables. They carry
22
+ //! a rowid-keyed copy of every message's text, so reading them would ship the
23
+ //! conversation twice under ids that mean nothing.
24
+ //! * `logs/agent.log` — unstructured, and the same content already arrives
25
+ //! through `messages`.
26
+ //! * `cache/` — provider model metadata (254 KB of OpenRouter's catalogue on
27
+ //! the capture). Nothing about any session.
28
+ //! * `sandboxes/` — the agent's own scratch working trees.
29
+ //!
30
+ //! Within the database, `async_delegations`, `session_model_usage`,
31
+ //! `gateway_routing`, `compression_locks` and `state_meta` are also not read:
32
+ //! they are gateway bookkeeping, and the parts worth reporting (token totals,
33
+ //! end reason) are already denormalised onto `sessions`.
34
+ //!
35
+ //! # Rows are mutated AND deleted, so the watermark is not fully safe
36
+ //!
37
+ //! [`Watermark::RowId`] is right for the happy path — `messages.id` is an
38
+ //! `INTEGER PRIMARY KEY AUTOINCREMENT`, Hermes appends in order, and a session
39
+ //! that just runs to completion is captured exactly once. It is not the whole
40
+ //! story, and pretending otherwise would hide two real gaps:
41
+ //!
42
+ //! * **Updates behind the watermark are missed.** `hermes_state.py` has five
43
+ //! `UPDATE messages SET active = …` statements plus `UPDATE messages SET
44
+ //! api_content = ?`, and ~20 `UPDATE sessions SET …`. A row's `active` flag
45
+ //! flipping to 0 after we shipped it is never re-read, so a message we
46
+ //! reported as in-context can silently leave the model's context. The
47
+ //! `sessions` updates matter less because session columns are read fresh on
48
+ //! every poll that touches one of that session's rows.
49
+ //! * **Deletions rewrite history.** Rewind and compaction issue `DELETE FROM
50
+ //! messages` in eight-plus places. Deleted rows are already shipped and stay
51
+ //! shipped; the product shows a turn Hermes no longer has.
52
+ //!
53
+ //! [`Watermark::UpdatedAt`] cannot fix either one, because `messages` has no
54
+ //! last-modified column to order by — `timestamp` is the message's own time and
55
+ //! is never touched by those updates. Closing the gap needs a real change-feed
56
+ //! (a content hash per session, or Hermes emitting one), which is follow-on
57
+ //! work rather than something to half-implement here.
58
+ //!
59
+ //! # Both session shapes are real
60
+ //!
61
+ //! `cwd` **is** populated for `source='cli'` sessions (5/5 on the probe, with
62
+ //! `git_branch` and `git_repo_root` beside it), correcting a long-standing
63
+ //! assumption that Hermes is cwd-less. Gateway sessions (Slack, Telegram) have
64
+ //! NULL `cwd`. Both are handled explicitly — see
65
+ //! [`transform::agent_id`].
66
+ //!
67
+ //! Session ids are `YYYYMMDD_HHMMSS_<6hex>`, not UUIDs. They are therefore
68
+ //! lexicographically time-sortable, which nothing here relies on but which is
69
+ //! worth knowing before someone writes a UUID-shaped validator.
70
+
71
+ pub mod transform;
72
+
73
+ use std::collections::HashMap;
74
+ use std::path::{Path, PathBuf};
75
+
76
+ use rusqlite::Connection;
77
+
78
+ use crate::sqlitepoll::{PollOutcome, PollRequest, SqliteFormat, Watermark};
79
+ use transform::{MessageRow, SessionMeta};
80
+
81
+ /// The format table the poller drives.
82
+ pub const FORMAT: SqliteFormat = SqliteFormat {
83
+ kind: "hermes",
84
+ // See the module docs: correct on the happy path, honestly incomplete under
85
+ // rewind and compaction, and `UpdatedAt` is not available as an alternative
86
+ // because `messages` has no last-modified column.
87
+ watermark: Watermark::RowId,
88
+ poll,
89
+ };
90
+
91
+ /// The agent id used when a message's session row cannot be found at all.
92
+ pub const DEFAULT_AGENT_ID: &str = "hermes";
93
+
94
+ /// Directory under the Hermes root that holds non-default profiles.
95
+ const PROFILES_DIR: &str = "profiles";
96
+
97
+ /// The database file inside every Hermes home.
98
+ const STATE_DB: &str = "state.db";
99
+
100
+ /// Columns read from `messages`. Listed explicitly rather than `SELECT *` so a
101
+ /// new Hermes column cannot shift a positional index underneath us.
102
+ const SELECT_MESSAGES: &str = "SELECT id, session_id, role, content, tool_call_id, tool_calls, \
103
+ tool_name, timestamp, finish_reason, active \
104
+ FROM messages WHERE id > ?1 ORDER BY id LIMIT ?2";
105
+
106
+ /// Columns read from `sessions`. See [`SessionMeta`] for what is left out and
107
+ /// why.
108
+ const SELECT_SESSION: &str = "SELECT source, model, cwd, git_branch, git_repo_root, chat_type, \
109
+ title, started_at, ended_at, end_reason, input_tokens, output_tokens \
110
+ FROM sessions WHERE id = ?1";
111
+
112
+ /// The surviving id range of one session's messages.
113
+ const SELECT_BOUNDS: &str = "SELECT MIN(id), MAX(id) FROM messages WHERE session_id = ?1";
114
+
115
+ /// Read messages past the watermark and turn them into events.
116
+ ///
117
+ /// Two queries per distinct session in the batch, not per row: the session
118
+ /// lookup and its id bounds are what let `agent_start` / `agent_end` fire
119
+ /// exactly once without the poller remembering anything between passes, and a
120
+ /// correlated subquery per row would pay for that on every message of a long
121
+ /// session instead of once.
122
+ fn poll(conn: &Connection, req: &PollRequest) -> rusqlite::Result<PollOutcome> {
123
+ // A zero limit would otherwise ask SQLite for nothing and then report
124
+ // `more`, spinning the drain loop.
125
+ let limit = req.max_rows.clamp(1, i64::MAX as u64) as i64;
126
+
127
+ let mut stmt = conn.prepare_cached(SELECT_MESSAGES)?;
128
+ let rows = stmt
129
+ .query_map((req.watermark, limit), |r| {
130
+ Ok(MessageRow {
131
+ id: r.get(0)?,
132
+ session_id: r.get(1)?,
133
+ role: r.get(2)?,
134
+ content: r.get(3)?,
135
+ tool_call_id: r.get(4)?,
136
+ tool_calls: r.get(5)?,
137
+ tool_name: r.get(6)?,
138
+ timestamp: r.get(7)?,
139
+ finish_reason: r.get(8)?,
140
+ // NOT NULL DEFAULT 1 in the schema, but `idx_messages_active_null`
141
+ // exists specifically for `active IS NULL`, so Hermes has clearly
142
+ // shipped rows without one. Treated as active, which is what the
143
+ // column default means.
144
+ active: r.get::<_, Option<i64>>(9)?.unwrap_or(1),
145
+ })
146
+ })?
147
+ .collect::<rusqlite::Result<Vec<_>>>()?;
148
+
149
+ let mut outcome = PollOutcome {
150
+ watermark: req.watermark,
151
+ rows_seen: rows.len() as u64,
152
+ // Hitting the limit means a backlog is waiting; the engine polls again
153
+ // immediately rather than sleeping, so a restart after downtime drains
154
+ // instead of trickling.
155
+ more: rows.len() as i64 >= limit,
156
+ ..Default::default()
157
+ };
158
+ if let Some(last) = rows.last() {
159
+ outcome.watermark = last.id;
160
+ }
161
+
162
+ let mut sessions: HashMap<String, Option<SessionMeta>> = HashMap::new();
163
+ for row in &rows {
164
+ if !sessions.contains_key(&row.session_id) {
165
+ let meta = load_session(conn, &row.session_id)?;
166
+ sessions.insert(row.session_id.clone(), meta);
167
+ }
168
+ }
169
+
170
+ // Tool-call id → name, so a result row is not a blank row. Scoped to this
171
+ // poll on purpose: carrying it across polls would make the output depend on
172
+ // what earlier passes happened to see, and the result row's own `tool_name`
173
+ // already covers a batch cut between a call and its result.
174
+ let mut pending: HashMap<String, String> = HashMap::new();
175
+
176
+ for row in &rows {
177
+ let meta = sessions.get(&row.session_id).and_then(Option::as_ref);
178
+ let agent_id = transform::agent_id(meta, &req.agent_id);
179
+
180
+ if let Some(meta) = meta
181
+ && meta.first_message_id == Some(row.id)
182
+ && let Some(ev) = transform::agent_start(row, meta, &agent_id, &req.environment)
183
+ {
184
+ outcome.events.push(ev);
185
+ }
186
+
187
+ outcome.events.extend(transform::message_events(
188
+ row,
189
+ meta,
190
+ &agent_id,
191
+ &req.environment,
192
+ &mut pending,
193
+ ));
194
+
195
+ // Gated on `ended_at` so a live session is not ended on every pass; see
196
+ // `transform::agent_end` for the race that gate costs.
197
+ if let Some(meta) = meta
198
+ && meta.last_message_id == Some(row.id)
199
+ && meta.ended_at.is_some()
200
+ && let Some(ev) = transform::agent_end(row, meta, &agent_id, &req.environment)
201
+ {
202
+ outcome.events.push(ev);
203
+ }
204
+ }
205
+
206
+ Ok(outcome)
207
+ }
208
+
209
+ /// One session's metadata and the id range of its surviving messages.
210
+ ///
211
+ /// A missing session row is `Ok(None)`, not an error: `messages.session_id` is
212
+ /// a foreign key, but a partially-applied rewind or a hand-edited database
213
+ /// would leave orphans, and one orphan must not stop the poll for every other
214
+ /// session in the batch.
215
+ fn load_session(conn: &Connection, session_id: &str) -> rusqlite::Result<Option<SessionMeta>> {
216
+ let mut stmt = conn.prepare_cached(SELECT_SESSION)?;
217
+ let meta = stmt.query_row([session_id], |r| {
218
+ Ok(SessionMeta {
219
+ source: r.get(0)?,
220
+ model: r.get(1)?,
221
+ cwd: r.get(2)?,
222
+ git_branch: r.get(3)?,
223
+ git_repo_root: r.get(4)?,
224
+ chat_type: r.get(5)?,
225
+ title: r.get(6)?,
226
+ started_at: r.get(7)?,
227
+ ended_at: r.get(8)?,
228
+ end_reason: r.get(9)?,
229
+ input_tokens: r.get(10)?,
230
+ output_tokens: r.get(11)?,
231
+ ..Default::default()
232
+ })
233
+ });
234
+ let mut meta = match meta {
235
+ Ok(meta) => meta,
236
+ Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
237
+ Err(err) => return Err(err),
238
+ };
239
+
240
+ let mut bounds = conn.prepare_cached(SELECT_BOUNDS)?;
241
+ let (first, last) = bounds.query_row([session_id], |r| {
242
+ Ok((r.get::<_, Option<i64>>(0)?, r.get::<_, Option<i64>>(1)?))
243
+ })?;
244
+ meta.first_message_id = first;
245
+ meta.last_message_id = last;
246
+ Ok(Some(meta))
247
+ }
248
+
249
+ /// The Hermes ROOT home — the directory that owns `profiles/`.
250
+ ///
251
+ /// `HERMES_HOME` is Hermes's own override and is honoured so a profile-scoped
252
+ /// shell and the collector agree on what "all profiles" means. It may point AT
253
+ /// a profile (`<root>/profiles/<name>`), because that is what the generated
254
+ /// per-profile alias wrapper exports; we climb back to `<root>` in that case so
255
+ /// discovery still sees every sibling profile rather than collecting from one
256
+ /// and silently ignoring the rest.
257
+ pub fn hermes_root() -> PathBuf {
258
+ let home = std::env::var_os("HERMES_HOME")
259
+ .map(PathBuf::from)
260
+ .filter(|p| !p.as_os_str().is_empty())
261
+ .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".hermes")))
262
+ .unwrap_or_else(|| PathBuf::from(".hermes"));
263
+ root_from_home(&home)
264
+ }
265
+
266
+ /// Climb from a possibly-profile-scoped Hermes home to the root that owns it.
267
+ pub fn root_from_home(home: &Path) -> PathBuf {
268
+ if let Some(parent) = home.parent()
269
+ && parent.file_name().is_some_and(|n| n == PROFILES_DIR)
270
+ && let Some(root) = parent.parent()
271
+ {
272
+ return root.to_path_buf();
273
+ }
274
+ home.to_path_buf()
275
+ }
276
+
277
+ /// Every Hermes database on disk: the root home's, then each profile's.
278
+ ///
279
+ /// A Hermes "profile" is not a column or a flag — it is a whole separate home
280
+ /// directory with its own `config.yaml`, `SOUL.md` and **its own `state.db`**.
281
+ /// Reading only `~/.hermes/state.db` makes every non-default profile's sessions
282
+ /// invisible, which is exactly the bug the audit pillar shipped with before
283
+ /// profile support landed.
284
+ ///
285
+ /// Each path is a separate logical stream and needs its own `state_dir`: the
286
+ /// sqlite poller keys its cursor on a fixed synthetic `(0, 0)`, so two profiles
287
+ /// sharing a state directory would overwrite each other's watermark.
288
+ ///
289
+ /// Fail-open — an unreadable `profiles/` just means "default only", which is
290
+ /// the single-profile install everyone starts with.
291
+ pub fn default_db_paths() -> Vec<PathBuf> {
292
+ db_paths_under(&hermes_root())
293
+ }
294
+
295
+ /// [`default_db_paths`] against an explicit root. Split out so it is testable
296
+ /// without mutating the process environment.
297
+ pub fn db_paths_under(root: &Path) -> Vec<PathBuf> {
298
+ let mut out = vec![root.join(STATE_DB)];
299
+ let Ok(entries) = std::fs::read_dir(root.join(PROFILES_DIR)) else {
300
+ return out;
301
+ };
302
+ let mut names: Vec<PathBuf> = entries
303
+ .filter_map(|e| e.ok())
304
+ .filter(|e| {
305
+ // A symlinked profile directory is legitimate and `is_dir()` on the
306
+ // DirEntry's file type is false for one, so the metadata (which
307
+ // follows the link) decides.
308
+ e.metadata().map(|m| m.is_dir()).unwrap_or(false)
309
+ && !e.file_name().to_string_lossy().starts_with('.')
310
+ })
311
+ .map(|e| e.path())
312
+ .collect();
313
+ // Sorted so the set of sources a daemon starts is stable across restarts
314
+ // and across machines; directory order is not.
315
+ names.sort();
316
+ out.extend(names.into_iter().map(|p| p.join(STATE_DB)));
317
+ out
318
+ }