failproofai 1.0.0-beta.2 → 1.0.0-beta.3
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.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.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.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.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.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/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- 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]__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]__0qucxyj._.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]__1psjybo._.js → [root-of-the-server]__10s_z4l._.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]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.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/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_0jfts82.js} +1 -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/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- 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 +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2195 -770
- package/dist/worker.mjs +365 -52
- package/package.json +2 -2
- 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/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/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
//! goose session capture — a [`sqlitepoll`](crate::sqlitepoll) adapter.
|
|
2
|
+
//!
|
|
3
|
+
//! goose (Block's local dev agent) keeps every session in ONE SQLite database,
|
|
4
|
+
//! `~/.local/share/goose/sessions/sessions.db`. There are no per-session files,
|
|
5
|
+
//! so there is nothing for [`filetail`](crate::filetail) to tail. Verified live
|
|
6
|
+
//! against goose v1.43.0, `schema_version` 15:
|
|
7
|
+
//!
|
|
8
|
+
//! ```text
|
|
9
|
+
//! sessions(id TEXT PK `YYYYMMDD_N`, name, description, user_set_name,
|
|
10
|
+
//! session_type TEXT NOT NULL DEFAULT 'user', working_dir TEXT NOT NULL,
|
|
11
|
+
//! created_at, updated_at, extension_data, …16 token/cost columns…,
|
|
12
|
+
//! schedule_id, recipe_json, provider_name, model_config_json,
|
|
13
|
+
//! goose_mode, archived_at, project_id, parent_session_id)
|
|
14
|
+
//! messages(id INTEGER PK AUTOINCREMENT, message_id TEXT, session_id TEXT NOT NULL,
|
|
15
|
+
//! role TEXT NOT NULL, content_json TEXT NOT NULL,
|
|
16
|
+
//! created_timestamp INTEGER NOT NULL, timestamp TIMESTAMP,
|
|
17
|
+
//! tokens INTEGER, metadata_json TEXT)
|
|
18
|
+
//! ```
|
|
19
|
+
//!
|
|
20
|
+
//! # `content_json` is a Claude-style typed-block ARRAY
|
|
21
|
+
//!
|
|
22
|
+
//! Not OpenAI-style like Devin. Shapes seen in the live corpus:
|
|
23
|
+
//!
|
|
24
|
+
//! ```text
|
|
25
|
+
//! {"type":"text","text":…}
|
|
26
|
+
//! {"type":"toolRequest","id":"tooluse_…",
|
|
27
|
+
//! "toolCall":{"status":"success","value":{"name":…,"arguments":{…}}},
|
|
28
|
+
//! "_meta":{"goose_extension":"developer"}}
|
|
29
|
+
//! {"type":"toolResponse","id":"tooluse_…",
|
|
30
|
+
//! "toolResult":{"status":"success","value":{"content":[{"type":"text","text":…}],
|
|
31
|
+
//! "structuredContent":{…},"isError":false}}}
|
|
32
|
+
//! ```
|
|
33
|
+
//!
|
|
34
|
+
//! Two properties of that layout drive most of the code below:
|
|
35
|
+
//!
|
|
36
|
+
//! * **A `toolResponse` arrives in a `role:'user'` row.** Branching on `role`
|
|
37
|
+
//! alone therefore renders every tool result as a user prompt — a session's
|
|
38
|
+
//! timeline turns into the model asking itself questions. Every branch here
|
|
39
|
+
//! is on the *block* type, with `role` used only to split plain text into a
|
|
40
|
+
//! request vs. a response.
|
|
41
|
+
//! * **Rows are NOT turn-aligned.** One assistant turn that emitted four
|
|
42
|
+
//! parallel `shell` calls produced *five* rows (a preamble text row plus one
|
|
43
|
+
//! row per call), and its four results came back as four more rows, all
|
|
44
|
+
//! sharing a single `created_timestamp`. Nothing here may assume one row is
|
|
45
|
+
//! one turn, and the sub-second ordering scheme exists because of the shared
|
|
46
|
+
//! timestamp — see [`transform::slot_micros`].
|
|
47
|
+
//!
|
|
48
|
+
//! # A rowid watermark is right for the happy path, and is NOT airtight
|
|
49
|
+
//!
|
|
50
|
+
//! [`Watermark::RowId`] is declared because `messages.id` is
|
|
51
|
+
//! `INTEGER PRIMARY KEY AUTOINCREMENT` and a normal session only ever appends:
|
|
52
|
+
//! a tool call's result is a *new* row, not an update to the call's row, which
|
|
53
|
+
//! is exactly the failure mode that forces opencode onto `UpdatedAt`.
|
|
54
|
+
//!
|
|
55
|
+
//! It is not a guarantee. The goose binary contains
|
|
56
|
+
//! `UPDATE messages SET content_json`, `UPDATE messages SET metadata_json` and
|
|
57
|
+
//! rewind's `DELETE FROM messages`. So:
|
|
58
|
+
//!
|
|
59
|
+
//! * a row **edited after** we shipped it keeps the shipped copy — the edit is
|
|
60
|
+
//! invisible to us, because `id` did not move;
|
|
61
|
+
//! * a row **deleted** behind the watermark leaves an event on the server with
|
|
62
|
+
//! no counterpart in goose's own history.
|
|
63
|
+
//!
|
|
64
|
+
//! There is no cheap fix: `messages` carries no `updated_at`, so `UpdatedAt`
|
|
65
|
+
//! has no column to order by, and re-reading the whole table every poll would
|
|
66
|
+
//! re-ship an entire history to be deduped on every pass. The honest position
|
|
67
|
+
//! is that this source is append-accurate and edit-blind, and that the two
|
|
68
|
+
//! writes above have not been observed firing in a normal session.
|
|
69
|
+
//!
|
|
70
|
+
//! # `session_type='hidden'` is filtered
|
|
71
|
+
//!
|
|
72
|
+
//! Those are `goose run --no-session` scratch runs, which the product should
|
|
73
|
+
//! not show as sessions at all. The other values (`user`, `subagent`,
|
|
74
|
+
//! `scheduled`, `terminal`, `acp`) are kept — filtering by an allowlist would
|
|
75
|
+
//! silently drop a session type the next goose release adds.
|
|
76
|
+
//!
|
|
77
|
+
//! Hidden rows still advance the watermark and still act as the *predecessor*
|
|
78
|
+
//! of the row after them, so filtering them can never strand a visible session
|
|
79
|
+
//! (see the `agent_end` rule below).
|
|
80
|
+
//!
|
|
81
|
+
//! # Where `agent_start` and `agent_end` come from
|
|
82
|
+
//!
|
|
83
|
+
//! goose writes no session-start or session-end marker, and the sqlite engine
|
|
84
|
+
//! exposes no idle callback the way [`filetail`](crate::filetail) does — a
|
|
85
|
+
//! [`SqliteFormat`] is one pure `poll`, so the wall clock is off limits
|
|
86
|
+
//! (consulting it would make two reads of the same rows produce different
|
|
87
|
+
//! bytes, which is precisely what the server's content-hash dedup relies on).
|
|
88
|
+
//! Both events are therefore derived from the row stream itself:
|
|
89
|
+
//!
|
|
90
|
+
//! * `agent_start` rides the session's **first** message row (`MIN(id)`), so it
|
|
91
|
+
//! is emitted exactly once and never depends on when we polled.
|
|
92
|
+
//! * `agent_end` rides the row **after** a session's last row: when row *R*
|
|
93
|
+
//! belongs to a different session than its immediate predecessor *P*, and *P*
|
|
94
|
+
//! is `MAX(id)` for its session, *P*'s session is finished and its end is
|
|
95
|
+
//! emitted at *P*'s timestamp. Each row is processed once, so this fires once.
|
|
96
|
+
//!
|
|
97
|
+
//! The known gap: the newest session in the database has no successor row, so
|
|
98
|
+
//! it stays open until goose is used again. That is deliberate. The
|
|
99
|
+
//! alternatives are worse — a wall-clock idle rule breaks purity, and
|
|
100
|
+
//! recomputing ends from the `sessions` table each poll re-ships an `agent_end`
|
|
101
|
+
//! per session forever, for the entire history, on an idle machine.
|
|
102
|
+
//!
|
|
103
|
+
//! A session that is *resumed* after another session wrote gets a second
|
|
104
|
+
//! `agent_end` (its `MAX(id)` moved). That reads correctly on a timeline: it
|
|
105
|
+
//! genuinely was two runs.
|
|
106
|
+
|
|
107
|
+
pub mod transform;
|
|
108
|
+
|
|
109
|
+
use std::collections::BTreeMap;
|
|
110
|
+
use std::path::PathBuf;
|
|
111
|
+
|
|
112
|
+
use rusqlite::{Connection, OptionalExtension, params};
|
|
113
|
+
use serde_json::Value;
|
|
114
|
+
|
|
115
|
+
use crate::sqlitepoll::{PollOutcome, PollRequest, SqliteFormat, Watermark};
|
|
116
|
+
use transform::{MessageRow, RowCtx};
|
|
117
|
+
|
|
118
|
+
/// The format table the sqlite engine drives.
|
|
119
|
+
pub const FORMAT: SqliteFormat = SqliteFormat {
|
|
120
|
+
kind: "goose",
|
|
121
|
+
watermark: Watermark::RowId,
|
|
122
|
+
poll,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/// The agent id used when a session has no usable `working_dir`.
|
|
126
|
+
pub const DEFAULT_AGENT_ID: &str = "goose";
|
|
127
|
+
|
|
128
|
+
/// `goose run --no-session` scratch runs.
|
|
129
|
+
const HIDDEN_SESSION_TYPE: &str = "hidden";
|
|
130
|
+
|
|
131
|
+
// ── database location ────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/// Where goose keeps its sessions database.
|
|
134
|
+
///
|
|
135
|
+
/// `GOOSE_DB_PATH` and `GOOSE_HOME` are the same overrides goose itself and
|
|
136
|
+
/// `lib/goose-sessions.ts` honour, so a relocated goose home stays covered by
|
|
137
|
+
/// both halves of the product rather than by whichever half was tested.
|
|
138
|
+
pub fn db_path() -> PathBuf {
|
|
139
|
+
db_path_from(
|
|
140
|
+
var("GOOSE_DB_PATH"),
|
|
141
|
+
var("GOOSE_HOME"),
|
|
142
|
+
var("XDG_DATA_HOME"),
|
|
143
|
+
var("HOME"),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn var(name: &str) -> Option<PathBuf> {
|
|
148
|
+
std::env::var_os(name)
|
|
149
|
+
.filter(|v| !v.is_empty())
|
|
150
|
+
.map(PathBuf::from)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The resolution order, split out so it is testable without mutating the
|
|
154
|
+
/// process environment (which is `unsafe` in edition 2024 and races every other
|
|
155
|
+
/// test in the binary).
|
|
156
|
+
///
|
|
157
|
+
/// `GOOSE_HOME` names the *data* directory that contains `sessions/`, matching
|
|
158
|
+
/// goose's own meaning of the variable — pointing it at a database file is a
|
|
159
|
+
/// mistake `GOOSE_DB_PATH` exists to serve.
|
|
160
|
+
pub fn db_path_from(
|
|
161
|
+
db: Option<PathBuf>,
|
|
162
|
+
goose_home: Option<PathBuf>,
|
|
163
|
+
xdg_data_home: Option<PathBuf>,
|
|
164
|
+
home: Option<PathBuf>,
|
|
165
|
+
) -> PathBuf {
|
|
166
|
+
if let Some(p) = db {
|
|
167
|
+
return p;
|
|
168
|
+
}
|
|
169
|
+
let data = goose_home.unwrap_or_else(|| {
|
|
170
|
+
xdg_data_home
|
|
171
|
+
.unwrap_or_else(|| home.unwrap_or_default().join(".local").join("share"))
|
|
172
|
+
.join("goose")
|
|
173
|
+
});
|
|
174
|
+
data.join("sessions").join("sessions.db")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── queries ──────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/// The batch. A LEFT JOIN rather than an inner one: the `session_id` foreign
|
|
180
|
+
/// key is declared but SQLite does not enforce it unless `PRAGMA foreign_keys`
|
|
181
|
+
/// is on, so an inner join would make a message with a missing session row
|
|
182
|
+
/// vanish silently instead of shipping with a fallback agent id.
|
|
183
|
+
const SELECT_ROWS: &str = "\
|
|
184
|
+
SELECT m.id, m.session_id, m.role, m.content_json, m.created_timestamp, m.metadata_json, \
|
|
185
|
+
s.session_type, s.working_dir, s.model_config_json, s.provider_name \
|
|
186
|
+
FROM messages m \
|
|
187
|
+
LEFT JOIN sessions s ON s.id = m.session_id \
|
|
188
|
+
WHERE m.id > ?1 \
|
|
189
|
+
ORDER BY m.id \
|
|
190
|
+
LIMIT ?2";
|
|
191
|
+
|
|
192
|
+
/// The row immediately before the batch, so the `agent_end` rule works across a
|
|
193
|
+
/// poll boundary too — otherwise a session whose last row ended the previous
|
|
194
|
+
/// batch would never be closed.
|
|
195
|
+
const SELECT_TAIL: &str = "\
|
|
196
|
+
SELECT m.id, m.session_id, m.created_timestamp, \
|
|
197
|
+
s.session_type, s.working_dir, s.model_config_json, s.provider_name \
|
|
198
|
+
FROM messages m \
|
|
199
|
+
LEFT JOIN sessions s ON s.id = m.session_id \
|
|
200
|
+
WHERE m.id <= ?1 \
|
|
201
|
+
ORDER BY m.id DESC \
|
|
202
|
+
LIMIT 1";
|
|
203
|
+
|
|
204
|
+
/// First and last message of a session, for the start/end rules. Served by
|
|
205
|
+
/// `idx_messages_session`, and cached per poll so a batch touching one session
|
|
206
|
+
/// runs it once.
|
|
207
|
+
const SELECT_BOUNDS: &str = "SELECT MIN(id), MAX(id) FROM messages WHERE session_id = ?1";
|
|
208
|
+
|
|
209
|
+
/// The `toolRequest` row for a result whose call landed in an earlier batch.
|
|
210
|
+
///
|
|
211
|
+
/// `LIKE` rather than a join because the id lives inside the JSON blob. The
|
|
212
|
+
/// pattern's `_` characters (every goose id starts `tooluse_`) are LIKE
|
|
213
|
+
/// wildcards, so this can over-match; that is harmless because the block it
|
|
214
|
+
/// finds is then parsed and its `id` compared exactly. Scoped to the session
|
|
215
|
+
/// and to rows before the result, so the scan is bounded by one session's
|
|
216
|
+
/// history and only runs when a call and its result straddle a poll boundary.
|
|
217
|
+
const SELECT_TOOL_REQUEST: &str = "\
|
|
218
|
+
SELECT content_json FROM messages \
|
|
219
|
+
WHERE session_id = ?1 AND id < ?2 AND content_json LIKE ?3 \
|
|
220
|
+
ORDER BY id DESC LIMIT 1";
|
|
221
|
+
|
|
222
|
+
// ── row types ────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/// The `sessions` columns joined onto every message row.
|
|
225
|
+
#[derive(Debug, Clone, Default)]
|
|
226
|
+
struct SessionCols {
|
|
227
|
+
session_type: Option<String>,
|
|
228
|
+
working_dir: Option<String>,
|
|
229
|
+
model_config_json: Option<String>,
|
|
230
|
+
provider_name: Option<String>,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
impl SessionCols {
|
|
234
|
+
fn is_hidden(&self) -> bool {
|
|
235
|
+
self.session_type.as_deref() == Some(HIDDEN_SESSION_TYPE)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// One row of the batch.
|
|
240
|
+
struct BatchRow {
|
|
241
|
+
msg: MessageRow,
|
|
242
|
+
cols: SessionCols,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// The predecessor row. Deliberately does not carry `content_json`: it exists
|
|
246
|
+
/// only to answer "did the previous row end its session", and that blob reached
|
|
247
|
+
/// 52 KB in the live corpus.
|
|
248
|
+
struct Tail {
|
|
249
|
+
id: i64,
|
|
250
|
+
session_id: String,
|
|
251
|
+
created_timestamp: i64,
|
|
252
|
+
cols: SessionCols,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── the poll ─────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/// Read rows past the watermark and turn them into events.
|
|
258
|
+
///
|
|
259
|
+
/// Pure with respect to `(connection contents, request)`: no clock, no counter,
|
|
260
|
+
/// no state carried between calls. Everything order-dependent is derived from
|
|
261
|
+
/// `messages.id`, so re-reading the same rows produces byte-identical events
|
|
262
|
+
/// and the server's content-hash dedup collapses them.
|
|
263
|
+
fn poll(conn: &Connection, req: &PollRequest) -> rusqlite::Result<PollOutcome> {
|
|
264
|
+
let rows = read_rows(conn, req)?;
|
|
265
|
+
|
|
266
|
+
let mut out = PollOutcome {
|
|
267
|
+
watermark: req.watermark,
|
|
268
|
+
rows_seen: rows.len() as u64,
|
|
269
|
+
// A backlog should drain rather than trickle. Guarded on a non-zero
|
|
270
|
+
// limit so a `max_rows: 0` misconfiguration cannot spin the drain loop.
|
|
271
|
+
more: req.max_rows > 0 && rows.len() as u64 >= req.max_rows,
|
|
272
|
+
events: Vec::new(),
|
|
273
|
+
};
|
|
274
|
+
let Some(last) = rows.last() else {
|
|
275
|
+
return Ok(out);
|
|
276
|
+
};
|
|
277
|
+
// Advanced past every row EXAMINED, not every row emitted, so filtered
|
|
278
|
+
// (hidden) rows are not re-read on every poll for the life of the machine.
|
|
279
|
+
out.watermark = last.msg.id.max(req.watermark);
|
|
280
|
+
|
|
281
|
+
let tool_names = resolve_tool_names(conn, &rows)?;
|
|
282
|
+
let mut bounds: BTreeMap<String, (i64, i64)> = BTreeMap::new();
|
|
283
|
+
let mut ctxs: BTreeMap<String, RowCtx> = BTreeMap::new();
|
|
284
|
+
let mut prev = read_tail(conn, req.watermark)?;
|
|
285
|
+
|
|
286
|
+
for row in &rows {
|
|
287
|
+
if let Some(p) = prev.as_ref()
|
|
288
|
+
&& p.session_id != row.msg.session_id
|
|
289
|
+
&& !p.cols.is_hidden()
|
|
290
|
+
&& session_bounds(conn, &mut bounds, &p.session_id)?.1 == p.id
|
|
291
|
+
{
|
|
292
|
+
let ctx = context(&mut ctxs, &p.session_id, &p.cols, req);
|
|
293
|
+
if let Some(ev) = transform::agent_end(&p.session_id, p.id, p.created_timestamp, ctx) {
|
|
294
|
+
out.events.push(ev);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if !row.cols.is_hidden() {
|
|
299
|
+
let is_first = session_bounds(conn, &mut bounds, &row.msg.session_id)?.0 == row.msg.id;
|
|
300
|
+
let ctx = context(&mut ctxs, &row.msg.session_id, &row.cols, req);
|
|
301
|
+
if is_first
|
|
302
|
+
&& let Some(ev) =
|
|
303
|
+
transform::agent_start(&row.msg, row.cols.working_dir.as_deref(), ctx)
|
|
304
|
+
{
|
|
305
|
+
out.events.push(ev);
|
|
306
|
+
}
|
|
307
|
+
out.events
|
|
308
|
+
.extend(transform::content_events(&row.msg, ctx, &tool_names));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
prev = Some(Tail {
|
|
312
|
+
id: row.msg.id,
|
|
313
|
+
session_id: row.msg.session_id.clone(),
|
|
314
|
+
created_timestamp: row.msg.created_timestamp,
|
|
315
|
+
cols: row.cols.clone(),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Ok(out)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn read_rows(conn: &Connection, req: &PollRequest) -> rusqlite::Result<Vec<BatchRow>> {
|
|
323
|
+
// Saturating rather than `as`: a wrapped negative LIMIT means "no limit" in
|
|
324
|
+
// SQLite, which would quietly turn a huge max_rows into an unbounded read.
|
|
325
|
+
let limit = i64::try_from(req.max_rows).unwrap_or(i64::MAX);
|
|
326
|
+
let mut stmt = conn.prepare(SELECT_ROWS)?;
|
|
327
|
+
let rows = stmt.query_map(params![req.watermark, limit], |r| {
|
|
328
|
+
let content: Option<String> = r.get(3)?;
|
|
329
|
+
Ok(BatchRow {
|
|
330
|
+
msg: MessageRow {
|
|
331
|
+
id: r.get(0)?,
|
|
332
|
+
session_id: r.get::<_, Option<String>>(1)?.unwrap_or_default(),
|
|
333
|
+
role: r.get::<_, Option<String>>(2)?.unwrap_or_default(),
|
|
334
|
+
created_timestamp: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
|
335
|
+
metadata_json: r.get(5)?,
|
|
336
|
+
blocks: transform::parse_blocks(content.as_deref()),
|
|
337
|
+
},
|
|
338
|
+
cols: SessionCols {
|
|
339
|
+
session_type: r.get(6)?,
|
|
340
|
+
working_dir: r.get(7)?,
|
|
341
|
+
model_config_json: r.get(8)?,
|
|
342
|
+
provider_name: r.get(9)?,
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
})?;
|
|
346
|
+
rows.collect()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn read_tail(conn: &Connection, watermark: i64) -> rusqlite::Result<Option<Tail>> {
|
|
350
|
+
conn.query_row(SELECT_TAIL, params![watermark], |r| {
|
|
351
|
+
Ok(Tail {
|
|
352
|
+
id: r.get(0)?,
|
|
353
|
+
session_id: r.get::<_, Option<String>>(1)?.unwrap_or_default(),
|
|
354
|
+
created_timestamp: r.get::<_, Option<i64>>(2)?.unwrap_or(0),
|
|
355
|
+
cols: SessionCols {
|
|
356
|
+
session_type: r.get(3)?,
|
|
357
|
+
working_dir: r.get(4)?,
|
|
358
|
+
model_config_json: r.get(5)?,
|
|
359
|
+
provider_name: r.get(6)?,
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
.optional()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn session_bounds(
|
|
367
|
+
conn: &Connection,
|
|
368
|
+
cache: &mut BTreeMap<String, (i64, i64)>,
|
|
369
|
+
session_id: &str,
|
|
370
|
+
) -> rusqlite::Result<(i64, i64)> {
|
|
371
|
+
if let Some(b) = cache.get(session_id) {
|
|
372
|
+
return Ok(*b);
|
|
373
|
+
}
|
|
374
|
+
let bounds = conn.query_row(SELECT_BOUNDS, params![session_id], |r| {
|
|
375
|
+
Ok((
|
|
376
|
+
r.get::<_, Option<i64>>(0)?.unwrap_or(0),
|
|
377
|
+
r.get::<_, Option<i64>>(1)?.unwrap_or(0),
|
|
378
|
+
))
|
|
379
|
+
})?;
|
|
380
|
+
cache.insert(session_id.to_string(), bounds);
|
|
381
|
+
Ok(bounds)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// The per-session envelope pieces, built once per poll.
|
|
385
|
+
///
|
|
386
|
+
/// Cached because `model_config_json` has to be parsed to find the model and
|
|
387
|
+
/// every row of a session repeats it — a 21-row session would otherwise parse
|
|
388
|
+
/// the same blob 21 times.
|
|
389
|
+
fn context<'a>(
|
|
390
|
+
cache: &'a mut BTreeMap<String, RowCtx>,
|
|
391
|
+
session_id: &str,
|
|
392
|
+
cols: &SessionCols,
|
|
393
|
+
req: &PollRequest,
|
|
394
|
+
) -> &'a RowCtx {
|
|
395
|
+
cache.entry(session_id.to_string()).or_insert_with(|| {
|
|
396
|
+
RowCtx::new(
|
|
397
|
+
cols.working_dir.as_deref(),
|
|
398
|
+
cols.model_config_json.as_deref(),
|
|
399
|
+
cols.provider_name.as_deref(),
|
|
400
|
+
&req.environment,
|
|
401
|
+
&req.agent_id,
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Map every tool-call id in the batch to its tool name.
|
|
407
|
+
///
|
|
408
|
+
/// The name appears on the `toolRequest` block and on NO `toolResponse` block,
|
|
409
|
+
/// so without this every result is a row in the product with a blank tool —
|
|
410
|
+
/// the same defect the Claude source's pending-tool state prevents. The
|
|
411
|
+
/// difference here is that the call and the result are separate *rows*, so a
|
|
412
|
+
/// poll boundary can land between them; the second pass goes back to the
|
|
413
|
+
/// database for exactly those.
|
|
414
|
+
fn resolve_tool_names(
|
|
415
|
+
conn: &Connection,
|
|
416
|
+
rows: &[BatchRow],
|
|
417
|
+
) -> rusqlite::Result<BTreeMap<String, String>> {
|
|
418
|
+
let mut names: BTreeMap<String, String> = BTreeMap::new();
|
|
419
|
+
for row in rows {
|
|
420
|
+
for block in &row.msg.blocks {
|
|
421
|
+
if let Some((id, name)) = transform::tool_request_name(block) {
|
|
422
|
+
names.insert(id, name);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let mut stmt = conn.prepare(SELECT_TOOL_REQUEST)?;
|
|
428
|
+
for row in rows {
|
|
429
|
+
for block in &row.msg.blocks {
|
|
430
|
+
if block.get("type").and_then(Value::as_str) != Some("toolResponse") {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
let Some(id) = block.get("id").and_then(Value::as_str) else {
|
|
434
|
+
continue;
|
|
435
|
+
};
|
|
436
|
+
if names.contains_key(id) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
let found: Option<String> = stmt
|
|
440
|
+
.query_row(
|
|
441
|
+
params![row.msg.session_id, row.msg.id, format!("%\"{id}\"%")],
|
|
442
|
+
|r| r.get(0),
|
|
443
|
+
)
|
|
444
|
+
.optional()?;
|
|
445
|
+
if let Some(content) = found
|
|
446
|
+
&& let Some(name) = transform::parse_blocks(Some(&content))
|
|
447
|
+
.iter()
|
|
448
|
+
.find_map(|b| transform::tool_request_name(b).filter(|(i, _)| i == id))
|
|
449
|
+
.map(|(_, n)| n)
|
|
450
|
+
{
|
|
451
|
+
names.insert(id.to_string(), name);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
Ok(names)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[cfg(test)]
|
|
459
|
+
mod tests {
|
|
460
|
+
use super::*;
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn the_database_path_prefers_the_explicit_override() {
|
|
464
|
+
// GOOSE_DB_PATH names the file; GOOSE_HOME names the data DIRECTORY.
|
|
465
|
+
// Conflating them puts the poller on `<db>/sessions/sessions.db`, which
|
|
466
|
+
// does not exist, and the source then reports "not installed" forever.
|
|
467
|
+
let explicit = db_path_from(
|
|
468
|
+
Some(PathBuf::from("/srv/goose.db")),
|
|
469
|
+
Some(PathBuf::from("/data/goose")),
|
|
470
|
+
None,
|
|
471
|
+
Some(PathBuf::from("/home/u")),
|
|
472
|
+
);
|
|
473
|
+
assert_eq!(explicit, PathBuf::from("/srv/goose.db"));
|
|
474
|
+
|
|
475
|
+
let homed = db_path_from(
|
|
476
|
+
None,
|
|
477
|
+
Some(PathBuf::from("/data/goose")),
|
|
478
|
+
None,
|
|
479
|
+
Some(PathBuf::from("/home/u")),
|
|
480
|
+
);
|
|
481
|
+
assert_eq!(homed, PathBuf::from("/data/goose/sessions/sessions.db"));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#[test]
|
|
485
|
+
fn xdg_data_home_is_honoured_before_the_dot_local_default() {
|
|
486
|
+
// goose builds its data dir from XDG_DATA_HOME, so a machine that moves
|
|
487
|
+
// it would otherwise be polled at a path goose never writes.
|
|
488
|
+
assert_eq!(
|
|
489
|
+
db_path_from(
|
|
490
|
+
None,
|
|
491
|
+
None,
|
|
492
|
+
Some(PathBuf::from("/xdg")),
|
|
493
|
+
Some("/home/u".into())
|
|
494
|
+
),
|
|
495
|
+
PathBuf::from("/xdg/goose/sessions/sessions.db")
|
|
496
|
+
);
|
|
497
|
+
assert_eq!(
|
|
498
|
+
db_path_from(None, None, None, Some("/home/u".into())),
|
|
499
|
+
PathBuf::from("/home/u/.local/share/goose/sessions/sessions.db")
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[test]
|
|
504
|
+
fn the_format_declares_a_rowid_watermark() {
|
|
505
|
+
// Documented as append-accurate and edit-blind. If goose ever gains an
|
|
506
|
+
// `updated_at` on `messages`, this is the switch — the engine already
|
|
507
|
+
// supports the other policy.
|
|
508
|
+
assert_eq!(FORMAT.watermark, Watermark::RowId);
|
|
509
|
+
assert_eq!(FORMAT.kind, "goose");
|
|
510
|
+
}
|
|
511
|
+
}
|