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,500 @@
|
|
|
1
|
+
//! OpenClaw transcript line → AgentEye events.
|
|
2
|
+
//!
|
|
3
|
+
//! Every function here is pure in `(line, ctx, offset, state)`, which is what
|
|
4
|
+
//! makes a live tail and a full re-read byte-identical and therefore
|
|
5
|
+
//! dedup-collapsible. Nothing may consult the clock, a counter, or anything
|
|
6
|
+
//! outside its arguments.
|
|
7
|
+
//!
|
|
8
|
+
//! Record shapes verified against a live containerised probe capture. Each
|
|
9
|
+
//! record chains to the previous one through `id`/`parentId` (8 hex chars), and
|
|
10
|
+
//! carries a record-level ISO-8601 `timestamp`:
|
|
11
|
+
//!
|
|
12
|
+
//! ```text
|
|
13
|
+
//! session {type:"session", version, id, timestamp, cwd}
|
|
14
|
+
//! model_change {type:"model_change", provider, modelId, …}
|
|
15
|
+
//! thinking_level_change {type:"thinking_level_change", thinkingLevel, …}
|
|
16
|
+
//! custom {type:"custom", customType, data:{…}, …}
|
|
17
|
+
//! message {type:"message", timestamp, message:{role, …}}
|
|
18
|
+
//! ```
|
|
19
|
+
//!
|
|
20
|
+
//! `message` has THREE roles, not two — `toolResult` is its own role on its own
|
|
21
|
+
//! record rather than a block inside a user turn the way Claude writes it:
|
|
22
|
+
//!
|
|
23
|
+
//! ```text
|
|
24
|
+
//! user {role:"user", content:[{type:"text",text}], timestamp}
|
|
25
|
+
//! assistant {role:"assistant", content:[{type:"text"|"toolCall", …}],
|
|
26
|
+
//! api, provider, model, usage, stopReason, responseId, timestamp}
|
|
27
|
+
//! toolResult {role:"toolResult", toolCallId, toolName,
|
|
28
|
+
//! content:[{type:"text",text}], details?, isError, timestamp}
|
|
29
|
+
//! ```
|
|
30
|
+
//!
|
|
31
|
+
//! # The two clocks
|
|
32
|
+
//!
|
|
33
|
+
//! Every record carries a record-level ISO-8601 `timestamp` AND an inner
|
|
34
|
+
//! `message.timestamp` in epoch **milliseconds**, and they are not the same
|
|
35
|
+
//! instant. On an assistant record the inner one is when the model request was
|
|
36
|
+
//! issued, the outer one is when the response was written: measured on the
|
|
37
|
+
//! capture, `2026-08-03T08:01:44.024Z` against an inner `1785744100111` =
|
|
38
|
+
//! `08:01:40.111`, a 3.9-second gap. Only the outer clock is present on all
|
|
39
|
+
//! five record types and only the outer clock advances with the file, so it is
|
|
40
|
+
//! the single source of event time here. Mixing the two — outer for a tool
|
|
41
|
+
//! result, inner for the assistant turn that requested it — would silently
|
|
42
|
+
//! reorder a session's timeline by seconds.
|
|
43
|
+
|
|
44
|
+
use serde_json::{Map, Value, json};
|
|
45
|
+
|
|
46
|
+
use crate::cursor::TailState;
|
|
47
|
+
use crate::filetail::Ctx;
|
|
48
|
+
|
|
49
|
+
/// Longest derived id component.
|
|
50
|
+
const MAX_ID_PART: usize = 48;
|
|
51
|
+
/// Longest session goal kept from the opening prompt.
|
|
52
|
+
const MAX_GOAL_CHARS: usize = 1024;
|
|
53
|
+
|
|
54
|
+
/// Make a derived id component safe and bounded.
|
|
55
|
+
pub fn sanitize_id_part(s: &str) -> String {
|
|
56
|
+
let cleaned: String = s
|
|
57
|
+
.chars()
|
|
58
|
+
.map(|c| {
|
|
59
|
+
if c.is_ascii_alphanumeric() || c == '.' || c == '_' {
|
|
60
|
+
c
|
|
61
|
+
} else {
|
|
62
|
+
'-'
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.collect();
|
|
66
|
+
cleaned
|
|
67
|
+
.split('-')
|
|
68
|
+
.filter(|p| !p.is_empty())
|
|
69
|
+
.collect::<Vec<_>>()
|
|
70
|
+
.join("-")
|
|
71
|
+
.chars()
|
|
72
|
+
.take(MAX_ID_PART)
|
|
73
|
+
.collect()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Normalize a transcript timestamp to the microsecond form ingest expects,
|
|
77
|
+
/// offset by `index` microseconds.
|
|
78
|
+
///
|
|
79
|
+
/// The offset is what keeps several events derived from ONE line in order: the
|
|
80
|
+
/// server sorts by `(ts, random id)`, so the two `toolCall` blocks OpenClaw
|
|
81
|
+
/// packs into a single assistant record — both of which really do share the
|
|
82
|
+
/// record's millisecond — would otherwise come back shuffled, showing a session
|
|
83
|
+
/// reading a file before it listed the directory. Index 0 is untouched, so the
|
|
84
|
+
/// primary event of a line keeps the line's exact time.
|
|
85
|
+
///
|
|
86
|
+
/// Saturates within the second rather than carrying, so an offset can never
|
|
87
|
+
/// reorder an event past a genuinely later line.
|
|
88
|
+
pub fn with_index(ts: &str, index: usize) -> Option<String> {
|
|
89
|
+
let body = ts.strip_suffix('Z')?;
|
|
90
|
+
let (main, frac) = match body.rsplit_once('.') {
|
|
91
|
+
Some((m, f)) => (m, f),
|
|
92
|
+
None => (body, ""),
|
|
93
|
+
};
|
|
94
|
+
// OpenClaw writes milliseconds; pad to microseconds so every event has one
|
|
95
|
+
// shape and sorts against the hook source's events correctly.
|
|
96
|
+
let mut micros: u32 = if frac.is_empty() {
|
|
97
|
+
0
|
|
98
|
+
} else {
|
|
99
|
+
let padded = format!("{frac:0<6}");
|
|
100
|
+
padded.get(..6)?.parse().ok()?
|
|
101
|
+
};
|
|
102
|
+
micros = (micros + index.min(999) as u32).min(999_999);
|
|
103
|
+
Some(format!("{main}.{micros:06}Z"))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// The record-level timestamp — never `message.timestamp`. See the module docs.
|
|
107
|
+
fn ts_of(v: &Value) -> Option<&str> {
|
|
108
|
+
v.get("timestamp")?.as_str()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn str_at<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
|
|
112
|
+
v.get(key)?.as_str()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The envelope every emitted event carries.
|
|
116
|
+
///
|
|
117
|
+
/// `openclaw_line_offset` is the dedup discriminator: two identical events from
|
|
118
|
+
/// different lines must hash differently, or the server collapses them — and a
|
|
119
|
+
/// session that asks the same question twice is completely ordinary.
|
|
120
|
+
fn base(ctx: &Ctx, kind: &str, ts: &str, index: usize, offset: u64) -> Option<Map<String, Value>> {
|
|
121
|
+
let mut m = Map::new();
|
|
122
|
+
m.insert("timestamp".into(), json!(with_index(ts, index)?));
|
|
123
|
+
m.insert("session_id".into(), json!(ctx.session_id));
|
|
124
|
+
m.insert("agent_id".into(), json!(ctx.agent_id));
|
|
125
|
+
m.insert("type".into(), json!(kind));
|
|
126
|
+
m.insert("environment".into(), json!(ctx.environment));
|
|
127
|
+
m.insert("openclaw_line_offset".into(), json!(offset));
|
|
128
|
+
if index > 0 {
|
|
129
|
+
m.insert("openclaw_block_index".into(), json!(index));
|
|
130
|
+
}
|
|
131
|
+
Some(m)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Flatten a content array to its text.
|
|
135
|
+
///
|
|
136
|
+
/// OpenClaw's content is always an array of typed blocks, for all three roles —
|
|
137
|
+
/// a user prompt is `[{type:"text",text}]`, not the bare string Claude writes.
|
|
138
|
+
/// The string arm is tolerance for a shape change, not an observed shape:
|
|
139
|
+
/// without it a future bare string would be rendered through `to_string()` and
|
|
140
|
+
/// arrive quoted and backslash-escaped.
|
|
141
|
+
fn joined_text(content: &Value) -> String {
|
|
142
|
+
match content {
|
|
143
|
+
Value::Array(items) => items
|
|
144
|
+
.iter()
|
|
145
|
+
.filter(|b| str_at(b, "type") == Some("text"))
|
|
146
|
+
.filter_map(|b| str_at(b, "text"))
|
|
147
|
+
.collect::<Vec<_>>()
|
|
148
|
+
.join("\n"),
|
|
149
|
+
Value::String(s) => s.clone(),
|
|
150
|
+
Value::Null => String::new(),
|
|
151
|
+
other => other.to_string(),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Build the session's `agent_start` from its header lines.
|
|
156
|
+
///
|
|
157
|
+
/// Scans for the first line carrying a timestamp rather than trusting line 1,
|
|
158
|
+
/// because a session with no start event is not merely incomplete — the server
|
|
159
|
+
/// selects sessions on this event, so it is absent from the product entirely.
|
|
160
|
+
pub fn agent_start(header: &[String], ctx: &Ctx, offset: u64) -> Option<(Value, Option<String>)> {
|
|
161
|
+
let mut first_ts: Option<String> = None;
|
|
162
|
+
let mut cwd: Option<String> = None;
|
|
163
|
+
let mut version: Option<u64> = None;
|
|
164
|
+
let mut provider: Option<String> = None;
|
|
165
|
+
let mut model: Option<String> = None;
|
|
166
|
+
let mut goal: Option<String> = None;
|
|
167
|
+
|
|
168
|
+
for line in header {
|
|
169
|
+
let Ok(v) = serde_json::from_str::<Value>(line) else {
|
|
170
|
+
continue;
|
|
171
|
+
};
|
|
172
|
+
if first_ts.is_none()
|
|
173
|
+
&& let Some(t) = ts_of(&v)
|
|
174
|
+
{
|
|
175
|
+
first_ts = Some(t.to_string());
|
|
176
|
+
}
|
|
177
|
+
match str_at(&v, "type") {
|
|
178
|
+
Some("session") => {
|
|
179
|
+
if cwd.is_none() {
|
|
180
|
+
cwd = str_at(&v, "cwd").map(str::to_string);
|
|
181
|
+
}
|
|
182
|
+
if version.is_none() {
|
|
183
|
+
version = v.get("version").and_then(|x| x.as_u64());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// The model reaches the start event from the same record that
|
|
187
|
+
// seeds `TailState`, so the session header and its first prompt
|
|
188
|
+
// agree on it instead of the header saying nothing.
|
|
189
|
+
Some("model_change") => {
|
|
190
|
+
if provider.is_none() {
|
|
191
|
+
provider = str_at(&v, "provider").map(str::to_string);
|
|
192
|
+
}
|
|
193
|
+
if model.is_none() {
|
|
194
|
+
model = str_at(&v, "modelId").map(str::to_string);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// The session's goal is its first real human prompt.
|
|
198
|
+
Some("message") if goal.is_none() => {
|
|
199
|
+
let Some(message) = v.get("message") else {
|
|
200
|
+
continue;
|
|
201
|
+
};
|
|
202
|
+
if str_at(message, "role") != Some("user") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let text = joined_text(message.get("content").unwrap_or(&Value::Null));
|
|
206
|
+
if !text.is_empty() {
|
|
207
|
+
goal = Some(text.chars().take(MAX_GOAL_CHARS).collect());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
_ => {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let ts = first_ts?;
|
|
215
|
+
let mut m = base(ctx, "agent_start", &ts, 0, offset)?;
|
|
216
|
+
if let Some(g) = goal {
|
|
217
|
+
m.insert("goal".into(), json!(g));
|
|
218
|
+
}
|
|
219
|
+
// The agent workspace, not a project directory — recorded for provenance,
|
|
220
|
+
// never used to group sessions. See the module docs on `mod.rs`.
|
|
221
|
+
if let Some(c) = cwd {
|
|
222
|
+
m.insert("openclaw_cwd".into(), json!(c));
|
|
223
|
+
}
|
|
224
|
+
if let Some(v) = version {
|
|
225
|
+
m.insert("openclaw_session_version".into(), json!(v));
|
|
226
|
+
}
|
|
227
|
+
if let Some(p) = provider {
|
|
228
|
+
m.insert("openclaw_provider".into(), json!(p));
|
|
229
|
+
}
|
|
230
|
+
if let Some(x) = model {
|
|
231
|
+
m.insert("model".into(), json!(x));
|
|
232
|
+
}
|
|
233
|
+
Some((Value::Object(m), Some(ts)))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// The single `agent_end`, derived deterministically from the last timestamp
|
|
237
|
+
/// and the file size (used as its offset, so it is unique per session).
|
|
238
|
+
///
|
|
239
|
+
/// Index 999 puts it after every content event sharing that timestamp — the
|
|
240
|
+
/// server's tie-break is a random id, so without this a session can appear to
|
|
241
|
+
/// end before its last turn.
|
|
242
|
+
pub fn agent_end(ctx: &Ctx, last_ts: &str, size: u64) -> Value {
|
|
243
|
+
match base(ctx, "agent_end", last_ts, 999, size) {
|
|
244
|
+
Some(m) => Value::Object(m),
|
|
245
|
+
// `base` only fails on an unparseable timestamp, which cannot happen
|
|
246
|
+
// here: `last_ts` came from a line this transform already accepted.
|
|
247
|
+
None => json!({
|
|
248
|
+
"timestamp": last_ts,
|
|
249
|
+
"session_id": ctx.session_id,
|
|
250
|
+
"agent_id": ctx.agent_id,
|
|
251
|
+
"type": "agent_end",
|
|
252
|
+
"environment": ctx.environment,
|
|
253
|
+
}),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// One content line to its timestamp and the events it yields.
|
|
258
|
+
pub fn transform_line(
|
|
259
|
+
line: &str,
|
|
260
|
+
ctx: &Ctx,
|
|
261
|
+
offset: u64,
|
|
262
|
+
state: &mut TailState,
|
|
263
|
+
) -> (Option<String>, Vec<Value>) {
|
|
264
|
+
let Ok(v) = serde_json::from_str::<Value>(line) else {
|
|
265
|
+
return (None, Vec::new());
|
|
266
|
+
};
|
|
267
|
+
let Some(ts) = ts_of(&v).map(str::to_string) else {
|
|
268
|
+
return (None, Vec::new());
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
let events = match str_at(&v, "type") {
|
|
272
|
+
Some("message") => message_events(&v, ctx, &ts, offset, state),
|
|
273
|
+
// Not an event, but the record that makes the session's FIRST prompt
|
|
274
|
+
// render with a model. A user record names no model, and the server
|
|
275
|
+
// builds a `model_request` row's summary from the model alone — so
|
|
276
|
+
// without this the opening row of every session is blank, which is the
|
|
277
|
+
// row most likely to be looked at.
|
|
278
|
+
Some("model_change") => {
|
|
279
|
+
if let Some(m) = str_at(&v, "modelId") {
|
|
280
|
+
state.last_model = Some(m.to_string());
|
|
281
|
+
}
|
|
282
|
+
Vec::new()
|
|
283
|
+
}
|
|
284
|
+
// `session`, `thinking_level_change` and `custom` describe the harness,
|
|
285
|
+
// not the conversation. They still return their timestamp above, so a
|
|
286
|
+
// trailing `custom` record — which is exactly what the capture ends
|
|
287
|
+
// with — carries `agent_end` to the true end of the session rather than
|
|
288
|
+
// leaving it stuck at the last message.
|
|
289
|
+
_ => Vec::new(),
|
|
290
|
+
};
|
|
291
|
+
(Some(ts), events)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn message_events(
|
|
295
|
+
v: &Value,
|
|
296
|
+
ctx: &Ctx,
|
|
297
|
+
ts: &str,
|
|
298
|
+
offset: u64,
|
|
299
|
+
state: &mut TailState,
|
|
300
|
+
) -> Vec<Value> {
|
|
301
|
+
let Some(message) = v.get("message") else {
|
|
302
|
+
return Vec::new();
|
|
303
|
+
};
|
|
304
|
+
match str_at(message, "role") {
|
|
305
|
+
Some("user") => user_events(message, ctx, ts, offset, state),
|
|
306
|
+
Some("assistant") => assistant_events(message, ctx, ts, offset, state),
|
|
307
|
+
Some("toolResult") => tool_result_events(message, ctx, ts, offset, state),
|
|
308
|
+
_ => Vec::new(),
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// A `user` record is a human prompt. Tool results are their own role in
|
|
313
|
+
/// OpenClaw, so unlike Claude this branch never has to disambiguate.
|
|
314
|
+
fn user_events(message: &Value, ctx: &Ctx, ts: &str, offset: u64, state: &TailState) -> Vec<Value> {
|
|
315
|
+
let text = joined_text(message.get("content").unwrap_or(&Value::Null));
|
|
316
|
+
if text.is_empty() {
|
|
317
|
+
return Vec::new();
|
|
318
|
+
}
|
|
319
|
+
let Some(mut m) = base(ctx, "model_request", ts, 0, offset) else {
|
|
320
|
+
return Vec::new();
|
|
321
|
+
};
|
|
322
|
+
// Inherited from the last `model_change` or assistant turn: a user record
|
|
323
|
+
// names no model.
|
|
324
|
+
if let Some(model) = &state.last_model {
|
|
325
|
+
m.insert("model".into(), json!(model));
|
|
326
|
+
}
|
|
327
|
+
m.insert(
|
|
328
|
+
"messages".into(),
|
|
329
|
+
json!([{ "role": "user", "content": text }]),
|
|
330
|
+
);
|
|
331
|
+
vec![Value::Object(m)]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// An `assistant` record is text and/or tool calls, plus token usage.
|
|
335
|
+
fn assistant_events(
|
|
336
|
+
message: &Value,
|
|
337
|
+
ctx: &Ctx,
|
|
338
|
+
ts: &str,
|
|
339
|
+
offset: u64,
|
|
340
|
+
state: &mut TailState,
|
|
341
|
+
) -> Vec<Value> {
|
|
342
|
+
let model = str_at(message, "model");
|
|
343
|
+
if let Some(m) = model {
|
|
344
|
+
state.last_model = Some(m.to_string());
|
|
345
|
+
}
|
|
346
|
+
let provider = str_at(message, "provider");
|
|
347
|
+
let response_id = str_at(message, "responseId");
|
|
348
|
+
let stop_reason = str_at(message, "stopReason");
|
|
349
|
+
|
|
350
|
+
let Some(blocks) = message.get("content").and_then(|c| c.as_array()) else {
|
|
351
|
+
return Vec::new();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
let mut out: Vec<Value> = Vec::new();
|
|
355
|
+
for (i, block) in blocks.iter().enumerate() {
|
|
356
|
+
match str_at(block, "type") {
|
|
357
|
+
Some("text") => {
|
|
358
|
+
let Some(text) = str_at(block, "text").filter(|t| !t.is_empty()) else {
|
|
359
|
+
continue;
|
|
360
|
+
};
|
|
361
|
+
let Some(mut m) = base(ctx, "model_response", ts, i, offset) else {
|
|
362
|
+
continue;
|
|
363
|
+
};
|
|
364
|
+
m.insert("role".into(), json!("assistant"));
|
|
365
|
+
m.insert("content".into(), json!(text));
|
|
366
|
+
if let Some(x) = model {
|
|
367
|
+
m.insert("model".into(), json!(x));
|
|
368
|
+
}
|
|
369
|
+
if let Some(x) = provider {
|
|
370
|
+
m.insert("openclaw_provider".into(), json!(x));
|
|
371
|
+
}
|
|
372
|
+
if let Some(x) = response_id {
|
|
373
|
+
m.insert("openclaw_response_id".into(), json!(x));
|
|
374
|
+
}
|
|
375
|
+
if let Some(x) = stop_reason {
|
|
376
|
+
m.insert("openclaw_stop_reason".into(), json!(x));
|
|
377
|
+
}
|
|
378
|
+
out.push(Value::Object(m));
|
|
379
|
+
}
|
|
380
|
+
Some("toolCall") => {
|
|
381
|
+
let Some(mut m) = base(ctx, "tool_use", ts, i, offset) else {
|
|
382
|
+
continue;
|
|
383
|
+
};
|
|
384
|
+
let name = str_at(block, "name").unwrap_or("tool");
|
|
385
|
+
// Fall back to an offset-derived id so two identical calls in
|
|
386
|
+
// one record cannot hash-collapse into a single row.
|
|
387
|
+
let id = str_at(block, "id")
|
|
388
|
+
.map(str::to_string)
|
|
389
|
+
.unwrap_or_else(|| format!("openclaw-{offset}-{i}"));
|
|
390
|
+
state.remember_tool(id.clone(), name.to_string());
|
|
391
|
+
m.insert("tool_name".into(), json!(name));
|
|
392
|
+
m.insert("tool_call_id".into(), json!(id));
|
|
393
|
+
// `arguments` only. The sibling `partialArgs` is the same
|
|
394
|
+
// object re-encoded as a JSON string — shipping both doubles
|
|
395
|
+
// every tool call's payload to say the same thing twice.
|
|
396
|
+
if let Some(args) = block.get("arguments") {
|
|
397
|
+
m.insert("input".into(), args.clone());
|
|
398
|
+
}
|
|
399
|
+
if let Some(x) = model {
|
|
400
|
+
m.insert("model".into(), json!(x));
|
|
401
|
+
}
|
|
402
|
+
if let Some(x) = response_id {
|
|
403
|
+
m.insert("openclaw_response_id".into(), json!(x));
|
|
404
|
+
}
|
|
405
|
+
out.push(Value::Object(m));
|
|
406
|
+
}
|
|
407
|
+
// Anything else (a future thinking/reasoning block) is skipped
|
|
408
|
+
// rather than guessed at, but still consumes its index, so the
|
|
409
|
+
// blocks around it keep their relative order.
|
|
410
|
+
_ => {}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Attach usage to the FIRST event of the record, so a record yielding
|
|
415
|
+
// several events reports its tokens once.
|
|
416
|
+
//
|
|
417
|
+
// No message-id guard here, unlike the Claude source: OpenClaw writes one
|
|
418
|
+
// record per API response — every block of a response shares that record's
|
|
419
|
+
// `content` array, and each record carries its own `responseId` — so usage
|
|
420
|
+
// is never repeated across lines and gating on an id could only drop a
|
|
421
|
+
// legitimate second response.
|
|
422
|
+
if let Some(usage) = message.get("usage")
|
|
423
|
+
&& let Some(first) = out.first_mut()
|
|
424
|
+
&& let Some(obj) = first.as_object_mut()
|
|
425
|
+
{
|
|
426
|
+
if let Some(n) = usage.get("input").and_then(|x| x.as_u64()) {
|
|
427
|
+
obj.insert("input_tokens".into(), json!(n));
|
|
428
|
+
}
|
|
429
|
+
if let Some(n) = usage.get("output").and_then(|x| x.as_u64()) {
|
|
430
|
+
obj.insert("output_tokens".into(), json!(n));
|
|
431
|
+
}
|
|
432
|
+
obj.insert("openclaw_usage".into(), usage.clone());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
out
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// A `toolResult` record — its own role on its own record, paired back to the
|
|
439
|
+
/// call by `toolCallId` == the call's `id`.
|
|
440
|
+
fn tool_result_events(
|
|
441
|
+
message: &Value,
|
|
442
|
+
ctx: &Ctx,
|
|
443
|
+
ts: &str,
|
|
444
|
+
offset: u64,
|
|
445
|
+
state: &TailState,
|
|
446
|
+
) -> Vec<Value> {
|
|
447
|
+
let Some(mut m) = base(ctx, "tool_result", ts, 0, offset) else {
|
|
448
|
+
return Vec::new();
|
|
449
|
+
};
|
|
450
|
+
let call_id = str_at(message, "toolCallId");
|
|
451
|
+
if let Some(id) = call_id {
|
|
452
|
+
m.insert("tool_call_id".into(), json!(id));
|
|
453
|
+
}
|
|
454
|
+
// OpenClaw usually names the tool on the result too. When it does not, the
|
|
455
|
+
// name carried from the call is the only thing standing between this and a
|
|
456
|
+
// blank row, because the server builds a result row's summary from the tool
|
|
457
|
+
// name alone.
|
|
458
|
+
let name = str_at(message, "toolName")
|
|
459
|
+
.filter(|n| !n.is_empty())
|
|
460
|
+
.or_else(|| call_id.and_then(|id| state.tool_name(id)));
|
|
461
|
+
if let Some(n) = name {
|
|
462
|
+
m.insert("tool_name".into(), json!(n));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let output = joined_text(message.get("content").unwrap_or(&Value::Null));
|
|
466
|
+
m.insert("output".into(), json!(output));
|
|
467
|
+
|
|
468
|
+
// `details` is absent on some results — the capture's `read` result has
|
|
469
|
+
// none while its `exec` results do — so every field under it is optional
|
|
470
|
+
// and its absence must not cost the whole event.
|
|
471
|
+
if let Some(details) = message.get("details") {
|
|
472
|
+
if let Some(d) = details.get("durationMs").and_then(|x| x.as_f64()) {
|
|
473
|
+
m.insert("duration_ms".into(), json!(d));
|
|
474
|
+
}
|
|
475
|
+
if let Some(s) = str_at(details, "status") {
|
|
476
|
+
m.insert("openclaw_tool_status".into(), json!(s));
|
|
477
|
+
}
|
|
478
|
+
if let Some(c) = details.get("exitCode").and_then(|x| x.as_i64()) {
|
|
479
|
+
m.insert("openclaw_exit_code".into(), json!(c));
|
|
480
|
+
}
|
|
481
|
+
// `details.aggregated` and `details.cwd` are deliberately dropped:
|
|
482
|
+
// `aggregated` is a byte-for-byte copy of the text content above, and
|
|
483
|
+
// `cwd` is the same fixed agent workspace on every result in the
|
|
484
|
+
// session.
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if message.get("isError").and_then(|e| e.as_bool()) == Some(true) {
|
|
488
|
+
// The server's `is_error` is a truthiness check, so an empty string
|
|
489
|
+
// would render a failed tool call as a success.
|
|
490
|
+
let reason = if output.trim().is_empty() {
|
|
491
|
+
"openclaw reported a tool failure with no message".to_string()
|
|
492
|
+
} else {
|
|
493
|
+
output
|
|
494
|
+
};
|
|
495
|
+
m.insert("error".into(), json!(reason));
|
|
496
|
+
m.insert("error_type".into(), json!("openclaw_tool_error"));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
vec![Value::Object(m)]
|
|
500
|
+
}
|