failproofai 1.0.0-beta.2 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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.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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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.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/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/app/api/audit/run/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/reminder/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 +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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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/build-manifest.json +3 -3
- 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 +1 -1
- 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/build-manifest.json +3 -3
- 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 +1 -1
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- 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/chunks/[externals]__0h9_83k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- 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/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_1q9xxca.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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 +32 -20
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/Cargo.lock +1992 -198
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
- package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
- package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
- package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
- package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
- package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
- package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
- package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
- package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
- package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
- package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
- package/.next/standalone/package.json +15 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2255 -779
- package/dist/worker.mjs +365 -52
- package/package.json +15 -9
- package/scripts/build-daemon-packages.mjs +210 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/prune-standalone.mjs +6 -0
- package/scripts/publish-aliases.mjs +62 -41
- package/src/hooks/builtin-policies.ts +139 -0
- package/src/hooks/cloud-connection.ts +205 -0
- package/src/hooks/cloud-enrollment-cli.ts +227 -0
- package/src/hooks/cloud-enrollment.ts +173 -0
- package/src/hooks/cloud-managed-policies.ts +138 -0
- package/src/hooks/collector-config.ts +226 -0
- package/src/hooks/configure-wizard.ts +221 -5
- package/src/hooks/custom-hooks-loader.ts +69 -25
- package/src/hooks/daemon-client.ts +18 -0
- package/src/hooks/daemon-download.ts +137 -27
- package/src/hooks/daemon-service.ts +23 -8
- package/src/hooks/handler.ts +144 -6
- package/src/hooks/hook-activity-store.ts +50 -0
- package/src/hooks/loader-utils.ts +6 -4
- package/src/hooks/policy-types.ts +35 -0
- package/src/hooks/session-pause-cli.ts +157 -0
- package/src/hooks/session-pause.ts +236 -0
- package/src/hooks/tui.ts +115 -0
- package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
//! opencode session capture — a [`sqlitepoll`](crate::sqlitepoll) adapter.
|
|
2
|
+
//!
|
|
3
|
+
//! opencode keeps every session on the machine in ONE SQLite database at
|
|
4
|
+
//! `$XDG_DATA_HOME/opencode/opencode.db` (`~/.local/share/opencode/opencode.db`
|
|
5
|
+
//! on a default install). We open it read-only, through its WAL, and never
|
|
6
|
+
//! write, checkpoint or vacuum it — see [`crate::sqlitepoll::open_readonly`].
|
|
7
|
+
//!
|
|
8
|
+
//! # Rows mutate for ~12 seconds after they are inserted, and that is the design
|
|
9
|
+
//!
|
|
10
|
+
//! This is the source the [`Watermark::UpdatedAt`] machinery exists for. A tool
|
|
11
|
+
//! call and its result are the SAME `part` row: the row appears with
|
|
12
|
+
//! `state.status:"running"` and an EMPTY `state.output`, and the real result
|
|
13
|
+
//! arrives as a later `UPDATE` to that row. Directly observed on the probe
|
|
14
|
+
//! capture — one row changed three times, and `state.output` was populated only
|
|
15
|
+
//! on the third, 12.0 seconds after the row first appeared:
|
|
16
|
+
//!
|
|
17
|
+
//! ```text
|
|
18
|
+
//! t+0.0s NEW rowid=20 type=tool tool=bash status=running out='' tu=…539061
|
|
19
|
+
//! t+0.3s MUT rowid=20 type=tool tool=bash status=running out='' tu=…539110
|
|
20
|
+
//! t+12.0s MUT rowid=20 type=tool tool=bash status=completed out='SECOND_MARKER' tu=…551135
|
|
21
|
+
//! ```
|
|
22
|
+
//!
|
|
23
|
+
//! **A rowid watermark ships every tool call in this product with an empty
|
|
24
|
+
//! result, forever, while looking like it works.** Nothing errors, nothing is
|
|
25
|
+
//! missing from the event stream, and every `tool_result` is blank. So this
|
|
26
|
+
//! format declares [`Watermark::UpdatedAt`] and queries
|
|
27
|
+
//! `time_updated > :watermark ORDER BY time_updated, id`, which brings a
|
|
28
|
+
//! mutated row back through the transform.
|
|
29
|
+
//!
|
|
30
|
+
//! Re-emitting a re-read row is safe: a completed row is stable (verified
|
|
31
|
+
//! byte-identical across two later turns), so the server's content dedup
|
|
32
|
+
//! collapses an unchanged re-read, and a row that genuinely changed is stored
|
|
33
|
+
//! as the new fact it is. The transforms are written to make that hold — see
|
|
34
|
+
//! the notes on `tool_use` and `agent_start` in [`transform`], both of which
|
|
35
|
+
//! avoid fields that opencode rewrites so their re-reads hash the same.
|
|
36
|
+
//!
|
|
37
|
+
//! The second half of the story is that a row is only shipped once opencode
|
|
38
|
+
//! has finished writing it, which the row itself says: a text part carries
|
|
39
|
+
//! `time.end` when the stream is done, a tool part a terminal `state.status`.
|
|
40
|
+
//! Without that, a 30-second streamed answer polled every few seconds becomes
|
|
41
|
+
//! a dozen `model_response` rows, each a longer prefix of the same text.
|
|
42
|
+
//!
|
|
43
|
+
//! # `session.directory`, never `session.path` or `project_id`
|
|
44
|
+
//!
|
|
45
|
+
//! The agent id is `opencode-<project>` derived from `session.directory`, which
|
|
46
|
+
//! is the absolute working directory and is reliable. The two neighbouring
|
|
47
|
+
//! columns are traps, both verified on the capture:
|
|
48
|
+
//!
|
|
49
|
+
//! * `session.path` is relative to a git worktree, so it is **the empty string
|
|
50
|
+
//! at the repository root** — exactly the case that matters most. (Non-git
|
|
51
|
+
//! directories get a relative-looking `tmp/probe-opencode`, which is worse:
|
|
52
|
+
//! it looks usable.)
|
|
53
|
+
//! * `session.project_id` is a synthetic `"global"` for every session started
|
|
54
|
+
//! outside a git repository, so it groups unrelated projects together.
|
|
55
|
+
//!
|
|
56
|
+
//! # The `event` table is a better tail target that we deliberately do not use
|
|
57
|
+
//!
|
|
58
|
+
//! opencode also keeps an append-only, event-sourced `event(id, aggregate_id,
|
|
59
|
+
//! seq, type, data)` table with `UNIQUE(aggregate_id, seq)` — a monotonic
|
|
60
|
+
//! per-session sequence, which is a far nicer thing to tail than a mutating
|
|
61
|
+
//! row. It is not the durable copy: **three of the shipped schema migrations
|
|
62
|
+
//! contain `DELETE FROM event`** (counted in the 1.18.11 binary), so
|
|
63
|
+
//! history behind a watermark can vanish on upgrade. `message` and `part` have
|
|
64
|
+
//! no such migration and are the durable source; `event` is a cache in front of
|
|
65
|
+
//! them. Worth revisiting if opencode ever commits to retaining it.
|
|
66
|
+
//!
|
|
67
|
+
//! # `session_message` is dead
|
|
68
|
+
//!
|
|
69
|
+
//! `session_message` looks like exactly the table this source wants and is
|
|
70
|
+
//! permanently empty — it is legacy, and four migrations `DELETE FROM` it.
|
|
71
|
+
//! All content lives in `part.data`; `message.data` is metadata only (`role`,
|
|
72
|
+
//! tokens, timing, `finish`), which is why every part is read joined to its
|
|
73
|
+
//! message rather than instead of it.
|
|
74
|
+
//!
|
|
75
|
+
//! # Scope
|
|
76
|
+
//!
|
|
77
|
+
//! `text` and `tool` parts, plus the session's start and end. `step-start`,
|
|
78
|
+
//! `step-finish`, `reasoning`, `snapshot`, `patch` and `file` parts emit
|
|
79
|
+
//! nothing: they are turn structure or need event types of their own, and
|
|
80
|
+
//! ignoring an unrecognised part type is also what keeps a new opencode
|
|
81
|
+
//! release from breaking this source rather than merely widening it.
|
|
82
|
+
|
|
83
|
+
pub mod transform;
|
|
84
|
+
|
|
85
|
+
use std::path::PathBuf;
|
|
86
|
+
|
|
87
|
+
use rusqlite::{Connection, ToSql};
|
|
88
|
+
use serde_json::Value;
|
|
89
|
+
|
|
90
|
+
use crate::sqlitepoll::{PollOutcome, PollRequest, SqliteFormat, Watermark};
|
|
91
|
+
use transform::{PartRow, SessionRow};
|
|
92
|
+
|
|
93
|
+
/// The format the poller drives.
|
|
94
|
+
pub const FORMAT: SqliteFormat = SqliteFormat {
|
|
95
|
+
kind: "opencode",
|
|
96
|
+
watermark: Watermark::UpdatedAt,
|
|
97
|
+
poll,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/// The agent id used when a session's directory yields no usable project.
|
|
101
|
+
pub const DEFAULT_AGENT_ID: &str = "opencode";
|
|
102
|
+
|
|
103
|
+
/// `$XDG_DATA_HOME/opencode/opencode.db`, else `~/.local/share/opencode/…`.
|
|
104
|
+
///
|
|
105
|
+
/// opencode resolves its data directory with the `xdg-basedir` package, which
|
|
106
|
+
/// returns `~/.local/share` on macOS as well as Linux — so this one rule
|
|
107
|
+
/// covers both platforms rather than needing a `cfg`.
|
|
108
|
+
pub fn default_db_path() -> PathBuf {
|
|
109
|
+
std::env::var_os("XDG_DATA_HOME")
|
|
110
|
+
.map(PathBuf::from)
|
|
111
|
+
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
|
|
112
|
+
.unwrap_or_else(|| PathBuf::from(".local/share"))
|
|
113
|
+
.join("opencode")
|
|
114
|
+
.join("opencode.db")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Everything a `session` row contributes, including the two facts that need a
|
|
118
|
+
/// correlated subquery: its earliest part (the goal, and proof the session was
|
|
119
|
+
/// actually used) and its latest message (whether a turn is in flight).
|
|
120
|
+
const SESSION_SELECT: &str = "\
|
|
121
|
+
SELECT s.id, s.directory, s.version, s.slug, s.parent_id, s.time_created, s.time_updated, \
|
|
122
|
+
s.tokens_input, s.tokens_output, s.tokens_reasoning, \
|
|
123
|
+
s.tokens_cache_read, s.tokens_cache_write, \
|
|
124
|
+
(SELECT p.data FROM part p WHERE p.session_id = s.id \
|
|
125
|
+
ORDER BY p.time_created, p.id LIMIT 1), \
|
|
126
|
+
(SELECT m.data FROM message m WHERE m.session_id = s.id \
|
|
127
|
+
ORDER BY m.time_created DESC, m.id DESC LIMIT 1) \
|
|
128
|
+
FROM session s";
|
|
129
|
+
const SESSION_ORDER: &str = " ORDER BY s.time_updated, s.id";
|
|
130
|
+
|
|
131
|
+
/// A part joined to its message (for the role and model — a part carries
|
|
132
|
+
/// neither) and to its session (for the directory the agent id derives from).
|
|
133
|
+
const PART_SELECT: &str = "\
|
|
134
|
+
SELECT p.id, p.session_id, p.message_id, p.time_created, p.time_updated, p.data, \
|
|
135
|
+
m.data, s.directory \
|
|
136
|
+
FROM part p \
|
|
137
|
+
JOIN message m ON m.id = p.message_id \
|
|
138
|
+
JOIN session s ON s.id = p.session_id";
|
|
139
|
+
const PART_ORDER: &str = " ORDER BY p.time_updated, p.id";
|
|
140
|
+
|
|
141
|
+
/// One poll: read both streams past the watermark, merge them into one
|
|
142
|
+
/// time-ordered batch, and transform.
|
|
143
|
+
///
|
|
144
|
+
/// Pure with respect to `(database contents, request)`, which the module docs
|
|
145
|
+
/// explain is what makes re-reading a mutated row safe.
|
|
146
|
+
fn poll(conn: &Connection, req: &PollRequest) -> rusqlite::Result<PollOutcome> {
|
|
147
|
+
let limit = req.max_rows.max(1);
|
|
148
|
+
// One row past the limit, because the extra row is the only thing that
|
|
149
|
+
// reveals a batch ending in the MIDDLE of a millisecond. `time_updated` is
|
|
150
|
+
// milliseconds, so several rows routinely share one value; cutting between
|
|
151
|
+
// two of them and then asking for `time_updated > :last` skips the rest of
|
|
152
|
+
// that millisecond forever.
|
|
153
|
+
let fetch = limit.saturating_add(1) as i64;
|
|
154
|
+
|
|
155
|
+
let mut rows = read_after(conn, req.watermark, fetch)?;
|
|
156
|
+
let mut rows_seen = rows.len() as u64;
|
|
157
|
+
let more = rows_seen > limit;
|
|
158
|
+
|
|
159
|
+
if more {
|
|
160
|
+
let cut = rows[limit as usize].time_updated();
|
|
161
|
+
rows.truncate(limit as usize);
|
|
162
|
+
while rows.last().is_some_and(|r| r.time_updated() == cut) {
|
|
163
|
+
rows.pop();
|
|
164
|
+
}
|
|
165
|
+
if rows.is_empty() {
|
|
166
|
+
// Degenerate: a single millisecond holds more rows than the whole
|
|
167
|
+
// limit, so trimming to a millisecond boundary leaves nothing and
|
|
168
|
+
// the watermark can never advance past it. Take that millisecond
|
|
169
|
+
// whole instead — it is one instant of one product's writes, so it
|
|
170
|
+
// is bounded however small the configured limit is.
|
|
171
|
+
rows = read_at(conn, cut)?;
|
|
172
|
+
rows_seen += rows.len() as u64;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Only ever the last SHIPPED row: a watermark past a row whose events were
|
|
177
|
+
// trimmed away would drop it.
|
|
178
|
+
let watermark = rows.last().map_or(req.watermark, Row::time_updated);
|
|
179
|
+
let events = rows
|
|
180
|
+
.iter()
|
|
181
|
+
.flat_map(|r| r.events(&req.environment, &req.agent_id))
|
|
182
|
+
.collect();
|
|
183
|
+
|
|
184
|
+
Ok(PollOutcome {
|
|
185
|
+
events,
|
|
186
|
+
watermark,
|
|
187
|
+
more,
|
|
188
|
+
rows_seen,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Both streams strictly past the watermark, merged and ordered.
|
|
193
|
+
fn read_after(conn: &Connection, watermark: i64, limit: i64) -> rusqlite::Result<Vec<Row>> {
|
|
194
|
+
let sessions = format!("{SESSION_SELECT} WHERE s.time_updated > ?1{SESSION_ORDER} LIMIT ?2");
|
|
195
|
+
let parts = format!("{PART_SELECT} WHERE p.time_updated > ?1{PART_ORDER} LIMIT ?2");
|
|
196
|
+
merge(
|
|
197
|
+
query(conn, &sessions, &[&watermark, &limit], map_session)?,
|
|
198
|
+
query(conn, &parts, &[&watermark, &limit], map_part)?,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Both streams for exactly one millisecond, unlimited. The safety valve for
|
|
203
|
+
/// the degenerate case in [`poll`].
|
|
204
|
+
fn read_at(conn: &Connection, at: i64) -> rusqlite::Result<Vec<Row>> {
|
|
205
|
+
let sessions = format!("{SESSION_SELECT} WHERE s.time_updated = ?1{SESSION_ORDER}");
|
|
206
|
+
let parts = format!("{PART_SELECT} WHERE p.time_updated = ?1{PART_ORDER}");
|
|
207
|
+
merge(
|
|
208
|
+
query(conn, &sessions, &[&at], map_session)?,
|
|
209
|
+
query(conn, &parts, &[&at], map_part)?,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn merge(sessions: Vec<SessionRow>, parts: Vec<PartRow>) -> rusqlite::Result<Vec<Row>> {
|
|
214
|
+
let mut rows: Vec<Row> = sessions
|
|
215
|
+
.into_iter()
|
|
216
|
+
.map(Row::Session)
|
|
217
|
+
.chain(parts.into_iter().map(Row::Part))
|
|
218
|
+
.collect();
|
|
219
|
+
// `(time_updated, table, id)` and never the insertion order of the two
|
|
220
|
+
// queries: the batch has to come out the same way every time or a re-read
|
|
221
|
+
// would not reproduce the same bytes.
|
|
222
|
+
rows.sort_by(|a, b| {
|
|
223
|
+
(a.time_updated(), a.table_rank(), a.id()).cmp(&(b.time_updated(), b.table_rank(), b.id()))
|
|
224
|
+
});
|
|
225
|
+
Ok(rows)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn query<T>(
|
|
229
|
+
conn: &Connection,
|
|
230
|
+
sql: &str,
|
|
231
|
+
args: &[&dyn ToSql],
|
|
232
|
+
map: fn(&rusqlite::Row) -> rusqlite::Result<T>,
|
|
233
|
+
) -> rusqlite::Result<Vec<T>> {
|
|
234
|
+
let mut stmt = conn.prepare(sql)?;
|
|
235
|
+
let rows = stmt.query_map(args, map)?;
|
|
236
|
+
rows.collect()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// One row from either stream.
|
|
240
|
+
enum Row {
|
|
241
|
+
Session(SessionRow),
|
|
242
|
+
Part(PartRow),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
impl Row {
|
|
246
|
+
fn time_updated(&self) -> i64 {
|
|
247
|
+
match self {
|
|
248
|
+
Row::Session(s) => s.time_updated,
|
|
249
|
+
Row::Part(p) => p.time_updated,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Sessions before parts within one millisecond, so a session's
|
|
254
|
+
/// `agent_start` is spooled ahead of the content of the turn that created
|
|
255
|
+
/// it on a first full read.
|
|
256
|
+
fn table_rank(&self) -> u8 {
|
|
257
|
+
match self {
|
|
258
|
+
Row::Session(_) => 0,
|
|
259
|
+
Row::Part(_) => 1,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn id(&self) -> &str {
|
|
264
|
+
match self {
|
|
265
|
+
Row::Session(s) => &s.id,
|
|
266
|
+
Row::Part(p) => &p.id,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn events(&self, environment: &str, fallback_agent_id: &str) -> Vec<Value> {
|
|
271
|
+
match self {
|
|
272
|
+
Row::Session(s) => transform::session_events(s, environment, fallback_agent_id),
|
|
273
|
+
Row::Part(p) => transform::part_events(p, environment, fallback_agent_id),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn map_session(r: &rusqlite::Row) -> rusqlite::Result<SessionRow> {
|
|
279
|
+
Ok(SessionRow {
|
|
280
|
+
id: r.get(0)?,
|
|
281
|
+
directory: r.get::<_, Option<String>>(1)?.unwrap_or_default(),
|
|
282
|
+
version: r.get(2)?,
|
|
283
|
+
slug: r.get(3)?,
|
|
284
|
+
parent_id: r.get(4)?,
|
|
285
|
+
time_created: r.get::<_, Option<i64>>(5)?.unwrap_or_default(),
|
|
286
|
+
time_updated: r.get::<_, Option<i64>>(6)?.unwrap_or_default(),
|
|
287
|
+
tokens_input: r.get::<_, Option<i64>>(7)?.unwrap_or_default(),
|
|
288
|
+
tokens_output: r.get::<_, Option<i64>>(8)?.unwrap_or_default(),
|
|
289
|
+
tokens_reasoning: r.get::<_, Option<i64>>(9)?.unwrap_or_default(),
|
|
290
|
+
tokens_cache_read: r.get::<_, Option<i64>>(10)?.unwrap_or_default(),
|
|
291
|
+
tokens_cache_write: r.get::<_, Option<i64>>(11)?.unwrap_or_default(),
|
|
292
|
+
first_part: parse_json(r.get::<_, Option<String>>(12)?),
|
|
293
|
+
last_message: parse_json(r.get::<_, Option<String>>(13)?),
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fn map_part(r: &rusqlite::Row) -> rusqlite::Result<PartRow> {
|
|
298
|
+
Ok(PartRow {
|
|
299
|
+
id: r.get(0)?,
|
|
300
|
+
session_id: r.get(1)?,
|
|
301
|
+
message_id: r.get(2)?,
|
|
302
|
+
time_created: r.get::<_, Option<i64>>(3)?.unwrap_or_default(),
|
|
303
|
+
time_updated: r.get::<_, Option<i64>>(4)?.unwrap_or_default(),
|
|
304
|
+
data: parse_json(r.get::<_, Option<String>>(5)?).unwrap_or(Value::Null),
|
|
305
|
+
message: parse_json(r.get::<_, Option<String>>(6)?).unwrap_or(Value::Null),
|
|
306
|
+
directory: r.get::<_, Option<String>>(7)?.unwrap_or_default(),
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Unparseable JSON is a row we cannot describe, not a poll we should fail:
|
|
311
|
+
/// one malformed `data` blob must not stop the other product's whole history
|
|
312
|
+
/// from shipping.
|
|
313
|
+
fn parse_json(raw: Option<String>) -> Option<Value> {
|
|
314
|
+
serde_json::from_str(raw.as_deref()?).ok()
|
|
315
|
+
}
|