@team-agent/installer 0.5.52 → 0.5.54
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +12 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +324 -57
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +89 -4
- package/crates/team-agent/src/mcp_server/types.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +65 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +100 -21
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +85 -24
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +395 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +53 -92
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- package/skills/team-agent/SKILL.md +11 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! leader::start — leader_start_plan / start_leader / leader_session_name(派生 tmux session 名)。
|
|
2
2
|
|
|
3
3
|
use std::collections::BTreeMap;
|
|
4
|
-
use std::io::IsTerminal;
|
|
4
|
+
use std::io::{IsTerminal, Read, Write};
|
|
5
5
|
use std::path::{Path, PathBuf};
|
|
6
6
|
use std::process::{Command, Stdio};
|
|
7
7
|
|
|
@@ -243,14 +243,10 @@ pub fn execute_leader_plan(
|
|
|
243
243
|
} else if plan.is_external_leader {
|
|
244
244
|
persist_external_leader_topology_marker(plan, workspace)?;
|
|
245
245
|
}
|
|
246
|
-
let
|
|
247
|
-
let code = status.code();
|
|
248
|
-
if !status.success() {
|
|
249
|
-
return Err(LeaderError::Start(
|
|
250
|
-
"leader launcher exited with status {}",
|
|
251
|
-
code.map(|c| c.to_string())
|
|
252
|
-
.unwrap_or_else(|| "signal".to_string())
|
|
253
|
-
)));
|
|
246
|
+
let process = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
|
|
247
|
+
let code = process.status.code();
|
|
248
|
+
if !process.status.success() {
|
|
249
|
+
return Err(LeaderError::Start(leader_launcher_failure(&process)));
|
|
254
250
|
}
|
|
255
251
|
if detached {
|
|
256
252
|
Ok(LeaderLaunchOutcome {
|
|
@@ -471,14 +467,10 @@ fn execute_managed_leader_plan(
|
|
|
471
467
|
workspace.to_path_buf(),
|
|
472
468
|
spawned.pane_id.as_str().to_string(),
|
|
473
469
|
);
|
|
474
|
-
let
|
|
475
|
-
let code = status.code();
|
|
476
|
-
if !status.success() {
|
|
477
|
-
return Err(LeaderError::Start(
|
|
478
|
-
"leader launcher exited with status {}",
|
|
479
|
-
code.map(|c| c.to_string())
|
|
480
|
-
.unwrap_or_else(|| "signal".to_string())
|
|
481
|
-
)));
|
|
470
|
+
let process = run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace)?;
|
|
471
|
+
let code = process.status.code();
|
|
472
|
+
if !process.status.success() {
|
|
473
|
+
return Err(LeaderError::Start(leader_launcher_failure(&process)));
|
|
482
474
|
}
|
|
483
475
|
ensure_managed_provider_live_after_attach(&transport, &spawned)?;
|
|
484
476
|
Ok(LeaderLaunchOutcome {
|
|
@@ -1213,7 +1205,7 @@ fn run_leader_argv(
|
|
|
1213
1205
|
env: &BTreeMap<String, String>,
|
|
1214
1206
|
plan: &LeaderStartPlan,
|
|
1215
1207
|
workspace: &Path,
|
|
1216
|
-
) -> Result<
|
|
1208
|
+
) -> Result<LeaderProcessExit, LeaderError> {
|
|
1217
1209
|
let Some(program) = argv.first() else {
|
|
1218
1210
|
return Err(LeaderError::Start(
|
|
1219
1211
|
"leader launch argv is empty".to_string(),
|
|
@@ -1234,8 +1226,13 @@ fn run_leader_argv(
|
|
|
1234
1226
|
command
|
|
1235
1227
|
.args(argv.iter().skip(1))
|
|
1236
1228
|
.stdin(Stdio::inherit())
|
|
1237
|
-
.stdout(Stdio::inherit())
|
|
1238
|
-
|
|
1229
|
+
.stdout(Stdio::inherit());
|
|
1230
|
+
let capture_stderr = plan.mode != LeaderStartMode::ExecProvider;
|
|
1231
|
+
command.stderr(if capture_stderr {
|
|
1232
|
+
Stdio::piped()
|
|
1233
|
+
} else {
|
|
1234
|
+
Stdio::inherit()
|
|
1235
|
+
});
|
|
1239
1236
|
// 0.4.x order fix: env_remove MUST run AFTER envs(). The `env` map comes
|
|
1240
1237
|
// from `merged_exec_env` which seeds with `std::env::vars().collect()` —
|
|
1241
1238
|
// calling `.envs(env)` re-adds every inherited CLAUDE_CODE_* the launching
|
|
@@ -1248,10 +1245,109 @@ fn run_leader_argv(
|
|
|
1248
1245
|
command.env_remove(key);
|
|
1249
1246
|
}
|
|
1250
1247
|
let mut child = command.spawn()?;
|
|
1248
|
+
let stderr_reader = child.stderr.take().map(|mut child_stderr| {
|
|
1249
|
+
std::thread::spawn(move || {
|
|
1250
|
+
let mut captured = Vec::new();
|
|
1251
|
+
let mut buf = [0_u8; 4096];
|
|
1252
|
+
loop {
|
|
1253
|
+
let Ok(read) = child_stderr.read(&mut buf) else {
|
|
1254
|
+
break;
|
|
1255
|
+
};
|
|
1256
|
+
if read == 0 {
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
let _ = std::io::stderr().write_all(&buf[..read]);
|
|
1260
|
+
push_bounded_stderr(&mut captured, &buf[..read]);
|
|
1261
|
+
}
|
|
1262
|
+
captured
|
|
1263
|
+
})
|
|
1264
|
+
});
|
|
1251
1265
|
if plan.mode == LeaderStartMode::ExecProvider {
|
|
1252
1266
|
spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
|
|
1253
1267
|
}
|
|
1254
|
-
child.wait().map_err(LeaderError::Io)
|
|
1268
|
+
let status = child.wait().map_err(LeaderError::Io)?;
|
|
1269
|
+
let stderr = match stderr_reader {
|
|
1270
|
+
Some(reader) => match reader.join() {
|
|
1271
|
+
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
|
|
1272
|
+
Err(_) => String::new(),
|
|
1273
|
+
},
|
|
1274
|
+
None => String::new(),
|
|
1275
|
+
};
|
|
1276
|
+
Ok(LeaderProcessExit { status, stderr })
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const LEADER_STDERR_LIMIT: usize = 8192;
|
|
1280
|
+
|
|
1281
|
+
struct LeaderProcessExit {
|
|
1282
|
+
status: std::process::ExitStatus,
|
|
1283
|
+
stderr: String,
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
fn push_bounded_stderr(captured: &mut Vec<u8>, chunk: &[u8]) {
|
|
1287
|
+
if chunk.len() >= LEADER_STDERR_LIMIT {
|
|
1288
|
+
captured.clear();
|
|
1289
|
+
captured.extend_from_slice(&chunk[chunk.len() - LEADER_STDERR_LIMIT..]);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
let excess = captured
|
|
1293
|
+
.len()
|
|
1294
|
+
.saturating_add(chunk.len())
|
|
1295
|
+
.saturating_sub(LEADER_STDERR_LIMIT);
|
|
1296
|
+
if excess > 0 {
|
|
1297
|
+
captured.drain(..excess);
|
|
1298
|
+
}
|
|
1299
|
+
captured.extend_from_slice(chunk);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
fn leader_launcher_failure(process: &LeaderProcessExit) -> String {
|
|
1303
|
+
let status = process
|
|
1304
|
+
.status
|
|
1305
|
+
.code()
|
|
1306
|
+
.map(|code| code.to_string())
|
|
1307
|
+
.unwrap_or_else(|| "signal".to_string());
|
|
1308
|
+
format_launcher_failure(&status, process.status.code().is_none(), &process.stderr)
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
fn format_launcher_failure(status: &str, signaled: bool, raw_stderr: &str) -> String {
|
|
1312
|
+
let stderr_class = classify_launcher_stderr(raw_stderr, signaled);
|
|
1313
|
+
let stderr = bounded_stderr_text(crate::redaction::redact_external_text(raw_stderr.trim()));
|
|
1314
|
+
let stderr = if stderr.is_empty() {
|
|
1315
|
+
"<empty>".to_string()
|
|
1316
|
+
} else {
|
|
1317
|
+
stderr
|
|
1318
|
+
};
|
|
1319
|
+
format!(
|
|
1320
|
+
"leader launcher exited with status {status}; stderr_class={stderr_class}; stderr_excerpt={stderr}"
|
|
1321
|
+
)
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
fn bounded_stderr_text(stderr: String) -> String {
|
|
1325
|
+
if stderr.len() <= LEADER_STDERR_LIMIT {
|
|
1326
|
+
return stderr;
|
|
1327
|
+
}
|
|
1328
|
+
let mut start = stderr.len() - LEADER_STDERR_LIMIT;
|
|
1329
|
+
while !stderr.is_char_boundary(start) {
|
|
1330
|
+
start += 1;
|
|
1331
|
+
}
|
|
1332
|
+
stderr[start..].to_string()
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
fn classify_launcher_stderr(stderr: &str, signaled: bool) -> &'static str {
|
|
1336
|
+
let stderr = stderr.to_ascii_lowercase();
|
|
1337
|
+
if stderr.contains("server exited") {
|
|
1338
|
+
"server_exited"
|
|
1339
|
+
} else if stderr.contains("no server running") || stderr.contains("failed to connect to server")
|
|
1340
|
+
{
|
|
1341
|
+
"no_server"
|
|
1342
|
+
} else if stderr.contains("not a terminal") {
|
|
1343
|
+
"not_a_terminal"
|
|
1344
|
+
} else if stderr.contains("can't find session") || stderr.contains("no sessions") {
|
|
1345
|
+
"target_missing"
|
|
1346
|
+
} else if signaled {
|
|
1347
|
+
"signal"
|
|
1348
|
+
} else {
|
|
1349
|
+
"non_zero"
|
|
1350
|
+
}
|
|
1255
1351
|
}
|
|
1256
1352
|
|
|
1257
1353
|
fn spawn_exec_provider_startup_prompt_handler(provider: Provider, workspace: PathBuf) {
|
|
@@ -1447,8 +1543,9 @@ mod tests {
|
|
|
1447
1543
|
};
|
|
1448
1544
|
|
|
1449
1545
|
use super::{
|
|
1450
|
-
ensure_managed_provider_live_after_attach, execute_leader_plan,
|
|
1451
|
-
handle_exec_provider_startup_prompts, shlex_quote,
|
|
1546
|
+
ensure_managed_provider_live_after_attach, execute_leader_plan, format_launcher_failure,
|
|
1547
|
+
handle_exec_provider_startup_prompts, push_bounded_stderr, shlex_quote,
|
|
1548
|
+
LEADER_STDERR_LIMIT,
|
|
1452
1549
|
};
|
|
1453
1550
|
|
|
1454
1551
|
struct ScriptedTransport {
|
|
@@ -1981,4 +2078,28 @@ mod tests {
|
|
|
1981
2078
|
external/attach paths; body excerpt: {body}"
|
|
1982
2079
|
);
|
|
1983
2080
|
}
|
|
2081
|
+
|
|
2082
|
+
#[test]
|
|
2083
|
+
fn launcher_failure_redacts_credentials_before_persistence() {
|
|
2084
|
+
let failure = format_launcher_failure(
|
|
2085
|
+
"1",
|
|
2086
|
+
false,
|
|
2087
|
+
"open terminal failed: not a terminal API_TOKEN=secret-value \
|
|
2088
|
+
Authorization: Bearer abcdefghijklmnopqrstuvwxyz",
|
|
2089
|
+
);
|
|
2090
|
+
|
|
2091
|
+
assert!(failure.contains("stderr_class=not_a_terminal"));
|
|
2092
|
+
assert!(failure.contains("[REDACTED]"));
|
|
2093
|
+
assert!(!failure.contains("secret-value"));
|
|
2094
|
+
assert!(!failure.contains("abcdefghijklmnopqrstuvwxyz"));
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
#[test]
|
|
2098
|
+
fn launcher_stderr_capture_is_bounded_to_tail() {
|
|
2099
|
+
let mut captured = vec![b'a'; LEADER_STDERR_LIMIT - 2];
|
|
2100
|
+
push_bounded_stderr(&mut captured, b"XYZ");
|
|
2101
|
+
|
|
2102
|
+
assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
|
|
2103
|
+
assert!(captured.ends_with(b"XYZ"));
|
|
2104
|
+
}
|
|
1984
2105
|
}
|
|
@@ -226,6 +226,9 @@ fn leader_receiver_struct_serializes_with_python_field_names() {
|
|
|
226
226
|
pane_tty: Some("/dev/ttys001".into()),
|
|
227
227
|
pane_current_command: Some("claude".into()),
|
|
228
228
|
tmux_socket: None,
|
|
229
|
+
scope_authority: None,
|
|
230
|
+
authorized_team_workspace: None,
|
|
231
|
+
binding_nonce: None,
|
|
229
232
|
fingerprint: Some("fp".into()),
|
|
230
233
|
leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
|
|
231
234
|
owner_epoch: Some(OwnerEpoch(3)),
|
|
@@ -263,6 +263,12 @@ pub struct LeaderReceiver {
|
|
|
263
263
|
pub pane_current_command: Option<String>,
|
|
264
264
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
265
265
|
pub tmux_socket: Option<String>,
|
|
266
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
267
|
+
pub scope_authority: Option<String>,
|
|
268
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
269
|
+
pub authorized_team_workspace: Option<PathBuf>,
|
|
270
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
271
|
+
pub binding_nonce: Option<String>,
|
|
266
272
|
/// `_target_fingerprint(pane_info)`。
|
|
267
273
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
268
274
|
pub fingerprint: Option<String>,
|
|
@@ -28,6 +28,11 @@ pub(super) fn rollback_add_agent_atomic(
|
|
|
28
28
|
agent_id: &AgentId,
|
|
29
29
|
reason: &str,
|
|
30
30
|
) {
|
|
31
|
+
let _ = std::fs::remove_dir_all(
|
|
32
|
+
run_workspace
|
|
33
|
+
.join(".team/runtime/copilot-instructions")
|
|
34
|
+
.join(agent_id.as_str()),
|
|
35
|
+
);
|
|
31
36
|
let spec_restored = if let Some(text) = pre_spec_text {
|
|
32
37
|
std::fs::write(spec_path, text).is_ok()
|
|
33
38
|
} else {
|
|
@@ -93,6 +93,10 @@ pub(super) fn running_agent_state(
|
|
|
93
93
|
"attribution_confidence".to_string(),
|
|
94
94
|
serde_json::Value::Null,
|
|
95
95
|
);
|
|
96
|
+
state.insert(
|
|
97
|
+
"capture_state".to_string(),
|
|
98
|
+
serde_json::json!("pending_first_turn"),
|
|
99
|
+
);
|
|
96
100
|
if let Some(started_agent) = started_agent {
|
|
97
101
|
persist_started_agent_plan_state(&mut state, started_agent);
|
|
98
102
|
if let Some(layout_window) = started_agent.layout_window.as_ref() {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
use crate::lifecycle::*;
|
|
2
|
+
use crate::model::ids::AgentId;
|
|
3
|
+
use crate::provider::SessionId;
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
|
|
8
|
+
pub fn clone_agent(
|
|
9
|
+
workspace: &Path,
|
|
10
|
+
source_agent_id: &AgentId,
|
|
11
|
+
as_agent_id: &AgentId,
|
|
12
|
+
label: Option<&str>,
|
|
13
|
+
open_display: bool,
|
|
14
|
+
team: Option<&str>,
|
|
15
|
+
) -> Result<CloneAgentReport, LifecycleError> {
|
|
16
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
17
|
+
workspace,
|
|
18
|
+
team,
|
|
19
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
20
|
+
)
|
|
21
|
+
.map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
|
|
22
|
+
ensure_owner_allowed_for_state(&selected.state, Some(source_agent_id))?;
|
|
23
|
+
let source_agent = selected
|
|
24
|
+
.state
|
|
25
|
+
.get("agents")
|
|
26
|
+
.and_then(|agents| agents.get(source_agent_id.as_str()))
|
|
27
|
+
.ok_or_else(|| {
|
|
28
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
29
|
+
})?;
|
|
30
|
+
let source_session_id = source_agent
|
|
31
|
+
.get("session_id")
|
|
32
|
+
.and_then(serde_json::Value::as_str)
|
|
33
|
+
.filter(|value| !value.is_empty())
|
|
34
|
+
.map(SessionId::new);
|
|
35
|
+
let mut materialized = materialize_latest_role(
|
|
36
|
+
&selected.run_workspace,
|
|
37
|
+
&selected.team_dir,
|
|
38
|
+
&selected.state,
|
|
39
|
+
source_agent_id,
|
|
40
|
+
as_agent_id,
|
|
41
|
+
label,
|
|
42
|
+
)?;
|
|
43
|
+
let spec_path = selected
|
|
44
|
+
.spec_path
|
|
45
|
+
.as_ref()
|
|
46
|
+
.ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
|
|
47
|
+
let spec = crate::model::yaml::loads(
|
|
48
|
+
&std::fs::read_to_string(spec_path)
|
|
49
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?,
|
|
50
|
+
)
|
|
51
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
52
|
+
clamp_materialized_role_to_leader(materialized.path(), &spec)?;
|
|
53
|
+
let added = add_agent(
|
|
54
|
+
&selected.run_workspace,
|
|
55
|
+
as_agent_id,
|
|
56
|
+
materialized.path(),
|
|
57
|
+
open_display,
|
|
58
|
+
Some(selected.team_key.as_str()),
|
|
59
|
+
)?;
|
|
60
|
+
let verified = read_agent_session(
|
|
61
|
+
&selected.run_workspace,
|
|
62
|
+
selected.team_key.as_str(),
|
|
63
|
+
as_agent_id,
|
|
64
|
+
source_session_id.as_ref(),
|
|
65
|
+
);
|
|
66
|
+
let (session_id, backing_path, backing_state) = match verified {
|
|
67
|
+
Some((session_id, backing_path)) => (
|
|
68
|
+
Some(session_id),
|
|
69
|
+
Some(backing_path),
|
|
70
|
+
CloneBackingState::Verified,
|
|
71
|
+
),
|
|
72
|
+
None => (None, None, CloneBackingState::PendingFirstTurn),
|
|
73
|
+
};
|
|
74
|
+
materialized.keep();
|
|
75
|
+
Ok(CloneAgentReport {
|
|
76
|
+
source_agent_id: source_agent_id.clone(),
|
|
77
|
+
new_agent_id: as_agent_id.clone(),
|
|
78
|
+
env: added.env,
|
|
79
|
+
session_id,
|
|
80
|
+
backing_path,
|
|
81
|
+
backing_state,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn read_agent_session(
|
|
86
|
+
workspace: &Path,
|
|
87
|
+
team_key: &str,
|
|
88
|
+
agent_id: &AgentId,
|
|
89
|
+
source_session_id: Option<&SessionId>,
|
|
90
|
+
) -> Option<(SessionId, std::path::PathBuf)> {
|
|
91
|
+
let state = crate::state::projection::select_runtime_state(workspace, Some(team_key)).ok()?;
|
|
92
|
+
let agent = state
|
|
93
|
+
.get("agents")
|
|
94
|
+
.and_then(|agents| agents.get(agent_id.as_str()))?;
|
|
95
|
+
let session = agent
|
|
96
|
+
.get("session_id")
|
|
97
|
+
.and_then(serde_json::Value::as_str)
|
|
98
|
+
.filter(|value| !value.is_empty())?;
|
|
99
|
+
let backing = agent
|
|
100
|
+
.get("rollout_path")
|
|
101
|
+
.and_then(serde_json::Value::as_str)
|
|
102
|
+
.filter(|value| !value.is_empty())
|
|
103
|
+
.map(std::path::PathBuf::from)?;
|
|
104
|
+
let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
|
|
105
|
+
(distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
|
|
106
|
+
}
|