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,500 @@
1
+ //! OpenClaw transcript line → AgentEye events.
2
+ //!
3
+ //! Every function here is pure in `(line, ctx, offset, state)`, which is what
4
+ //! makes a live tail and a full re-read byte-identical and therefore
5
+ //! dedup-collapsible. Nothing may consult the clock, a counter, or anything
6
+ //! outside its arguments.
7
+ //!
8
+ //! Record shapes verified against a live containerised probe capture. Each
9
+ //! record chains to the previous one through `id`/`parentId` (8 hex chars), and
10
+ //! carries a record-level ISO-8601 `timestamp`:
11
+ //!
12
+ //! ```text
13
+ //! session {type:"session", version, id, timestamp, cwd}
14
+ //! model_change {type:"model_change", provider, modelId, …}
15
+ //! thinking_level_change {type:"thinking_level_change", thinkingLevel, …}
16
+ //! custom {type:"custom", customType, data:{…}, …}
17
+ //! message {type:"message", timestamp, message:{role, …}}
18
+ //! ```
19
+ //!
20
+ //! `message` has THREE roles, not two — `toolResult` is its own role on its own
21
+ //! record rather than a block inside a user turn the way Claude writes it:
22
+ //!
23
+ //! ```text
24
+ //! user {role:"user", content:[{type:"text",text}], timestamp}
25
+ //! assistant {role:"assistant", content:[{type:"text"|"toolCall", …}],
26
+ //! api, provider, model, usage, stopReason, responseId, timestamp}
27
+ //! toolResult {role:"toolResult", toolCallId, toolName,
28
+ //! content:[{type:"text",text}], details?, isError, timestamp}
29
+ //! ```
30
+ //!
31
+ //! # The two clocks
32
+ //!
33
+ //! Every record carries a record-level ISO-8601 `timestamp` AND an inner
34
+ //! `message.timestamp` in epoch **milliseconds**, and they are not the same
35
+ //! instant. On an assistant record the inner one is when the model request was
36
+ //! issued, the outer one is when the response was written: measured on the
37
+ //! capture, `2026-08-03T08:01:44.024Z` against an inner `1785744100111` =
38
+ //! `08:01:40.111`, a 3.9-second gap. Only the outer clock is present on all
39
+ //! five record types and only the outer clock advances with the file, so it is
40
+ //! the single source of event time here. Mixing the two — outer for a tool
41
+ //! result, inner for the assistant turn that requested it — would silently
42
+ //! reorder a session's timeline by seconds.
43
+
44
+ use serde_json::{Map, Value, json};
45
+
46
+ use crate::cursor::TailState;
47
+ use crate::filetail::Ctx;
48
+
49
+ /// Longest derived id component.
50
+ const MAX_ID_PART: usize = 48;
51
+ /// Longest session goal kept from the opening prompt.
52
+ const MAX_GOAL_CHARS: usize = 1024;
53
+
54
+ /// Make a derived id component safe and bounded.
55
+ pub fn sanitize_id_part(s: &str) -> String {
56
+ let cleaned: String = s
57
+ .chars()
58
+ .map(|c| {
59
+ if c.is_ascii_alphanumeric() || c == '.' || c == '_' {
60
+ c
61
+ } else {
62
+ '-'
63
+ }
64
+ })
65
+ .collect();
66
+ cleaned
67
+ .split('-')
68
+ .filter(|p| !p.is_empty())
69
+ .collect::<Vec<_>>()
70
+ .join("-")
71
+ .chars()
72
+ .take(MAX_ID_PART)
73
+ .collect()
74
+ }
75
+
76
+ /// Normalize a transcript timestamp to the microsecond form ingest expects,
77
+ /// offset by `index` microseconds.
78
+ ///
79
+ /// The offset is what keeps several events derived from ONE line in order: the
80
+ /// server sorts by `(ts, random id)`, so the two `toolCall` blocks OpenClaw
81
+ /// packs into a single assistant record — both of which really do share the
82
+ /// record's millisecond — would otherwise come back shuffled, showing a session
83
+ /// reading a file before it listed the directory. Index 0 is untouched, so the
84
+ /// primary event of a line keeps the line's exact time.
85
+ ///
86
+ /// Saturates within the second rather than carrying, so an offset can never
87
+ /// reorder an event past a genuinely later line.
88
+ pub fn with_index(ts: &str, index: usize) -> Option<String> {
89
+ let body = ts.strip_suffix('Z')?;
90
+ let (main, frac) = match body.rsplit_once('.') {
91
+ Some((m, f)) => (m, f),
92
+ None => (body, ""),
93
+ };
94
+ // OpenClaw writes milliseconds; pad to microseconds so every event has one
95
+ // shape and sorts against the hook source's events correctly.
96
+ let mut micros: u32 = if frac.is_empty() {
97
+ 0
98
+ } else {
99
+ let padded = format!("{frac:0<6}");
100
+ padded.get(..6)?.parse().ok()?
101
+ };
102
+ micros = (micros + index.min(999) as u32).min(999_999);
103
+ Some(format!("{main}.{micros:06}Z"))
104
+ }
105
+
106
+ /// The record-level timestamp — never `message.timestamp`. See the module docs.
107
+ fn ts_of(v: &Value) -> Option<&str> {
108
+ v.get("timestamp")?.as_str()
109
+ }
110
+
111
+ fn str_at<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
112
+ v.get(key)?.as_str()
113
+ }
114
+
115
+ /// The envelope every emitted event carries.
116
+ ///
117
+ /// `openclaw_line_offset` is the dedup discriminator: two identical events from
118
+ /// different lines must hash differently, or the server collapses them — and a
119
+ /// session that asks the same question twice is completely ordinary.
120
+ fn base(ctx: &Ctx, kind: &str, ts: &str, index: usize, offset: u64) -> Option<Map<String, Value>> {
121
+ let mut m = Map::new();
122
+ m.insert("timestamp".into(), json!(with_index(ts, index)?));
123
+ m.insert("session_id".into(), json!(ctx.session_id));
124
+ m.insert("agent_id".into(), json!(ctx.agent_id));
125
+ m.insert("type".into(), json!(kind));
126
+ m.insert("environment".into(), json!(ctx.environment));
127
+ m.insert("openclaw_line_offset".into(), json!(offset));
128
+ if index > 0 {
129
+ m.insert("openclaw_block_index".into(), json!(index));
130
+ }
131
+ Some(m)
132
+ }
133
+
134
+ /// Flatten a content array to its text.
135
+ ///
136
+ /// OpenClaw's content is always an array of typed blocks, for all three roles —
137
+ /// a user prompt is `[{type:"text",text}]`, not the bare string Claude writes.
138
+ /// The string arm is tolerance for a shape change, not an observed shape:
139
+ /// without it a future bare string would be rendered through `to_string()` and
140
+ /// arrive quoted and backslash-escaped.
141
+ fn joined_text(content: &Value) -> String {
142
+ match content {
143
+ Value::Array(items) => items
144
+ .iter()
145
+ .filter(|b| str_at(b, "type") == Some("text"))
146
+ .filter_map(|b| str_at(b, "text"))
147
+ .collect::<Vec<_>>()
148
+ .join("\n"),
149
+ Value::String(s) => s.clone(),
150
+ Value::Null => String::new(),
151
+ other => other.to_string(),
152
+ }
153
+ }
154
+
155
+ /// Build the session's `agent_start` from its header lines.
156
+ ///
157
+ /// Scans for the first line carrying a timestamp rather than trusting line 1,
158
+ /// because a session with no start event is not merely incomplete — the server
159
+ /// selects sessions on this event, so it is absent from the product entirely.
160
+ pub fn agent_start(header: &[String], ctx: &Ctx, offset: u64) -> Option<(Value, Option<String>)> {
161
+ let mut first_ts: Option<String> = None;
162
+ let mut cwd: Option<String> = None;
163
+ let mut version: Option<u64> = None;
164
+ let mut provider: Option<String> = None;
165
+ let mut model: Option<String> = None;
166
+ let mut goal: Option<String> = None;
167
+
168
+ for line in header {
169
+ let Ok(v) = serde_json::from_str::<Value>(line) else {
170
+ continue;
171
+ };
172
+ if first_ts.is_none()
173
+ && let Some(t) = ts_of(&v)
174
+ {
175
+ first_ts = Some(t.to_string());
176
+ }
177
+ match str_at(&v, "type") {
178
+ Some("session") => {
179
+ if cwd.is_none() {
180
+ cwd = str_at(&v, "cwd").map(str::to_string);
181
+ }
182
+ if version.is_none() {
183
+ version = v.get("version").and_then(|x| x.as_u64());
184
+ }
185
+ }
186
+ // The model reaches the start event from the same record that
187
+ // seeds `TailState`, so the session header and its first prompt
188
+ // agree on it instead of the header saying nothing.
189
+ Some("model_change") => {
190
+ if provider.is_none() {
191
+ provider = str_at(&v, "provider").map(str::to_string);
192
+ }
193
+ if model.is_none() {
194
+ model = str_at(&v, "modelId").map(str::to_string);
195
+ }
196
+ }
197
+ // The session's goal is its first real human prompt.
198
+ Some("message") if goal.is_none() => {
199
+ let Some(message) = v.get("message") else {
200
+ continue;
201
+ };
202
+ if str_at(message, "role") != Some("user") {
203
+ continue;
204
+ }
205
+ let text = joined_text(message.get("content").unwrap_or(&Value::Null));
206
+ if !text.is_empty() {
207
+ goal = Some(text.chars().take(MAX_GOAL_CHARS).collect());
208
+ }
209
+ }
210
+ _ => {}
211
+ }
212
+ }
213
+
214
+ let ts = first_ts?;
215
+ let mut m = base(ctx, "agent_start", &ts, 0, offset)?;
216
+ if let Some(g) = goal {
217
+ m.insert("goal".into(), json!(g));
218
+ }
219
+ // The agent workspace, not a project directory — recorded for provenance,
220
+ // never used to group sessions. See the module docs on `mod.rs`.
221
+ if let Some(c) = cwd {
222
+ m.insert("openclaw_cwd".into(), json!(c));
223
+ }
224
+ if let Some(v) = version {
225
+ m.insert("openclaw_session_version".into(), json!(v));
226
+ }
227
+ if let Some(p) = provider {
228
+ m.insert("openclaw_provider".into(), json!(p));
229
+ }
230
+ if let Some(x) = model {
231
+ m.insert("model".into(), json!(x));
232
+ }
233
+ Some((Value::Object(m), Some(ts)))
234
+ }
235
+
236
+ /// The single `agent_end`, derived deterministically from the last timestamp
237
+ /// and the file size (used as its offset, so it is unique per session).
238
+ ///
239
+ /// Index 999 puts it after every content event sharing that timestamp — the
240
+ /// server's tie-break is a random id, so without this a session can appear to
241
+ /// end before its last turn.
242
+ pub fn agent_end(ctx: &Ctx, last_ts: &str, size: u64) -> Value {
243
+ match base(ctx, "agent_end", last_ts, 999, size) {
244
+ Some(m) => Value::Object(m),
245
+ // `base` only fails on an unparseable timestamp, which cannot happen
246
+ // here: `last_ts` came from a line this transform already accepted.
247
+ None => json!({
248
+ "timestamp": last_ts,
249
+ "session_id": ctx.session_id,
250
+ "agent_id": ctx.agent_id,
251
+ "type": "agent_end",
252
+ "environment": ctx.environment,
253
+ }),
254
+ }
255
+ }
256
+
257
+ /// One content line to its timestamp and the events it yields.
258
+ pub fn transform_line(
259
+ line: &str,
260
+ ctx: &Ctx,
261
+ offset: u64,
262
+ state: &mut TailState,
263
+ ) -> (Option<String>, Vec<Value>) {
264
+ let Ok(v) = serde_json::from_str::<Value>(line) else {
265
+ return (None, Vec::new());
266
+ };
267
+ let Some(ts) = ts_of(&v).map(str::to_string) else {
268
+ return (None, Vec::new());
269
+ };
270
+
271
+ let events = match str_at(&v, "type") {
272
+ Some("message") => message_events(&v, ctx, &ts, offset, state),
273
+ // Not an event, but the record that makes the session's FIRST prompt
274
+ // render with a model. A user record names no model, and the server
275
+ // builds a `model_request` row's summary from the model alone — so
276
+ // without this the opening row of every session is blank, which is the
277
+ // row most likely to be looked at.
278
+ Some("model_change") => {
279
+ if let Some(m) = str_at(&v, "modelId") {
280
+ state.last_model = Some(m.to_string());
281
+ }
282
+ Vec::new()
283
+ }
284
+ // `session`, `thinking_level_change` and `custom` describe the harness,
285
+ // not the conversation. They still return their timestamp above, so a
286
+ // trailing `custom` record — which is exactly what the capture ends
287
+ // with — carries `agent_end` to the true end of the session rather than
288
+ // leaving it stuck at the last message.
289
+ _ => Vec::new(),
290
+ };
291
+ (Some(ts), events)
292
+ }
293
+
294
+ fn message_events(
295
+ v: &Value,
296
+ ctx: &Ctx,
297
+ ts: &str,
298
+ offset: u64,
299
+ state: &mut TailState,
300
+ ) -> Vec<Value> {
301
+ let Some(message) = v.get("message") else {
302
+ return Vec::new();
303
+ };
304
+ match str_at(message, "role") {
305
+ Some("user") => user_events(message, ctx, ts, offset, state),
306
+ Some("assistant") => assistant_events(message, ctx, ts, offset, state),
307
+ Some("toolResult") => tool_result_events(message, ctx, ts, offset, state),
308
+ _ => Vec::new(),
309
+ }
310
+ }
311
+
312
+ /// A `user` record is a human prompt. Tool results are their own role in
313
+ /// OpenClaw, so unlike Claude this branch never has to disambiguate.
314
+ fn user_events(message: &Value, ctx: &Ctx, ts: &str, offset: u64, state: &TailState) -> Vec<Value> {
315
+ let text = joined_text(message.get("content").unwrap_or(&Value::Null));
316
+ if text.is_empty() {
317
+ return Vec::new();
318
+ }
319
+ let Some(mut m) = base(ctx, "model_request", ts, 0, offset) else {
320
+ return Vec::new();
321
+ };
322
+ // Inherited from the last `model_change` or assistant turn: a user record
323
+ // names no model.
324
+ if let Some(model) = &state.last_model {
325
+ m.insert("model".into(), json!(model));
326
+ }
327
+ m.insert(
328
+ "messages".into(),
329
+ json!([{ "role": "user", "content": text }]),
330
+ );
331
+ vec![Value::Object(m)]
332
+ }
333
+
334
+ /// An `assistant` record is text and/or tool calls, plus token usage.
335
+ fn assistant_events(
336
+ message: &Value,
337
+ ctx: &Ctx,
338
+ ts: &str,
339
+ offset: u64,
340
+ state: &mut TailState,
341
+ ) -> Vec<Value> {
342
+ let model = str_at(message, "model");
343
+ if let Some(m) = model {
344
+ state.last_model = Some(m.to_string());
345
+ }
346
+ let provider = str_at(message, "provider");
347
+ let response_id = str_at(message, "responseId");
348
+ let stop_reason = str_at(message, "stopReason");
349
+
350
+ let Some(blocks) = message.get("content").and_then(|c| c.as_array()) else {
351
+ return Vec::new();
352
+ };
353
+
354
+ let mut out: Vec<Value> = Vec::new();
355
+ for (i, block) in blocks.iter().enumerate() {
356
+ match str_at(block, "type") {
357
+ Some("text") => {
358
+ let Some(text) = str_at(block, "text").filter(|t| !t.is_empty()) else {
359
+ continue;
360
+ };
361
+ let Some(mut m) = base(ctx, "model_response", ts, i, offset) else {
362
+ continue;
363
+ };
364
+ m.insert("role".into(), json!("assistant"));
365
+ m.insert("content".into(), json!(text));
366
+ if let Some(x) = model {
367
+ m.insert("model".into(), json!(x));
368
+ }
369
+ if let Some(x) = provider {
370
+ m.insert("openclaw_provider".into(), json!(x));
371
+ }
372
+ if let Some(x) = response_id {
373
+ m.insert("openclaw_response_id".into(), json!(x));
374
+ }
375
+ if let Some(x) = stop_reason {
376
+ m.insert("openclaw_stop_reason".into(), json!(x));
377
+ }
378
+ out.push(Value::Object(m));
379
+ }
380
+ Some("toolCall") => {
381
+ let Some(mut m) = base(ctx, "tool_use", ts, i, offset) else {
382
+ continue;
383
+ };
384
+ let name = str_at(block, "name").unwrap_or("tool");
385
+ // Fall back to an offset-derived id so two identical calls in
386
+ // one record cannot hash-collapse into a single row.
387
+ let id = str_at(block, "id")
388
+ .map(str::to_string)
389
+ .unwrap_or_else(|| format!("openclaw-{offset}-{i}"));
390
+ state.remember_tool(id.clone(), name.to_string());
391
+ m.insert("tool_name".into(), json!(name));
392
+ m.insert("tool_call_id".into(), json!(id));
393
+ // `arguments` only. The sibling `partialArgs` is the same
394
+ // object re-encoded as a JSON string — shipping both doubles
395
+ // every tool call's payload to say the same thing twice.
396
+ if let Some(args) = block.get("arguments") {
397
+ m.insert("input".into(), args.clone());
398
+ }
399
+ if let Some(x) = model {
400
+ m.insert("model".into(), json!(x));
401
+ }
402
+ if let Some(x) = response_id {
403
+ m.insert("openclaw_response_id".into(), json!(x));
404
+ }
405
+ out.push(Value::Object(m));
406
+ }
407
+ // Anything else (a future thinking/reasoning block) is skipped
408
+ // rather than guessed at, but still consumes its index, so the
409
+ // blocks around it keep their relative order.
410
+ _ => {}
411
+ }
412
+ }
413
+
414
+ // Attach usage to the FIRST event of the record, so a record yielding
415
+ // several events reports its tokens once.
416
+ //
417
+ // No message-id guard here, unlike the Claude source: OpenClaw writes one
418
+ // record per API response — every block of a response shares that record's
419
+ // `content` array, and each record carries its own `responseId` — so usage
420
+ // is never repeated across lines and gating on an id could only drop a
421
+ // legitimate second response.
422
+ if let Some(usage) = message.get("usage")
423
+ && let Some(first) = out.first_mut()
424
+ && let Some(obj) = first.as_object_mut()
425
+ {
426
+ if let Some(n) = usage.get("input").and_then(|x| x.as_u64()) {
427
+ obj.insert("input_tokens".into(), json!(n));
428
+ }
429
+ if let Some(n) = usage.get("output").and_then(|x| x.as_u64()) {
430
+ obj.insert("output_tokens".into(), json!(n));
431
+ }
432
+ obj.insert("openclaw_usage".into(), usage.clone());
433
+ }
434
+
435
+ out
436
+ }
437
+
438
+ /// A `toolResult` record — its own role on its own record, paired back to the
439
+ /// call by `toolCallId` == the call's `id`.
440
+ fn tool_result_events(
441
+ message: &Value,
442
+ ctx: &Ctx,
443
+ ts: &str,
444
+ offset: u64,
445
+ state: &TailState,
446
+ ) -> Vec<Value> {
447
+ let Some(mut m) = base(ctx, "tool_result", ts, 0, offset) else {
448
+ return Vec::new();
449
+ };
450
+ let call_id = str_at(message, "toolCallId");
451
+ if let Some(id) = call_id {
452
+ m.insert("tool_call_id".into(), json!(id));
453
+ }
454
+ // OpenClaw usually names the tool on the result too. When it does not, the
455
+ // name carried from the call is the only thing standing between this and a
456
+ // blank row, because the server builds a result row's summary from the tool
457
+ // name alone.
458
+ let name = str_at(message, "toolName")
459
+ .filter(|n| !n.is_empty())
460
+ .or_else(|| call_id.and_then(|id| state.tool_name(id)));
461
+ if let Some(n) = name {
462
+ m.insert("tool_name".into(), json!(n));
463
+ }
464
+
465
+ let output = joined_text(message.get("content").unwrap_or(&Value::Null));
466
+ m.insert("output".into(), json!(output));
467
+
468
+ // `details` is absent on some results — the capture's `read` result has
469
+ // none while its `exec` results do — so every field under it is optional
470
+ // and its absence must not cost the whole event.
471
+ if let Some(details) = message.get("details") {
472
+ if let Some(d) = details.get("durationMs").and_then(|x| x.as_f64()) {
473
+ m.insert("duration_ms".into(), json!(d));
474
+ }
475
+ if let Some(s) = str_at(details, "status") {
476
+ m.insert("openclaw_tool_status".into(), json!(s));
477
+ }
478
+ if let Some(c) = details.get("exitCode").and_then(|x| x.as_i64()) {
479
+ m.insert("openclaw_exit_code".into(), json!(c));
480
+ }
481
+ // `details.aggregated` and `details.cwd` are deliberately dropped:
482
+ // `aggregated` is a byte-for-byte copy of the text content above, and
483
+ // `cwd` is the same fixed agent workspace on every result in the
484
+ // session.
485
+ }
486
+
487
+ if message.get("isError").and_then(|e| e.as_bool()) == Some(true) {
488
+ // The server's `is_error` is a truthiness check, so an empty string
489
+ // would render a failed tool call as a success.
490
+ let reason = if output.trim().is_empty() {
491
+ "openclaw reported a tool failure with no message".to_string()
492
+ } else {
493
+ output
494
+ };
495
+ m.insert("error".into(), json!(reason));
496
+ m.insert("error_type".into(), json!("openclaw_tool_error"));
497
+ }
498
+
499
+ vec![Value::Object(m)]
500
+ }