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,393 @@
|
|
|
1
|
+
//! Claude subagent transcripts: file identity, the parent link, and the
|
|
2
|
+
//! disjointness the two `Format`s rest on.
|
|
3
|
+
//!
|
|
4
|
+
//! Path shapes and record shapes are verbatim from the 124 files under
|
|
5
|
+
//! `~/.claude/projects/**/subagents/` (Claude Code 2.1.220).
|
|
6
|
+
|
|
7
|
+
use std::fs;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::sync::atomic::AtomicBool;
|
|
11
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
12
|
+
|
|
13
|
+
use fpai_collect::filetail::{self, Ctx, Params, RereadPolicy, Spec};
|
|
14
|
+
use fpai_collect::sources::claude;
|
|
15
|
+
use fpai_collect::supervisor::Shutdown;
|
|
16
|
+
use serde_json::{Value, json};
|
|
17
|
+
|
|
18
|
+
/// A real parent session id, and a real 17-hex agent id.
|
|
19
|
+
const PARENT: &str = "3ee9c788-8772-4f92-be0b-a80ede7ac48e";
|
|
20
|
+
const AGENT: &str = "a5a59824a02ebc510";
|
|
21
|
+
|
|
22
|
+
fn tmpdir(name: &str) -> PathBuf {
|
|
23
|
+
let d = std::env::temp_dir().join(format!(
|
|
24
|
+
"fpai-clsub-{}-{}-{}",
|
|
25
|
+
name,
|
|
26
|
+
std::process::id(),
|
|
27
|
+
SystemTime::now()
|
|
28
|
+
.duration_since(UNIX_EPOCH)
|
|
29
|
+
.unwrap()
|
|
30
|
+
.as_nanos()
|
|
31
|
+
));
|
|
32
|
+
fs::create_dir_all(&d).unwrap();
|
|
33
|
+
d
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// `projects/<slug>/<parent>/subagents/`.
|
|
37
|
+
fn flat(root: &Path) -> PathBuf {
|
|
38
|
+
root.join("-home-u-repo").join(PARENT).join("subagents")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// `projects/<slug>/<parent>/subagents/workflows/wf_<id>/` — the same tree with
|
|
42
|
+
/// two extra levels inserted.
|
|
43
|
+
fn workflow(root: &Path) -> PathBuf {
|
|
44
|
+
flat(root).join("workflows").join("wf_ca1e341f-c30")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn ctx() -> Ctx {
|
|
48
|
+
Ctx {
|
|
49
|
+
session_id: format!("{PARENT}:{AGENT}"),
|
|
50
|
+
agent_id: "claude-Explore".into(),
|
|
51
|
+
environment: "local".into(),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The first line of a real subagent transcript: a genuine timestamped `user`
|
|
56
|
+
/// turn carrying the PARENT's `sessionId` and its own `agentId`.
|
|
57
|
+
fn sub_prompt(ts: &str, text: &str) -> String {
|
|
58
|
+
json!({"parentUuid":null,"isSidechain":true,"agentId":AGENT,"type":"user",
|
|
59
|
+
"uuid":"669bdae5-3111-4d87-bb17-f18874e96232","timestamp":ts,
|
|
60
|
+
"cwd":"/home/u/repo","sessionId":PARENT,"version":"2.1.220",
|
|
61
|
+
"gitBranch":"feat/collector",
|
|
62
|
+
"message":{"role":"user","content":text}})
|
|
63
|
+
.to_string()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn sub_tool_use(ts: &str, call_id: &str, name: &str) -> String {
|
|
67
|
+
json!({"isSidechain":true,"agentId":AGENT,"type":"assistant","timestamp":ts,
|
|
68
|
+
"attributionAgent":"Explore","sessionId":PARENT,
|
|
69
|
+
"message":{"model":"claude-opus-4-8","id":"msg_01","role":"assistant",
|
|
70
|
+
"content":[{"type":"tool_use","id":call_id,"name":name,"input":{"command":"ls"}}],
|
|
71
|
+
"usage":{"input_tokens":2,"output_tokens":91}}})
|
|
72
|
+
.to_string()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── file identity ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn a_subagent_transcript_is_claimed_in_both_the_flat_and_workflow_layouts() {
|
|
79
|
+
// The workflow layout inserts `workflows/wf_<id>/` between `subagents/` and
|
|
80
|
+
// the file. Anchoring on a depth count instead of the literal component
|
|
81
|
+
// would be confidently wrong on exactly one of the two shapes.
|
|
82
|
+
let root = Path::new("/p");
|
|
83
|
+
let is = claude::SUBAGENT_FORMAT.is_source_file;
|
|
84
|
+
assert!(is(&flat(root).join(format!("agent-{AGENT}.jsonl"))));
|
|
85
|
+
assert!(is(&workflow(root).join(format!("agent-{AGENT}.jsonl"))));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn the_subagent_format_rejects_its_siblings() {
|
|
90
|
+
let root = Path::new("/p");
|
|
91
|
+
let is = claude::SUBAGENT_FORMAT.is_source_file;
|
|
92
|
+
// A different schema entirely (`{type, key, agentId}`, no timestamps) that
|
|
93
|
+
// lives in both layouts. Tailing it would invent a session out of nothing.
|
|
94
|
+
assert!(!is(&flat(root).join("journal.jsonl")));
|
|
95
|
+
assert!(!is(&workflow(root).join("journal.jsonl")));
|
|
96
|
+
// The sidecar this format READS for the agent type; it is rewritten and is
|
|
97
|
+
// not a transcript.
|
|
98
|
+
assert!(!is(&flat(root).join(format!("agent-{AGENT}.meta.json"))));
|
|
99
|
+
// An empty agent id would key a session on nothing.
|
|
100
|
+
assert!(!is(&flat(root).join("agent-.jsonl")));
|
|
101
|
+
// `agent-*.jsonl` outside the tree is not ours.
|
|
102
|
+
assert!(!is(&root.join("-home-u-repo").join("agent-x.jsonl")));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn the_two_claude_formats_are_disjoint_over_every_path_in_the_tree() {
|
|
107
|
+
// If both claimed one file it would be tailed twice and every line would
|
|
108
|
+
// ship under two different session ids — silently doubling a session's
|
|
109
|
+
// token totals and its tool-call counts.
|
|
110
|
+
let root = Path::new("/p");
|
|
111
|
+
let proj = root.join("-home-u-repo");
|
|
112
|
+
let paths = [
|
|
113
|
+
proj.join(format!("{PARENT}.jsonl")),
|
|
114
|
+
proj.join(format!("{PARENT}.jsonl.tool-calls.json")),
|
|
115
|
+
proj.join("journal.jsonl"),
|
|
116
|
+
flat(root).join(format!("agent-{AGENT}.jsonl")),
|
|
117
|
+
flat(root).join(format!("agent-{AGENT}.meta.json")),
|
|
118
|
+
flat(root).join("journal.jsonl"),
|
|
119
|
+
workflow(root).join(format!("agent-{AGENT}.jsonl")),
|
|
120
|
+
workflow(root).join("journal.jsonl"),
|
|
121
|
+
proj.join("notes.txt"),
|
|
122
|
+
];
|
|
123
|
+
for p in paths {
|
|
124
|
+
let main = (claude::FORMAT.is_source_file)(&p);
|
|
125
|
+
let sub = (claude::SUBAGENT_FORMAT.is_source_file)(&p);
|
|
126
|
+
assert!(!(main && sub), "both formats claim {}", p.display());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn a_subagent_becomes_a_child_session_keyed_on_its_parent_and_its_agent_id() {
|
|
132
|
+
let root = Path::new("/p");
|
|
133
|
+
let expected = format!("{PARENT}:{AGENT}");
|
|
134
|
+
let id = claude::SUBAGENT_FORMAT.session_id_from_path;
|
|
135
|
+
assert_eq!(
|
|
136
|
+
id(&flat(root).join(format!("agent-{AGENT}.jsonl"))).as_deref(),
|
|
137
|
+
Some(expected.as_str())
|
|
138
|
+
);
|
|
139
|
+
// The workflow layout carries the SAME parent id, two levels deeper. The
|
|
140
|
+
// parent is the directory containing `subagents/`, never a fixed depth.
|
|
141
|
+
assert_eq!(
|
|
142
|
+
id(&workflow(root).join(format!("agent-{AGENT}.jsonl"))).as_deref(),
|
|
143
|
+
Some(expected.as_str())
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn a_layout_with_no_subagents_component_yields_none_rather_than_a_guess() {
|
|
149
|
+
// `None` routes to the engine's synthetic dev+inode id and a warning, which
|
|
150
|
+
// is recoverable. A guessed parent would file a subagent's turns under some
|
|
151
|
+
// other session, which is not.
|
|
152
|
+
let id = claude::SUBAGENT_FORMAT.session_id_from_path;
|
|
153
|
+
assert_eq!(id(Path::new("/p/proj/agent-abc.jsonl")), None);
|
|
154
|
+
assert_eq!(id(Path::new("/p/proj/subagents/journal.jsonl")), None);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── agent type ───────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn the_agent_type_comes_from_the_sidecar_because_it_is_on_no_transcript_line() {
|
|
161
|
+
// `agentType` appears zero times in the transcripts themselves; the
|
|
162
|
+
// `.meta.json` sidecar is the only place it is written as such.
|
|
163
|
+
let dir = tmpdir("sidecar");
|
|
164
|
+
let subs = flat(&dir);
|
|
165
|
+
fs::create_dir_all(&subs).unwrap();
|
|
166
|
+
let transcript = subs.join(format!("agent-{AGENT}.jsonl"));
|
|
167
|
+
fs::write(&transcript, b"").unwrap();
|
|
168
|
+
fs::write(
|
|
169
|
+
subs.join(format!("agent-{AGENT}.meta.json")),
|
|
170
|
+
br#"{"agentType":"Explore","description":"Map the daemon","spawnDepth":1}"#,
|
|
171
|
+
)
|
|
172
|
+
.unwrap();
|
|
173
|
+
|
|
174
|
+
let derived = (claude::SUBAGENT_FORMAT.agent_id_from_path)(&transcript, &[]);
|
|
175
|
+
assert_eq!(derived.as_deref(), Some("claude-Explore"));
|
|
176
|
+
fs::remove_dir_all(&dir).ok();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn the_sidecar_wins_over_the_in_file_field_because_it_has_no_startup_window() {
|
|
181
|
+
// Ordering is load-bearing: `agent_id` is frozen onto the cursor at
|
|
182
|
+
// discovery and a subagent's `agent_start` resolves on byte 0, while
|
|
183
|
+
// `attributionAgent` is not written until line 3-4. A file discovered one
|
|
184
|
+
// line in would otherwise freeze the fallback onto every event.
|
|
185
|
+
let dir = tmpdir("sidecar-wins");
|
|
186
|
+
let subs = flat(&dir);
|
|
187
|
+
fs::create_dir_all(&subs).unwrap();
|
|
188
|
+
let transcript = subs.join(format!("agent-{AGENT}.jsonl"));
|
|
189
|
+
fs::write(&transcript, b"").unwrap();
|
|
190
|
+
fs::write(
|
|
191
|
+
subs.join(format!("agent-{AGENT}.meta.json")),
|
|
192
|
+
br#"{"agentType":"Explore"}"#,
|
|
193
|
+
)
|
|
194
|
+
.unwrap();
|
|
195
|
+
let header = vec![r#"{"type":"assistant","attributionAgent":"general-purpose"}"#.to_string()];
|
|
196
|
+
assert_eq!(
|
|
197
|
+
(claude::SUBAGENT_FORMAT.agent_id_from_path)(&transcript, &header).as_deref(),
|
|
198
|
+
Some("claude-Explore")
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// A malformed sidecar degrades to the in-file field rather than failing
|
|
202
|
+
// capture — the two agreed in 122 of 122 measured, so it is a real answer.
|
|
203
|
+
fs::write(subs.join(format!("agent-{AGENT}.meta.json")), b"{not json").unwrap();
|
|
204
|
+
assert_eq!(
|
|
205
|
+
(claude::SUBAGENT_FORMAT.agent_id_from_path)(&transcript, &header).as_deref(),
|
|
206
|
+
Some("claude-general-purpose")
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Neither available: `None` hands the engine its configured default, so an
|
|
210
|
+
// unnameable subagent is still captured.
|
|
211
|
+
assert_eq!(
|
|
212
|
+
(claude::SUBAGENT_FORMAT.agent_id_from_path)(&transcript, &[]),
|
|
213
|
+
None
|
|
214
|
+
);
|
|
215
|
+
fs::remove_dir_all(&dir).ok();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── the parent link ──────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
fn the_start_event_names_the_parent_without_ever_saying_parent_id() {
|
|
222
|
+
// ⚠️ The dashboard matches `parent_id` against an AGENT id, not a session
|
|
223
|
+
// id — so putting a session UUID there resolves to nothing and silently
|
|
224
|
+
// mis-links every subagent. These two names are the contract.
|
|
225
|
+
let header = vec![sub_prompt("2026-07-18T09:42:04.058Z", "map the daemon")];
|
|
226
|
+
let (ev, ts) = (claude::SUBAGENT_FORMAT.agent_start)(&header, &ctx(), 0).unwrap();
|
|
227
|
+
assert_eq!(ev["type"], "agent_start");
|
|
228
|
+
assert_eq!(ts.as_deref(), Some("2026-07-18T09:42:04.058Z"));
|
|
229
|
+
assert_eq!(ev["claude_parent_session_id"], PARENT);
|
|
230
|
+
assert_eq!(ev["claude_agent_id"], AGENT);
|
|
231
|
+
assert!(
|
|
232
|
+
ev.get("parent_id").is_none(),
|
|
233
|
+
"parent_id would be matched against an agent_id and resolve to nothing"
|
|
234
|
+
);
|
|
235
|
+
// A subagent's first line IS its task prompt, so the goal resolves at once.
|
|
236
|
+
assert_eq!(ev["goal"], "map the daemon");
|
|
237
|
+
assert_eq!(ev["session_id"], format!("{PARENT}:{AGENT}"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn a_main_session_start_carries_no_parent_link() {
|
|
242
|
+
// The two formats share `transform::agent_start`; only the subagent wrapper
|
|
243
|
+
// may add the link, or a top-level session would claim a parent.
|
|
244
|
+
let header = vec![
|
|
245
|
+
r#"{"type":"user","timestamp":"2026-07-18T09:42:04.058Z","cwd":"/home/u/repo",
|
|
246
|
+
"message":{"role":"user","content":"hi"}}"#
|
|
247
|
+
.to_string(),
|
|
248
|
+
];
|
|
249
|
+
let main_ctx = Ctx {
|
|
250
|
+
session_id: PARENT.into(),
|
|
251
|
+
agent_id: "claude-repo".into(),
|
|
252
|
+
environment: "local".into(),
|
|
253
|
+
};
|
|
254
|
+
let (ev, _) = (claude::FORMAT.agent_start)(&header, &main_ctx, 0).unwrap();
|
|
255
|
+
assert!(ev.get("claude_parent_session_id").is_none());
|
|
256
|
+
assert!(ev.get("claude_agent_id").is_none());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── engine ───────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
fn spec(root: PathBuf, spool: PathBuf, state: PathBuf) -> Spec {
|
|
262
|
+
Spec {
|
|
263
|
+
format: claude::SUBAGENT_FORMAT,
|
|
264
|
+
roots: vec![root],
|
|
265
|
+
spool_dir: spool,
|
|
266
|
+
state_dir: state,
|
|
267
|
+
poll_interval: Duration::from_millis(200),
|
|
268
|
+
params: Params {
|
|
269
|
+
agent_id: claude::SUBAGENT_DEFAULT_AGENT_ID.into(),
|
|
270
|
+
environment: "local".into(),
|
|
271
|
+
end_idle_mins: 0,
|
|
272
|
+
max_read_bytes: 8 * 1024 * 1024,
|
|
273
|
+
max_batch_bytes: 8 * 1024 * 1024,
|
|
274
|
+
since_days: None,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn spooled(dir: &Path) -> Vec<Value> {
|
|
280
|
+
let mut out = Vec::new();
|
|
281
|
+
let Ok(rd) = fs::read_dir(dir) else {
|
|
282
|
+
return out;
|
|
283
|
+
};
|
|
284
|
+
for e in rd.filter_map(|e| e.ok()) {
|
|
285
|
+
let p = e.path();
|
|
286
|
+
if p.extension().and_then(|x| x.to_str()) != Some("jsonl") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for l in fs::read_to_string(&p).unwrap().lines() {
|
|
290
|
+
if !l.trim().is_empty() {
|
|
291
|
+
out.push(serde_json::from_str(l).unwrap());
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
out
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async fn run_briefly(s: Spec, ms: u64) {
|
|
299
|
+
let sd = Shutdown::for_test(Arc::new(AtomicBool::new(false)));
|
|
300
|
+
let _ = tokio::time::timeout(Duration::from_millis(ms), filetail::run(s, sd)).await;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
304
|
+
async fn a_workflow_subagent_lands_as_a_child_session_named_after_its_type() {
|
|
305
|
+
let root = tmpdir("wf-root");
|
|
306
|
+
let spool = tmpdir("wf-spool");
|
|
307
|
+
let state = tmpdir("wf-state");
|
|
308
|
+
|
|
309
|
+
// The deep layout, plus both decoys that share the directory.
|
|
310
|
+
let dir = workflow(&root);
|
|
311
|
+
fs::create_dir_all(&dir).unwrap();
|
|
312
|
+
fs::write(
|
|
313
|
+
dir.join(format!("agent-{AGENT}.jsonl")),
|
|
314
|
+
[
|
|
315
|
+
sub_prompt("2026-07-18T09:42:04.058Z", "map the daemon"),
|
|
316
|
+
sub_tool_use("2026-07-18T09:42:05.000Z", "toolu_01", "Grep"),
|
|
317
|
+
]
|
|
318
|
+
.join("\n")
|
|
319
|
+
+ "\n",
|
|
320
|
+
)
|
|
321
|
+
.unwrap();
|
|
322
|
+
fs::write(
|
|
323
|
+
dir.join(format!("agent-{AGENT}.meta.json")),
|
|
324
|
+
br#"{"agentType":"general-purpose","spawnDepth":1}"#,
|
|
325
|
+
)
|
|
326
|
+
.unwrap();
|
|
327
|
+
fs::write(
|
|
328
|
+
dir.join("journal.jsonl"),
|
|
329
|
+
b"{\"type\":\"started\",\"key\":\"v2:abc\",\"agentId\":\"aother\"}\n",
|
|
330
|
+
)
|
|
331
|
+
.unwrap();
|
|
332
|
+
|
|
333
|
+
run_briefly(spec(root.clone(), spool.clone(), state.clone()), 1200).await;
|
|
334
|
+
|
|
335
|
+
let ev = spooled(&spool);
|
|
336
|
+
let types: Vec<&str> = ev.iter().filter_map(|e| e["type"].as_str()).collect();
|
|
337
|
+
assert!(types.contains(&"agent_start"), "got {types:?}");
|
|
338
|
+
assert!(types.contains(&"tool_use"), "got {types:?}");
|
|
339
|
+
assert!(types.contains(&"agent_end"), "got {types:?}");
|
|
340
|
+
|
|
341
|
+
let want = format!("{PARENT}:{AGENT}");
|
|
342
|
+
for e in &ev {
|
|
343
|
+
assert_eq!(e["session_id"], want, "one child session, not the parent's");
|
|
344
|
+
assert_eq!(e["agent_id"], "claude-general-purpose");
|
|
345
|
+
}
|
|
346
|
+
// journal.jsonl must contribute nothing at all.
|
|
347
|
+
assert!(
|
|
348
|
+
ev.iter().all(|e| e["session_id"] == want),
|
|
349
|
+
"journal.jsonl was tailed"
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
fs::remove_dir_all(&root).ok();
|
|
353
|
+
fs::remove_dir_all(&spool).ok();
|
|
354
|
+
fs::remove_dir_all(&state).ok();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
358
|
+
async fn the_main_format_walking_the_same_root_ignores_subagent_files() {
|
|
359
|
+
// Both sources are pointed at ONE root by the daemon, so this is the live
|
|
360
|
+
// arrangement: if the main format claimed these files, every subagent line
|
|
361
|
+
// would ship a second time under the parent's session id.
|
|
362
|
+
let root = tmpdir("shared-root");
|
|
363
|
+
let spool = tmpdir("shared-spool");
|
|
364
|
+
let state = tmpdir("shared-state");
|
|
365
|
+
|
|
366
|
+
let dir = flat(&root);
|
|
367
|
+
fs::create_dir_all(&dir).unwrap();
|
|
368
|
+
fs::write(
|
|
369
|
+
dir.join(format!("agent-{AGENT}.jsonl")),
|
|
370
|
+
sub_prompt("2026-07-18T09:42:04.058Z", "sub work") + "\n",
|
|
371
|
+
)
|
|
372
|
+
.unwrap();
|
|
373
|
+
|
|
374
|
+
let mut s = spec(root.clone(), spool.clone(), state.clone());
|
|
375
|
+
s.format = claude::FORMAT;
|
|
376
|
+
s.params.agent_id = claude::DEFAULT_AGENT_ID.into();
|
|
377
|
+
run_briefly(s, 1000).await;
|
|
378
|
+
|
|
379
|
+
assert!(
|
|
380
|
+
spooled(&spool).is_empty(),
|
|
381
|
+
"the main format must not claim subagent transcripts"
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
fs::remove_dir_all(&root).ok();
|
|
385
|
+
fs::remove_dir_all(&spool).ok();
|
|
386
|
+
fs::remove_dir_all(&state).ok();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#[test]
|
|
390
|
+
fn subagent_transcripts_are_byte_tailable_like_the_main_ones() {
|
|
391
|
+
assert_eq!(claude::SUBAGENT_FORMAT.reread, RereadPolicy::ByteCursor);
|
|
392
|
+
assert_eq!(claude::SUBAGENT_FORMAT.kind, "claude-subagent");
|
|
393
|
+
}
|