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,315 @@
1
+ //! opencode session capture — a [`sqlitepoll`](crate::sqlitepoll) adapter.
2
+ //!
3
+ //! opencode keeps every session on the machine in ONE SQLite database at
4
+ //! `$XDG_DATA_HOME/opencode/opencode.db` (`~/.local/share/opencode/opencode.db`
5
+ //! on a default install). We open it read-only, through its WAL, and never
6
+ //! write, checkpoint or vacuum it — see [`crate::sqlitepoll::open_readonly`].
7
+ //!
8
+ //! # Rows mutate for ~12 seconds after they are inserted, and that is the design
9
+ //!
10
+ //! This is the source the [`Watermark::UpdatedAt`] machinery exists for. A tool
11
+ //! call and its result are the SAME `part` row: the row appears with
12
+ //! `state.status:"running"` and an EMPTY `state.output`, and the real result
13
+ //! arrives as a later `UPDATE` to that row. Directly observed on the probe
14
+ //! capture — one row changed three times, and `state.output` was populated only
15
+ //! on the third, 12.0 seconds after the row first appeared:
16
+ //!
17
+ //! ```text
18
+ //! t+0.0s NEW rowid=20 type=tool tool=bash status=running out='' tu=…539061
19
+ //! t+0.3s MUT rowid=20 type=tool tool=bash status=running out='' tu=…539110
20
+ //! t+12.0s MUT rowid=20 type=tool tool=bash status=completed out='SECOND_MARKER' tu=…551135
21
+ //! ```
22
+ //!
23
+ //! **A rowid watermark ships every tool call in this product with an empty
24
+ //! result, forever, while looking like it works.** Nothing errors, nothing is
25
+ //! missing from the event stream, and every `tool_result` is blank. So this
26
+ //! format declares [`Watermark::UpdatedAt`] and queries
27
+ //! `time_updated > :watermark ORDER BY time_updated, id`, which brings a
28
+ //! mutated row back through the transform.
29
+ //!
30
+ //! Re-emitting a re-read row is safe: a completed row is stable (verified
31
+ //! byte-identical across two later turns), so the server's content dedup
32
+ //! collapses an unchanged re-read, and a row that genuinely changed is stored
33
+ //! as the new fact it is. The transforms are written to make that hold — see
34
+ //! the notes on `tool_use` and `agent_start` in [`transform`], both of which
35
+ //! avoid fields that opencode rewrites so their re-reads hash the same.
36
+ //!
37
+ //! The second half of the story is that a row is only shipped once opencode
38
+ //! has finished writing it, which the row itself says: a text part carries
39
+ //! `time.end` when the stream is done, a tool part a terminal `state.status`.
40
+ //! Without that, a 30-second streamed answer polled every few seconds becomes
41
+ //! a dozen `model_response` rows, each a longer prefix of the same text.
42
+ //!
43
+ //! # `session.directory`, never `session.path` or `project_id`
44
+ //!
45
+ //! The agent id is `opencode-<project>` derived from `session.directory`, which
46
+ //! is the absolute working directory and is reliable. The two neighbouring
47
+ //! columns are traps, both verified on the capture:
48
+ //!
49
+ //! * `session.path` is relative to a git worktree, so it is **the empty string
50
+ //! at the repository root** — exactly the case that matters most. (Non-git
51
+ //! directories get a relative-looking `tmp/probe-opencode`, which is worse:
52
+ //! it looks usable.)
53
+ //! * `session.project_id` is a synthetic `"global"` for every session started
54
+ //! outside a git repository, so it groups unrelated projects together.
55
+ //!
56
+ //! # The `event` table is a better tail target that we deliberately do not use
57
+ //!
58
+ //! opencode also keeps an append-only, event-sourced `event(id, aggregate_id,
59
+ //! seq, type, data)` table with `UNIQUE(aggregate_id, seq)` — a monotonic
60
+ //! per-session sequence, which is a far nicer thing to tail than a mutating
61
+ //! row. It is not the durable copy: **three of the shipped schema migrations
62
+ //! contain `DELETE FROM event`** (counted in the 1.18.11 binary), so
63
+ //! history behind a watermark can vanish on upgrade. `message` and `part` have
64
+ //! no such migration and are the durable source; `event` is a cache in front of
65
+ //! them. Worth revisiting if opencode ever commits to retaining it.
66
+ //!
67
+ //! # `session_message` is dead
68
+ //!
69
+ //! `session_message` looks like exactly the table this source wants and is
70
+ //! permanently empty — it is legacy, and four migrations `DELETE FROM` it.
71
+ //! All content lives in `part.data`; `message.data` is metadata only (`role`,
72
+ //! tokens, timing, `finish`), which is why every part is read joined to its
73
+ //! message rather than instead of it.
74
+ //!
75
+ //! # Scope
76
+ //!
77
+ //! `text` and `tool` parts, plus the session's start and end. `step-start`,
78
+ //! `step-finish`, `reasoning`, `snapshot`, `patch` and `file` parts emit
79
+ //! nothing: they are turn structure or need event types of their own, and
80
+ //! ignoring an unrecognised part type is also what keeps a new opencode
81
+ //! release from breaking this source rather than merely widening it.
82
+
83
+ pub mod transform;
84
+
85
+ use std::path::PathBuf;
86
+
87
+ use rusqlite::{Connection, ToSql};
88
+ use serde_json::Value;
89
+
90
+ use crate::sqlitepoll::{PollOutcome, PollRequest, SqliteFormat, Watermark};
91
+ use transform::{PartRow, SessionRow};
92
+
93
+ /// The format the poller drives.
94
+ pub const FORMAT: SqliteFormat = SqliteFormat {
95
+ kind: "opencode",
96
+ watermark: Watermark::UpdatedAt,
97
+ poll,
98
+ };
99
+
100
+ /// The agent id used when a session's directory yields no usable project.
101
+ pub const DEFAULT_AGENT_ID: &str = "opencode";
102
+
103
+ /// `$XDG_DATA_HOME/opencode/opencode.db`, else `~/.local/share/opencode/…`.
104
+ ///
105
+ /// opencode resolves its data directory with the `xdg-basedir` package, which
106
+ /// returns `~/.local/share` on macOS as well as Linux — so this one rule
107
+ /// covers both platforms rather than needing a `cfg`.
108
+ pub fn default_db_path() -> PathBuf {
109
+ std::env::var_os("XDG_DATA_HOME")
110
+ .map(PathBuf::from)
111
+ .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
112
+ .unwrap_or_else(|| PathBuf::from(".local/share"))
113
+ .join("opencode")
114
+ .join("opencode.db")
115
+ }
116
+
117
+ /// Everything a `session` row contributes, including the two facts that need a
118
+ /// correlated subquery: its earliest part (the goal, and proof the session was
119
+ /// actually used) and its latest message (whether a turn is in flight).
120
+ const SESSION_SELECT: &str = "\
121
+ SELECT s.id, s.directory, s.version, s.slug, s.parent_id, s.time_created, s.time_updated, \
122
+ s.tokens_input, s.tokens_output, s.tokens_reasoning, \
123
+ s.tokens_cache_read, s.tokens_cache_write, \
124
+ (SELECT p.data FROM part p WHERE p.session_id = s.id \
125
+ ORDER BY p.time_created, p.id LIMIT 1), \
126
+ (SELECT m.data FROM message m WHERE m.session_id = s.id \
127
+ ORDER BY m.time_created DESC, m.id DESC LIMIT 1) \
128
+ FROM session s";
129
+ const SESSION_ORDER: &str = " ORDER BY s.time_updated, s.id";
130
+
131
+ /// A part joined to its message (for the role and model — a part carries
132
+ /// neither) and to its session (for the directory the agent id derives from).
133
+ const PART_SELECT: &str = "\
134
+ SELECT p.id, p.session_id, p.message_id, p.time_created, p.time_updated, p.data, \
135
+ m.data, s.directory \
136
+ FROM part p \
137
+ JOIN message m ON m.id = p.message_id \
138
+ JOIN session s ON s.id = p.session_id";
139
+ const PART_ORDER: &str = " ORDER BY p.time_updated, p.id";
140
+
141
+ /// One poll: read both streams past the watermark, merge them into one
142
+ /// time-ordered batch, and transform.
143
+ ///
144
+ /// Pure with respect to `(database contents, request)`, which the module docs
145
+ /// explain is what makes re-reading a mutated row safe.
146
+ fn poll(conn: &Connection, req: &PollRequest) -> rusqlite::Result<PollOutcome> {
147
+ let limit = req.max_rows.max(1);
148
+ // One row past the limit, because the extra row is the only thing that
149
+ // reveals a batch ending in the MIDDLE of a millisecond. `time_updated` is
150
+ // milliseconds, so several rows routinely share one value; cutting between
151
+ // two of them and then asking for `time_updated > :last` skips the rest of
152
+ // that millisecond forever.
153
+ let fetch = limit.saturating_add(1) as i64;
154
+
155
+ let mut rows = read_after(conn, req.watermark, fetch)?;
156
+ let mut rows_seen = rows.len() as u64;
157
+ let more = rows_seen > limit;
158
+
159
+ if more {
160
+ let cut = rows[limit as usize].time_updated();
161
+ rows.truncate(limit as usize);
162
+ while rows.last().is_some_and(|r| r.time_updated() == cut) {
163
+ rows.pop();
164
+ }
165
+ if rows.is_empty() {
166
+ // Degenerate: a single millisecond holds more rows than the whole
167
+ // limit, so trimming to a millisecond boundary leaves nothing and
168
+ // the watermark can never advance past it. Take that millisecond
169
+ // whole instead — it is one instant of one product's writes, so it
170
+ // is bounded however small the configured limit is.
171
+ rows = read_at(conn, cut)?;
172
+ rows_seen += rows.len() as u64;
173
+ }
174
+ }
175
+
176
+ // Only ever the last SHIPPED row: a watermark past a row whose events were
177
+ // trimmed away would drop it.
178
+ let watermark = rows.last().map_or(req.watermark, Row::time_updated);
179
+ let events = rows
180
+ .iter()
181
+ .flat_map(|r| r.events(&req.environment, &req.agent_id))
182
+ .collect();
183
+
184
+ Ok(PollOutcome {
185
+ events,
186
+ watermark,
187
+ more,
188
+ rows_seen,
189
+ })
190
+ }
191
+
192
+ /// Both streams strictly past the watermark, merged and ordered.
193
+ fn read_after(conn: &Connection, watermark: i64, limit: i64) -> rusqlite::Result<Vec<Row>> {
194
+ let sessions = format!("{SESSION_SELECT} WHERE s.time_updated > ?1{SESSION_ORDER} LIMIT ?2");
195
+ let parts = format!("{PART_SELECT} WHERE p.time_updated > ?1{PART_ORDER} LIMIT ?2");
196
+ merge(
197
+ query(conn, &sessions, &[&watermark, &limit], map_session)?,
198
+ query(conn, &parts, &[&watermark, &limit], map_part)?,
199
+ )
200
+ }
201
+
202
+ /// Both streams for exactly one millisecond, unlimited. The safety valve for
203
+ /// the degenerate case in [`poll`].
204
+ fn read_at(conn: &Connection, at: i64) -> rusqlite::Result<Vec<Row>> {
205
+ let sessions = format!("{SESSION_SELECT} WHERE s.time_updated = ?1{SESSION_ORDER}");
206
+ let parts = format!("{PART_SELECT} WHERE p.time_updated = ?1{PART_ORDER}");
207
+ merge(
208
+ query(conn, &sessions, &[&at], map_session)?,
209
+ query(conn, &parts, &[&at], map_part)?,
210
+ )
211
+ }
212
+
213
+ fn merge(sessions: Vec<SessionRow>, parts: Vec<PartRow>) -> rusqlite::Result<Vec<Row>> {
214
+ let mut rows: Vec<Row> = sessions
215
+ .into_iter()
216
+ .map(Row::Session)
217
+ .chain(parts.into_iter().map(Row::Part))
218
+ .collect();
219
+ // `(time_updated, table, id)` and never the insertion order of the two
220
+ // queries: the batch has to come out the same way every time or a re-read
221
+ // would not reproduce the same bytes.
222
+ rows.sort_by(|a, b| {
223
+ (a.time_updated(), a.table_rank(), a.id()).cmp(&(b.time_updated(), b.table_rank(), b.id()))
224
+ });
225
+ Ok(rows)
226
+ }
227
+
228
+ fn query<T>(
229
+ conn: &Connection,
230
+ sql: &str,
231
+ args: &[&dyn ToSql],
232
+ map: fn(&rusqlite::Row) -> rusqlite::Result<T>,
233
+ ) -> rusqlite::Result<Vec<T>> {
234
+ let mut stmt = conn.prepare(sql)?;
235
+ let rows = stmt.query_map(args, map)?;
236
+ rows.collect()
237
+ }
238
+
239
+ /// One row from either stream.
240
+ enum Row {
241
+ Session(SessionRow),
242
+ Part(PartRow),
243
+ }
244
+
245
+ impl Row {
246
+ fn time_updated(&self) -> i64 {
247
+ match self {
248
+ Row::Session(s) => s.time_updated,
249
+ Row::Part(p) => p.time_updated,
250
+ }
251
+ }
252
+
253
+ /// Sessions before parts within one millisecond, so a session's
254
+ /// `agent_start` is spooled ahead of the content of the turn that created
255
+ /// it on a first full read.
256
+ fn table_rank(&self) -> u8 {
257
+ match self {
258
+ Row::Session(_) => 0,
259
+ Row::Part(_) => 1,
260
+ }
261
+ }
262
+
263
+ fn id(&self) -> &str {
264
+ match self {
265
+ Row::Session(s) => &s.id,
266
+ Row::Part(p) => &p.id,
267
+ }
268
+ }
269
+
270
+ fn events(&self, environment: &str, fallback_agent_id: &str) -> Vec<Value> {
271
+ match self {
272
+ Row::Session(s) => transform::session_events(s, environment, fallback_agent_id),
273
+ Row::Part(p) => transform::part_events(p, environment, fallback_agent_id),
274
+ }
275
+ }
276
+ }
277
+
278
+ fn map_session(r: &rusqlite::Row) -> rusqlite::Result<SessionRow> {
279
+ Ok(SessionRow {
280
+ id: r.get(0)?,
281
+ directory: r.get::<_, Option<String>>(1)?.unwrap_or_default(),
282
+ version: r.get(2)?,
283
+ slug: r.get(3)?,
284
+ parent_id: r.get(4)?,
285
+ time_created: r.get::<_, Option<i64>>(5)?.unwrap_or_default(),
286
+ time_updated: r.get::<_, Option<i64>>(6)?.unwrap_or_default(),
287
+ tokens_input: r.get::<_, Option<i64>>(7)?.unwrap_or_default(),
288
+ tokens_output: r.get::<_, Option<i64>>(8)?.unwrap_or_default(),
289
+ tokens_reasoning: r.get::<_, Option<i64>>(9)?.unwrap_or_default(),
290
+ tokens_cache_read: r.get::<_, Option<i64>>(10)?.unwrap_or_default(),
291
+ tokens_cache_write: r.get::<_, Option<i64>>(11)?.unwrap_or_default(),
292
+ first_part: parse_json(r.get::<_, Option<String>>(12)?),
293
+ last_message: parse_json(r.get::<_, Option<String>>(13)?),
294
+ })
295
+ }
296
+
297
+ fn map_part(r: &rusqlite::Row) -> rusqlite::Result<PartRow> {
298
+ Ok(PartRow {
299
+ id: r.get(0)?,
300
+ session_id: r.get(1)?,
301
+ message_id: r.get(2)?,
302
+ time_created: r.get::<_, Option<i64>>(3)?.unwrap_or_default(),
303
+ time_updated: r.get::<_, Option<i64>>(4)?.unwrap_or_default(),
304
+ data: parse_json(r.get::<_, Option<String>>(5)?).unwrap_or(Value::Null),
305
+ message: parse_json(r.get::<_, Option<String>>(6)?).unwrap_or(Value::Null),
306
+ directory: r.get::<_, Option<String>>(7)?.unwrap_or_default(),
307
+ })
308
+ }
309
+
310
+ /// Unparseable JSON is a row we cannot describe, not a poll we should fail:
311
+ /// one malformed `data` blob must not stop the other product's whole history
312
+ /// from shipping.
313
+ fn parse_json(raw: Option<String>) -> Option<Value> {
314
+ serde_json::from_str(raw.as_deref()?).ok()
315
+ }