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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -13
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +7 -8
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +6 -7
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -41
- package/.next/standalone/.next/server/app/settings/page.js +9 -12
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
- package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
- package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
- package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/pack-actions.ts +0 -12
- package/.next/standalone/app/policies/hooks-client.tsx +0 -9
- package/.next/standalone/app/settings/page.tsx +1 -20
- package/.next/standalone/app/settings/settings-client.tsx +1 -27
- package/.next/standalone/app/settings/settings.css +0 -79
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
- package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
- package/.next/standalone/sdk/python/skill/references/events.md +17 -8
- package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
- package/.next/standalone/sdk/python/skill/references/install.md +3 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
- package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
- package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
- package/.next/standalone/sdk/typescript/LICENSE +42 -0
- package/.next/standalone/sdk/typescript/README.md +552 -0
- package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
- package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
- package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
- package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
- package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
- package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
- package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
- package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
- package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
- package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
- package/.next/standalone/sdk/typescript/package.json +401 -0
- package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
- package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
- package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
- package/.next/standalone/sdk/typescript/src/context.ts +214 -0
- package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
- package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
- package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
- package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
- package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
- package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
- package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
- package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
- package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
- package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
- package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
- package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
- package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
- package/.next/standalone/sdk/typescript/src/events.ts +879 -0
- package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
- package/.next/standalone/sdk/typescript/src/index.ts +186 -0
- package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
- package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
- package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
- package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
- package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
- package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
- package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
- package/.next/standalone/sdk/typescript/src/next.ts +115 -0
- package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
- package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
- package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
- package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
- package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
- package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
- package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
- package/.next/standalone/sdk/typescript/src/version.ts +5 -0
- package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
- package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
- package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
- package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
- package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
- package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
- package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
- package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
- package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
- package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
- package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
- package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
- package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
- package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
- package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
- package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
- package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
- package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
- package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
- package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
- package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
- package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
- package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
- package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
- package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
- package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
- package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
- package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
- package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
- package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
- package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
- package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +2 -115
- package/dist/cli.mjs +6538 -13686
- package/dist/index.js +1 -19
- package/dist/worker.mjs +2085 -8012
- package/package.json +9 -9
- package/pi-extension/index.ts +0 -11
- package/scripts/build-policy-pack.mjs +2 -39
- package/src/hooks/builtin-policies.ts +3 -21
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +0 -22
- package/src/hooks/custom-hooks-loader.ts +7 -45
- package/src/hooks/custom-hooks-registry.ts +1 -45
- package/src/hooks/first-run-gate.ts +0 -5
- package/src/hooks/fp-home.ts +0 -23
- package/src/hooks/handler.ts +6 -265
- package/src/hooks/hook-activity-store.ts +1 -105
- package/src/hooks/hook-telemetry.ts +0 -41
- package/src/hooks/loader-utils.ts +0 -6
- package/src/hooks/pack-cli.ts +17 -278
- package/src/hooks/pack-manifest.ts +7 -479
- package/src/hooks/pack-store.ts +10 -156
- package/src/hooks/policy-catalog.ts +0 -65
- package/src/hooks/policy-evaluator.ts +796 -940
- package/src/hooks/policy-registry.ts +0 -25
- package/src/hooks/policy-types.ts +0 -126
- package/src/hooks/worker-server.ts +26 -119
- package/src/index.ts +0 -6
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
- package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
- package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
- package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
- package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
- package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
- package/.next/standalone/app/actions/get-jev-config.ts +0 -409
- package/.next/standalone/app/actions/update-jev-config.ts +0 -420
- package/.next/standalone/app/components/jev-notices.tsx +0 -96
- package/.next/standalone/app/settings/jev-panel.tsx +0 -469
- package/src/hooks/effective-reviewers.ts +0 -79
- package/src/hooks/jev-activity.ts +0 -385
- package/src/hooks/jev-cli.ts +0 -1193
- package/src/hooks/policy-authority.ts +0 -333
- package/src/hooks/policy-reviewability.ts +0 -229
- package/src/hooks/semantic/combine.ts +0 -541
- package/src/hooks/semantic/compile.ts +0 -176
- package/src/hooks/semantic/decide.ts +0 -392
- package/src/hooks/semantic/envelope.ts +0 -1296
- package/src/hooks/semantic/evaluator.ts +0 -547
- package/src/hooks/semantic/facts.ts +0 -292
- package/src/hooks/semantic/intent.ts +0 -1190
- package/src/hooks/semantic/jev-client.ts +0 -643
- package/src/hooks/semantic/jev-config.ts +0 -594
- package/src/hooks/semantic/jev-review.ts +0 -374
- package/src/hooks/semantic/jev-stats.ts +0 -289
- package/src/hooks/semantic/jev-throttle.ts +0 -421
- package/src/hooks/semantic/pack-policies.ts +0 -251
- package/src/hooks/semantic/policies.ts +0 -596
- package/src/hooks/semantic/precondition-names.ts +0 -60
- package/src/hooks/semantic/preconditions.ts +0 -58
- package/src/hooks/semantic/redact.ts +0 -2910
- package/src/hooks/semantic/types.ts +0 -145
- package/src/hooks/semver-precedence.ts +0 -128
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
- /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
|
+
}
|