failproofai 1.0.7-beta.1 → 1.0.7

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 (429) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  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 +6 -6
  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/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +5 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -13
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +7 -8
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +6 -7
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -41
  63. package/.next/standalone/.next/server/app/settings/page.js +9 -12
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  75. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
  98. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
  104. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  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 +23 -56
  121. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
  122. package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
  123. package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
  125. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
  126. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
  127. package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
  130. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
  131. package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
  133. package/.next/standalone/SECURITY.md +53 -0
  134. package/.next/standalone/app/actions/pack-actions.ts +0 -12
  135. package/.next/standalone/app/policies/hooks-client.tsx +0 -9
  136. package/.next/standalone/app/settings/page.tsx +1 -20
  137. package/.next/standalone/app/settings/settings-client.tsx +1 -27
  138. package/.next/standalone/app/settings/settings.css +0 -79
  139. package/.next/standalone/package.json +9 -9
  140. package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
  141. package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
  142. package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
  143. package/.next/standalone/sdk/python/skill/references/events.md +17 -8
  144. package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
  145. package/.next/standalone/sdk/python/skill/references/install.md +3 -0
  146. package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
  147. package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
  148. package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
  149. package/.next/standalone/sdk/typescript/LICENSE +42 -0
  150. package/.next/standalone/sdk/typescript/README.md +552 -0
  151. package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
  152. package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
  153. package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
  154. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
  156. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
  157. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
  158. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
  159. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
  160. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
  161. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
  162. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
  163. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
  164. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
  165. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
  174. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
  175. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
  176. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
  177. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
  180. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
  181. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
  182. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
  183. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
  184. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
  185. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
  186. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
  187. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
  188. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
  189. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
  190. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
  191. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
  192. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
  193. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
  194. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
  195. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
  196. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
  197. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
  198. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
  199. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
  200. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
  201. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
  202. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
  203. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
  204. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
  205. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
  206. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
  207. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
  208. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
  209. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
  210. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
  211. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
  212. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
  213. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
  214. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
  215. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
  216. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
  217. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
  218. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
  219. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
  220. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
  221. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
  222. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
  223. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
  224. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
  225. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
  226. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
  227. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
  228. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
  229. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
  230. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
  231. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
  232. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
  233. package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
  234. package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
  235. package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
  236. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
  237. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
  238. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
  239. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
  240. package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
  241. package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
  242. package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
  243. package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
  244. package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
  245. package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
  246. package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
  247. package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
  248. package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
  249. package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
  250. package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
  251. package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
  252. package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
  253. package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
  254. package/.next/standalone/sdk/typescript/package.json +401 -0
  255. package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
  256. package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
  257. package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
  258. package/.next/standalone/sdk/typescript/src/context.ts +214 -0
  259. package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
  260. package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
  261. package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
  262. package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
  263. package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
  264. package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
  265. package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
  266. package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
  267. package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
  268. package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
  269. package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
  270. package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
  271. package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
  272. package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
  273. package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
  274. package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
  275. package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
  276. package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
  277. package/.next/standalone/sdk/typescript/src/events.ts +879 -0
  278. package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
  279. package/.next/standalone/sdk/typescript/src/index.ts +186 -0
  280. package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
  281. package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
  282. package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
  283. package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
  284. package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
  285. package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
  286. package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
  287. package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
  288. package/.next/standalone/sdk/typescript/src/next.ts +115 -0
  289. package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
  290. package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
  291. package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
  292. package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
  293. package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
  294. package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
  295. package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
  296. package/.next/standalone/sdk/typescript/src/version.ts +5 -0
  297. package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
  298. package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
  299. package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
  300. package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
  301. package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
  302. package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
  303. package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
  304. package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
  305. package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
  306. package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
  307. package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
  308. package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
  309. package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
  310. package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
  311. package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
  312. package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
  313. package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
  314. package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
  315. package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
  316. package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
  317. package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
  318. package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
  319. package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
  320. package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
  321. package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
  322. package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
  323. package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
  324. package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
  325. package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
  326. package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
  327. package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
  328. package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
  329. package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
  330. package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
  331. package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
  332. package/.next/standalone/server.js +1 -1
  333. package/bin/failproofai.mjs +2 -115
  334. package/dist/cli.mjs +6538 -13686
  335. package/dist/index.js +1 -19
  336. package/dist/worker.mjs +2085 -8012
  337. package/package.json +9 -9
  338. package/pi-extension/index.ts +0 -11
  339. package/scripts/build-policy-pack.mjs +2 -39
  340. package/src/hooks/builtin-policies.ts +3 -21
  341. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  342. package/src/hooks/cloud-managed-policies.ts +0 -22
  343. package/src/hooks/custom-hooks-loader.ts +7 -45
  344. package/src/hooks/custom-hooks-registry.ts +1 -45
  345. package/src/hooks/first-run-gate.ts +0 -5
  346. package/src/hooks/fp-home.ts +0 -23
  347. package/src/hooks/handler.ts +6 -265
  348. package/src/hooks/hook-activity-store.ts +1 -105
  349. package/src/hooks/hook-telemetry.ts +0 -41
  350. package/src/hooks/loader-utils.ts +0 -6
  351. package/src/hooks/pack-cli.ts +17 -278
  352. package/src/hooks/pack-manifest.ts +7 -479
  353. package/src/hooks/pack-store.ts +10 -156
  354. package/src/hooks/policy-catalog.ts +0 -65
  355. package/src/hooks/policy-evaluator.ts +796 -940
  356. package/src/hooks/policy-registry.ts +0 -25
  357. package/src/hooks/policy-types.ts +0 -126
  358. package/src/hooks/worker-server.ts +26 -119
  359. package/src/index.ts +0 -6
  360. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  361. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  362. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
  363. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
  364. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
  365. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
  367. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
  368. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
  370. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
  371. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  372. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
  383. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  384. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
  385. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
  386. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
  387. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  388. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  390. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  392. package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
  393. package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
  394. package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
  395. package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
  396. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
  397. package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
  398. package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
  399. package/.next/standalone/app/actions/get-jev-config.ts +0 -409
  400. package/.next/standalone/app/actions/update-jev-config.ts +0 -420
  401. package/.next/standalone/app/components/jev-notices.tsx +0 -96
  402. package/.next/standalone/app/settings/jev-panel.tsx +0 -469
  403. package/src/hooks/effective-reviewers.ts +0 -79
  404. package/src/hooks/jev-activity.ts +0 -385
  405. package/src/hooks/jev-cli.ts +0 -1193
  406. package/src/hooks/policy-authority.ts +0 -333
  407. package/src/hooks/policy-reviewability.ts +0 -229
  408. package/src/hooks/semantic/combine.ts +0 -541
  409. package/src/hooks/semantic/compile.ts +0 -176
  410. package/src/hooks/semantic/decide.ts +0 -392
  411. package/src/hooks/semantic/envelope.ts +0 -1296
  412. package/src/hooks/semantic/evaluator.ts +0 -547
  413. package/src/hooks/semantic/facts.ts +0 -292
  414. package/src/hooks/semantic/intent.ts +0 -1190
  415. package/src/hooks/semantic/jev-client.ts +0 -643
  416. package/src/hooks/semantic/jev-config.ts +0 -594
  417. package/src/hooks/semantic/jev-review.ts +0 -374
  418. package/src/hooks/semantic/jev-stats.ts +0 -289
  419. package/src/hooks/semantic/jev-throttle.ts +0 -421
  420. package/src/hooks/semantic/pack-policies.ts +0 -251
  421. package/src/hooks/semantic/policies.ts +0 -596
  422. package/src/hooks/semantic/precondition-names.ts +0 -60
  423. package/src/hooks/semantic/preconditions.ts +0 -58
  424. package/src/hooks/semantic/redact.ts +0 -2910
  425. package/src/hooks/semantic/types.ts +0 -145
  426. package/src/hooks/semver-precedence.ts +0 -128
  427. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
  428. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
  429. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
@@ -0,0 +1,934 @@
1
+ import {
2
+ closeSync,
3
+ fsyncSync,
4
+ mkdirSync,
5
+ openSync,
6
+ renameSync,
7
+ rmSync,
8
+ writeSync,
9
+ constants as fsConstants,
10
+ } from "node:fs";
11
+ import { mkdir, open } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+
14
+ import { runExitClosers } from "./exit.js";
15
+ import { logException, logger } from "./logger.js";
16
+ import { redactJsonLine, redactionEnabled } from "./redact.js";
17
+ import { getBaseDir } from "./resolver.js";
18
+
19
+ /**
20
+ * Per-process batch counter, so two batches written inside the same millisecond
21
+ * cannot land on the same filename.
22
+ *
23
+ * The timestamp alone is not enough, and the way it fails is invisible. Two
24
+ * batches in the same millisecond produce the same stem, and the second rename
25
+ * overwrites the first — no exception, no log line, no trace that the events
26
+ * had ever existed. Three routine situations hit it:
27
+ *
28
+ * * the exit flush racing the interval's own final cycle, which is exactly
29
+ * when the last events of a run are written;
30
+ * * any caller invoking `flushNow()` from more than one place;
31
+ * * several agent processes sharing one spool root — the normal deployment.
32
+ * Nothing in the stem identifies the writer, so unrelated processes would
33
+ * silently overwrite each other's batches.
34
+ *
35
+ * Hence the pid as well as the counter: the counter fixes the in-process race
36
+ * and the pid fixes the cross-process one. The daemons require only that a
37
+ * batch file end in `.jsonl` and not `.tmp` (`crates/fpai-collect/src/spool.rs`),
38
+ * so the rest of the stem is ours to make unique.
39
+ */
40
+ let batchSeq = 0;
41
+
42
+ /**
43
+ * Hard cap on the in-memory queue, matching `events.ts`'s `PENDING_CAP`.
44
+ *
45
+ * `submit()` is called from the caller's own agent loop and must never block or
46
+ * throw, so it cannot apply backpressure — which leaves an unbounded queue as
47
+ * the only other option, and that is a memory leak wearing a different hat. Any
48
+ * condition that stops the spool draining (a full disk, a read-only mount) then
49
+ * converts a telemetry outage into an OOM kill of the host agent. Losing the
50
+ * oldest events is the better failure: it is bounded, it is logged, and the
51
+ * events most worth having are the recent ones.
52
+ *
53
+ * At the default 500 ms interval a process would have to emit 20,000
54
+ * events/second to reach it. Anything that does hit this cap is not a busy
55
+ * agent, it is a spool that has stopped.
56
+ *
57
+ * A COUNT alone is not the bound this comment claims, because it says nothing
58
+ * about how big an event is. `integrations/core.ts` budgets 128 KiB of `fw_*`
59
+ * extras per event on top of the declared fields, so 10,000 of them is ~1.3 GB.
60
+ * So the queue is bounded by BYTES as well, below.
61
+ */
62
+ const QUEUE_CAP = 10_000;
63
+
64
+ /**
65
+ * The ceiling, enforced against MEASURED bytes rather than an estimate. Chosen
66
+ * to sit well under the memory a small container is given (512 MB is the common
67
+ * floor), because the whole point is that a spool which has stopped draining
68
+ * must not take the customer's agent down with it.
69
+ */
70
+ const QUEUE_BYTE_CAP = 64 * 1024 * 1024;
71
+
72
+ /**
73
+ * Per-STRING cap inside one event, mirroring `MAX_FIELD_BYTES` in
74
+ * `crates/fpai-collect/src/spool.rs`. The Rust spool writer has always enforced
75
+ * this; an SDK publishing into the same directories must too.
76
+ */
77
+ const MAX_FIELD_BYTES = 1024 * 1024;
78
+
79
+ /**
80
+ * Roll a batch file once it reaches this, mirroring `DEFAULT_MAX_BATCH_BYTES`
81
+ * in `spool.rs` and staying under the uploader's `DEFAULT_MAX_UPLOAD_BYTES`.
82
+ *
83
+ * `uploader.rs` documents the invariant this restores: "A single line longer
84
+ * than max is emitted alone rather than dropped: the spool writer already
85
+ * guarantees no such line exists." `splitLines` can only split on newlines — so
86
+ * one oversized event would be POSTed whole, rejected, and the WHOLE spool file
87
+ * (every unrelated event batched with it) parked, retried three times and
88
+ * poisoned. Never delivered, and nothing in the host process would ever learn.
89
+ */
90
+ const MAX_BATCH_BYTES = 8 * 1024 * 1024;
91
+
92
+ /**
93
+ * An encoded event above this is over-large on its own and gets its fields
94
+ * capped. Sits below `MAX_BATCH_BYTES` so a capped event still leaves room for
95
+ * the batch framing around it.
96
+ */
97
+ const MAX_EVENT_BYTES = 4 * 1024 * 1024;
98
+
99
+ /**
100
+ * How deep `sanitize` will walk before giving up on a branch. Guards the
101
+ * fallback path against a stack overflow, which would defeat the point of
102
+ * having a fallback at all.
103
+ */
104
+ const MAX_SANITIZE_DEPTH = 50;
105
+
106
+ /**
107
+ * How `JSON.stringify` writes a lone surrogate.
108
+ *
109
+ * Unlike Python's `ensure_ascii=True`, JavaScript emits non-ASCII characters
110
+ * literally, so this substring can only appear because a real lone surrogate
111
+ * was escaped (ES2019 well-formed `JSON.stringify`) or because the payload's
112
+ * own text contained the literal characters `\ud8…`. The second is rare enough
113
+ * to be worth the certainty; it costs one re-encode and changes nothing.
114
+ *
115
+ * The lead nibble matters. `\ud` alone also matches U+D000–U+D7FF, which is
116
+ * most of the Hangul syllable block. Real surrogates are U+D800–U+DFFF, whose
117
+ * escapes all begin `\ud8`, `\ud9`, `\uda`…`\udf`.
118
+ */
119
+ const SURROGATE_ESCAPES: readonly string[] = [..."89abcdefABCDEF"].map((c) => `\\ud${c}`);
120
+
121
+ const FIELD_TRUNCATION_MARKER = "…[truncated]";
122
+ const CYCLE_MARKER = "<circular reference>";
123
+ const DEPTH_MARKER = "<max depth exceeded>";
124
+
125
+ /**
126
+ * Every live writer. Both the exit flush and `flushAllNow()` iterate this
127
+ * rather than binding to one instance.
128
+ *
129
+ * Node has no `fork()` hazard to guard here the way the Python SDK does: a
130
+ * `worker_threads` Worker and a `cluster` child each load this module afresh
131
+ * and get their own writer with its own timer, so there is no inherited queue
132
+ * with no thread to drain it.
133
+ */
134
+ const liveWriters = new Set<EventWriter>();
135
+
136
+ /**
137
+ * A flush interval the timer can actually run on.
138
+ *
139
+ * Rejected at the boundary, where a caller still has a stack trace pointing at
140
+ * their own `configure()` call:
141
+ *
142
+ * -1 -> a timer that fires immediately and forever
143
+ * NaN -> Node coerces to 1 ms; a busy loop rewriting the spool
144
+ * Inf -> Node coerces to 1 ms; the same busy loop
145
+ * 0 -> waits not at all; pins a core and rewrites the spool as fast as
146
+ * the disk allows
147
+ *
148
+ * Every one of those is a telemetry library becoming the reason the host agent
149
+ * is slow, and none of them throws on its own.
150
+ */
151
+ export function validatedInterval(flushInterval: number): number {
152
+ const interval = Number(flushInterval);
153
+ if (!Number.isFinite(interval) || interval <= 0) {
154
+ throw new Error(
155
+ `flushInterval must be a finite number greater than zero, got ${String(flushInterval)}`,
156
+ );
157
+ }
158
+ return interval;
159
+ }
160
+
161
+ /**
162
+ * Roughly how many bytes `value` will occupy once encoded.
163
+ *
164
+ * Walks NODES, not characters: `.length` on a string is O(1), so an ordinary
165
+ * event costs microseconds even though it may carry megabytes of text. That is
166
+ * what makes it affordable on `submit`, which runs on the caller's agent loop.
167
+ *
168
+ * Deliberately approximate — it ignores JSON punctuation and escaping — because
169
+ * it backs a backstop against unbounded growth, not an exact quota.
170
+ */
171
+ export function approxSize(value: unknown, depth = 0): number {
172
+ if (depth > MAX_SANITIZE_DEPTH) return 16;
173
+ if (value === null || value === undefined) return 8;
174
+ const kind = typeof value;
175
+ if (kind === "boolean" || kind === "number" || kind === "bigint") return 8;
176
+ if (kind === "string") return (value as string).length;
177
+ // NOTHING here may throw. `submit` runs on the caller's agent loop, so an
178
+ // exception escaping this function is a telemetry call taking down the host
179
+ // agent — the one failure mode this whole module is written to avoid. A
180
+ // getter on the caller's object can throw anything at all.
181
+ try {
182
+ if (ArrayBuffer.isView(value)) return (value).byteLength;
183
+ if (Array.isArray(value)) {
184
+ let total = 0;
185
+ for (const item of value) total += approxSize(item, depth + 1);
186
+ return total;
187
+ }
188
+ if (value instanceof Map) {
189
+ let total = 0;
190
+ for (const [key, item] of value) {
191
+ total += (typeof key === "string" ? key.length : 16) + approxSize(item, depth + 1);
192
+ }
193
+ return total;
194
+ }
195
+ if (value instanceof Set) {
196
+ let total = 0;
197
+ for (const item of value) total += approxSize(item, depth + 1);
198
+ return total;
199
+ }
200
+ if (kind === "object") {
201
+ let total = 0;
202
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
203
+ total += key.length + approxSize(item, depth + 1);
204
+ }
205
+ return total;
206
+ }
207
+ } catch {
208
+ return 16;
209
+ }
210
+ return 16;
211
+ }
212
+
213
+ /** Drop a trailing high surrogate so a slice never invents a lone one. */
214
+ function sliceWithoutSplitting(value: string, end: number): string {
215
+ if (end <= 0) return "";
216
+ const code = value.charCodeAt(end - 1);
217
+ const safeEnd = code >= 0xd800 && code <= 0xdbff ? end - 1 : end;
218
+ return value.slice(0, safeEnd);
219
+ }
220
+
221
+ /**
222
+ * Truncate every string in `value` to `limit`, marking what was cut.
223
+ *
224
+ * Mirrors `truncate_strings` in `crates/fpai-collect/src/spool.rs`, which has
225
+ * always enforced this on the Rust side of the same spool.
226
+ */
227
+ export function capFields(value: unknown, limit: number, depth = 0): unknown {
228
+ if (depth > MAX_SANITIZE_DEPTH) return value;
229
+ // Same rule as `approxSize`: never throw. This runs from `encodeEntry`, whose
230
+ // contract is that ONE bad event is dropped alone rather than taking the
231
+ // batch beside it down.
232
+ try {
233
+ if (typeof value === "string" && value.length > limit) {
234
+ return sliceWithoutSplitting(value, limit) + FIELD_TRUNCATION_MARKER;
235
+ }
236
+ if (Array.isArray(value)) return value.map((item) => capFields(item, limit, depth + 1));
237
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
238
+ const out: Record<string, unknown> = {};
239
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
240
+ out[key] = capFields(item, limit, depth + 1);
241
+ }
242
+ return out;
243
+ }
244
+ } catch {
245
+ return value;
246
+ }
247
+ return value;
248
+ }
249
+
250
+ /**
251
+ * A string with any lone surrogate made inert.
252
+ *
253
+ * `\ud800`-`\udfff` outside a valid pair is what a byte sequence that is not
254
+ * valid UTF-8 becomes when it is decoded leniently — a filesystem path, a
255
+ * truncated tool output. `JSON.stringify` escapes them happily, so nothing
256
+ * fails locally, and then the SERVER skips the whole event: verified against a
257
+ * live ingest, `{"accepted":0,"skipped":1}` at 200 OK. Replacing each with its
258
+ * visible escape keeps the byte in the payload instead of dropping it.
259
+ */
260
+ export function scrubSurrogates(value: string): string {
261
+ // Matches a high surrogate not followed by a low one, or a low surrogate not
262
+ // preceded by a high one.
263
+ return value.replace(
264
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
265
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Rewrite one payload into something `JSON.stringify` can definitely encode.
271
+ *
272
+ * Only ever reached from `encodeEntry`'s fallback, so it may be slow; it must
273
+ * not be lossy in the ordinary case, and it must not throw.
274
+ *
275
+ * `seen` tracks the objects on the CURRENT PATH, not every object visited. A
276
+ * payload that mentions the same object twice as siblings is a DAG, not a
277
+ * cycle, and JSON encodes it fine — flagging it would corrupt a perfectly good
278
+ * event.
279
+ */
280
+ export function sanitize(value: unknown, seen: Set<object>, depth = 0): unknown {
281
+ if (depth > MAX_SANITIZE_DEPTH) return DEPTH_MARKER;
282
+ if (value === undefined || value === null) return null;
283
+ const kind = typeof value;
284
+ // NaN / Infinity / -Infinity. `JSON.stringify` already writes these as `null`,
285
+ // but making it explicit here means the sanitized copy and the strict copy
286
+ // agree rather than differing by an encoder default.
287
+ if (kind === "number") return Number.isFinite(value) ? value : null;
288
+ if (kind === "bigint") return (value as bigint).toString();
289
+ if (kind === "boolean") return value;
290
+ if (kind === "string") return scrubSurrogates(value as string);
291
+ if (kind === "function" || kind === "symbol") return null;
292
+
293
+ const object = value;
294
+ if (seen.has(object)) return CYCLE_MARKER;
295
+
296
+ try {
297
+ if (object instanceof Date) {
298
+ return Number.isNaN(object.getTime()) ? null : object.toISOString();
299
+ }
300
+ if (ArrayBuffer.isView(object)) return null;
301
+
302
+ seen.add(object);
303
+ try {
304
+ if (Array.isArray(object)) {
305
+ return object.map((item) => sanitize(item, seen, depth + 1));
306
+ }
307
+ if (object instanceof Set) {
308
+ return [...object].map((item) => sanitize(item, seen, depth + 1));
309
+ }
310
+ const entries =
311
+ object instanceof Map
312
+ ? [...object.entries()].map(([k, v]) => [String(k), v] as const)
313
+ : Object.entries(object as Record<string, unknown>);
314
+ const out: Record<string, unknown> = {};
315
+ for (const [key, item] of entries) {
316
+ // Keys go through the SAME surrogate scrub as values. A filesystem
317
+ // path, the realistic source, is most naturally a KEY
318
+ // (`{path: contents}`). An unscrubbed key reaches the wire as a JSON
319
+ // lone-surrogate escape, ingest answers 200 `{"accepted":0,"skipped":1}`,
320
+ // and the uploader parks that batch and poisons it after three retries
321
+ // — so one bad key loses every event batched with it.
322
+ out[scrubSurrogates(key)] = sanitize(item, seen, depth + 1);
323
+ }
324
+ return out;
325
+ } finally {
326
+ seen.delete(object);
327
+ }
328
+ } catch {
329
+ seen.delete(object);
330
+ return null;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * The replacer for the FAST path. Handles the values `JSON.stringify` cannot,
336
+ * and nothing else — this is the encoder that must stay byte-identical for
337
+ * every payload that was already valid JSON.
338
+ *
339
+ * Mirrors Python's `default=str`: the values that have an obvious textual form
340
+ * get one, and everything else falls through to the sanitized rebuild below.
341
+ * Functions and symbols are DROPPED rather than stringified — Python's
342
+ * `default=str` would put a lambda's source location on the wire, which is
343
+ * worse than an absent key.
344
+ */
345
+ function replacer(this: unknown, _key: string, value: unknown): unknown {
346
+ if (typeof value === "bigint") return value.toString();
347
+ if (value instanceof Map) return Object.fromEntries(value);
348
+ if (value instanceof Set) return [...value];
349
+ if (value instanceof Error) {
350
+ return { name: value.name, message: value.message, stack: value.stack };
351
+ }
352
+ return value;
353
+ }
354
+
355
+ /**
356
+ * One event as a JSON line, or null if it cannot be encoded at all.
357
+ *
358
+ * THE POINT IS ISOLATION. A single `JSON.stringify` over the whole batch means
359
+ * one unencodable payload takes every event beside it down: the flush restores
360
+ * the batch, the next interval retries the identical batch, and the spool never
361
+ * advances again. An object holding a back-reference — an ordinary thing to
362
+ * hand a telemetry call, and the shape of every framework's run context —
363
+ * permanently ends recording for the process.
364
+ *
365
+ * So: try strict first (the fast path, byte-identical to the plain encoder for
366
+ * every payload that was already valid JSON), fall back to a sanitised copy,
367
+ * and only then give up on that ONE event.
368
+ */
369
+ export function encodeEntry(entry: Record<string, unknown>): string | null {
370
+ let encoded: string | null = null;
371
+ // Not a narrow catch on TypeError. A `toJSON()` or a getter runs the CALLER'S
372
+ // code, which can throw anything at all — a RuntimeError out of a lazy ORM
373
+ // attribute, an error out of a property that touches the network. Those would
374
+ // propagate out of the batch write and put the whole batch back on the queue
375
+ // to be retried identically forever: the exact wedge this function exists to
376
+ // prevent, reached through a different error type.
377
+ try {
378
+ encoded = JSON.stringify(entry, replacer) ?? null;
379
+ } catch {
380
+ encoded = null;
381
+ }
382
+
383
+ if (encoded !== null && !SURROGATE_ESCAPES.some((escape) => encoded.includes(escape))) {
384
+ return capEncoded(entry, encoded);
385
+ }
386
+
387
+ try {
388
+ const sanitized = sanitize(entry, new Set()) as Record<string, unknown>;
389
+ const text = JSON.stringify(sanitized, replacer);
390
+ if (text === undefined) return null;
391
+ return capEncoded(sanitized, text);
392
+ } catch (error) {
393
+ // Nothing left to try. Losing this event is the correct outcome; losing the
394
+ // batch around it is not.
395
+ logException(
396
+ `could not serialize an event (type=${String(entry?.type)}); dropping it`,
397
+ error,
398
+ );
399
+ return null;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Bound ONE event, re-encoding only when it is actually over-large.
405
+ *
406
+ * Why it has to happen at all: `uploader.rs` states the invariant it relies on
407
+ * — "A single line longer than max is emitted alone rather than dropped: the
408
+ * spool writer already guarantees no such line exists." The Rust spool writer
409
+ * does guarantee it (`truncate_strings` at `MAX_FIELD_BYTES`). An SDK writer
410
+ * publishing into the same directories must too — or one
411
+ * `toolResult({ output: <a large file> })` is written as a single line, POSTed
412
+ * whole because `splitLines` can only split on newlines, rejected, and the
413
+ * ENTIRE spool file parked, retried three times and poisoned. Every unrelated
414
+ * event batched alongside it goes too, and nothing in the host process ever
415
+ * learns.
416
+ */
417
+ function capEncoded(entry: Record<string, unknown>, encoded: string): string {
418
+ // `encoded.length` is UTF-16 units, not bytes, so the cheap check has to be
419
+ // the byte count — `Buffer.byteLength` is a native scan and costs far less
420
+ // than the walk it guards.
421
+ if (Buffer.byteLength(encoded, "utf8") <= MAX_EVENT_BYTES) return encoded;
422
+ const capped = capFields(entry, MAX_FIELD_BYTES);
423
+ let recoded: string;
424
+ try {
425
+ const text = JSON.stringify(capped, replacer);
426
+ if (text === undefined) return encoded;
427
+ recoded = text;
428
+ } catch {
429
+ return encoded;
430
+ }
431
+ logger.warn(
432
+ `truncated an oversized event (type=${String(entry?.type)}) from ` +
433
+ `${Buffer.byteLength(encoded, "utf8")} to ${Buffer.byteLength(recoded, "utf8")} bytes; ` +
434
+ `fields above ${MAX_FIELD_BYTES} bytes were cut so the batch stays deliverable`,
435
+ );
436
+ return recoded;
437
+ }
438
+
439
+ interface EncodedLine {
440
+ readonly text: string;
441
+ readonly bytes: number;
442
+ }
443
+
444
+ /** Split encoded lines into batches that each stay under `limit` bytes. */
445
+ export function roll(lines: readonly EncodedLine[], limit: number): EncodedLine[][] {
446
+ const chunks: EncodedLine[][] = [];
447
+ let chunk: EncodedLine[] = [];
448
+ let size = 0;
449
+ for (const line of lines) {
450
+ const cost = line.bytes + 1; // the newline this line will be joined with
451
+ if (chunk.length > 0 && size + cost > limit) {
452
+ chunks.push(chunk);
453
+ chunk = [];
454
+ size = 0;
455
+ }
456
+ chunk.push(line);
457
+ size += cost;
458
+ }
459
+ if (chunk.length > 0) chunks.push(chunk);
460
+ return chunks;
461
+ }
462
+
463
+ function batchStem(): string {
464
+ // `2026-09-23T12-34-56-789Z`: the timestamp orders batches for a human
465
+ // reading the directory, and the pid+counter suffix is what makes the name
466
+ // unique. See `batchSeq` above.
467
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
468
+ batchSeq += 1;
469
+ return `event-${stamp}-${process.pid}-${batchSeq - 1}`;
470
+ }
471
+
472
+ interface InFlight {
473
+ readonly chunks: EncodedLine[][];
474
+ /** Incremented in the SAME synchronous tick as the rename that made a chunk durable. */
475
+ done: number;
476
+ }
477
+
478
+ export class EventWriter {
479
+ private queue: Record<string, unknown>[] = [];
480
+ /**
481
+ * Encoded lines from a failed write, retried ahead of the queue.
482
+ *
483
+ * A plain "put the entries back" would re-encode them and, when a batch had
484
+ * already rolled into several files and only a later one failed, rewrite the
485
+ * files that succeeded — duplicating every event in them. Holding the
486
+ * ENCODED lines instead means a partial failure retries exactly what did not
487
+ * land.
488
+ */
489
+ private pending: EncodedLine[] = [];
490
+ private flushInterval: number;
491
+ private dropped = 0;
492
+ private queuedBytes = 0;
493
+ private timer: ReturnType<typeof setInterval> | null = null;
494
+ private flushing: Promise<void> | null = null;
495
+ private inFlight: InFlight | null = null;
496
+ private closed = false;
497
+
498
+ constructor(flushInterval = 0.5) {
499
+ this.flushInterval = validatedInterval(flushInterval);
500
+ this.startTimer();
501
+ liveWriters.add(this);
502
+ ensureExitHook();
503
+ }
504
+
505
+ private startTimer(): void {
506
+ if (this.timer !== null) clearInterval(this.timer);
507
+ this.timer = setInterval(() => {
508
+ void this.flush().catch((error: unknown) => {
509
+ // Recording must never die permanently because one flush hit a
510
+ // transient filesystem error. `flush` restores the encoded lines before
511
+ // rejecting, so the next interval retries them.
512
+ logException("event flush failed; buffered events will be retried", error);
513
+ });
514
+ }, this.flushInterval * 1000);
515
+ // The single most important line in this file for not being noticed: an
516
+ // un-unref'd interval keeps the Node event loop alive forever, so a script
517
+ // that merely imports this SDK never exits. `unref` makes the timer stop
518
+ // counting as a reason to stay running, exactly like the daemon thread the
519
+ // Python SDK uses.
520
+ this.timer.unref?.();
521
+ }
522
+
523
+ submit(entry: Record<string, unknown>): void {
524
+ if (this.closed) return;
525
+ const size = approxSize(entry);
526
+ // BOTH bounds, and the byte one against measured sizes. A count alone is
527
+ // not a memory bound (the adapters budget 128 KiB of extras per event, so
528
+ // 10,000 of them is ~1.3 GB), and an average-based byte bound is not one
529
+ // either until the average has been learned.
530
+ while (
531
+ this.queue.length > 0 &&
532
+ (this.queue.length >= QUEUE_CAP || this.queuedBytes + size > QUEUE_BYTE_CAP)
533
+ ) {
534
+ const evicted = this.queue.shift();
535
+ if (evicted === undefined) break;
536
+ this.queuedBytes = Math.max(0, this.queuedBytes - approxSize(evicted));
537
+ this.dropped += 1;
538
+ // Powers of ten, so a stuck spool says so without becoming the thing that
539
+ // fills the disk it is complaining about.
540
+ if (this.dropped === 1 || this.dropped % 1000 === 0) {
541
+ logger.warn(
542
+ `event queue is full (${this.queue.length} events, ${this.queuedBytes} bytes); ` +
543
+ `discarding oldest. ${this.dropped} dropped so far — the spool is not draining.`,
544
+ );
545
+ }
546
+ }
547
+ this.queue.push(entry);
548
+ this.queuedBytes += size;
549
+ }
550
+
551
+ setFlushInterval(interval: number): void {
552
+ // Validate first, assign second: a rejected value must leave the writer
553
+ // running on the interval it already had, not on a half-applied one.
554
+ this.flushInterval = validatedInterval(interval);
555
+ // Restart so the new interval applies from now, not from the end of a cycle
556
+ // that may be an hour long.
557
+ if (!this.closed) this.startTimer();
558
+ }
559
+
560
+ getFlushInterval(): number {
561
+ return this.flushInterval;
562
+ }
563
+
564
+ /** Drain and write any buffered entries immediately. */
565
+ async flushNow(): Promise<void> {
566
+ // Twice, on purpose. `flush()` hands back a flush that is ALREADY running
567
+ // if there is one, and that flush drained the queue before any event
568
+ // emitted since — so awaiting it alone resolves with those events still in
569
+ // memory. The second call either drains what is left or joins a newer
570
+ // flush that started after this call, which drained it too. Either way,
571
+ // every event submitted before `flushNow()` was called is on disk when it
572
+ // resolves; with nothing queued the second pass returns immediately.
573
+ await this.flush();
574
+ await this.flush();
575
+ }
576
+
577
+ /**
578
+ * The synchronous flush, for a signal handler or `process.on("exit")`.
579
+ *
580
+ * `exit` handlers may not await, so the periodic path's async writes are no
581
+ * use there. This one blocks, which is exactly right at the end of a process
582
+ * and exactly wrong in a steady-state agent loop.
583
+ */
584
+ flushSync(): void {
585
+ const carried = this.takeInFlightRemainder();
586
+ const lines = [...carried, ...this.drainToLines()];
587
+ if (lines.length === 0) return;
588
+ for (const chunk of roll(lines, MAX_BATCH_BYTES)) {
589
+ try {
590
+ this.writeOneFileSync(chunk);
591
+ } catch (error) {
592
+ logException("final flush failed; buffered events were lost", error);
593
+ return;
594
+ }
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Chunks an in-flight async write had not yet renamed.
600
+ *
601
+ * A chunk whose `renameSync` already ran is durable and complete on disk even
602
+ * though the promise that would have recorded it never resolved, so taking it
603
+ * again here would publish a byte-identical duplicate. `done` is incremented
604
+ * in the same synchronous tick as that rename precisely so this check has no
605
+ * window to be wrong in.
606
+ */
607
+ private takeInFlightRemainder(): EncodedLine[] {
608
+ const active = this.inFlight;
609
+ this.inFlight = null;
610
+ if (active === null) return [];
611
+ return active.chunks.slice(active.done).flat();
612
+ }
613
+
614
+ private drainToLines(): EncodedLine[] {
615
+ const entries = this.queue;
616
+ this.queue = [];
617
+ this.queuedBytes = 0;
618
+ const carried = this.pending;
619
+ this.pending = [];
620
+ return [...carried, ...this.encode(entries)];
621
+ }
622
+
623
+ /**
624
+ * Encode BEFORE touching the filesystem. An unencodable event is a permanent
625
+ * condition — retrying it produces the identical failure — so it is dropped
626
+ * here, while a filesystem error is raised from the write and the whole batch
627
+ * goes back to be retried.
628
+ */
629
+ private encode(entries: readonly Record<string, unknown>[]): EncodedLine[] {
630
+ const lines: EncodedLine[] = [];
631
+ let dropped = 0;
632
+ const redact = redactionEnabled(getBaseDir());
633
+ for (const entry of entries) {
634
+ let encoded = encodeEntry(entry);
635
+ if (encoded === null) {
636
+ dropped += 1;
637
+ continue;
638
+ }
639
+ if (redact) {
640
+ try {
641
+ encoded = redactJsonLine(encoded);
642
+ } catch (error) {
643
+ // A redactor that throws must not publish the unredacted line it was
644
+ // handed. Dropping the event is the only safe answer: the whole
645
+ // reason this runs is that the line may carry a credential.
646
+ logException("redaction failed; dropping the event rather than publishing it", error);
647
+ dropped += 1;
648
+ continue;
649
+ }
650
+ }
651
+ lines.push({ text: encoded, bytes: Buffer.byteLength(encoded, "utf8") });
652
+ }
653
+ if (dropped > 0) {
654
+ logger.error(
655
+ `dropped ${dropped} unpublishable event(s) from a batch of ${entries.length}; ` +
656
+ "the rest of the batch was published",
657
+ );
658
+ }
659
+ return lines;
660
+ }
661
+
662
+ /**
663
+ * Serialised, so a batch is never drained by two callers at once and — the
664
+ * case that actually bites — so a `flushNow()` from a signal handler waits
665
+ * for an in-flight write instead of racing it.
666
+ */
667
+ private flush(): Promise<void> {
668
+ if (this.flushing !== null) return this.flushing;
669
+ const run = this.flushOnce().finally(() => {
670
+ this.flushing = null;
671
+ });
672
+ this.flushing = run;
673
+ return run;
674
+ }
675
+
676
+ private async flushOnce(): Promise<void> {
677
+ const lines = this.drainToLines();
678
+ if (lines.length === 0) return;
679
+ const chunks = roll(lines, MAX_BATCH_BYTES);
680
+ const active: InFlight = { chunks, done: 0 };
681
+ this.inFlight = active;
682
+ try {
683
+ for (const chunk of chunks) {
684
+ await this.writeOneFile(chunk, active);
685
+ }
686
+ } catch (error) {
687
+ // Only what did NOT land goes back, at the FRONT — the events that
688
+ // already reached disk must not be published twice.
689
+ const remainder = active.chunks.slice(active.done).flat();
690
+ this.pending = [...remainder, ...this.pending];
691
+ this.queuedBytes += remainder.reduce((total, line) => total + line.bytes, 0);
692
+ this.boundPending();
693
+ throw error;
694
+ } finally {
695
+ if (this.inFlight === active) this.inFlight = null;
696
+ }
697
+ }
698
+
699
+ /**
700
+ * The retry buffer obeys the same ceilings as the queue. A spool that has
701
+ * stopped draining must not turn into unbounded growth just because the
702
+ * events are already encoded.
703
+ */
704
+ private boundPending(): void {
705
+ while (
706
+ this.pending.length > 0 &&
707
+ (this.pending.length > QUEUE_CAP || this.queuedBytes > QUEUE_BYTE_CAP)
708
+ ) {
709
+ const evicted = this.pending.shift();
710
+ if (evicted === undefined) break;
711
+ this.queuedBytes = Math.max(0, this.queuedBytes - evicted.bytes);
712
+ this.dropped += 1;
713
+ }
714
+ }
715
+
716
+ private eventsDir(): string {
717
+ return join(getBaseDir(), "events");
718
+ }
719
+
720
+ private content(lines: readonly EncodedLine[]): Buffer {
721
+ return Buffer.from(`${lines.map((line) => line.text).join("\n")}\n`, "utf8");
722
+ }
723
+
724
+ private async writeOneFile(lines: readonly EncodedLine[], active?: InFlight): Promise<void> {
725
+ const dir = this.eventsDir();
726
+ // 0700, and the batch below 0600. These files are not metadata: they carry
727
+ // goals, prompt text, tool arguments and tool output straight from the host
728
+ // agent. Under the ordinary umask 022 they would land 0644 inside 0755
729
+ // directories, so on any shared host — a build box, a bastion, a container
730
+ // with several service accounts — every other local user could read every
731
+ // agent transcript this SDK spools, for the whole flush+upload window and
732
+ // forever if no daemon is running. The daemon reads these as the SAME user,
733
+ // so tightening them costs no delivery.
734
+ await mkdir(dir, { recursive: true, mode: 0o700 });
735
+
736
+ const stem = batchStem();
737
+ const tmpPath = join(dir, `${stem}.tmp`);
738
+ const finalPath = join(dir, `${stem}.jsonl`);
739
+ const body = this.content(lines);
740
+
741
+ try {
742
+ // O_EXCL + an explicit 0600. The mode is applied at CREATE, so the payload
743
+ // is never briefly world-readable the way a follow-up chmod would leave it.
744
+ const handle = await open(
745
+ tmpPath,
746
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
747
+ 0o600,
748
+ );
749
+ try {
750
+ await handle.write(body);
751
+ // fsync BEFORE the rename. A rename is atomic with respect to readers,
752
+ // but atomic is not durable: it orders nothing against the page cache,
753
+ // so a power loss or kernel crash can leave a correctly-named,
754
+ // zero-length or truncated `.jsonl`. The collector reads whatever is
755
+ // there, POSTs it, and then DELETES the file (`remove_file` in
756
+ // `crates/fpai-collect/src/uploader.rs`) — so the loss is permanent and
757
+ // silent, and an empty batch is accepted with a 200. This repo's own
758
+ // Rust spool writer calls `sync_all()` here for exactly this reason.
759
+ await handle.sync();
760
+ } finally {
761
+ await handle.close();
762
+ }
763
+
764
+ // SYNCHRONOUS rename, deliberately, in an otherwise-async write. It is a
765
+ // metadata operation measured in microseconds, and doing it synchronously
766
+ // is what lets `done` be incremented in the same tick — so `flushSync`
767
+ // can never see a chunk as un-written after it has become durable, and
768
+ // never publishes a duplicate at exit.
769
+ renameSync(tmpPath, finalPath);
770
+ if (active) active.done += 1;
771
+ } catch (error) {
772
+ // Clean up the partial file on ANY failure. Each flush picks a fresh
773
+ // stem, so without this a persistent fault — a full disk, a read-only
774
+ // mount, a cross-device rename — strands one `.tmp` per flush cycle:
775
+ // roughly 170,000 files a day at the default interval, on the very disk
776
+ // that is already the problem. The watcher ignores them by extension, so
777
+ // nothing else would ever notice or collect them.
778
+ //
779
+ // The batch itself is NOT lost by this: the caller returns the lines to
780
+ // the retry buffer and the next cycle rewrites them under a new name.
781
+ rmSync(tmpPath, { force: true });
782
+ throw error;
783
+ }
784
+
785
+ this.syncDirectory(dir);
786
+ }
787
+
788
+ private writeOneFileSync(lines: readonly EncodedLine[]): void {
789
+ const dir = this.eventsDir();
790
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
791
+
792
+ const stem = batchStem();
793
+ const tmpPath = join(dir, `${stem}.tmp`);
794
+ const finalPath = join(dir, `${stem}.jsonl`);
795
+ const body = this.content(lines);
796
+
797
+ try {
798
+ const fd = openSync(
799
+ tmpPath,
800
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
801
+ 0o600,
802
+ );
803
+ try {
804
+ writeSync(fd, body);
805
+ fsyncSync(fd);
806
+ } finally {
807
+ closeSync(fd);
808
+ }
809
+ renameSync(tmpPath, finalPath);
810
+ } catch (error) {
811
+ rmSync(tmpPath, { force: true });
812
+ throw error;
813
+ }
814
+
815
+ this.syncDirectory(dir);
816
+ }
817
+
818
+ /**
819
+ * fsync the DIRECTORY, or the rename itself can be lost while the file's
820
+ * contents survive — leaving the batch on disk under its `.tmp` name, which
821
+ * the watcher ignores by design.
822
+ *
823
+ * Best-effort: opening a directory for fsync is POSIX behaviour, and
824
+ * platforms that refuse it (Windows) still get the content fsync above, which
825
+ * is the half that prevents a truncated delivery.
826
+ */
827
+ private syncDirectory(dir: string): void {
828
+ let fd: number;
829
+ try {
830
+ fd = openSync(dir, fsConstants.O_RDONLY);
831
+ } catch {
832
+ return;
833
+ }
834
+ try {
835
+ fsyncSync(fd);
836
+ } catch {
837
+ /* platform dependent */
838
+ } finally {
839
+ closeSync(fd);
840
+ }
841
+ }
842
+
843
+ /** Stop the timer and drop this writer from the exit hook. Tests only. */
844
+ close(): void {
845
+ this.closed = true;
846
+ if (this.timer !== null) clearInterval(this.timer);
847
+ this.timer = null;
848
+ liveWriters.delete(this);
849
+ releaseExitHook();
850
+ }
851
+
852
+ /** Diagnostics for tests and for anyone debugging a spool that is not draining. */
853
+ stats(): { queued: number; queuedBytes: number; dropped: number; pending: number } {
854
+ return {
855
+ queued: this.queue.length,
856
+ queuedBytes: this.queuedBytes,
857
+ dropped: this.dropped,
858
+ pending: this.pending.length,
859
+ };
860
+ }
861
+ }
862
+
863
+ /** Final synchronous flush for every live writer. */
864
+ export function flushAllSync(): void {
865
+ for (const writer of [...liveWriters]) {
866
+ try {
867
+ writer.flushSync();
868
+ } catch (error) {
869
+ logException("final flush failed; buffered events were lost", error);
870
+ }
871
+ }
872
+ }
873
+
874
+ /** Await a flush of every live writer. Exposed as `failproofai.flush()`. */
875
+ export async function flushAllNow(): Promise<void> {
876
+ await Promise.all(
877
+ [...liveWriters].map(async (writer) => {
878
+ try {
879
+ await writer.flushNow();
880
+ } catch (error) {
881
+ logException("flush failed", error);
882
+ }
883
+ }),
884
+ );
885
+ }
886
+
887
+ /**
888
+ * Register the exit flush, once, and only once there is something to flush.
889
+ *
890
+ * `exit` is the last point at which anything in this process runs, and it runs
891
+ * on a normal return, on an unhandled rejection that terminates, and on an
892
+ * explicit `process.exit()`. It may not await, which is why `flushSync` exists.
893
+ *
894
+ * Registered LAZILY and released when the last writer goes, for two reasons.
895
+ * It keeps a process that merely imports this package from carrying a hook it
896
+ * will never use — and, more practically, it stops the listener count growing
897
+ * when this module is evaluated more than once in one process, which happens
898
+ * whenever both halves of the dual build are loaded (a CommonJS app with an ESM
899
+ * dependency that also uses this SDK) and which Node reports as a
900
+ * `MaxListenersExceededWarning` in the host's own output.
901
+ *
902
+ * Exceptions are swallowed inside `flushAllSync` on purpose: an uncaught one
903
+ * here prints a full stack into the host agent's stderr during shutdown, where
904
+ * it reads as a crash in the application rather than a telemetry flush that
905
+ * failed.
906
+ *
907
+ * SIGINT/SIGTERM are NOT installed. Registering a signal handler CHANGES the
908
+ * process's behaviour — Node's default is to terminate, and a listener
909
+ * suppresses that — so a telemetry library that installed one would silently
910
+ * stop Ctrl-C from working. The README documents the two-line handler instead.
911
+ */
912
+ let exitHookInstalled = false;
913
+
914
+ /**
915
+ * Close what the process is abandoning (`exit.ts`), THEN flush — so the
916
+ * `agent_end` / `tool_result` those closers emit are in the final batch rather
917
+ * than queued behind a flush that already ran.
918
+ */
919
+ function onExit(exitCode: number): void {
920
+ runExitClosers(typeof exitCode === "number" ? exitCode : 0);
921
+ flushAllSync();
922
+ }
923
+
924
+ function ensureExitHook(): void {
925
+ if (exitHookInstalled) return;
926
+ exitHookInstalled = true;
927
+ process.on("exit", onExit);
928
+ }
929
+
930
+ function releaseExitHook(): void {
931
+ if (!exitHookInstalled || liveWriters.size > 0) return;
932
+ exitHookInstalled = false;
933
+ process.removeListener("exit", onExit);
934
+ }