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,318 @@
|
|
|
1
|
+
//! Hermes session capture — a [`sqlitepoll`](crate::sqlitepoll) adapter.
|
|
2
|
+
//!
|
|
3
|
+
//! Hermes (Nous Research's Slack/Telegram/CLI gateway) keeps everything in one
|
|
4
|
+
//! SQLite database at `~/.hermes/state.db`. We open it read-only through
|
|
5
|
+
//! [`crate::sqlitepoll::open_readonly`] and read two tables — `sessions` and
|
|
6
|
+
//! `messages`. Hermes's own files are never written, moved or deleted.
|
|
7
|
+
//!
|
|
8
|
+
//! # `~/.hermes/sessions/` must never be read, despite its name
|
|
9
|
+
//!
|
|
10
|
+
//! That directory holds no transcripts. It holds
|
|
11
|
+
//! `request_dump_<sessionId>_<ts>.json` error dumps — ~60 KB each on the probe
|
|
12
|
+
//! capture — and every one of them **embeds a partially-redacted Authorization
|
|
13
|
+
//! bearer token** for the model provider, because a request dump is the whole
|
|
14
|
+
//! outbound HTTP request. Uploading one would exfiltrate a credential to the
|
|
15
|
+
//! ingest endpoint. The directory name actively invites the wrong glob, which
|
|
16
|
+
//! is why this warning is here and not only in a commit message: nothing in
|
|
17
|
+
//! this module walks a directory at all, and nothing should start.
|
|
18
|
+
//!
|
|
19
|
+
//! Four more things in the Hermes home are out of scope for their own reasons:
|
|
20
|
+
//!
|
|
21
|
+
//! * `messages_fts*` / `messages_fts_trigram*` — FTS5 shadow tables. They carry
|
|
22
|
+
//! a rowid-keyed copy of every message's text, so reading them would ship the
|
|
23
|
+
//! conversation twice under ids that mean nothing.
|
|
24
|
+
//! * `logs/agent.log` — unstructured, and the same content already arrives
|
|
25
|
+
//! through `messages`.
|
|
26
|
+
//! * `cache/` — provider model metadata (254 KB of OpenRouter's catalogue on
|
|
27
|
+
//! the capture). Nothing about any session.
|
|
28
|
+
//! * `sandboxes/` — the agent's own scratch working trees.
|
|
29
|
+
//!
|
|
30
|
+
//! Within the database, `async_delegations`, `session_model_usage`,
|
|
31
|
+
//! `gateway_routing`, `compression_locks` and `state_meta` are also not read:
|
|
32
|
+
//! they are gateway bookkeeping, and the parts worth reporting (token totals,
|
|
33
|
+
//! end reason) are already denormalised onto `sessions`.
|
|
34
|
+
//!
|
|
35
|
+
//! # Rows are mutated AND deleted, so the watermark is not fully safe
|
|
36
|
+
//!
|
|
37
|
+
//! [`Watermark::RowId`] is right for the happy path — `messages.id` is an
|
|
38
|
+
//! `INTEGER PRIMARY KEY AUTOINCREMENT`, Hermes appends in order, and a session
|
|
39
|
+
//! that just runs to completion is captured exactly once. It is not the whole
|
|
40
|
+
//! story, and pretending otherwise would hide two real gaps:
|
|
41
|
+
//!
|
|
42
|
+
//! * **Updates behind the watermark are missed.** `hermes_state.py` has five
|
|
43
|
+
//! `UPDATE messages SET active = …` statements plus `UPDATE messages SET
|
|
44
|
+
//! api_content = ?`, and ~20 `UPDATE sessions SET …`. A row's `active` flag
|
|
45
|
+
//! flipping to 0 after we shipped it is never re-read, so a message we
|
|
46
|
+
//! reported as in-context can silently leave the model's context. The
|
|
47
|
+
//! `sessions` updates matter less because session columns are read fresh on
|
|
48
|
+
//! every poll that touches one of that session's rows.
|
|
49
|
+
//! * **Deletions rewrite history.** Rewind and compaction issue `DELETE FROM
|
|
50
|
+
//! messages` in eight-plus places. Deleted rows are already shipped and stay
|
|
51
|
+
//! shipped; the product shows a turn Hermes no longer has.
|
|
52
|
+
//!
|
|
53
|
+
//! [`Watermark::UpdatedAt`] cannot fix either one, because `messages` has no
|
|
54
|
+
//! last-modified column to order by — `timestamp` is the message's own time and
|
|
55
|
+
//! is never touched by those updates. Closing the gap needs a real change-feed
|
|
56
|
+
//! (a content hash per session, or Hermes emitting one), which is follow-on
|
|
57
|
+
//! work rather than something to half-implement here.
|
|
58
|
+
//!
|
|
59
|
+
//! # Both session shapes are real
|
|
60
|
+
//!
|
|
61
|
+
//! `cwd` **is** populated for `source='cli'` sessions (5/5 on the probe, with
|
|
62
|
+
//! `git_branch` and `git_repo_root` beside it), correcting a long-standing
|
|
63
|
+
//! assumption that Hermes is cwd-less. Gateway sessions (Slack, Telegram) have
|
|
64
|
+
//! NULL `cwd`. Both are handled explicitly — see
|
|
65
|
+
//! [`transform::agent_id`].
|
|
66
|
+
//!
|
|
67
|
+
//! Session ids are `YYYYMMDD_HHMMSS_<6hex>`, not UUIDs. They are therefore
|
|
68
|
+
//! lexicographically time-sortable, which nothing here relies on but which is
|
|
69
|
+
//! worth knowing before someone writes a UUID-shaped validator.
|
|
70
|
+
|
|
71
|
+
pub mod transform;
|
|
72
|
+
|
|
73
|
+
use std::collections::HashMap;
|
|
74
|
+
use std::path::{Path, PathBuf};
|
|
75
|
+
|
|
76
|
+
use rusqlite::Connection;
|
|
77
|
+
|
|
78
|
+
use crate::sqlitepoll::{PollOutcome, PollRequest, SqliteFormat, Watermark};
|
|
79
|
+
use transform::{MessageRow, SessionMeta};
|
|
80
|
+
|
|
81
|
+
/// The format table the poller drives.
|
|
82
|
+
pub const FORMAT: SqliteFormat = SqliteFormat {
|
|
83
|
+
kind: "hermes",
|
|
84
|
+
// See the module docs: correct on the happy path, honestly incomplete under
|
|
85
|
+
// rewind and compaction, and `UpdatedAt` is not available as an alternative
|
|
86
|
+
// because `messages` has no last-modified column.
|
|
87
|
+
watermark: Watermark::RowId,
|
|
88
|
+
poll,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/// The agent id used when a message's session row cannot be found at all.
|
|
92
|
+
pub const DEFAULT_AGENT_ID: &str = "hermes";
|
|
93
|
+
|
|
94
|
+
/// Directory under the Hermes root that holds non-default profiles.
|
|
95
|
+
const PROFILES_DIR: &str = "profiles";
|
|
96
|
+
|
|
97
|
+
/// The database file inside every Hermes home.
|
|
98
|
+
const STATE_DB: &str = "state.db";
|
|
99
|
+
|
|
100
|
+
/// Columns read from `messages`. Listed explicitly rather than `SELECT *` so a
|
|
101
|
+
/// new Hermes column cannot shift a positional index underneath us.
|
|
102
|
+
const SELECT_MESSAGES: &str = "SELECT id, session_id, role, content, tool_call_id, tool_calls, \
|
|
103
|
+
tool_name, timestamp, finish_reason, active \
|
|
104
|
+
FROM messages WHERE id > ?1 ORDER BY id LIMIT ?2";
|
|
105
|
+
|
|
106
|
+
/// Columns read from `sessions`. See [`SessionMeta`] for what is left out and
|
|
107
|
+
/// why.
|
|
108
|
+
const SELECT_SESSION: &str = "SELECT source, model, cwd, git_branch, git_repo_root, chat_type, \
|
|
109
|
+
title, started_at, ended_at, end_reason, input_tokens, output_tokens \
|
|
110
|
+
FROM sessions WHERE id = ?1";
|
|
111
|
+
|
|
112
|
+
/// The surviving id range of one session's messages.
|
|
113
|
+
const SELECT_BOUNDS: &str = "SELECT MIN(id), MAX(id) FROM messages WHERE session_id = ?1";
|
|
114
|
+
|
|
115
|
+
/// Read messages past the watermark and turn them into events.
|
|
116
|
+
///
|
|
117
|
+
/// Two queries per distinct session in the batch, not per row: the session
|
|
118
|
+
/// lookup and its id bounds are what let `agent_start` / `agent_end` fire
|
|
119
|
+
/// exactly once without the poller remembering anything between passes, and a
|
|
120
|
+
/// correlated subquery per row would pay for that on every message of a long
|
|
121
|
+
/// session instead of once.
|
|
122
|
+
fn poll(conn: &Connection, req: &PollRequest) -> rusqlite::Result<PollOutcome> {
|
|
123
|
+
// A zero limit would otherwise ask SQLite for nothing and then report
|
|
124
|
+
// `more`, spinning the drain loop.
|
|
125
|
+
let limit = req.max_rows.clamp(1, i64::MAX as u64) as i64;
|
|
126
|
+
|
|
127
|
+
let mut stmt = conn.prepare_cached(SELECT_MESSAGES)?;
|
|
128
|
+
let rows = stmt
|
|
129
|
+
.query_map((req.watermark, limit), |r| {
|
|
130
|
+
Ok(MessageRow {
|
|
131
|
+
id: r.get(0)?,
|
|
132
|
+
session_id: r.get(1)?,
|
|
133
|
+
role: r.get(2)?,
|
|
134
|
+
content: r.get(3)?,
|
|
135
|
+
tool_call_id: r.get(4)?,
|
|
136
|
+
tool_calls: r.get(5)?,
|
|
137
|
+
tool_name: r.get(6)?,
|
|
138
|
+
timestamp: r.get(7)?,
|
|
139
|
+
finish_reason: r.get(8)?,
|
|
140
|
+
// NOT NULL DEFAULT 1 in the schema, but `idx_messages_active_null`
|
|
141
|
+
// exists specifically for `active IS NULL`, so Hermes has clearly
|
|
142
|
+
// shipped rows without one. Treated as active, which is what the
|
|
143
|
+
// column default means.
|
|
144
|
+
active: r.get::<_, Option<i64>>(9)?.unwrap_or(1),
|
|
145
|
+
})
|
|
146
|
+
})?
|
|
147
|
+
.collect::<rusqlite::Result<Vec<_>>>()?;
|
|
148
|
+
|
|
149
|
+
let mut outcome = PollOutcome {
|
|
150
|
+
watermark: req.watermark,
|
|
151
|
+
rows_seen: rows.len() as u64,
|
|
152
|
+
// Hitting the limit means a backlog is waiting; the engine polls again
|
|
153
|
+
// immediately rather than sleeping, so a restart after downtime drains
|
|
154
|
+
// instead of trickling.
|
|
155
|
+
more: rows.len() as i64 >= limit,
|
|
156
|
+
..Default::default()
|
|
157
|
+
};
|
|
158
|
+
if let Some(last) = rows.last() {
|
|
159
|
+
outcome.watermark = last.id;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let mut sessions: HashMap<String, Option<SessionMeta>> = HashMap::new();
|
|
163
|
+
for row in &rows {
|
|
164
|
+
if !sessions.contains_key(&row.session_id) {
|
|
165
|
+
let meta = load_session(conn, &row.session_id)?;
|
|
166
|
+
sessions.insert(row.session_id.clone(), meta);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Tool-call id → name, so a result row is not a blank row. Scoped to this
|
|
171
|
+
// poll on purpose: carrying it across polls would make the output depend on
|
|
172
|
+
// what earlier passes happened to see, and the result row's own `tool_name`
|
|
173
|
+
// already covers a batch cut between a call and its result.
|
|
174
|
+
let mut pending: HashMap<String, String> = HashMap::new();
|
|
175
|
+
|
|
176
|
+
for row in &rows {
|
|
177
|
+
let meta = sessions.get(&row.session_id).and_then(Option::as_ref);
|
|
178
|
+
let agent_id = transform::agent_id(meta, &req.agent_id);
|
|
179
|
+
|
|
180
|
+
if let Some(meta) = meta
|
|
181
|
+
&& meta.first_message_id == Some(row.id)
|
|
182
|
+
&& let Some(ev) = transform::agent_start(row, meta, &agent_id, &req.environment)
|
|
183
|
+
{
|
|
184
|
+
outcome.events.push(ev);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
outcome.events.extend(transform::message_events(
|
|
188
|
+
row,
|
|
189
|
+
meta,
|
|
190
|
+
&agent_id,
|
|
191
|
+
&req.environment,
|
|
192
|
+
&mut pending,
|
|
193
|
+
));
|
|
194
|
+
|
|
195
|
+
// Gated on `ended_at` so a live session is not ended on every pass; see
|
|
196
|
+
// `transform::agent_end` for the race that gate costs.
|
|
197
|
+
if let Some(meta) = meta
|
|
198
|
+
&& meta.last_message_id == Some(row.id)
|
|
199
|
+
&& meta.ended_at.is_some()
|
|
200
|
+
&& let Some(ev) = transform::agent_end(row, meta, &agent_id, &req.environment)
|
|
201
|
+
{
|
|
202
|
+
outcome.events.push(ev);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Ok(outcome)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// One session's metadata and the id range of its surviving messages.
|
|
210
|
+
///
|
|
211
|
+
/// A missing session row is `Ok(None)`, not an error: `messages.session_id` is
|
|
212
|
+
/// a foreign key, but a partially-applied rewind or a hand-edited database
|
|
213
|
+
/// would leave orphans, and one orphan must not stop the poll for every other
|
|
214
|
+
/// session in the batch.
|
|
215
|
+
fn load_session(conn: &Connection, session_id: &str) -> rusqlite::Result<Option<SessionMeta>> {
|
|
216
|
+
let mut stmt = conn.prepare_cached(SELECT_SESSION)?;
|
|
217
|
+
let meta = stmt.query_row([session_id], |r| {
|
|
218
|
+
Ok(SessionMeta {
|
|
219
|
+
source: r.get(0)?,
|
|
220
|
+
model: r.get(1)?,
|
|
221
|
+
cwd: r.get(2)?,
|
|
222
|
+
git_branch: r.get(3)?,
|
|
223
|
+
git_repo_root: r.get(4)?,
|
|
224
|
+
chat_type: r.get(5)?,
|
|
225
|
+
title: r.get(6)?,
|
|
226
|
+
started_at: r.get(7)?,
|
|
227
|
+
ended_at: r.get(8)?,
|
|
228
|
+
end_reason: r.get(9)?,
|
|
229
|
+
input_tokens: r.get(10)?,
|
|
230
|
+
output_tokens: r.get(11)?,
|
|
231
|
+
..Default::default()
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
let mut meta = match meta {
|
|
235
|
+
Ok(meta) => meta,
|
|
236
|
+
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
|
237
|
+
Err(err) => return Err(err),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
let mut bounds = conn.prepare_cached(SELECT_BOUNDS)?;
|
|
241
|
+
let (first, last) = bounds.query_row([session_id], |r| {
|
|
242
|
+
Ok((r.get::<_, Option<i64>>(0)?, r.get::<_, Option<i64>>(1)?))
|
|
243
|
+
})?;
|
|
244
|
+
meta.first_message_id = first;
|
|
245
|
+
meta.last_message_id = last;
|
|
246
|
+
Ok(Some(meta))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// The Hermes ROOT home — the directory that owns `profiles/`.
|
|
250
|
+
///
|
|
251
|
+
/// `HERMES_HOME` is Hermes's own override and is honoured so a profile-scoped
|
|
252
|
+
/// shell and the collector agree on what "all profiles" means. It may point AT
|
|
253
|
+
/// a profile (`<root>/profiles/<name>`), because that is what the generated
|
|
254
|
+
/// per-profile alias wrapper exports; we climb back to `<root>` in that case so
|
|
255
|
+
/// discovery still sees every sibling profile rather than collecting from one
|
|
256
|
+
/// and silently ignoring the rest.
|
|
257
|
+
pub fn hermes_root() -> PathBuf {
|
|
258
|
+
let home = std::env::var_os("HERMES_HOME")
|
|
259
|
+
.map(PathBuf::from)
|
|
260
|
+
.filter(|p| !p.as_os_str().is_empty())
|
|
261
|
+
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".hermes")))
|
|
262
|
+
.unwrap_or_else(|| PathBuf::from(".hermes"));
|
|
263
|
+
root_from_home(&home)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Climb from a possibly-profile-scoped Hermes home to the root that owns it.
|
|
267
|
+
pub fn root_from_home(home: &Path) -> PathBuf {
|
|
268
|
+
if let Some(parent) = home.parent()
|
|
269
|
+
&& parent.file_name().is_some_and(|n| n == PROFILES_DIR)
|
|
270
|
+
&& let Some(root) = parent.parent()
|
|
271
|
+
{
|
|
272
|
+
return root.to_path_buf();
|
|
273
|
+
}
|
|
274
|
+
home.to_path_buf()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Every Hermes database on disk: the root home's, then each profile's.
|
|
278
|
+
///
|
|
279
|
+
/// A Hermes "profile" is not a column or a flag — it is a whole separate home
|
|
280
|
+
/// directory with its own `config.yaml`, `SOUL.md` and **its own `state.db`**.
|
|
281
|
+
/// Reading only `~/.hermes/state.db` makes every non-default profile's sessions
|
|
282
|
+
/// invisible, which is exactly the bug the audit pillar shipped with before
|
|
283
|
+
/// profile support landed.
|
|
284
|
+
///
|
|
285
|
+
/// Each path is a separate logical stream and needs its own `state_dir`: the
|
|
286
|
+
/// sqlite poller keys its cursor on a fixed synthetic `(0, 0)`, so two profiles
|
|
287
|
+
/// sharing a state directory would overwrite each other's watermark.
|
|
288
|
+
///
|
|
289
|
+
/// Fail-open — an unreadable `profiles/` just means "default only", which is
|
|
290
|
+
/// the single-profile install everyone starts with.
|
|
291
|
+
pub fn default_db_paths() -> Vec<PathBuf> {
|
|
292
|
+
db_paths_under(&hermes_root())
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// [`default_db_paths`] against an explicit root. Split out so it is testable
|
|
296
|
+
/// without mutating the process environment.
|
|
297
|
+
pub fn db_paths_under(root: &Path) -> Vec<PathBuf> {
|
|
298
|
+
let mut out = vec![root.join(STATE_DB)];
|
|
299
|
+
let Ok(entries) = std::fs::read_dir(root.join(PROFILES_DIR)) else {
|
|
300
|
+
return out;
|
|
301
|
+
};
|
|
302
|
+
let mut names: Vec<PathBuf> = entries
|
|
303
|
+
.filter_map(|e| e.ok())
|
|
304
|
+
.filter(|e| {
|
|
305
|
+
// A symlinked profile directory is legitimate and `is_dir()` on the
|
|
306
|
+
// DirEntry's file type is false for one, so the metadata (which
|
|
307
|
+
// follows the link) decides.
|
|
308
|
+
e.metadata().map(|m| m.is_dir()).unwrap_or(false)
|
|
309
|
+
&& !e.file_name().to_string_lossy().starts_with('.')
|
|
310
|
+
})
|
|
311
|
+
.map(|e| e.path())
|
|
312
|
+
.collect();
|
|
313
|
+
// Sorted so the set of sources a daemon starts is stable across restarts
|
|
314
|
+
// and across machines; directory order is not.
|
|
315
|
+
names.sort();
|
|
316
|
+
out.extend(names.into_iter().map(|p| p.join(STATE_DB)));
|
|
317
|
+
out
|
|
318
|
+
}
|