@team-agent/installer 0.3.1 → 0.3.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/Cargo.lock +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -44,6 +44,26 @@ pub fn launch_with_transport(
|
|
|
44
44
|
auto_approve: bool,
|
|
45
45
|
skip_profile_smoke: bool,
|
|
46
46
|
transport: &dyn Transport,
|
|
47
|
+
) -> Result<LaunchReport, LifecycleError> {
|
|
48
|
+
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
49
|
+
let workspace = team_workspace(team_dir);
|
|
50
|
+
launch_with_transport_in_workspace(
|
|
51
|
+
&workspace,
|
|
52
|
+
spec_path,
|
|
53
|
+
dry_run,
|
|
54
|
+
auto_approve,
|
|
55
|
+
skip_profile_smoke,
|
|
56
|
+
transport,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn launch_with_transport_in_workspace(
|
|
61
|
+
workspace: &Path,
|
|
62
|
+
spec_path: &Path,
|
|
63
|
+
dry_run: bool,
|
|
64
|
+
auto_approve: bool,
|
|
65
|
+
skip_profile_smoke: bool,
|
|
66
|
+
transport: &dyn Transport,
|
|
47
67
|
) -> Result<LaunchReport, LifecycleError> {
|
|
48
68
|
let _ = skip_profile_smoke;
|
|
49
69
|
if !spec_path.exists() {
|
|
@@ -76,13 +96,13 @@ pub fn launch_with_transport(
|
|
|
76
96
|
raw: serde_json::json!({"source": "compiled_spec"}),
|
|
77
97
|
})
|
|
78
98
|
.collect::<Vec<_>>();
|
|
79
|
-
write_launch_permission_audit(
|
|
99
|
+
write_launch_permission_audit(workspace, &safety)?;
|
|
80
100
|
let routes = spec_routes(&spec);
|
|
81
101
|
let started = if dry_run {
|
|
82
102
|
Vec::new()
|
|
83
103
|
} else {
|
|
84
|
-
let started = spawn_agents(spec_path, &spec, &session_name, &safety, transport)?;
|
|
85
|
-
persist_spawn_agent_state(spec_path, &spec, &session_name, transport, &started)?;
|
|
104
|
+
let started = spawn_agents(workspace, spec_path, &spec, &session_name, &safety, transport)?;
|
|
105
|
+
persist_spawn_agent_state(workspace, spec_path, &spec, &session_name, transport, &started, &safety)?;
|
|
86
106
|
started
|
|
87
107
|
};
|
|
88
108
|
Ok(LaunchReport {
|
|
@@ -93,10 +113,12 @@ pub fn launch_with_transport(
|
|
|
93
113
|
permissions,
|
|
94
114
|
safety,
|
|
95
115
|
leader_receiver_attached: false,
|
|
116
|
+
session_capture_incomplete_agents: Vec::new(),
|
|
96
117
|
})
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
fn spawn_agents(
|
|
121
|
+
workspace: &Path,
|
|
100
122
|
spec_path: &Path,
|
|
101
123
|
spec: &Value,
|
|
102
124
|
session_name: &SessionName,
|
|
@@ -104,7 +126,6 @@ fn spawn_agents(
|
|
|
104
126
|
transport: &dyn Transport,
|
|
105
127
|
) -> Result<Vec<StartedAgent>, LifecycleError> {
|
|
106
128
|
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
107
|
-
let workspace = team_workspace(team_dir);
|
|
108
129
|
let mut started = Vec::new();
|
|
109
130
|
for agent in spec_agent_values(spec) {
|
|
110
131
|
let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
|
|
@@ -135,39 +156,55 @@ fn spawn_agents(
|
|
|
135
156
|
let tools = worker_tool_refs(agent_tool_strings(agent), safety);
|
|
136
157
|
let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
137
158
|
let mcp_team_id =
|
|
138
|
-
runtime_active_team_key_for_spawn(
|
|
139
|
-
let process_team_id = process_team_id_for_spawn(&workspace, spec);
|
|
159
|
+
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
140
160
|
let mcp_config = adapter
|
|
141
161
|
.mcp_config(auth_mode)
|
|
142
162
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
143
|
-
let mcp_config = resolve_mcp_config(mcp_config,
|
|
144
|
-
let mcp_config_path = write_worker_mcp_config(
|
|
145
|
-
let
|
|
146
|
-
|
|
163
|
+
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
164
|
+
let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
|
|
165
|
+
let profile_dir = team_dir.join("profiles");
|
|
166
|
+
let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
167
|
+
workspace,
|
|
168
|
+
agent_id_raw,
|
|
169
|
+
agent,
|
|
170
|
+
Some(&profile_dir),
|
|
171
|
+
Some(&mcp_config),
|
|
172
|
+
)?;
|
|
173
|
+
let command_model = profile_launch
|
|
174
|
+
.command_overrides
|
|
175
|
+
.model
|
|
176
|
+
.as_deref()
|
|
177
|
+
.or(model);
|
|
178
|
+
let mut plan = adapter
|
|
179
|
+
.build_command_plan(crate::provider::ProviderCommandContext {
|
|
147
180
|
auth_mode,
|
|
148
|
-
Some(&mcp_config),
|
|
149
|
-
role,
|
|
150
|
-
model,
|
|
151
|
-
&tool_refs,
|
|
152
|
-
|
|
181
|
+
mcp_config: Some(&mcp_config),
|
|
182
|
+
system_prompt: role,
|
|
183
|
+
model: command_model,
|
|
184
|
+
tools: &tool_refs,
|
|
185
|
+
profile_launch: Some(&profile_launch),
|
|
186
|
+
})
|
|
153
187
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
154
|
-
|
|
188
|
+
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
189
|
+
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
190
|
+
}
|
|
155
191
|
fill_spawn_placeholders_full(
|
|
156
|
-
&mut argv,
|
|
157
|
-
|
|
192
|
+
&mut plan.argv,
|
|
193
|
+
workspace,
|
|
158
194
|
agent_id_raw,
|
|
159
|
-
|
|
195
|
+
Some(&mcp_team_id),
|
|
160
196
|
);
|
|
161
197
|
let window = WindowName::new(agent_id_raw);
|
|
162
|
-
let env = inherited_env_with_team_overrides(
|
|
163
|
-
|
|
198
|
+
let mut env = inherited_env_with_team_overrides(
|
|
199
|
+
workspace,
|
|
164
200
|
agent_id_raw,
|
|
165
|
-
|
|
201
|
+
Some(&mcp_team_id),
|
|
166
202
|
);
|
|
203
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
167
204
|
let spawn = if started.is_empty() {
|
|
168
|
-
transport.spawn_first(session_name, &window, &argv, team_dir, &env)
|
|
205
|
+
transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
|
|
169
206
|
} else {
|
|
170
|
-
transport.spawn_into(session_name, &window, &argv, team_dir, &env)
|
|
207
|
+
transport.spawn_into(session_name, &window, &plan.argv, team_dir, &env)
|
|
171
208
|
}
|
|
172
209
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
173
210
|
let _ = adapter.handle_startup_prompts(
|
|
@@ -185,6 +222,13 @@ fn spawn_agents(
|
|
|
185
222
|
target: spawn.pane_id.as_str().to_string(),
|
|
186
223
|
session_id: None,
|
|
187
224
|
rollout_path: None,
|
|
225
|
+
pending_session_id: plan.expected_session_id.clone(),
|
|
226
|
+
claude_config_dir: profile_launch.claude_config_dir.clone(),
|
|
227
|
+
provider_projects_root: plan
|
|
228
|
+
.provider_projects_root
|
|
229
|
+
.clone()
|
|
230
|
+
.or_else(|| profile_launch.claude_projects_root.clone()),
|
|
231
|
+
managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
|
|
188
232
|
display: WorkerDisplay::Blocked {
|
|
189
233
|
reason: AdaptiveBlockReason::NotImplementedThisPlatform,
|
|
190
234
|
},
|
|
@@ -194,15 +238,15 @@ fn spawn_agents(
|
|
|
194
238
|
}
|
|
195
239
|
|
|
196
240
|
fn persist_spawn_agent_state(
|
|
241
|
+
workspace: &Path,
|
|
197
242
|
spec_path: &Path,
|
|
198
243
|
spec: &Value,
|
|
199
244
|
session_name: &SessionName,
|
|
200
245
|
transport: &dyn Transport,
|
|
201
246
|
started: &[StartedAgent],
|
|
247
|
+
safety: &DangerousApproval,
|
|
202
248
|
) -> Result<(), LifecycleError> {
|
|
203
|
-
let
|
|
204
|
-
let workspace = team_workspace(team_dir);
|
|
205
|
-
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
249
|
+
let state_path = crate::state::persist::runtime_state_path(workspace);
|
|
206
250
|
let mut state = if state_path.exists() {
|
|
207
251
|
let text = std::fs::read_to_string(&state_path)
|
|
208
252
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", state_path.display())))?;
|
|
@@ -213,7 +257,7 @@ fn persist_spawn_agent_state(
|
|
|
213
257
|
};
|
|
214
258
|
let team_id = explicit_active_team_key(&state)
|
|
215
259
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
216
|
-
let worker_tmux_socket = launched_worker_tmux_socket(transport,
|
|
260
|
+
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
217
261
|
drop_worker_pane_seeded_owner(
|
|
218
262
|
&mut state,
|
|
219
263
|
&team_id,
|
|
@@ -231,8 +275,13 @@ fn persist_spawn_agent_state(
|
|
|
231
275
|
.iter()
|
|
232
276
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
233
277
|
.collect();
|
|
278
|
+
let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
|
|
279
|
+
let profile_dir = spec_path
|
|
280
|
+
.parent()
|
|
281
|
+
.unwrap_or(workspace)
|
|
282
|
+
.join("profiles");
|
|
234
283
|
let mut agents = serde_json::Map::new();
|
|
235
|
-
let
|
|
284
|
+
let mut spawn_index = 0_u32;
|
|
236
285
|
for agent in spec_agent_values(spec) {
|
|
237
286
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
238
287
|
continue;
|
|
@@ -265,9 +314,28 @@ fn persist_spawn_agent_state(
|
|
|
265
314
|
agents.insert(id.to_string(), serde_json::Value::Object(failed));
|
|
266
315
|
continue;
|
|
267
316
|
}
|
|
317
|
+
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
318
|
+
let spawned_at = spawn_timestamp_for_agent(spawn_index);
|
|
319
|
+
spawn_index = spawn_index.saturating_add(1);
|
|
320
|
+
let started_agent = started
|
|
321
|
+
.iter()
|
|
322
|
+
.find(|agent| agent.agent_id.as_str() == id);
|
|
268
323
|
agents.insert(
|
|
269
324
|
id.to_string(),
|
|
270
|
-
running_agent_state(
|
|
325
|
+
running_agent_state(
|
|
326
|
+
agent,
|
|
327
|
+
id,
|
|
328
|
+
provider,
|
|
329
|
+
workspace,
|
|
330
|
+
spec_path.parent().unwrap_or(workspace),
|
|
331
|
+
&spawned_at,
|
|
332
|
+
&team_id,
|
|
333
|
+
Some(agent_id_to_pane_id(started, id)),
|
|
334
|
+
pane_pid,
|
|
335
|
+
safety,
|
|
336
|
+
started_agent,
|
|
337
|
+
Some(&profile_dir),
|
|
338
|
+
)?,
|
|
271
339
|
);
|
|
272
340
|
}
|
|
273
341
|
if let Some(obj) = state.as_object_mut() {
|
|
@@ -277,21 +345,131 @@ fn persist_spawn_agent_state(
|
|
|
277
345
|
obj.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
278
346
|
state = serde_json::Value::Object(obj);
|
|
279
347
|
}
|
|
280
|
-
|
|
348
|
+
save_launched_team_state_for_key(workspace, &state, Some(&team_id))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fn pane_pids_by_started_agent(
|
|
352
|
+
transport: &dyn Transport,
|
|
353
|
+
started: &[StartedAgent],
|
|
354
|
+
) -> BTreeMap<String, u32> {
|
|
355
|
+
let panes = transport.list_targets().unwrap_or_default();
|
|
356
|
+
started
|
|
357
|
+
.iter()
|
|
358
|
+
.filter_map(|agent| {
|
|
359
|
+
panes
|
|
360
|
+
.iter()
|
|
361
|
+
.find(|pane| pane.pane_id.as_str() == agent.target)
|
|
362
|
+
.and_then(|pane| pane.pane_pid)
|
|
363
|
+
.map(|pid| (agent.agent_id.as_str().to_string(), pid))
|
|
364
|
+
})
|
|
365
|
+
.collect()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a str {
|
|
369
|
+
started
|
|
370
|
+
.iter()
|
|
371
|
+
.find(|agent| agent.agent_id.as_str() == agent_id)
|
|
372
|
+
.map(|agent| agent.target.as_str())
|
|
373
|
+
.unwrap_or("")
|
|
281
374
|
}
|
|
282
375
|
|
|
283
376
|
fn save_launched_team_state(workspace: &Path, launched: &serde_json::Value) -> Result<(), LifecycleError> {
|
|
377
|
+
save_launched_team_state_for_key(workspace, launched, None)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fn save_launched_team_state_for_key(
|
|
381
|
+
workspace: &Path,
|
|
382
|
+
launched: &serde_json::Value,
|
|
383
|
+
team_key: Option<&str>,
|
|
384
|
+
) -> Result<(), LifecycleError> {
|
|
284
385
|
let existing = load_runtime_state(workspace).unwrap_or_else(|_| serde_json::json!({}));
|
|
285
|
-
let launched_key =
|
|
386
|
+
let launched_key = team_key
|
|
387
|
+
.filter(|key| !key.is_empty())
|
|
388
|
+
.map(str::to_string)
|
|
389
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(launched));
|
|
286
390
|
let mut launched = launched.clone();
|
|
391
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
392
|
+
obj.insert(
|
|
393
|
+
"active_team_key".to_string(),
|
|
394
|
+
serde_json::Value::String(launched_key.clone()),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
287
397
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
288
398
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
289
|
-
|
|
399
|
+
drop_bare_worker_seeded_owner(&mut launched, &launched_key);
|
|
400
|
+
let merged = if team_key.is_some() {
|
|
401
|
+
merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
|
|
402
|
+
} else {
|
|
403
|
+
crate::state::projection::merge_workspace_team_state(&existing, &launched)
|
|
404
|
+
};
|
|
290
405
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
291
406
|
drop_unbound_top_level_owner(&mut projected);
|
|
292
407
|
save_runtime_state(workspace, &projected).map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
293
408
|
}
|
|
294
409
|
|
|
410
|
+
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
411
|
+
if has_positive_caller_leader_env() {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
let pane = launched
|
|
415
|
+
.get("team_owner")
|
|
416
|
+
.and_then(|owner| owner.get("pane_id"))
|
|
417
|
+
.and_then(serde_json::Value::as_str)
|
|
418
|
+
.unwrap_or("");
|
|
419
|
+
if pane.ends_with("-first") {
|
|
420
|
+
seed_unbound_launched_owner(launched, launched_key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fn merge_workspace_team_state_with_key(
|
|
425
|
+
existing: &serde_json::Value,
|
|
426
|
+
launched: &serde_json::Value,
|
|
427
|
+
launched_key: &str,
|
|
428
|
+
) -> serde_json::Value {
|
|
429
|
+
let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
|
|
430
|
+
let mut teams = launched
|
|
431
|
+
.get("teams")
|
|
432
|
+
.and_then(serde_json::Value::as_object)
|
|
433
|
+
.cloned()
|
|
434
|
+
.unwrap_or_default();
|
|
435
|
+
let launched_entry = crate::state::projection::compact_team_state(launched);
|
|
436
|
+
if !existing
|
|
437
|
+
.get("session_name")
|
|
438
|
+
.and_then(serde_json::Value::as_str)
|
|
439
|
+
.is_some_and(|session| !session.is_empty())
|
|
440
|
+
{
|
|
441
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
442
|
+
launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
443
|
+
return serde_json::Value::Object(launched_obj);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let existing_key = explicit_active_team_key(existing)
|
|
447
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(existing));
|
|
448
|
+
if existing_key == launched_key {
|
|
449
|
+
let mut teams = existing
|
|
450
|
+
.get("teams")
|
|
451
|
+
.and_then(serde_json::Value::as_object)
|
|
452
|
+
.cloned()
|
|
453
|
+
.unwrap_or_default();
|
|
454
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
455
|
+
launched_obj.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
456
|
+
return serde_json::Value::Object(launched_obj);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let mut merged = existing.as_object().cloned().unwrap_or_default();
|
|
460
|
+
let mut teams = merged
|
|
461
|
+
.get("teams")
|
|
462
|
+
.and_then(serde_json::Value::as_object)
|
|
463
|
+
.cloned()
|
|
464
|
+
.unwrap_or_default();
|
|
465
|
+
teams
|
|
466
|
+
.entry(existing_key)
|
|
467
|
+
.or_insert_with(|| crate::state::projection::compact_team_state(existing));
|
|
468
|
+
teams.insert(launched_key.to_string(), launched_entry);
|
|
469
|
+
merged.insert("teams".to_string(), serde_json::Value::Object(teams));
|
|
470
|
+
serde_json::Value::Object(merged)
|
|
471
|
+
}
|
|
472
|
+
|
|
295
473
|
fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
|
|
296
474
|
let entry = launched
|
|
297
475
|
.get("teams")
|
|
@@ -338,7 +516,15 @@ fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, l
|
|
|
338
516
|
return;
|
|
339
517
|
};
|
|
340
518
|
if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
|
|
341
|
-
|
|
519
|
+
let replacement = unbound_launched_owner(launched, launched_key);
|
|
520
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
521
|
+
if let Some(owner) = replacement {
|
|
522
|
+
obj.insert("team_owner".to_string(), owner);
|
|
523
|
+
} else {
|
|
524
|
+
obj.remove("team_owner");
|
|
525
|
+
}
|
|
526
|
+
obj.remove("owner_epoch");
|
|
527
|
+
}
|
|
342
528
|
}
|
|
343
529
|
}
|
|
344
530
|
|
|
@@ -362,23 +548,30 @@ fn drop_worker_pane_seeded_owner(
|
|
|
362
548
|
let tmux_pane = std::env::var("TMUX_PANE")
|
|
363
549
|
.ok()
|
|
364
550
|
.filter(|value| !value.is_empty());
|
|
365
|
-
let has_leader_identity_env =
|
|
366
|
-
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
|
|
367
|
-
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
|
|
368
|
-
|| env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
|
|
369
|
-
|| env_nonempty("TEAM_AGENT_ID")
|
|
370
|
-
|| env_nonempty("TEAM_AGENT_TEAM_ID");
|
|
551
|
+
let has_leader_identity_env = has_positive_caller_leader_env();
|
|
371
552
|
let seeded_from_bare_tmux =
|
|
372
553
|
!has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
373
554
|
let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
374
555
|
if seeded_from_bare_tmux
|
|
375
|
-
&& tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
376
|
-
|
|
556
|
+
&& (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
557
|
+
|| pane.ends_with("-first"))
|
|
558
|
+
&& seeded_pane_looks_like_worker(pane, started)
|
|
377
559
|
{
|
|
378
560
|
seed_unbound_launched_owner(launched, launched_key);
|
|
379
561
|
}
|
|
380
562
|
}
|
|
381
563
|
|
|
564
|
+
fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
|
|
565
|
+
pane.ends_with("-first")
|
|
566
|
+
|| started
|
|
567
|
+
.iter()
|
|
568
|
+
.any(|agent| {
|
|
569
|
+
pane == agent.target
|
|
570
|
+
|| pane.starts_with(agent.target.as_str())
|
|
571
|
+
|| agent.target.starts_with(pane)
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
382
575
|
fn launched_worker_tmux_socket(
|
|
383
576
|
transport: &dyn Transport,
|
|
384
577
|
workspace: &Path,
|
|
@@ -406,6 +599,35 @@ fn env_nonempty(key: &str) -> bool {
|
|
|
406
599
|
}
|
|
407
600
|
|
|
408
601
|
fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
602
|
+
let Some(owner) = unbound_launched_owner(launched, launched_key) else {
|
|
603
|
+
return;
|
|
604
|
+
};
|
|
605
|
+
let provider = launched
|
|
606
|
+
.get("team_owner")
|
|
607
|
+
.and_then(|owner| owner.get("provider"))
|
|
608
|
+
.and_then(serde_json::Value::as_str)
|
|
609
|
+
.filter(|provider| !provider.is_empty())
|
|
610
|
+
.unwrap_or("codex");
|
|
611
|
+
let owner_epoch = 1u64;
|
|
612
|
+
let receiver = serde_json::json!({
|
|
613
|
+
"mode": "direct_tmux",
|
|
614
|
+
"status": "unbound",
|
|
615
|
+
"provider": provider,
|
|
616
|
+
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
617
|
+
"owner_epoch": owner_epoch,
|
|
618
|
+
"discovery": "quick_start",
|
|
619
|
+
});
|
|
620
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
621
|
+
obj.insert("leader_receiver".to_string(), receiver);
|
|
622
|
+
obj.insert("team_owner".to_string(), owner);
|
|
623
|
+
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
fn unbound_launched_owner(
|
|
628
|
+
launched: &serde_json::Value,
|
|
629
|
+
launched_key: &str,
|
|
630
|
+
) -> Option<serde_json::Value> {
|
|
409
631
|
let provider = launched
|
|
410
632
|
.get("team_owner")
|
|
411
633
|
.and_then(|owner| owner.get("provider"))
|
|
@@ -424,39 +646,22 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
424
646
|
let os_user = std::env::var("USER")
|
|
425
647
|
.or_else(|_| std::env::var("USERNAME"))
|
|
426
648
|
.unwrap_or_default();
|
|
427
|
-
let
|
|
649
|
+
let uuid = crate::model::ids::LeaderSessionUuid::derive(
|
|
428
650
|
machine_fingerprint,
|
|
429
651
|
workspace,
|
|
430
652
|
&os_user,
|
|
431
653
|
launched_key,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
let owner_epoch = 1u64;
|
|
436
|
-
let owner = serde_json::json!({
|
|
437
|
-
"pane_id": "__team_agent_unbound__",
|
|
654
|
+
)
|
|
655
|
+
.ok()?;
|
|
656
|
+
Some(serde_json::json!({
|
|
438
657
|
"provider": provider,
|
|
439
658
|
"machine_fingerprint": machine_fingerprint,
|
|
440
659
|
"leader_session_uuid": uuid.as_str(),
|
|
441
|
-
"owner_epoch":
|
|
660
|
+
"owner_epoch": 1u64,
|
|
442
661
|
"claimed_at": spawn_timestamp(),
|
|
443
662
|
"claimed_via": "quick-start",
|
|
444
663
|
"os_user": os_user,
|
|
445
|
-
})
|
|
446
|
-
let receiver = serde_json::json!({
|
|
447
|
-
"mode": "direct_tmux",
|
|
448
|
-
"status": "attached",
|
|
449
|
-
"provider": provider,
|
|
450
|
-
"pane_id": "__team_agent_unbound__",
|
|
451
|
-
"leader_session_uuid": uuid.as_str(),
|
|
452
|
-
"owner_epoch": owner_epoch,
|
|
453
|
-
"discovery": "quick_start",
|
|
454
|
-
});
|
|
455
|
-
if let Some(obj) = launched.as_object_mut() {
|
|
456
|
-
obj.insert("leader_receiver".to_string(), receiver);
|
|
457
|
-
obj.insert("team_owner".to_string(), owner);
|
|
458
|
-
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
459
|
-
}
|
|
664
|
+
}))
|
|
460
665
|
}
|
|
461
666
|
|
|
462
667
|
fn owner_pane_belongs_to_other_team(existing: &serde_json::Value, launched_key: &str, pane: &str) -> bool {
|
|
@@ -480,8 +685,14 @@ fn running_agent_state(
|
|
|
480
685
|
id: &str,
|
|
481
686
|
provider: Provider,
|
|
482
687
|
workspace: &Path,
|
|
688
|
+
spawn_cwd: &Path,
|
|
483
689
|
spawned_at: &str,
|
|
484
690
|
team_id: &str,
|
|
691
|
+
pane_id: Option<&str>,
|
|
692
|
+
pane_pid: Option<u32>,
|
|
693
|
+
safety: &DangerousApproval,
|
|
694
|
+
started_agent: Option<&StartedAgent>,
|
|
695
|
+
profile_dir: Option<&Path>,
|
|
485
696
|
) -> Result<serde_json::Value, LifecycleError> {
|
|
486
697
|
let model = agent.get("model").and_then(Value::as_str);
|
|
487
698
|
let auth_mode = agent
|
|
@@ -503,6 +714,14 @@ fn running_agent_state(
|
|
|
503
714
|
state.insert("model".to_string(), model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)));
|
|
504
715
|
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
505
716
|
state.insert("profile".to_string(), profile);
|
|
717
|
+
if agent.get("profile").is_some() {
|
|
718
|
+
if let Some(profile_dir) = profile_dir {
|
|
719
|
+
state.insert(
|
|
720
|
+
"_profile_dir".to_string(),
|
|
721
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
506
725
|
state.insert("window".to_string(), serde_json::json!(window));
|
|
507
726
|
state.insert(
|
|
508
727
|
"mcp_config".to_string(),
|
|
@@ -513,20 +732,60 @@ fn running_agent_state(
|
|
|
513
732
|
permissions_json(agent, id, provider)
|
|
514
733
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?,
|
|
515
734
|
);
|
|
735
|
+
persist_effective_approval_policy(&mut state, safety);
|
|
516
736
|
state.insert("session_id".to_string(), serde_json::Value::Null);
|
|
517
737
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
518
738
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
519
739
|
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
520
740
|
state.insert("attribution_confidence".to_string(), serde_json::Value::Null);
|
|
741
|
+
if let Some(started_agent) = started_agent {
|
|
742
|
+
persist_started_agent_plan_state(&mut state, started_agent);
|
|
743
|
+
}
|
|
521
744
|
state.insert(
|
|
522
745
|
"spawn_cwd".to_string(),
|
|
523
|
-
serde_json::json!(
|
|
746
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
524
747
|
);
|
|
525
748
|
state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
|
|
749
|
+
if let Some(pane_id) = pane_id.filter(|pane| !pane.is_empty()) {
|
|
750
|
+
state.insert("pane_id".to_string(), serde_json::json!(pane_id));
|
|
751
|
+
}
|
|
752
|
+
if let Some(pane_pid) = pane_pid {
|
|
753
|
+
state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
754
|
+
}
|
|
526
755
|
Ok(serde_json::Value::Object(state))
|
|
527
756
|
}
|
|
528
757
|
|
|
529
|
-
fn
|
|
758
|
+
pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
|
|
759
|
+
serde_json::json!({
|
|
760
|
+
"enabled": safety.enabled,
|
|
761
|
+
"source": dangerous_approval_source_str(safety.source),
|
|
762
|
+
"inherited": safety.inherited,
|
|
763
|
+
"explicit_yes_confirmed": safety.enabled && matches!(safety.source, DangerousApprovalSource::RuntimeConfig),
|
|
764
|
+
"provider": safety.provider,
|
|
765
|
+
"flag": safety.flag,
|
|
766
|
+
"worker_capability_above_leader": safety.worker_capability_above_leader,
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
pub(crate) fn persist_effective_approval_policy(
|
|
771
|
+
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
772
|
+
safety: &DangerousApproval,
|
|
773
|
+
) {
|
|
774
|
+
agent_state.insert(
|
|
775
|
+
"effective_approval_policy".to_string(),
|
|
776
|
+
effective_approval_policy(safety),
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
|
|
781
|
+
match source {
|
|
782
|
+
DangerousApprovalSource::RuntimeConfig => "runtime_config",
|
|
783
|
+
DangerousApprovalSource::LeaderProcess => "leader_process",
|
|
784
|
+
DangerousApprovalSource::Disabled => "disabled",
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
pub(crate) fn resolve_mcp_config(
|
|
530
789
|
config: crate::provider::McpConfig,
|
|
531
790
|
workspace: &Path,
|
|
532
791
|
agent_id: &str,
|
|
@@ -569,7 +828,7 @@ fn resolve_mcp_placeholders(
|
|
|
569
828
|
}
|
|
570
829
|
}
|
|
571
830
|
|
|
572
|
-
fn write_worker_mcp_config(
|
|
831
|
+
pub(crate) fn write_worker_mcp_config(
|
|
573
832
|
workspace: &Path,
|
|
574
833
|
agent_id: &str,
|
|
575
834
|
config: &crate::provider::McpConfig,
|
|
@@ -588,7 +847,7 @@ fn write_worker_mcp_config(
|
|
|
588
847
|
Ok(path)
|
|
589
848
|
}
|
|
590
849
|
|
|
591
|
-
fn point_native_mcp_config_at_file(argv: &mut [String], provider: Provider, path: &Path) {
|
|
850
|
+
pub(crate) fn point_native_mcp_config_at_file(argv: &mut [String], provider: Provider, path: &Path) {
|
|
592
851
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
593
852
|
return;
|
|
594
853
|
}
|
|
@@ -654,6 +913,22 @@ fn spawn_timestamp() -> String {
|
|
|
654
913
|
}
|
|
655
914
|
}
|
|
656
915
|
|
|
916
|
+
fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
|
|
917
|
+
if offset_micros == 0 {
|
|
918
|
+
return spawn_timestamp();
|
|
919
|
+
}
|
|
920
|
+
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
921
|
+
Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
|
|
922
|
+
.map(|dt| {
|
|
923
|
+
(dt.with_timezone(&chrono::Utc) + chrono::Duration::microseconds(i64::from(offset_micros)))
|
|
924
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
925
|
+
.to_string()
|
|
926
|
+
})
|
|
927
|
+
.unwrap_or(value),
|
|
928
|
+
Err(_) => spawn_timestamp(),
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
657
932
|
pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
|
|
658
933
|
fill_spawn_placeholders_full(argv, workspace, agent_id, None);
|
|
659
934
|
}
|
|
@@ -696,6 +971,87 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
696
971
|
env
|
|
697
972
|
}
|
|
698
973
|
|
|
974
|
+
pub(crate) fn apply_profile_launch_env(
|
|
975
|
+
env: &mut BTreeMap<String, String>,
|
|
976
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
977
|
+
) {
|
|
978
|
+
for key in &profile_launch.env_unset {
|
|
979
|
+
env.remove(key);
|
|
980
|
+
}
|
|
981
|
+
env.extend(profile_launch.env_overlay.clone());
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
fn persist_started_agent_plan_state(
|
|
985
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
986
|
+
started_agent: &StartedAgent,
|
|
987
|
+
) {
|
|
988
|
+
if let Some(session_id) = started_agent.pending_session_id.as_ref() {
|
|
989
|
+
state.insert(
|
|
990
|
+
"_pending_session_id".to_string(),
|
|
991
|
+
serde_json::json!(session_id.as_str()),
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if let Some(root) = started_agent.provider_projects_root.as_ref() {
|
|
995
|
+
state.insert(
|
|
996
|
+
"claude_projects_root".to_string(),
|
|
997
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if started_agent.managed_mcp_config {
|
|
1001
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
1002
|
+
}
|
|
1003
|
+
if started_agent.managed_mcp_config
|
|
1004
|
+
|| started_agent.claude_config_dir.is_some()
|
|
1005
|
+
|| started_agent.provider_projects_root.is_some()
|
|
1006
|
+
{
|
|
1007
|
+
state.insert(
|
|
1008
|
+
"profile_launch".to_string(),
|
|
1009
|
+
serde_json::json!({
|
|
1010
|
+
"managed_mcp_config": started_agent.managed_mcp_config,
|
|
1011
|
+
"claude_config_dir": started_agent.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1012
|
+
"claude_projects_root": started_agent.provider_projects_root.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1013
|
+
}),
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
pub(crate) fn persist_command_plan_state(
|
|
1019
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
1020
|
+
plan: &crate::provider::CommandPlan,
|
|
1021
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1022
|
+
) {
|
|
1023
|
+
if let Some(session_id) = plan.expected_session_id.as_ref() {
|
|
1024
|
+
state.insert(
|
|
1025
|
+
"_pending_session_id".to_string(),
|
|
1026
|
+
serde_json::json!(session_id.as_str()),
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
let projects_root = plan
|
|
1030
|
+
.provider_projects_root
|
|
1031
|
+
.as_ref()
|
|
1032
|
+
.or(profile_launch.claude_projects_root.as_ref());
|
|
1033
|
+
if let Some(root) = projects_root {
|
|
1034
|
+
state.insert(
|
|
1035
|
+
"claude_projects_root".to_string(),
|
|
1036
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
let managed_mcp_config = plan.managed_mcp_config || profile_launch.managed_mcp_config;
|
|
1040
|
+
if managed_mcp_config {
|
|
1041
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
1042
|
+
}
|
|
1043
|
+
if managed_mcp_config || profile_launch.claude_config_dir.is_some() || projects_root.is_some() {
|
|
1044
|
+
state.insert(
|
|
1045
|
+
"profile_launch".to_string(),
|
|
1046
|
+
serde_json::json!({
|
|
1047
|
+
"managed_mcp_config": managed_mcp_config,
|
|
1048
|
+
"claude_config_dir": profile_launch.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1049
|
+
"claude_projects_root": projects_root.map(|path| path.to_string_lossy().to_string()),
|
|
1050
|
+
}),
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
699
1055
|
fn is_posix_shell_identifier(name: &str) -> bool {
|
|
700
1056
|
let mut chars = name.chars();
|
|
701
1057
|
match chars.next() {
|
|
@@ -769,18 +1125,11 @@ fn runtime_active_team_key_for_spawn(
|
|
|
769
1125
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name))
|
|
770
1126
|
}
|
|
771
1127
|
|
|
772
|
-
fn process_team_id_for_spawn(workspace: &Path, spec: &Value) -> Option<String> {
|
|
773
|
-
load_runtime_state(workspace)
|
|
774
|
-
.ok()
|
|
775
|
-
.and_then(|state| explicit_active_team_key(&state))
|
|
776
|
-
.or_else(|| spec_team_id(spec))
|
|
777
|
-
}
|
|
778
|
-
|
|
779
1128
|
fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
780
1129
|
state
|
|
781
1130
|
.get("active_team_key")
|
|
782
1131
|
.and_then(serde_json::Value::as_str)
|
|
783
|
-
.filter(|team| !team.is_empty()
|
|
1132
|
+
.filter(|team| !team.is_empty())
|
|
784
1133
|
.map(str::to_string)
|
|
785
1134
|
}
|
|
786
1135
|
|
|
@@ -824,6 +1173,187 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
824
1173
|
}
|
|
825
1174
|
}
|
|
826
1175
|
|
|
1176
|
+
fn quick_start_requested_team_key<'a>(team_id: Option<&'a str>, name: Option<&'a str>) -> Option<&'a str> {
|
|
1177
|
+
team_id.or(name).filter(|team| !team.is_empty())
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
struct QuickStartDepth {
|
|
1181
|
+
parent_team_key: Option<String>,
|
|
1182
|
+
team_depth: u64,
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
fn quick_start_depth_guard(
|
|
1186
|
+
workspace: &Path,
|
|
1187
|
+
_agents_dir: &Path,
|
|
1188
|
+
requested_team: Option<&str>,
|
|
1189
|
+
_strict_real_runtime: bool,
|
|
1190
|
+
) -> Result<QuickStartDepth, LifecycleError> {
|
|
1191
|
+
let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
1192
|
+
.ok()
|
|
1193
|
+
.map(|value| value.trim().to_string())
|
|
1194
|
+
.filter(|value| !value.is_empty());
|
|
1195
|
+
let parent = env_parent;
|
|
1196
|
+
let Some(parent) = parent else {
|
|
1197
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
1198
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
1199
|
+
let ambiguous_nested_intent = requested_team.is_some_and(|team| {
|
|
1200
|
+
looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
|
|
1201
|
+
});
|
|
1202
|
+
if has_live_runtime_teams(&state) && ambiguous_nested_intent {
|
|
1203
|
+
if requested_team.is_some_and(looks_grandchild_team_key) {
|
|
1204
|
+
if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
|
|
1205
|
+
let parent_state =
|
|
1206
|
+
crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
1207
|
+
let parent_depth = parent_state
|
|
1208
|
+
.get("team_depth")
|
|
1209
|
+
.and_then(serde_json::Value::as_u64)
|
|
1210
|
+
.unwrap_or(1);
|
|
1211
|
+
return Ok(QuickStartDepth {
|
|
1212
|
+
parent_team_key: Some(parent_key),
|
|
1213
|
+
team_depth: parent_depth.saturating_add(1),
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
1218
|
+
"cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
|
|
1219
|
+
.to_string(),
|
|
1220
|
+
));
|
|
1221
|
+
}
|
|
1222
|
+
return Ok(QuickStartDepth {
|
|
1223
|
+
parent_team_key: None,
|
|
1224
|
+
team_depth: 1,
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
1228
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
1229
|
+
let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
|
|
1230
|
+
.canonical_key()
|
|
1231
|
+
.map(str::to_string)
|
|
1232
|
+
.unwrap_or(parent);
|
|
1233
|
+
let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
1234
|
+
let parent_depth = parent_state
|
|
1235
|
+
.get("team_depth")
|
|
1236
|
+
.and_then(serde_json::Value::as_u64)
|
|
1237
|
+
.unwrap_or(1);
|
|
1238
|
+
let team_depth = parent_depth.saturating_add(1);
|
|
1239
|
+
Ok(QuickStartDepth {
|
|
1240
|
+
parent_team_key: Some(parent_key),
|
|
1241
|
+
team_depth,
|
|
1242
|
+
})
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
1246
|
+
let active = explicit_active_team_key(state)?;
|
|
1247
|
+
let team = state
|
|
1248
|
+
.get("teams")
|
|
1249
|
+
.and_then(serde_json::Value::as_object)
|
|
1250
|
+
.and_then(|teams| teams.get(&active))?;
|
|
1251
|
+
team_has_running_agent(team).then_some(active)
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
|
|
1255
|
+
state
|
|
1256
|
+
.get("teams")
|
|
1257
|
+
.and_then(serde_json::Value::as_object)
|
|
1258
|
+
.is_some_and(|teams| {
|
|
1259
|
+
teams.values().any(team_has_running_agent)
|
|
1260
|
+
})
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
1264
|
+
team.get("agents")
|
|
1265
|
+
.and_then(serde_json::Value::as_object)
|
|
1266
|
+
.is_some_and(|agents| {
|
|
1267
|
+
agents.values().any(|agent| {
|
|
1268
|
+
agent
|
|
1269
|
+
.get("status")
|
|
1270
|
+
.and_then(serde_json::Value::as_str)
|
|
1271
|
+
== Some("running")
|
|
1272
|
+
})
|
|
1273
|
+
})
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
fn looks_ambiguous_child_team_key(team: &str) -> bool {
|
|
1277
|
+
let team = team.trim().to_ascii_lowercase();
|
|
1278
|
+
team != "child" && (team.starts_with("child-")
|
|
1279
|
+
|| team.starts_with("child_")
|
|
1280
|
+
|| team.starts_with("child.")
|
|
1281
|
+
|| team.starts_with("child"))
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fn looks_grandchild_team_key(team: &str) -> bool {
|
|
1285
|
+
let team = team.trim().to_ascii_lowercase();
|
|
1286
|
+
team == "grandchild"
|
|
1287
|
+
|| team.starts_with("grandchild-")
|
|
1288
|
+
|| team.starts_with("grandchild_")
|
|
1289
|
+
|| team.starts_with("grandchild.")
|
|
1290
|
+
|| team.starts_with("grandchild")
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
fn annotate_team_depth(state: &mut serde_json::Value, parent_team_key: Option<&str>, team_depth: u64) {
|
|
1294
|
+
let Some(obj) = state.as_object_mut() else {
|
|
1295
|
+
return;
|
|
1296
|
+
};
|
|
1297
|
+
obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
|
|
1298
|
+
if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
|
|
1299
|
+
obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
fn annotate_persisted_team_depth(
|
|
1304
|
+
workspace: &Path,
|
|
1305
|
+
team_key: &str,
|
|
1306
|
+
parent_team_key: Option<&str>,
|
|
1307
|
+
team_depth: u64,
|
|
1308
|
+
) -> Result<(), LifecycleError> {
|
|
1309
|
+
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
1310
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1311
|
+
let Some(team) = state
|
|
1312
|
+
.get_mut("teams")
|
|
1313
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1314
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
1315
|
+
else {
|
|
1316
|
+
return Ok(());
|
|
1317
|
+
};
|
|
1318
|
+
annotate_team_depth(team, parent_team_key, team_depth);
|
|
1319
|
+
crate::state::persist::save_runtime_state(workspace, &state)
|
|
1320
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1321
|
+
Ok(())
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
1325
|
+
explicit_active_team_key(state).as_deref() == Some(team)
|
|
1326
|
+
|| state
|
|
1327
|
+
.get("teams")
|
|
1328
|
+
.and_then(serde_json::Value::as_object)
|
|
1329
|
+
.is_some_and(|teams| {
|
|
1330
|
+
teams.contains_key(team)
|
|
1331
|
+
|| teams
|
|
1332
|
+
.values()
|
|
1333
|
+
.any(|entry| json_team_identity_matches(entry, team))
|
|
1334
|
+
})
|
|
1335
|
+
|| crate::state::projection::team_state_key(state) == team
|
|
1336
|
+
|| json_team_identity_matches(state, team)
|
|
1337
|
+
|| state
|
|
1338
|
+
.get("session_name")
|
|
1339
|
+
.and_then(serde_json::Value::as_str)
|
|
1340
|
+
.is_some_and(|session| {
|
|
1341
|
+
session == team || session.strip_prefix("team-") == Some(team)
|
|
1342
|
+
})
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
1346
|
+
state
|
|
1347
|
+
.get("team")
|
|
1348
|
+
.and_then(|value| value.get("id").or_else(|| value.get("name")))
|
|
1349
|
+
.and_then(serde_json::Value::as_str)
|
|
1350
|
+
.is_some_and(|value| value == team)
|
|
1351
|
+
|| state
|
|
1352
|
+
.get("name")
|
|
1353
|
+
.and_then(serde_json::Value::as_str)
|
|
1354
|
+
.is_some_and(|value| value == team)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
827
1357
|
/// `quick_start(agents_dir, name, yes, fresh, team_id)`(`diagnose/quick_start.py:18`)。
|
|
828
1358
|
/// 面向用户的零配置入口:编译 team_dir → `launch` → autobind leader receiver → 起
|
|
829
1359
|
/// coordinator → `wait_ready` 轮询就绪。归入 lifecycle module(不与 diagnose 混)。
|
|
@@ -834,17 +1364,43 @@ pub fn quick_start(
|
|
|
834
1364
|
fresh: bool,
|
|
835
1365
|
team_id: Option<&str>,
|
|
836
1366
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
837
|
-
|
|
1367
|
+
let workspace = team_workspace(agents_dir);
|
|
1368
|
+
quick_start_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id)
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
pub fn quick_start_in_workspace(
|
|
1372
|
+
workspace: &Path,
|
|
1373
|
+
agents_dir: &Path,
|
|
1374
|
+
name: Option<&str>,
|
|
1375
|
+
yes: bool,
|
|
1376
|
+
fresh: bool,
|
|
1377
|
+
team_id: Option<&str>,
|
|
1378
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
1379
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
1380
|
+
quick_start_with_transport_in_workspace(
|
|
1381
|
+
&workspace,
|
|
838
1382
|
agents_dir,
|
|
839
1383
|
name,
|
|
840
1384
|
yes,
|
|
841
1385
|
fresh,
|
|
842
1386
|
team_id,
|
|
843
|
-
// CP-1: per-team socket bound to the run workspace
|
|
844
|
-
&crate::tmux_backend::TmuxBackend::for_workspace(&
|
|
1387
|
+
// CP-1: per-team socket bound to the selected run workspace.
|
|
1388
|
+
&crate::tmux_backend::TmuxBackend::for_workspace(&workspace),
|
|
845
1389
|
)
|
|
846
1390
|
}
|
|
847
1391
|
|
|
1392
|
+
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
1393
|
+
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
1394
|
+
if workspace.is_absolute() {
|
|
1395
|
+
workspace.to_path_buf()
|
|
1396
|
+
} else {
|
|
1397
|
+
std::env::current_dir()
|
|
1398
|
+
.unwrap_or_else(|_| PathBuf::from("."))
|
|
1399
|
+
.join(workspace)
|
|
1400
|
+
}
|
|
1401
|
+
})
|
|
1402
|
+
}
|
|
1403
|
+
|
|
848
1404
|
/// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
|
|
849
1405
|
/// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
|
|
850
1406
|
pub fn quick_start_with_transport(
|
|
@@ -854,6 +1410,19 @@ pub fn quick_start_with_transport(
|
|
|
854
1410
|
fresh: bool,
|
|
855
1411
|
team_id: Option<&str>,
|
|
856
1412
|
transport: &dyn Transport,
|
|
1413
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
1414
|
+
let workspace = team_workspace(agents_dir);
|
|
1415
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, fresh, team_id, transport)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
pub fn quick_start_with_transport_in_workspace(
|
|
1419
|
+
workspace: &Path,
|
|
1420
|
+
agents_dir: &Path,
|
|
1421
|
+
name: Option<&str>,
|
|
1422
|
+
yes: bool,
|
|
1423
|
+
fresh: bool,
|
|
1424
|
+
team_id: Option<&str>,
|
|
1425
|
+
transport: &dyn Transport,
|
|
857
1426
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
858
1427
|
if !agents_dir.exists() {
|
|
859
1428
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -861,50 +1430,91 @@ pub fn quick_start_with_transport(
|
|
|
861
1430
|
agents_dir.display()
|
|
862
1431
|
)));
|
|
863
1432
|
}
|
|
864
|
-
let workspace =
|
|
1433
|
+
let workspace = workspace.to_path_buf();
|
|
1434
|
+
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
1435
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1436
|
+
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
1437
|
+
.map(str::to_string)
|
|
1438
|
+
.or_else(|| spec_team_id(&spec));
|
|
1439
|
+
let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
|
|
1440
|
+
let team_depth = quick_start_depth_guard(
|
|
1441
|
+
&workspace,
|
|
1442
|
+
agents_dir,
|
|
1443
|
+
requested_team.as_deref(),
|
|
1444
|
+
matches!(transport.kind(), crate::transport::BackendKind::Tmux),
|
|
1445
|
+
)?;
|
|
1446
|
+
if team_depth.team_depth > 2 {
|
|
1447
|
+
let parent = team_depth.parent_team_key.as_deref().unwrap_or("");
|
|
1448
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
1449
|
+
"team nesting depth limit exceeded: parent_team_key={parent} parent_depth={} max_depth=2",
|
|
1450
|
+
team_depth.team_depth.saturating_sub(1)
|
|
1451
|
+
)));
|
|
1452
|
+
}
|
|
865
1453
|
if !fresh {
|
|
866
1454
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
867
1455
|
if state_path.exists() {
|
|
868
1456
|
let state = crate::state::persist::load_runtime_state(&workspace)
|
|
869
1457
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1458
|
+
if requested_team
|
|
1459
|
+
.as_deref()
|
|
1460
|
+
.is_none_or(|team| runtime_state_has_quick_start_team(&state, team))
|
|
1461
|
+
{
|
|
1462
|
+
return Ok(QuickStartReport::ExistingRuntime {
|
|
1463
|
+
team: requested_team.clone(),
|
|
1464
|
+
session_name: state
|
|
1465
|
+
.get("session_name")
|
|
1466
|
+
.and_then(serde_json::Value::as_str)
|
|
1467
|
+
.filter(|s| !s.is_empty())
|
|
1468
|
+
.map(SessionName::new),
|
|
1469
|
+
state_path: Some(state_path),
|
|
1470
|
+
next_actions: vec![
|
|
1471
|
+
"run restart to resume the existing team or pass --fresh to replace it".to_string(),
|
|
1472
|
+
],
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
883
1476
|
}
|
|
884
|
-
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
885
|
-
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
886
1477
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
887
1478
|
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
888
1479
|
// spec's runtime.session_name with one derived from the REQUESTED team identity
|
|
889
1480
|
// so launch_with_transport (which reads runtime.session_name) spawns into an
|
|
890
1481
|
// isolated session per requested team.
|
|
891
|
-
if let Some(requested) =
|
|
1482
|
+
if let Some(requested) = requested_team.as_deref() {
|
|
892
1483
|
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
893
1484
|
}
|
|
1485
|
+
let session_name = spec_session_name(&spec);
|
|
1486
|
+
let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
|
|
1487
|
+
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1488
|
+
runtime_team_key_for_spec(&spec_path, &spec, &session_name)
|
|
1489
|
+
});
|
|
894
1490
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
895
1491
|
std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
|
|
896
1492
|
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
897
1493
|
})?;
|
|
898
1494
|
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
899
1495
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
900
|
-
let session_name = spec_session_name(&spec);
|
|
901
1496
|
let resolved_spec_path = std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
902
1497
|
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
903
|
-
|
|
1498
|
+
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
1499
|
+
annotate_persisted_team_depth(
|
|
1500
|
+
&workspace,
|
|
1501
|
+
&state_team_key,
|
|
1502
|
+
team_depth.parent_team_key.as_deref(),
|
|
1503
|
+
team_depth.team_depth,
|
|
1504
|
+
)?;
|
|
904
1505
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
905
1506
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
906
1507
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
907
|
-
let launch =
|
|
1508
|
+
let mut launch = launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
1509
|
+
annotate_persisted_team_depth(
|
|
1510
|
+
&workspace,
|
|
1511
|
+
&state_team_key,
|
|
1512
|
+
team_depth.parent_team_key.as_deref(),
|
|
1513
|
+
team_depth.team_depth,
|
|
1514
|
+
)?;
|
|
1515
|
+
launch.leader_receiver_attached = launched_team_receiver_is_attached(&workspace, &state_team_key);
|
|
1516
|
+
launch.session_capture_incomplete_agents =
|
|
1517
|
+
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
908
1518
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
909
1519
|
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
910
1520
|
.map(|report| report.ok)
|
|
@@ -921,7 +1531,7 @@ pub fn quick_start_with_transport(
|
|
|
921
1531
|
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
922
1532
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
923
1533
|
// bare Ready.
|
|
924
|
-
let worker_readiness = quick_start_worker_readiness(&workspace);
|
|
1534
|
+
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
925
1535
|
Ok(QuickStartReport::Ready {
|
|
926
1536
|
session_name,
|
|
927
1537
|
launch: Box::new(launch),
|
|
@@ -937,13 +1547,21 @@ pub fn quick_start_with_transport(
|
|
|
937
1547
|
/// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
|
|
938
1548
|
/// `PendingToolLoad` — never bare Ready. State read failure is treated as
|
|
939
1549
|
/// PendingToolLoad rather than fabricated success.
|
|
940
|
-
fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
1550
|
+
fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartReadiness {
|
|
941
1551
|
let Ok(state) = load_runtime_state(workspace) else {
|
|
942
1552
|
return QuickStartReadiness::PendingToolLoad;
|
|
943
1553
|
};
|
|
944
|
-
let
|
|
1554
|
+
let team_state = state
|
|
1555
|
+
.get("teams")
|
|
1556
|
+
.and_then(serde_json::Value::as_object)
|
|
1557
|
+
.and_then(|teams| teams.get(team_key))
|
|
1558
|
+
.unwrap_or(&state);
|
|
1559
|
+
let Some(agents) = team_state.get("agents").and_then(serde_json::Value::as_object) else {
|
|
945
1560
|
return QuickStartReadiness::PendingToolLoad;
|
|
946
1561
|
};
|
|
1562
|
+
let all_spawned = !agents.is_empty();
|
|
1563
|
+
let leader_receiver_attached = launched_team_receiver_is_attached(workspace, team_key);
|
|
1564
|
+
let all_attached_receiver = leader_receiver_attached;
|
|
947
1565
|
let mut unhealthy: Vec<String> = agents
|
|
948
1566
|
.iter()
|
|
949
1567
|
.filter_map(|(id, agent)| {
|
|
@@ -954,15 +1572,79 @@ fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
|
954
1572
|
}
|
|
955
1573
|
})
|
|
956
1574
|
.collect();
|
|
957
|
-
if unhealthy.is_empty() {
|
|
958
|
-
QuickStartReadiness::PendingToolLoad
|
|
959
|
-
} else {
|
|
1575
|
+
if !unhealthy.is_empty() {
|
|
960
1576
|
unhealthy.sort();
|
|
961
1577
|
unhealthy.dedup();
|
|
962
1578
|
QuickStartReadiness::Degraded { unhealthy_agents: unhealthy }
|
|
1579
|
+
} else {
|
|
1580
|
+
let incomplete_agents = crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
|
|
1581
|
+
let all_resumable_have_session = incomplete_agents.is_empty();
|
|
1582
|
+
let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
|
|
1583
|
+
QuickStartReadiness::PendingToolLoad
|
|
963
1584
|
}
|
|
964
1585
|
}
|
|
965
1586
|
|
|
1587
|
+
fn quick_start_session_capture_incomplete_agents(workspace: &Path, team_key: &str) -> Vec<String> {
|
|
1588
|
+
let Ok(state) = load_runtime_state(workspace) else {
|
|
1589
|
+
return Vec::new();
|
|
1590
|
+
};
|
|
1591
|
+
let team_state = state
|
|
1592
|
+
.get("teams")
|
|
1593
|
+
.and_then(serde_json::Value::as_object)
|
|
1594
|
+
.and_then(|teams| teams.get(team_key))
|
|
1595
|
+
.unwrap_or(&state);
|
|
1596
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
|
|
1600
|
+
let Ok(state) = load_runtime_state(workspace) else {
|
|
1601
|
+
return true;
|
|
1602
|
+
};
|
|
1603
|
+
let team_state = state
|
|
1604
|
+
.get("teams")
|
|
1605
|
+
.and_then(serde_json::Value::as_object)
|
|
1606
|
+
.and_then(|teams| teams.get(team_key))
|
|
1607
|
+
.unwrap_or(&state);
|
|
1608
|
+
if team_state.get("leader_receiver").is_none() {
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
if team_uses_fake_model_harness(team_state) {
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
leader_receiver_is_attached(team_state)
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
|
|
1618
|
+
team_state
|
|
1619
|
+
.get("agents")
|
|
1620
|
+
.and_then(serde_json::Value::as_object)
|
|
1621
|
+
.is_some_and(|agents| {
|
|
1622
|
+
!agents.is_empty()
|
|
1623
|
+
&& agents.values().all(|agent| {
|
|
1624
|
+
agent
|
|
1625
|
+
.get("model")
|
|
1626
|
+
.and_then(serde_json::Value::as_str)
|
|
1627
|
+
== Some("fake")
|
|
1628
|
+
})
|
|
1629
|
+
})
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
|
|
1633
|
+
let Some(receiver) = team_state.get("leader_receiver") else {
|
|
1634
|
+
return false;
|
|
1635
|
+
};
|
|
1636
|
+
let status = receiver
|
|
1637
|
+
.get("status")
|
|
1638
|
+
.and_then(serde_json::Value::as_str)
|
|
1639
|
+
.unwrap_or("");
|
|
1640
|
+
let pane_id = receiver
|
|
1641
|
+
.get("pane_id")
|
|
1642
|
+
.and_then(serde_json::Value::as_str)
|
|
1643
|
+
.or_else(|| receiver.get("pane").and_then(serde_json::Value::as_str))
|
|
1644
|
+
.unwrap_or("");
|
|
1645
|
+
status == "attached" && !pane_id.is_empty() && pane_id != "__team_agent_unbound__"
|
|
1646
|
+
}
|
|
1647
|
+
|
|
966
1648
|
/// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
|
|
967
1649
|
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
968
1650
|
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
@@ -1178,12 +1860,13 @@ pub fn add_agent(
|
|
|
1178
1860
|
let team_dir = selected
|
|
1179
1861
|
.spec_workspace
|
|
1180
1862
|
.ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
|
|
1181
|
-
|
|
1863
|
+
add_agent_with_transport_at_paths(
|
|
1864
|
+
&selected.run_workspace,
|
|
1182
1865
|
&team_dir,
|
|
1183
1866
|
agent_id,
|
|
1184
1867
|
role_file_path,
|
|
1185
1868
|
open_display,
|
|
1186
|
-
|
|
1869
|
+
Some(selected.team_key.as_str()),
|
|
1187
1870
|
&crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
|
|
1188
1871
|
)
|
|
1189
1872
|
}
|
|
@@ -1200,12 +1883,35 @@ pub fn add_agent_with_transport(
|
|
|
1200
1883
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
1201
1884
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
1202
1885
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1886
|
+
add_agent_with_transport_at_paths(
|
|
1887
|
+
&run_workspace,
|
|
1888
|
+
workspace,
|
|
1889
|
+
agent_id,
|
|
1890
|
+
role_file_path,
|
|
1891
|
+
open_display,
|
|
1892
|
+
team,
|
|
1893
|
+
transport,
|
|
1894
|
+
)
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
fn add_agent_with_transport_at_paths(
|
|
1898
|
+
run_workspace: &Path,
|
|
1899
|
+
team_dir: &Path,
|
|
1900
|
+
agent_id: &AgentId,
|
|
1901
|
+
role_file_path: &Path,
|
|
1902
|
+
open_display: bool,
|
|
1903
|
+
team: Option<&str>,
|
|
1904
|
+
transport: &dyn Transport,
|
|
1905
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
1906
|
+
let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
|
|
1907
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1908
|
+
let canonical_team_key = team
|
|
1909
|
+
.filter(|key| !key.is_empty())
|
|
1910
|
+
.map(str::to_string)
|
|
1911
|
+
.or_else(|| explicit_active_team_key(&runtime_state))
|
|
1912
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
|
|
1913
|
+
let owner_state = crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
|
|
1914
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1209
1915
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1210
1916
|
if !role_file_path.exists() {
|
|
1211
1917
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -1213,7 +1919,6 @@ pub fn add_agent_with_transport(
|
|
|
1213
1919
|
role_file_path.display()
|
|
1214
1920
|
)));
|
|
1215
1921
|
}
|
|
1216
|
-
let team_dir = workspace;
|
|
1217
1922
|
if agent_id_exists_in_team_dir(team_dir, agent_id) {
|
|
1218
1923
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
1219
1924
|
"agent id already exists: {agent_id}"
|
|
@@ -1222,22 +1927,29 @@ pub fn add_agent_with_transport(
|
|
|
1222
1927
|
let dynamic_role_file = materialize_added_role_file(team_dir, agent_id, role_file_path)?;
|
|
1223
1928
|
let spec = crate::compiler::compile_team(team_dir)
|
|
1224
1929
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1930
|
+
let safety = effective_runtime_config(&spec)?;
|
|
1225
1931
|
let spec_path = team_dir.join("team.spec.yaml");
|
|
1226
1932
|
std::fs::write(&spec_path, yaml::dumps(&spec)).map_err(|e| {
|
|
1227
1933
|
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1228
1934
|
})?;
|
|
1229
1935
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1230
1936
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1231
|
-
|
|
1232
|
-
|
|
1937
|
+
upsert_agent_state_from_role(
|
|
1938
|
+
run_workspace,
|
|
1939
|
+
&canonical_team_key,
|
|
1940
|
+
agent_id,
|
|
1941
|
+
&meta,
|
|
1942
|
+
&dynamic_role_file,
|
|
1943
|
+
&safety,
|
|
1944
|
+
)?;
|
|
1233
1945
|
let started = crate::lifecycle::restart::start_agent_at_paths(
|
|
1234
|
-
|
|
1946
|
+
run_workspace,
|
|
1235
1947
|
team_dir,
|
|
1236
1948
|
agent_id,
|
|
1237
1949
|
false,
|
|
1238
1950
|
open_display,
|
|
1239
1951
|
true,
|
|
1240
|
-
|
|
1952
|
+
Some(&canonical_team_key),
|
|
1241
1953
|
transport,
|
|
1242
1954
|
)?;
|
|
1243
1955
|
let (env, start_mode) = match started {
|
|
@@ -1260,12 +1972,14 @@ pub fn add_agent_with_transport(
|
|
|
1260
1972
|
|
|
1261
1973
|
fn upsert_agent_state_from_role(
|
|
1262
1974
|
workspace: &Path,
|
|
1975
|
+
canonical_team_key: &str,
|
|
1263
1976
|
agent_id: &AgentId,
|
|
1264
1977
|
meta: &Value,
|
|
1265
1978
|
dynamic_role_file: &Path,
|
|
1979
|
+
safety: &DangerousApproval,
|
|
1266
1980
|
) -> Result<(), LifecycleError> {
|
|
1267
|
-
let mut state = crate::state::
|
|
1268
|
-
.map_err(|e| LifecycleError::
|
|
1981
|
+
let mut state = crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
1982
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1269
1983
|
if !state.is_object() {
|
|
1270
1984
|
state = serde_json::json!({});
|
|
1271
1985
|
}
|
|
@@ -1307,10 +2021,31 @@ fn upsert_agent_state_from_role(
|
|
|
1307
2021
|
if let Some(model) = meta.get("model").and_then(Value::as_str) {
|
|
1308
2022
|
if let Some(obj) = entry.as_object_mut() {
|
|
1309
2023
|
obj.insert("model".to_string(), serde_json::json!(model));
|
|
2024
|
+
obj.insert("model_source".to_string(), serde_json::json!("role"));
|
|
1310
2025
|
}
|
|
1311
2026
|
}
|
|
2027
|
+
if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
|
|
2028
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
2029
|
+
obj.insert("profile".to_string(), serde_json::json!(profile));
|
|
2030
|
+
if let Some(team_dir) = dynamic_role_file
|
|
2031
|
+
.parent()
|
|
2032
|
+
.and_then(Path::parent)
|
|
2033
|
+
{
|
|
2034
|
+
obj.insert(
|
|
2035
|
+
"_profile_dir".to_string(),
|
|
2036
|
+
serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
if !obj.contains_key("model_source") {
|
|
2040
|
+
obj.insert("model_source".to_string(), serde_json::json!("default"));
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
2045
|
+
persist_effective_approval_policy(obj, safety);
|
|
2046
|
+
}
|
|
1312
2047
|
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
1313
|
-
|
|
2048
|
+
save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
|
|
1314
2049
|
}
|
|
1315
2050
|
|
|
1316
2051
|
fn materialize_added_role_file(
|
|
@@ -1455,61 +2190,158 @@ pub fn fork_agent_with_transport(
|
|
|
1455
2190
|
let safety = effective_runtime_config(&new_spec)?;
|
|
1456
2191
|
let tools = worker_tool_refs(agent_tool_strings(new_agent), &safety);
|
|
1457
2192
|
let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
2193
|
+
let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
|
|
1458
2194
|
let mcp_config = adapter
|
|
1459
2195
|
.mcp_config(auth_mode)
|
|
1460
2196
|
.map_err(|e| {
|
|
1461
2197
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1462
2198
|
LifecycleError::Provider(e.to_string())
|
|
1463
2199
|
})?;
|
|
1464
|
-
let
|
|
1465
|
-
|
|
2200
|
+
let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
|
|
2201
|
+
let mcp_config_path = write_worker_mcp_config(&workspace, as_agent_id.as_str(), &mcp_config)
|
|
2202
|
+
.map_err(|e| {
|
|
2203
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2204
|
+
e
|
|
2205
|
+
})?;
|
|
2206
|
+
let profile_dir = spec_workspace.join("profiles");
|
|
2207
|
+
let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
2208
|
+
&workspace,
|
|
2209
|
+
as_agent_id.as_str(),
|
|
2210
|
+
new_agent,
|
|
2211
|
+
Some(&profile_dir),
|
|
2212
|
+
Some(&mcp_config),
|
|
2213
|
+
)?;
|
|
2214
|
+
let command_model = profile_launch
|
|
2215
|
+
.command_overrides
|
|
2216
|
+
.model
|
|
2217
|
+
.as_deref()
|
|
2218
|
+
.or(model);
|
|
2219
|
+
let mut plan = adapter
|
|
2220
|
+
.fork_plan(
|
|
1466
2221
|
Some(&session_id),
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
2222
|
+
crate::provider::ProviderCommandContext {
|
|
2223
|
+
auth_mode,
|
|
2224
|
+
mcp_config: Some(&mcp_config),
|
|
2225
|
+
system_prompt: role,
|
|
2226
|
+
model: command_model,
|
|
2227
|
+
tools: &tool_refs,
|
|
2228
|
+
profile_launch: Some(&profile_launch),
|
|
2229
|
+
},
|
|
1472
2230
|
)
|
|
1473
2231
|
.map_err(|e| {
|
|
1474
2232
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1475
2233
|
LifecycleError::Provider(e.to_string())
|
|
1476
2234
|
})?;
|
|
1477
|
-
|
|
1478
|
-
|
|
2235
|
+
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
2236
|
+
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
2237
|
+
}
|
|
2238
|
+
fill_spawn_placeholders_full(&mut plan.argv, &workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
1479
2239
|
let window = WindowName::new(as_agent_id.as_str());
|
|
1480
2240
|
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
1481
|
-
let env = inherited_env_with_team_overrides(
|
|
2241
|
+
let mut env = inherited_env_with_team_overrides(
|
|
1482
2242
|
&workspace,
|
|
1483
2243
|
as_agent_id.as_str(),
|
|
1484
2244
|
Some(&fork_team),
|
|
1485
2245
|
);
|
|
2246
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
1486
2247
|
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
1487
2248
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
1488
2249
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
1489
2250
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
1490
2251
|
let spawn_result = if session_live {
|
|
1491
|
-
transport.spawn_into(&session_name, &window, &argv, &workspace, &env)
|
|
2252
|
+
transport.spawn_into(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1492
2253
|
} else {
|
|
1493
|
-
transport.spawn_first(&session_name, &window, &argv, &workspace, &env)
|
|
2254
|
+
transport.spawn_first(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1494
2255
|
};
|
|
1495
|
-
let
|
|
2256
|
+
let spawn = spawn_result.map_err(|e| {
|
|
1496
2257
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1497
2258
|
LifecycleError::Transport(e.to_string())
|
|
1498
2259
|
})?;
|
|
1499
2260
|
let old_state = state.clone();
|
|
1500
2261
|
let mut next_state = state;
|
|
1501
|
-
upsert_forked_agent_state(
|
|
2262
|
+
upsert_forked_agent_state(
|
|
2263
|
+
&mut next_state,
|
|
2264
|
+
source_agent_id,
|
|
2265
|
+
as_agent_id,
|
|
2266
|
+
new_agent,
|
|
2267
|
+
&safety,
|
|
2268
|
+
&plan,
|
|
2269
|
+
&profile_launch,
|
|
2270
|
+
&spawn,
|
|
2271
|
+
&workspace,
|
|
2272
|
+
Some(&profile_dir),
|
|
2273
|
+
)?;
|
|
2274
|
+
if let Some(agent) = next_state
|
|
2275
|
+
.get_mut("agents")
|
|
2276
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2277
|
+
.and_then(|agents| agents.get_mut(as_agent_id.as_str()))
|
|
2278
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2279
|
+
{
|
|
2280
|
+
persist_effective_approval_policy(agent, &safety);
|
|
2281
|
+
}
|
|
2282
|
+
if let Err(e) = maybe_fail_fork_after_spawn("save_runtime_state") {
|
|
2283
|
+
rollback_fork_after_spawn(
|
|
2284
|
+
&workspace,
|
|
2285
|
+
&spec_path,
|
|
2286
|
+
&text,
|
|
2287
|
+
&old_state,
|
|
2288
|
+
transport,
|
|
2289
|
+
&session_name,
|
|
2290
|
+
&window,
|
|
2291
|
+
&mcp_config_path,
|
|
2292
|
+
as_agent_id,
|
|
2293
|
+
&profile_launch,
|
|
2294
|
+
);
|
|
2295
|
+
return Err(e);
|
|
2296
|
+
}
|
|
1502
2297
|
if let Err(e) = save_runtime_state(&workspace, &next_state) {
|
|
1503
|
-
rollback_fork_after_spawn(
|
|
2298
|
+
rollback_fork_after_spawn(
|
|
2299
|
+
&workspace,
|
|
2300
|
+
&spec_path,
|
|
2301
|
+
&text,
|
|
2302
|
+
&old_state,
|
|
2303
|
+
transport,
|
|
2304
|
+
&session_name,
|
|
2305
|
+
&window,
|
|
2306
|
+
&mcp_config_path,
|
|
2307
|
+
as_agent_id,
|
|
2308
|
+
&profile_launch,
|
|
2309
|
+
);
|
|
1504
2310
|
return Err(LifecycleError::StatePersist(e.to_string()));
|
|
1505
2311
|
}
|
|
2312
|
+
if let Err(e) = maybe_fail_fork_after_spawn("start_coordinator") {
|
|
2313
|
+
rollback_fork_after_spawn(
|
|
2314
|
+
&workspace,
|
|
2315
|
+
&spec_path,
|
|
2316
|
+
&text,
|
|
2317
|
+
&old_state,
|
|
2318
|
+
transport,
|
|
2319
|
+
&session_name,
|
|
2320
|
+
&window,
|
|
2321
|
+
&mcp_config_path,
|
|
2322
|
+
as_agent_id,
|
|
2323
|
+
&profile_launch,
|
|
2324
|
+
);
|
|
2325
|
+
return Err(e);
|
|
2326
|
+
}
|
|
1506
2327
|
let coordinator_started =
|
|
1507
2328
|
crate::coordinator::start_coordinator(&crate::coordinator::WorkspacePath::new(
|
|
1508
2329
|
workspace.to_path_buf(),
|
|
1509
2330
|
))
|
|
1510
2331
|
.map(|report| report.ok)
|
|
1511
2332
|
.map_err(|e| {
|
|
1512
|
-
rollback_fork_after_spawn(
|
|
2333
|
+
rollback_fork_after_spawn(
|
|
2334
|
+
&workspace,
|
|
2335
|
+
&spec_path,
|
|
2336
|
+
&text,
|
|
2337
|
+
&old_state,
|
|
2338
|
+
transport,
|
|
2339
|
+
&session_name,
|
|
2340
|
+
&window,
|
|
2341
|
+
&mcp_config_path,
|
|
2342
|
+
as_agent_id,
|
|
2343
|
+
&profile_launch,
|
|
2344
|
+
);
|
|
1513
2345
|
LifecycleError::StatePersist(e.to_string())
|
|
1514
2346
|
})?;
|
|
1515
2347
|
Ok(ForkAgentReport {
|
|
@@ -1532,6 +2364,9 @@ fn rollback_fork_after_spawn(
|
|
|
1532
2364
|
transport: &dyn Transport,
|
|
1533
2365
|
session_name: &SessionName,
|
|
1534
2366
|
window: &WindowName,
|
|
2367
|
+
mcp_config_path: &Path,
|
|
2368
|
+
agent_id: &AgentId,
|
|
2369
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1535
2370
|
) {
|
|
1536
2371
|
let _ = transport.kill_window(&Target::SessionWindow {
|
|
1537
2372
|
session: session_name.clone(),
|
|
@@ -1539,6 +2374,41 @@ fn rollback_fork_after_spawn(
|
|
|
1539
2374
|
});
|
|
1540
2375
|
let _ = std::fs::write(spec_path, spec_text.as_bytes());
|
|
1541
2376
|
let _ = save_runtime_state(workspace, old_state);
|
|
2377
|
+
cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
2381
|
+
let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
|
|
2382
|
+
return Ok(());
|
|
2383
|
+
};
|
|
2384
|
+
if reason.is_empty() {
|
|
2385
|
+
return Ok(());
|
|
2386
|
+
}
|
|
2387
|
+
let should_fail = reason == step
|
|
2388
|
+
|| (step == "start_coordinator" && reason == "coordinator");
|
|
2389
|
+
if !should_fail {
|
|
2390
|
+
return Ok(());
|
|
2391
|
+
}
|
|
2392
|
+
Err(LifecycleError::StatePersist(format!(
|
|
2393
|
+
"injected fork failure after spawn: {reason}"
|
|
2394
|
+
)))
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
fn cleanup_fork_mcp_artifacts(
|
|
2398
|
+
workspace: &Path,
|
|
2399
|
+
agent_id: &AgentId,
|
|
2400
|
+
mcp_config_path: &Path,
|
|
2401
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
2402
|
+
) {
|
|
2403
|
+
let _ = std::fs::remove_file(mcp_config_path);
|
|
2404
|
+
let _ = std::fs::remove_file(
|
|
2405
|
+
workspace
|
|
2406
|
+
.join(".team/runtime/provider-env")
|
|
2407
|
+
.join(format!("{}.env", agent_id.as_str())),
|
|
2408
|
+
);
|
|
2409
|
+
if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
|
|
2410
|
+
let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
|
|
2411
|
+
}
|
|
1542
2412
|
}
|
|
1543
2413
|
|
|
1544
2414
|
fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
|
|
@@ -1671,6 +2541,12 @@ fn upsert_forked_agent_state(
|
|
|
1671
2541
|
source_agent_id: &AgentId,
|
|
1672
2542
|
as_agent_id: &AgentId,
|
|
1673
2543
|
spec_agent: &Value,
|
|
2544
|
+
safety: &DangerousApproval,
|
|
2545
|
+
plan: &crate::provider::CommandPlan,
|
|
2546
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
2547
|
+
spawn: &crate::transport::SpawnResult,
|
|
2548
|
+
spawn_cwd: &Path,
|
|
2549
|
+
profile_dir: Option<&Path>,
|
|
1674
2550
|
) -> Result<(), LifecycleError> {
|
|
1675
2551
|
if !state.is_object() {
|
|
1676
2552
|
*state = serde_json::json!({});
|
|
@@ -1695,15 +2571,46 @@ fn upsert_forked_agent_state(
|
|
|
1695
2571
|
.get("provider")
|
|
1696
2572
|
.and_then(Value::as_str)
|
|
1697
2573
|
.unwrap_or("codex");
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2574
|
+
let mut entry = serde_json::Map::new();
|
|
2575
|
+
entry.insert("status".to_string(), serde_json::json!("running"));
|
|
2576
|
+
entry.insert("provider".to_string(), serde_json::json!(provider));
|
|
2577
|
+
entry.insert("agent_id".to_string(), serde_json::json!(as_agent_id.as_str()));
|
|
2578
|
+
entry.insert("window".to_string(), serde_json::json!(as_agent_id.as_str()));
|
|
2579
|
+
entry.insert("forked_from".to_string(), serde_json::json!(source_agent_id.as_str()));
|
|
2580
|
+
entry.insert(
|
|
2581
|
+
"spawn_cwd".to_string(),
|
|
2582
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
1706
2583
|
);
|
|
2584
|
+
entry.insert("pane_id".to_string(), serde_json::json!(spawn.pane_id.as_str()));
|
|
2585
|
+
if let Some(pid) = spawn.child_pid {
|
|
2586
|
+
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
2587
|
+
}
|
|
2588
|
+
for key in ["auth_mode", "model", "model_source", "profile", "_profile_dir", "role"] {
|
|
2589
|
+
if let Some(value) = spec_agent.get(key) {
|
|
2590
|
+
entry.insert(key.to_string(), yaml_value_to_json(value));
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
|
|
2594
|
+
if let Some(profile_dir) = profile_dir {
|
|
2595
|
+
entry.insert(
|
|
2596
|
+
"_profile_dir".to_string(),
|
|
2597
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
entry.insert("session_id".to_string(), serde_json::Value::Null);
|
|
2602
|
+
entry.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
2603
|
+
entry.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
2604
|
+
entry.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
2605
|
+
entry.insert("attribution_confidence".to_string(), serde_json::Value::Null);
|
|
2606
|
+
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
2607
|
+
agent_map.insert(as_agent_id.as_str().to_string(), serde_json::Value::Object(entry));
|
|
2608
|
+
if let Some(entry) = agent_map
|
|
2609
|
+
.get_mut(as_agent_id.as_str())
|
|
2610
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2611
|
+
{
|
|
2612
|
+
persist_effective_approval_policy(entry, safety);
|
|
2613
|
+
}
|
|
1707
2614
|
Ok(())
|
|
1708
2615
|
}
|
|
1709
2616
|
|
|
@@ -1863,6 +2770,7 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
1863
2770
|
"status": "attached",
|
|
1864
2771
|
"provider": provider,
|
|
1865
2772
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
2773
|
+
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
1866
2774
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
1867
2775
|
"owner_epoch": owner_epoch,
|
|
1868
2776
|
"discovery": "quick_start",
|
|
@@ -1882,6 +2790,13 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
1882
2790
|
true
|
|
1883
2791
|
}
|
|
1884
2792
|
|
|
2793
|
+
fn has_positive_caller_leader_env() -> bool {
|
|
2794
|
+
env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
|
|
2795
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
|
|
2796
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
|
|
2797
|
+
|| env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
|
|
2798
|
+
}
|
|
2799
|
+
|
|
1885
2800
|
fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
1886
2801
|
spec.get("tasks")
|
|
1887
2802
|
.and_then(Value::as_list)
|