@team-agent/installer 0.5.42 → 0.5.44
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +126 -16
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +87 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +199 -89
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/registry.rs +1 -1
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +55 -15
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +54 -24
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +8 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +40 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -1,217 +1,243 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
|
|
5
|
+
// =====================================================================
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn should_reread_no_file_when_current_mtime_none() {
|
|
9
|
+
// current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
|
|
10
|
+
let d = should_reread(None, None, None, 100.0, 60.0);
|
|
11
|
+
assert!(!d.reread);
|
|
12
|
+
assert_eq!(d.reason, RereadReason::NoFile);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn should_reread_never_classified() {
|
|
17
|
+
// last_classified_mtime is None → {reread:true, never_classified}。
|
|
18
|
+
let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
|
|
19
|
+
assert!(d.reread);
|
|
20
|
+
assert_eq!(d.reason, RereadReason::NeverClassified);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn should_reread_file_changed() {
|
|
25
|
+
// current != last_classified → {reread:true, file_changed}。
|
|
26
|
+
let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
|
|
27
|
+
assert!(d.reread);
|
|
28
|
+
assert_eq!(d.reason, RereadReason::FileChanged);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn should_reread_quiescent_already_classified_at_and_past_debounce() {
|
|
33
|
+
// current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
|
|
34
|
+
// silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
|
|
35
|
+
let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
|
|
36
|
+
assert!(!d.reread);
|
|
37
|
+
assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
|
|
38
|
+
// 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
|
|
39
|
+
let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
|
|
40
|
+
assert!(!b.reread);
|
|
41
|
+
assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn should_reread_unchanged_within_debounce() {
|
|
46
|
+
// current==last_classified 且 silent_for < debounce → unchanged。
|
|
47
|
+
// now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
|
|
48
|
+
let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
|
|
49
|
+
assert!(!d.reread);
|
|
50
|
+
assert_eq!(d.reason, RereadReason::Unchanged);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[test]
|
|
54
|
+
fn on_file_changed_adds_node_sorted_and_records_mtime() {
|
|
55
|
+
// golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
|
|
56
|
+
let s0 = on_file_changed(None, "b", 1.0);
|
|
57
|
+
assert_eq!(s0.pending, vec!["b".to_string()]);
|
|
58
|
+
assert_eq!(s0.mtimes.get("b"), Some(&1.0));
|
|
59
|
+
let s1 = on_file_changed(Some(&s0), "a", 2.0);
|
|
60
|
+
assert_eq!(
|
|
61
|
+
s1.pending,
|
|
62
|
+
vec!["a".to_string(), "b".to_string()],
|
|
63
|
+
"pending 必须 sorted"
|
|
64
|
+
);
|
|
65
|
+
assert_eq!(s1.mtimes.get("a"), Some(&2.0));
|
|
66
|
+
// 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
|
|
67
|
+
let s2 = on_file_changed(Some(&s1), "b", 3.0);
|
|
68
|
+
assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
|
|
69
|
+
assert_eq!(s2.mtimes.get("b"), Some(&3.0));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
|
|
74
|
+
// golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
|
|
75
|
+
let mut st = on_file_changed(None, "b", 3.0);
|
|
76
|
+
st = on_file_changed(Some(&st), "a", 2.0);
|
|
77
|
+
let (drained, after) = take_pending(Some(&st));
|
|
78
|
+
assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
|
|
79
|
+
assert!(after.pending.is_empty());
|
|
80
|
+
// mtimes 不被 drain 清空。
|
|
81
|
+
assert_eq!(after.mtimes.get("a"), Some(&2.0));
|
|
82
|
+
assert_eq!(after.mtimes.get("b"), Some(&3.0));
|
|
83
|
+
// 再 drain → 空。
|
|
84
|
+
let (drained2, _after2) = take_pending(Some(&after));
|
|
85
|
+
assert!(drained2.is_empty());
|
|
86
|
+
// drain None → ([], default state)。
|
|
87
|
+
let (none_drained, none_state) = take_pending(None);
|
|
88
|
+
assert!(none_drained.is_empty());
|
|
89
|
+
assert!(none_state.pending.is_empty());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =====================================================================
|
|
93
|
+
// 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
|
|
94
|
+
// =====================================================================
|
|
95
|
+
|
|
96
|
+
// 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
|
|
97
|
+
// 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
|
|
98
|
+
#[test]
|
|
99
|
+
fn leader_session_name_formula_and_sanitization() {
|
|
100
|
+
// 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
|
|
101
|
+
// sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
|
|
102
|
+
// 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
|
|
103
|
+
let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
|
|
104
|
+
let weird = base.join("My Proj!@#name");
|
|
105
|
+
std::fs::create_dir_all(&weird).unwrap();
|
|
106
|
+
let got = leader_session_name(Provider::Codex, &weird);
|
|
107
|
+
// 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
|
|
108
|
+
let s = got.as_str();
|
|
109
|
+
assert!(
|
|
110
|
+
s.starts_with("team-agent-leader-codex-My_Proj___name-"),
|
|
111
|
+
"got {s}"
|
|
112
|
+
);
|
|
113
|
+
// sha1[:8] 后缀:8 个 hex。
|
|
114
|
+
let suffix = s.rsplit('-').next().unwrap();
|
|
115
|
+
assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
|
|
116
|
+
assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
|
|
120
|
+
#[test]
|
|
121
|
+
fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
|
|
122
|
+
let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
|
|
123
|
+
// 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
|
|
124
|
+
let allsym = base.join("@@@");
|
|
125
|
+
std::fs::create_dir_all(&allsym).unwrap();
|
|
126
|
+
let got = leader_session_name(Provider::Codex, &allsym);
|
|
127
|
+
assert!(
|
|
128
|
+
got.as_str().contains("-workspace-"),
|
|
129
|
+
"全符号 folder 应回退 'workspace',got {}",
|
|
130
|
+
got.as_str()
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
|
|
135
|
+
#[test]
|
|
136
|
+
fn leader_session_name_uses_claude_code_provider_string() {
|
|
137
|
+
let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
|
|
138
|
+
let dir = base.join("proj");
|
|
139
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
140
|
+
let got = leader_session_name(Provider::ClaudeCode, &dir);
|
|
141
|
+
assert!(
|
|
142
|
+
got.as_str()
|
|
143
|
+
.starts_with("team-agent-leader-claude_code-proj-"),
|
|
144
|
+
"got {}",
|
|
145
|
+
got.as_str()
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn leader_session_name_uses_copilot_provider_string() {
|
|
151
|
+
let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
|
|
152
|
+
let dir = base.join("proj");
|
|
153
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
154
|
+
let got = leader_session_name(Provider::Copilot, &dir);
|
|
155
|
+
assert!(
|
|
156
|
+
got.as_str().starts_with("team-agent-leader-copilot-proj-"),
|
|
157
|
+
"got {}",
|
|
158
|
+
got.as_str()
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =====================================================================
|
|
163
|
+
// 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
|
|
164
|
+
// =====================================================================
|
|
165
|
+
|
|
166
|
+
// $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
|
|
167
|
+
// 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
|
|
168
|
+
#[test]
|
|
169
|
+
fn bind_owner_refuses_when_caller_pane_missing() {
|
|
170
|
+
// 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
|
|
171
|
+
if std::env::var_os("TMUX_PANE").is_some() {
|
|
172
|
+
// 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
|
|
173
|
+
return;
|
|
13
174
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
fn should_reread_file_changed() {
|
|
25
|
-
// current != last_classified → {reread:true, file_changed}。
|
|
26
|
-
let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
|
|
27
|
-
assert!(d.reread);
|
|
28
|
-
assert_eq!(d.reason, RereadReason::FileChanged);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
#[test]
|
|
32
|
-
fn should_reread_quiescent_already_classified_at_and_past_debounce() {
|
|
33
|
-
// current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
|
|
34
|
-
// silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
|
|
35
|
-
let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
|
|
36
|
-
assert!(!d.reread);
|
|
37
|
-
assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
|
|
38
|
-
// 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
|
|
39
|
-
let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
|
|
40
|
-
assert!(!b.reread);
|
|
41
|
-
assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
#[test]
|
|
45
|
-
fn should_reread_unchanged_within_debounce() {
|
|
46
|
-
// current==last_classified 且 silent_for < debounce → unchanged。
|
|
47
|
-
// now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
|
|
48
|
-
let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
|
|
49
|
-
assert!(!d.reread);
|
|
50
|
-
assert_eq!(d.reason, RereadReason::Unchanged);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#[test]
|
|
54
|
-
fn on_file_changed_adds_node_sorted_and_records_mtime() {
|
|
55
|
-
// golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
|
|
56
|
-
let s0 = on_file_changed(None, "b", 1.0);
|
|
57
|
-
assert_eq!(s0.pending, vec!["b".to_string()]);
|
|
58
|
-
assert_eq!(s0.mtimes.get("b"), Some(&1.0));
|
|
59
|
-
let s1 = on_file_changed(Some(&s0), "a", 2.0);
|
|
60
|
-
assert_eq!(s1.pending, vec!["a".to_string(), "b".to_string()], "pending 必须 sorted");
|
|
61
|
-
assert_eq!(s1.mtimes.get("a"), Some(&2.0));
|
|
62
|
-
// 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
|
|
63
|
-
let s2 = on_file_changed(Some(&s1), "b", 3.0);
|
|
64
|
-
assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
|
|
65
|
-
assert_eq!(s2.mtimes.get("b"), Some(&3.0));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
#[test]
|
|
69
|
-
fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
|
|
70
|
-
// golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
|
|
71
|
-
let mut st = on_file_changed(None, "b", 3.0);
|
|
72
|
-
st = on_file_changed(Some(&st), "a", 2.0);
|
|
73
|
-
let (drained, after) = take_pending(Some(&st));
|
|
74
|
-
assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
|
|
75
|
-
assert!(after.pending.is_empty());
|
|
76
|
-
// mtimes 不被 drain 清空。
|
|
77
|
-
assert_eq!(after.mtimes.get("a"), Some(&2.0));
|
|
78
|
-
assert_eq!(after.mtimes.get("b"), Some(&3.0));
|
|
79
|
-
// 再 drain → 空。
|
|
80
|
-
let (drained2, _after2) = take_pending(Some(&after));
|
|
81
|
-
assert!(drained2.is_empty());
|
|
82
|
-
// drain None → ([], default state)。
|
|
83
|
-
let (none_drained, none_state) = take_pending(None);
|
|
84
|
-
assert!(none_drained.is_empty());
|
|
85
|
-
assert!(none_state.pending.is_empty());
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// =====================================================================
|
|
89
|
-
// 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
|
|
90
|
-
// =====================================================================
|
|
91
|
-
|
|
92
|
-
// 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
|
|
93
|
-
// 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
|
|
94
|
-
#[test]
|
|
95
|
-
fn leader_session_name_formula_and_sanitization() {
|
|
96
|
-
// 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
|
|
97
|
-
// sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
|
|
98
|
-
// 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
|
|
99
|
-
let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
|
|
100
|
-
let weird = base.join("My Proj!@#name");
|
|
101
|
-
std::fs::create_dir_all(&weird).unwrap();
|
|
102
|
-
let got = leader_session_name(Provider::Codex, &weird);
|
|
103
|
-
// 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
|
|
104
|
-
let s = got.as_str();
|
|
105
|
-
assert!(s.starts_with("team-agent-leader-codex-My_Proj___name-"), "got {s}");
|
|
106
|
-
// sha1[:8] 后缀:8 个 hex。
|
|
107
|
-
let suffix = s.rsplit('-').next().unwrap();
|
|
108
|
-
assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
|
|
109
|
-
assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
|
|
113
|
-
#[test]
|
|
114
|
-
fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
|
|
115
|
-
let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
|
|
116
|
-
// 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
|
|
117
|
-
let allsym = base.join("@@@");
|
|
118
|
-
std::fs::create_dir_all(&allsym).unwrap();
|
|
119
|
-
let got = leader_session_name(Provider::Codex, &allsym);
|
|
120
|
-
assert!(
|
|
121
|
-
got.as_str().contains("-workspace-"),
|
|
122
|
-
"全符号 folder 应回退 'workspace',got {}",
|
|
123
|
-
got.as_str()
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
|
|
128
|
-
#[test]
|
|
129
|
-
fn leader_session_name_uses_claude_code_provider_string() {
|
|
130
|
-
let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
|
|
131
|
-
let dir = base.join("proj");
|
|
132
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
133
|
-
let got = leader_session_name(Provider::ClaudeCode, &dir);
|
|
134
|
-
assert!(got.as_str().starts_with("team-agent-leader-claude_code-proj-"), "got {}", got.as_str());
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
#[test]
|
|
138
|
-
fn leader_session_name_uses_copilot_provider_string() {
|
|
139
|
-
let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
|
|
140
|
-
let dir = base.join("proj");
|
|
141
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
142
|
-
let got = leader_session_name(Provider::Copilot, &dir);
|
|
143
|
-
assert!(
|
|
144
|
-
got.as_str().starts_with("team-agent-leader-copilot-proj-"),
|
|
145
|
-
"got {}",
|
|
146
|
-
got.as_str()
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// =====================================================================
|
|
151
|
-
// 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
|
|
152
|
-
// =====================================================================
|
|
153
|
-
|
|
154
|
-
// $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
|
|
155
|
-
// 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
|
|
156
|
-
#[test]
|
|
157
|
-
fn bind_owner_refuses_when_caller_pane_missing() {
|
|
158
|
-
// 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
|
|
159
|
-
if std::env::var_os("TMUX_PANE").is_some() {
|
|
160
|
-
// 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
|
|
164
|
-
std::fs::create_dir_all(&ws).unwrap();
|
|
165
|
-
let team = TeamKey::new("default");
|
|
166
|
-
let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
|
|
167
|
-
assert!(!res.ok);
|
|
168
|
-
assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
|
|
169
|
-
// caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
|
|
170
|
-
assert_eq!(res.caller_pane_id, PaneId::new(""));
|
|
171
|
-
assert_eq!(res.caller_current_command, "");
|
|
172
|
-
assert_eq!(
|
|
175
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
|
|
176
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
177
|
+
let team = TeamKey::new("default");
|
|
178
|
+
let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
|
|
179
|
+
assert!(!res.ok);
|
|
180
|
+
assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
|
|
181
|
+
// caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
|
|
182
|
+
assert_eq!(res.caller_pane_id, PaneId::new(""));
|
|
183
|
+
assert_eq!(res.caller_current_command, "");
|
|
184
|
+
assert_eq!(
|
|
173
185
|
res.hint.as_deref(),
|
|
174
186
|
Some("run team-agent from inside your leader pane (the tmux pane you want to own this team).")
|
|
175
187
|
);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
188
|
+
assert_eq!(res.team_id, team);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
|
|
192
|
+
#[test]
|
|
193
|
+
fn owner_bind_refused_event_name_is_owner_bind_refused() {
|
|
194
|
+
assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
|
|
198
|
+
// 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
|
|
199
|
+
// old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
|
|
200
|
+
// unimplemented → RED。
|
|
201
|
+
#[test]
|
|
202
|
+
fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
|
|
203
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
|
|
204
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
205
|
+
let caller = PaneId::new("%7");
|
|
206
|
+
let derived = uuid("fp", "/ws", "u", "default");
|
|
207
|
+
let full = derived.as_str().to_string();
|
|
208
|
+
assert_eq!(full.len(), 32, "derive 产 32 hex");
|
|
209
|
+
let prefix12 = full[..12].to_string();
|
|
210
|
+
emit_owner_bound_event(
|
|
211
|
+
&ws,
|
|
212
|
+
&caller,
|
|
213
|
+
"codex",
|
|
214
|
+
&derived,
|
|
215
|
+
&TeamKey::new("default"),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
.unwrap();
|
|
219
|
+
// 读回审计事件:恰一条 owner.bound_from_caller_pane。
|
|
220
|
+
let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
|
|
221
|
+
let ev = events
|
|
222
|
+
.iter()
|
|
223
|
+
.find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
|
|
224
|
+
.expect("必写 owner.bound_from_caller_pane");
|
|
225
|
+
assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
|
|
226
|
+
assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
|
|
227
|
+
assert_eq!(ev["team_id"], serde_json::json!("default"));
|
|
228
|
+
// derived_uuid_prefix == derived[:12](只前缀,12 hex)。
|
|
229
|
+
assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
|
|
230
|
+
// old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)。
|
|
231
|
+
assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
|
|
232
|
+
// no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
|
|
233
|
+
let raw = serde_json::to_string(ev).unwrap();
|
|
234
|
+
assert!(
|
|
235
|
+
!raw.contains(&full),
|
|
236
|
+
"审计事件绝不泄露完整 leader_session_uuid"
|
|
237
|
+
);
|
|
238
|
+
// 审计事件名字节锁。
|
|
239
|
+
assert_eq!(
|
|
240
|
+
LeaderEvent::OwnerBoundFromCallerPane.name(),
|
|
241
|
+
"owner.bound_from_caller_pane"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -69,10 +69,10 @@ pub mod transport;
|
|
|
69
69
|
// step 10-12 (leader/messaging/coordinator) — ROUND-0.5 类型+fn surface 骨架(fn body unimplemented!(),
|
|
70
70
|
// P2 porter 落实现)。leader=lease/owner-bind/idle-takeover;messaging=send/deliver/retry/watchers;
|
|
71
71
|
// coordinator=daemon tick(§10 no-panic,Result<TickReport,TickError>)。
|
|
72
|
-
pub mod leader;
|
|
73
|
-
pub mod messaging;
|
|
74
72
|
pub mod coordinator;
|
|
75
73
|
pub mod diagnose;
|
|
74
|
+
pub mod leader;
|
|
75
|
+
pub mod messaging;
|
|
76
76
|
|
|
77
77
|
// step 13-15 (lifecycle/mcp_server/cli/packaging) — ROUND-0.5b behavioral-rich 骨架(entry-fn 签名 +
|
|
78
78
|
// 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
|
|
@@ -87,11 +87,11 @@ pub mod lifecycle;
|
|
|
87
87
|
pub mod platform;
|
|
88
88
|
// 0.3.28 — unified adaptive layout manager (single source of truth for tmux
|
|
89
89
|
// topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
|
|
90
|
+
pub mod cli;
|
|
90
91
|
pub mod layout;
|
|
91
|
-
pub mod topology;
|
|
92
92
|
pub mod mcp_server;
|
|
93
|
-
pub mod cli;
|
|
94
93
|
pub mod packaging;
|
|
94
|
+
pub mod topology;
|
|
95
95
|
|
|
96
96
|
// fake-worker — subscription-free backing program for Provider::Fake; lets the real spawn path
|
|
97
97
|
// (launch dry_run=false → tmux window) be exercised with no real provider (port of fake_worker.py).
|
|
@@ -217,7 +217,10 @@ fn open_adaptive_worker_displays(
|
|
|
217
217
|
workspace_window: target.window.clone(),
|
|
218
218
|
pane_id: target.pane_id.clone(),
|
|
219
219
|
pane_title: Some(agent_id),
|
|
220
|
-
target: target
|
|
220
|
+
target: target
|
|
221
|
+
.pane_id
|
|
222
|
+
.as_ref()
|
|
223
|
+
.map(|pane| pane.as_str().to_string()),
|
|
221
224
|
target_worker_session: Some(session_name.as_str().to_string()),
|
|
222
225
|
linked_session: None,
|
|
223
226
|
leader_session: Some(session_name.clone()),
|
|
@@ -279,7 +282,9 @@ fn close_adaptive_displays(
|
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
if let Some(leader_session) = adaptive_leader_session(&state) {
|
|
282
|
-
for target in
|
|
285
|
+
for target in
|
|
286
|
+
close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str())
|
|
287
|
+
{
|
|
283
288
|
orphans_cleaned.push(target.clone());
|
|
284
289
|
if seen.insert(target.clone()) {
|
|
285
290
|
closed.push(target);
|
|
@@ -481,4 +486,3 @@ fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str)
|
|
|
481
486
|
.filter(|s| !s.is_empty())
|
|
482
487
|
.map(str::to_string)
|
|
483
488
|
}
|
|
484
|
-
|
|
@@ -139,7 +139,12 @@ pub fn launch_with_transport_in_workspace(
|
|
|
139
139
|
.map(|id| id == agent.agent_id.as_str())
|
|
140
140
|
.unwrap_or(false)
|
|
141
141
|
})
|
|
142
|
-
.and_then(|entry|
|
|
142
|
+
.and_then(|entry| {
|
|
143
|
+
entry
|
|
144
|
+
.get("provider")
|
|
145
|
+
.and_then(Value::as_str)
|
|
146
|
+
.map(str::to_string)
|
|
147
|
+
})
|
|
143
148
|
.unwrap_or_else(|| "fake".to_string());
|
|
144
149
|
crate::lifecycle::restart::write_worker_spawn_timing_event(
|
|
145
150
|
workspace,
|
|
@@ -371,9 +376,13 @@ fn spawn_agents(
|
|
|
371
376
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
372
377
|
// Python providers.py:145 + launch/core.py:253 — fresh launch runs the worker
|
|
373
378
|
// with cwd=workspace, same as the RS fork/add and restart paths.
|
|
374
|
-
let env_unset
|
|
375
|
-
profile_launch.env_unset.iter().cloned().collect(),
|
|
379
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
376
380
|
provider,
|
|
381
|
+
&mut env,
|
|
382
|
+
extend_worker_env_unset_for_effort(
|
|
383
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
384
|
+
provider,
|
|
385
|
+
),
|
|
377
386
|
);
|
|
378
387
|
// BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
|
|
379
388
|
// per-worker AGENTS.md(B2 灵魂件降级):写
|
|
@@ -3970,6 +3979,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
3970
3979
|
pre_spec_text.as_deref(),
|
|
3971
3980
|
pre_runtime_state.as_ref(),
|
|
3972
3981
|
agent_id,
|
|
3982
|
+
"state_upsert_failed",
|
|
3973
3983
|
);
|
|
3974
3984
|
return Err(error);
|
|
3975
3985
|
}
|
|
@@ -3991,6 +4001,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
3991
4001
|
pre_spec_text.as_deref(),
|
|
3992
4002
|
pre_runtime_state.as_ref(),
|
|
3993
4003
|
agent_id,
|
|
4004
|
+
"start_agent_failed",
|
|
3994
4005
|
);
|
|
3995
4006
|
return Err(error);
|
|
3996
4007
|
}
|
|
@@ -4007,6 +4018,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
4007
4018
|
pre_spec_text.as_deref(),
|
|
4008
4019
|
pre_runtime_state.as_ref(),
|
|
4009
4020
|
agent_id,
|
|
4021
|
+
"added_agent_paused",
|
|
4010
4022
|
);
|
|
4011
4023
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
4012
4024
|
"added agent {agent_id} is paused"
|
|
@@ -4032,12 +4044,21 @@ fn rollback_add_agent_atomic(
|
|
|
4032
4044
|
pre_spec_text: Option<&str>,
|
|
4033
4045
|
pre_runtime_state: Option<&serde_json::Value>,
|
|
4034
4046
|
agent_id: &AgentId,
|
|
4047
|
+
reason: &str,
|
|
4035
4048
|
) {
|
|
4036
|
-
if let Some(text) = pre_spec_text {
|
|
4037
|
-
|
|
4049
|
+
let spec_restored = if let Some(text) = pre_spec_text {
|
|
4050
|
+
std::fs::write(spec_path, text).is_ok()
|
|
4038
4051
|
} else {
|
|
4039
|
-
|
|
4040
|
-
|
|
4052
|
+
std::fs::remove_file(spec_path)
|
|
4053
|
+
.or_else(|error| {
|
|
4054
|
+
if error.kind() == std::io::ErrorKind::NotFound {
|
|
4055
|
+
Ok(())
|
|
4056
|
+
} else {
|
|
4057
|
+
Err(error)
|
|
4058
|
+
}
|
|
4059
|
+
})
|
|
4060
|
+
.is_ok()
|
|
4061
|
+
};
|
|
4041
4062
|
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.4):
|
|
4042
4063
|
// rollback must tombstone the newly-added agent so the persist merge
|
|
4043
4064
|
// does not re-attach a `roster_stub` from the latest on disk. Without
|
|
@@ -4045,12 +4066,13 @@ fn rollback_add_agent_atomic(
|
|
|
4045
4066
|
// survives the restore-from-pre_state pass and the retry sees
|
|
4046
4067
|
// "agent id already exists".
|
|
4047
4068
|
let deleted = [agent_id.as_str()];
|
|
4048
|
-
if let Some(state) = pre_runtime_state {
|
|
4049
|
-
|
|
4069
|
+
let state_restored = if let Some(state) = pre_runtime_state {
|
|
4070
|
+
crate::state::persist::save_runtime_state_with_deleted_agents(
|
|
4050
4071
|
run_workspace,
|
|
4051
4072
|
state,
|
|
4052
4073
|
&deleted,
|
|
4053
|
-
)
|
|
4074
|
+
)
|
|
4075
|
+
.is_ok()
|
|
4054
4076
|
} else {
|
|
4055
4077
|
// No prior runtime state — drop just the agent we added (load → strip → save).
|
|
4056
4078
|
if let Ok(mut state) = crate::state::persist::load_runtime_state(run_workspace) {
|
|
@@ -4073,13 +4095,27 @@ fn rollback_add_agent_atomic(
|
|
|
4073
4095
|
}
|
|
4074
4096
|
}
|
|
4075
4097
|
}
|
|
4076
|
-
|
|
4098
|
+
crate::state::persist::save_runtime_state_with_deleted_agents(
|
|
4077
4099
|
run_workspace,
|
|
4078
4100
|
&state,
|
|
4079
4101
|
&deleted,
|
|
4080
|
-
)
|
|
4102
|
+
)
|
|
4103
|
+
.is_ok()
|
|
4104
|
+
} else {
|
|
4105
|
+
false
|
|
4081
4106
|
}
|
|
4082
|
-
}
|
|
4107
|
+
};
|
|
4108
|
+
let rollback_ok = spec_restored && state_restored;
|
|
4109
|
+
let _ = crate::event_log::EventLog::new(run_workspace).write(
|
|
4110
|
+
"add_agent.rollback",
|
|
4111
|
+
serde_json::json!({
|
|
4112
|
+
"agent_id": agent_id.as_str(),
|
|
4113
|
+
"reason": reason,
|
|
4114
|
+
"rollback_ok": rollback_ok,
|
|
4115
|
+
"spec_restored": spec_restored,
|
|
4116
|
+
"state_restored": state_restored,
|
|
4117
|
+
}),
|
|
4118
|
+
);
|
|
4083
4119
|
}
|
|
4084
4120
|
|
|
4085
4121
|
fn upsert_agent_state_from_role(
|
|
@@ -4524,9 +4560,13 @@ pub fn fork_agent_with_transport(
|
|
|
4524
4560
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
4525
4561
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
4526
4562
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
4527
|
-
let env_unset
|
|
4528
|
-
profile_launch.env_unset.iter().cloned().collect(),
|
|
4563
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
4529
4564
|
provider,
|
|
4565
|
+
&mut env,
|
|
4566
|
+
extend_worker_env_unset_for_effort(
|
|
4567
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
4568
|
+
provider,
|
|
4569
|
+
),
|
|
4530
4570
|
);
|
|
4531
4571
|
let spawn_result = if session_live {
|
|
4532
4572
|
transport.spawn_into_with_env_unset(
|