@team-agent/installer 0.5.51 → 0.5.53
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 +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/named_address.rs +35 -67
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- 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 +526 -73
- 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 +12 -1
- 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/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- 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 +20 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -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/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/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- 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 +321 -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.rs +39 -6
- 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/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- 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/skills/team-agent/SKILL.md +8 -3
|
@@ -18,13 +18,15 @@ pub fn start_agent(
|
|
|
18
18
|
team: Option<&str>,
|
|
19
19
|
) -> Result<StartAgentOutcome, LifecycleError> {
|
|
20
20
|
let paths = lifecycle_paths(workspace, team)?;
|
|
21
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
22
|
+
let team = canonical_team.as_deref();
|
|
21
23
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
22
24
|
workspace: &paths.run_workspace,
|
|
23
25
|
operation: "start-agent",
|
|
24
26
|
team,
|
|
25
27
|
agent_id: Some(agent_id),
|
|
26
28
|
})?;
|
|
27
|
-
let transport =
|
|
29
|
+
let transport = paths.tmux_backend()?;
|
|
28
30
|
start_agent_at_paths(
|
|
29
31
|
&paths.run_workspace,
|
|
30
32
|
&paths.spec_workspace,
|
|
@@ -54,9 +56,12 @@ pub fn start_agent_with_transport(
|
|
|
54
56
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
55
57
|
run_workspace: workspace.to_path_buf(),
|
|
56
58
|
spec_workspace: workspace.to_path_buf(),
|
|
59
|
+
selected: None,
|
|
57
60
|
},
|
|
58
61
|
Err(error) => return Err(error),
|
|
59
62
|
};
|
|
63
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
64
|
+
let team = canonical_team.as_deref();
|
|
60
65
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
61
66
|
workspace: &paths.run_workspace,
|
|
62
67
|
operation: "start-agent",
|
|
@@ -778,13 +783,15 @@ pub fn stop_agent(
|
|
|
778
783
|
team: Option<&str>,
|
|
779
784
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
780
785
|
let paths = lifecycle_paths(workspace, team)?;
|
|
786
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
787
|
+
let team = canonical_team.as_deref();
|
|
781
788
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
782
789
|
workspace: &paths.run_workspace,
|
|
783
790
|
operation: "stop-agent",
|
|
784
791
|
team,
|
|
785
792
|
agent_id: Some(agent_id),
|
|
786
793
|
})?;
|
|
787
|
-
let transport =
|
|
794
|
+
let transport = paths.tmux_backend()?;
|
|
788
795
|
stop_agent_at_paths(
|
|
789
796
|
&paths.run_workspace,
|
|
790
797
|
&paths.spec_workspace,
|
|
@@ -801,6 +808,8 @@ pub fn stop_agent_with_transport(
|
|
|
801
808
|
transport: &dyn crate::transport::Transport,
|
|
802
809
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
803
810
|
let paths = lifecycle_paths(workspace, team)?;
|
|
811
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
812
|
+
let team = canonical_team.as_deref();
|
|
804
813
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
805
814
|
workspace: &paths.run_workspace,
|
|
806
815
|
operation: "stop-agent",
|
|
@@ -840,9 +849,12 @@ pub(crate) fn start_agent_at_paths_for_recovery(
|
|
|
840
849
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
841
850
|
run_workspace: workspace.to_path_buf(),
|
|
842
851
|
spec_workspace: workspace.to_path_buf(),
|
|
852
|
+
selected: None,
|
|
843
853
|
},
|
|
844
854
|
Err(error) => return Err(RecoveryError::Lifecycle(error.to_string())),
|
|
845
855
|
};
|
|
856
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
857
|
+
let team = canonical_team.as_deref();
|
|
846
858
|
// Best-effort stop: if the live pane still exists we tear it down so the
|
|
847
859
|
// subsequent start creates a fresh provider process instead of a Noop.
|
|
848
860
|
// Stop errors are absorbed into the start attempt result — the important
|
|
@@ -1137,7 +1149,7 @@ fn mark_agent_started(
|
|
|
1137
1149
|
}
|
|
1138
1150
|
agent.insert(
|
|
1139
1151
|
"spawned_at".to_string(),
|
|
1140
|
-
serde_json::json!(
|
|
1152
|
+
serde_json::json!(spawn.spawned_at.as_str()),
|
|
1141
1153
|
);
|
|
1142
1154
|
agent.insert(
|
|
1143
1155
|
"spawn_cwd".to_string(),
|
|
@@ -1217,13 +1229,15 @@ pub fn reset_agent(
|
|
|
1217
1229
|
});
|
|
1218
1230
|
}
|
|
1219
1231
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1232
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1233
|
+
let team = canonical_team.as_deref();
|
|
1220
1234
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1221
1235
|
workspace: &paths.run_workspace,
|
|
1222
1236
|
operation: "reset-agent",
|
|
1223
1237
|
team,
|
|
1224
1238
|
agent_id: Some(agent_id),
|
|
1225
1239
|
})?;
|
|
1226
|
-
let transport =
|
|
1240
|
+
let transport = paths.tmux_backend()?;
|
|
1227
1241
|
reset_agent_at_paths(
|
|
1228
1242
|
&paths.run_workspace,
|
|
1229
1243
|
&paths.spec_workspace,
|
|
@@ -1249,6 +1263,8 @@ pub fn reset_agent_with_transport(
|
|
|
1249
1263
|
});
|
|
1250
1264
|
}
|
|
1251
1265
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1266
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1267
|
+
let team = canonical_team.as_deref();
|
|
1252
1268
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1253
1269
|
workspace: &paths.run_workspace,
|
|
1254
1270
|
operation: "reset-agent",
|
|
@@ -2,6 +2,7 @@ use super::*;
|
|
|
2
2
|
|
|
3
3
|
pub(super) struct SpawnedAgentWindow {
|
|
4
4
|
pub spawn: crate::transport::SpawnResult,
|
|
5
|
+
pub spawned_at: String,
|
|
5
6
|
pub plan: crate::provider::CommandPlan,
|
|
6
7
|
pub profile_launch: crate::provider::ProviderProfileLaunch,
|
|
7
8
|
pub layout_placement: Option<crate::lifecycle::launch::LayoutPlacement>,
|
|
@@ -327,6 +328,14 @@ pub(super) fn spawn_agent_window(
|
|
|
327
328
|
);
|
|
328
329
|
crate::lifecycle::launch::apply_profile_launch_env(&mut env, &profile_launch);
|
|
329
330
|
crate::lifecycle::launch::apply_mcp_auto_approval_env(&mut env, safety);
|
|
331
|
+
if provider == crate::provider::Provider::Copilot {
|
|
332
|
+
crate::lifecycle::launch::apply_copilot_instructions_overlay(
|
|
333
|
+
workspace,
|
|
334
|
+
agent_id.as_str(),
|
|
335
|
+
&system_prompt,
|
|
336
|
+
&mut env,
|
|
337
|
+
)?;
|
|
338
|
+
}
|
|
330
339
|
// 0.3.28 Step 3: per Python parity, worker spawn cwd is ALWAYS `workspace`.
|
|
331
340
|
// The persisted-state `agent.spawn_cwd` override is ignored (it was a
|
|
332
341
|
// Rust-only extension that drifted to `.team/runtime/<team_key>/` after
|
|
@@ -359,6 +368,7 @@ pub(super) fn spawn_agent_window(
|
|
|
359
368
|
// failure can now be diagnosed from events.jsonl — the recorded
|
|
360
369
|
// expected_session_id == state._pending_session_id (after
|
|
361
370
|
// mark_agent_started persists the same plan tuple).
|
|
371
|
+
let spawned_at = crate::lifecycle::launch::spawn_timestamp();
|
|
362
372
|
{
|
|
363
373
|
let session_id_in_argv = plan
|
|
364
374
|
.argv
|
|
@@ -385,6 +395,7 @@ pub(super) fn spawn_agent_window(
|
|
|
385
395
|
"env_unset": env_unset,
|
|
386
396
|
"tmux_start_mode": tmux_start_mode_pre_spawn,
|
|
387
397
|
"spawn_epoch": spawn_epoch,
|
|
398
|
+
"spawned_at": spawned_at.as_str(),
|
|
388
399
|
"source": "restart",
|
|
389
400
|
"tmux_endpoint": tmux_endpoint,
|
|
390
401
|
"tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
|
|
@@ -521,6 +532,7 @@ pub(super) fn spawn_agent_window(
|
|
|
521
532
|
}
|
|
522
533
|
Ok(SpawnedAgentWindow {
|
|
523
534
|
spawn,
|
|
535
|
+
spawned_at,
|
|
524
536
|
plan,
|
|
525
537
|
profile_launch,
|
|
526
538
|
layout_placement: layout_placement.cloned(),
|
|
@@ -449,6 +449,15 @@ fn restart_with_selected_team_and_transport(
|
|
|
449
449
|
moved leader to dedicated session)",
|
|
450
450
|
session_name.as_str()
|
|
451
451
|
);
|
|
452
|
+
let targets = vec![session_name.as_str().to_string()];
|
|
453
|
+
crate::kill_audit::pre_kill_audit(
|
|
454
|
+
&crate::event_log::EventLog::new(&selected.run_workspace),
|
|
455
|
+
transport,
|
|
456
|
+
"restart.stale_worker_session",
|
|
457
|
+
crate::kill_audit::KILL_SESSION,
|
|
458
|
+
&targets,
|
|
459
|
+
)
|
|
460
|
+
.map_err(|error| LifecycleError::Transport(format!("pre-kill audit failed: {error}")))?;
|
|
452
461
|
transport
|
|
453
462
|
.kill_session(&session_name)
|
|
454
463
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
@@ -1984,7 +1993,7 @@ fn mark_agent_respawned(
|
|
|
1984
1993
|
}
|
|
1985
1994
|
agent.insert(
|
|
1986
1995
|
"spawned_at".to_string(),
|
|
1987
|
-
serde_json::json!(
|
|
1996
|
+
serde_json::json!(spawn.spawned_at.as_str()),
|
|
1988
1997
|
);
|
|
1989
1998
|
agent.insert(
|
|
1990
1999
|
"spawn_cwd".to_string(),
|
|
@@ -2462,6 +2471,7 @@ impl Clone for ParallelSpawnResult {
|
|
|
2462
2471
|
Self {
|
|
2463
2472
|
spawn: SpawnedAgentWindow {
|
|
2464
2473
|
spawn: self.spawn.spawn.clone(),
|
|
2474
|
+
spawned_at: self.spawn.spawned_at.clone(),
|
|
2465
2475
|
plan: self.spawn.plan.clone(),
|
|
2466
2476
|
profile_launch: self.spawn.profile_launch.clone(),
|
|
2467
2477
|
layout_placement: self.spawn.layout_placement.clone(),
|
|
@@ -3119,10 +3129,16 @@ fn restart_candidate_from_state(
|
|
|
3119
3129
|
.filter(|s| !s.is_empty())
|
|
3120
3130
|
.map(SessionName::new)
|
|
3121
3131
|
.unwrap_or_else(|| SessionName::new(team_name));
|
|
3132
|
+
let retired = retired_agent_ids(state);
|
|
3122
3133
|
let mut agents = state
|
|
3123
3134
|
.get("agents")
|
|
3124
3135
|
.and_then(serde_json::Value::as_object)
|
|
3125
|
-
.map(|map|
|
|
3136
|
+
.map(|map| {
|
|
3137
|
+
map.keys()
|
|
3138
|
+
.filter(|agent_id| !retired.contains(agent_id.as_str()))
|
|
3139
|
+
.map(AgentId::new)
|
|
3140
|
+
.collect::<Vec<_>>()
|
|
3141
|
+
})
|
|
3126
3142
|
.unwrap_or_default();
|
|
3127
3143
|
agents.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
|
3128
3144
|
RestartCandidate {
|
|
@@ -3135,6 +3151,19 @@ fn restart_candidate_from_state(
|
|
|
3135
3151
|
}
|
|
3136
3152
|
}
|
|
3137
3153
|
|
|
3154
|
+
fn retired_agent_ids(state: &serde_json::Value) -> std::collections::BTreeSet<String> {
|
|
3155
|
+
state
|
|
3156
|
+
.get("agent_lifecycle")
|
|
3157
|
+
.and_then(serde_json::Value::as_object)
|
|
3158
|
+
.into_iter()
|
|
3159
|
+
.flat_map(|lifecycle| lifecycle.iter())
|
|
3160
|
+
.filter(|(_, entry)| {
|
|
3161
|
+
entry.get("state").and_then(serde_json::Value::as_str) == Some("retired")
|
|
3162
|
+
})
|
|
3163
|
+
.map(|(agent_id, _)| agent_id.clone())
|
|
3164
|
+
.collect()
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3138
3167
|
/// E5 task#3 / RC-A6a:每次 restart 都以**角色定义**(team_dir 的 TEAM.md+agents/*.md)
|
|
3139
3168
|
/// compile_team 重建 runtime spec(覆盖),保留运行期 override(session_name 必须延续,
|
|
3140
3169
|
/// 否则 tmux session 对不上)。写到 .team/runtime/<team_key>/team.spec.yaml。
|
|
@@ -3204,6 +3233,7 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
3204
3233
|
// 静态 team_dir/agents/*.md + state 记录的 dynamic role source。缺文件 fail-closed
|
|
3205
3234
|
// 三行式,不静默 prune 已 live 的 helper(persist SaveConflict 保护继续生效)。
|
|
3206
3235
|
merge_state_dynamic_role_files(&mut spec, run_workspace, &team_dir, team_key, state)?;
|
|
3236
|
+
filter_retired_agents_from_spec(&mut spec, &retired_agent_ids(state));
|
|
3207
3237
|
// 写 runtime spec(覆盖,原子 tmp+rename;Bug2)。
|
|
3208
3238
|
let spec_path = crate::model::paths::runtime_spec_path(run_workspace, team_key);
|
|
3209
3239
|
crate::lifecycle::launch::write_spec_atomic(&spec_path, &spec)?;
|
|
@@ -3216,6 +3246,15 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
3216
3246
|
Ok(spec)
|
|
3217
3247
|
}
|
|
3218
3248
|
|
|
3249
|
+
fn filter_retired_agents_from_spec(
|
|
3250
|
+
spec: &mut YamlValue,
|
|
3251
|
+
retired: &std::collections::BTreeSet<String>,
|
|
3252
|
+
) {
|
|
3253
|
+
for agent_id in retired {
|
|
3254
|
+
*spec = super::remove::spec_without_agent(spec, &AgentId::new(agent_id));
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3219
3258
|
/// 0.5.30 (`add-agent-restart-saveconflict-locate.md` §4/§11): 把 add-agent
|
|
3220
3259
|
/// 写入 `state.agents.<id>.dynamic_role_file` 的动态 role 文档合并回 restart
|
|
3221
3260
|
/// 重建 spec。规则:
|
|
@@ -3418,6 +3457,76 @@ mod tests {
|
|
|
3418
3457
|
TransportError,
|
|
3419
3458
|
};
|
|
3420
3459
|
|
|
3460
|
+
#[test]
|
|
3461
|
+
fn retired_filter_clears_every_spec_reference_via_remove_primitive() {
|
|
3462
|
+
let mut spec = crate::model::yaml::loads(
|
|
3463
|
+
r#"
|
|
3464
|
+
name: retirement-filter
|
|
3465
|
+
agents:
|
|
3466
|
+
- id: gone
|
|
3467
|
+
- id: keep
|
|
3468
|
+
runtime:
|
|
3469
|
+
startup_order:
|
|
3470
|
+
- gone
|
|
3471
|
+
- keep
|
|
3472
|
+
routing:
|
|
3473
|
+
default_assignee: gone
|
|
3474
|
+
rules:
|
|
3475
|
+
- match: gone-task
|
|
3476
|
+
assign_to: gone
|
|
3477
|
+
- match: keep-task
|
|
3478
|
+
assign_to: keep
|
|
3479
|
+
tasks:
|
|
3480
|
+
- title: gone-task
|
|
3481
|
+
assignee: gone
|
|
3482
|
+
- title: keep-task
|
|
3483
|
+
assignee: keep
|
|
3484
|
+
"#,
|
|
3485
|
+
)
|
|
3486
|
+
.expect("fixture spec");
|
|
3487
|
+
let retired = std::collections::BTreeSet::from(["gone".to_string()]);
|
|
3488
|
+
|
|
3489
|
+
filter_retired_agents_from_spec(&mut spec, &retired);
|
|
3490
|
+
|
|
3491
|
+
let agents = spec
|
|
3492
|
+
.get("agents")
|
|
3493
|
+
.and_then(YamlValue::as_list)
|
|
3494
|
+
.expect("agents");
|
|
3495
|
+
assert!(agents.iter().all(|agent| {
|
|
3496
|
+
agent.get("id").and_then(YamlValue::as_str) != Some("gone")
|
|
3497
|
+
}));
|
|
3498
|
+
let startup_order = spec
|
|
3499
|
+
.get("runtime")
|
|
3500
|
+
.and_then(|runtime| runtime.get("startup_order"))
|
|
3501
|
+
.and_then(YamlValue::as_list)
|
|
3502
|
+
.expect("startup_order");
|
|
3503
|
+
assert!(startup_order
|
|
3504
|
+
.iter()
|
|
3505
|
+
.all(|agent_id| agent_id.as_str() != Some("gone")));
|
|
3506
|
+
let routing = spec.get("routing").expect("routing");
|
|
3507
|
+
assert_ne!(
|
|
3508
|
+
routing
|
|
3509
|
+
.get("default_assignee")
|
|
3510
|
+
.and_then(YamlValue::as_str),
|
|
3511
|
+
Some("gone"),
|
|
3512
|
+
"spec_default_assignee must not return a retired agent"
|
|
3513
|
+
);
|
|
3514
|
+
let rules = routing
|
|
3515
|
+
.get("rules")
|
|
3516
|
+
.and_then(YamlValue::as_list)
|
|
3517
|
+
.expect("routing rules");
|
|
3518
|
+
assert!(rules.iter().all(|rule| {
|
|
3519
|
+
rule.get("assign_to").and_then(YamlValue::as_str) != Some("gone")
|
|
3520
|
+
}));
|
|
3521
|
+
let tasks = spec
|
|
3522
|
+
.get("tasks")
|
|
3523
|
+
.and_then(YamlValue::as_list)
|
|
3524
|
+
.expect("tasks");
|
|
3525
|
+
assert!(tasks.iter().all(|task| {
|
|
3526
|
+
task.get("assignee").and_then(YamlValue::as_str) != Some("gone")
|
|
3527
|
+
}));
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3421
3530
|
#[test]
|
|
3422
3531
|
fn resume_decision_event_uses_central_scrub_and_preserves_required_failure() {
|
|
3423
3532
|
let marker = "synthetic-resume-decision-marker";
|
|
@@ -3590,6 +3699,7 @@ mod tests {
|
|
|
3590
3699
|
}
|
|
3591
3700
|
}
|
|
3592
3701
|
});
|
|
3702
|
+
let before = chrono::Utc::now();
|
|
3593
3703
|
let spawn = SpawnedAgentWindow {
|
|
3594
3704
|
spawn: SpawnResult {
|
|
3595
3705
|
pane_id: PaneId::new("%new"),
|
|
@@ -3597,14 +3707,13 @@ mod tests {
|
|
|
3597
3707
|
window: WindowName::new("w1"),
|
|
3598
3708
|
child_pid: Some(2020),
|
|
3599
3709
|
},
|
|
3710
|
+
spawned_at: chrono::Utc::now().to_rfc3339(),
|
|
3600
3711
|
plan: crate::provider::CommandPlan::argv_only(vec!["codex".to_string()]),
|
|
3601
3712
|
profile_launch: crate::provider::ProviderProfileLaunch::default(),
|
|
3602
3713
|
layout_placement: None,
|
|
3603
3714
|
spawn_cwd: std::path::PathBuf::from("/tmp/team-epoch"),
|
|
3604
3715
|
owner_team_id: None,
|
|
3605
3716
|
};
|
|
3606
|
-
let before = chrono::Utc::now();
|
|
3607
|
-
|
|
3608
3717
|
mark_agent_respawned(
|
|
3609
3718
|
&mut state,
|
|
3610
3719
|
&AgentId::new("w1"),
|
|
@@ -15,13 +15,15 @@ pub fn remove_agent(
|
|
|
15
15
|
team: Option<&str>,
|
|
16
16
|
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
17
17
|
let paths = lifecycle_paths(workspace, team)?;
|
|
18
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
19
|
+
let team = canonical_team.as_deref();
|
|
18
20
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
19
21
|
workspace: &paths.run_workspace,
|
|
20
22
|
operation: "remove-agent",
|
|
21
23
|
team,
|
|
22
24
|
agent_id: Some(agent_id),
|
|
23
25
|
})?;
|
|
24
|
-
let transport =
|
|
26
|
+
let transport = paths.tmux_backend()?;
|
|
25
27
|
remove_agent_at_paths(
|
|
26
28
|
&paths.run_workspace,
|
|
27
29
|
&paths.spec_workspace,
|
|
@@ -42,6 +44,8 @@ pub fn remove_agent_with_transport(
|
|
|
42
44
|
transport: &dyn crate::transport::Transport,
|
|
43
45
|
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
44
46
|
let paths = lifecycle_paths(workspace, team)?;
|
|
47
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
48
|
+
let team = canonical_team.as_deref();
|
|
45
49
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
46
50
|
workspace: &paths.run_workspace,
|
|
47
51
|
operation: "remove-agent",
|
|
@@ -68,6 +72,8 @@ pub(crate) fn remove_agent_with_transport_locked(
|
|
|
68
72
|
transport: &dyn crate::transport::Transport,
|
|
69
73
|
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
70
74
|
let paths = lifecycle_paths(workspace, team)?;
|
|
75
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
76
|
+
let team = canonical_team.as_deref();
|
|
71
77
|
remove_agent_at_paths(
|
|
72
78
|
&paths.run_workspace,
|
|
73
79
|
&paths.spec_workspace,
|
|
@@ -94,6 +100,8 @@ impl ForceRecreateSnapshot {
|
|
|
94
100
|
transport: &dyn crate::transport::Transport,
|
|
95
101
|
) -> Result<Self, LifecycleError> {
|
|
96
102
|
let paths = lifecycle_paths(workspace, team)?;
|
|
103
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
104
|
+
let team = canonical_team.as_deref();
|
|
97
105
|
let seat = resolve_seat(
|
|
98
106
|
&paths.run_workspace,
|
|
99
107
|
&paths.spec_workspace,
|
|
@@ -215,7 +223,9 @@ pub fn remove_agent_flag_requirements(
|
|
|
215
223
|
team: Option<&str>,
|
|
216
224
|
) -> Result<RemoveAgentFlagRequirements, LifecycleError> {
|
|
217
225
|
let paths = lifecycle_paths(workspace, team)?;
|
|
218
|
-
let
|
|
226
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
227
|
+
let team = canonical_team.as_deref();
|
|
228
|
+
let transport = paths.tmux_backend()?;
|
|
219
229
|
Ok(remove_agent_preflight(
|
|
220
230
|
&paths.run_workspace,
|
|
221
231
|
&paths.spec_workspace,
|
|
@@ -564,14 +574,16 @@ fn remove_agent_inner(
|
|
|
564
574
|
// (team projection) — NOT a raw save, so other teams in a multi-team workspace are preserved.
|
|
565
575
|
let mut removed_state = working_state;
|
|
566
576
|
remove_agent_from_state(&mut removed_state, agent_id)?;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
577
|
+
mark_agent_retired_in_state(&mut removed_state, agent_id)?;
|
|
578
|
+
crate::state::repository::StateRepository::new(paths.run_workspace)
|
|
579
|
+
.save(
|
|
580
|
+
crate::state::repository::StateWriteIntent::RemoveAgent {
|
|
581
|
+
team_key,
|
|
582
|
+
agent_id: agent_id.as_str(),
|
|
583
|
+
},
|
|
584
|
+
&removed_state,
|
|
585
|
+
)
|
|
586
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
575
587
|
cleared_locations.push(serde_json::json!("state.json:agents"));
|
|
576
588
|
write_remove_step_event(
|
|
577
589
|
paths.run_workspace,
|
|
@@ -783,6 +795,57 @@ fn remove_agent_from_state(
|
|
|
783
795
|
}
|
|
784
796
|
}
|
|
785
797
|
|
|
798
|
+
pub(crate) fn mark_agent_retired_in_state(
|
|
799
|
+
state: &mut serde_json::Value,
|
|
800
|
+
agent_id: &AgentId,
|
|
801
|
+
) -> Result<(), LifecycleError> {
|
|
802
|
+
let root = state.as_object_mut().ok_or_else(|| {
|
|
803
|
+
LifecycleError::StatePersist("runtime state root is not an object".to_string())
|
|
804
|
+
})?;
|
|
805
|
+
let lifecycle = root
|
|
806
|
+
.entry("agent_lifecycle".to_string())
|
|
807
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
808
|
+
let lifecycle = lifecycle.as_object_mut().ok_or_else(|| {
|
|
809
|
+
LifecycleError::StatePersist("runtime state agent_lifecycle is not an object".to_string())
|
|
810
|
+
})?;
|
|
811
|
+
let entry = lifecycle
|
|
812
|
+
.entry(agent_id.as_str().to_string())
|
|
813
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
814
|
+
if entry.get("state").and_then(serde_json::Value::as_str) == Some("retired") {
|
|
815
|
+
return Ok(());
|
|
816
|
+
}
|
|
817
|
+
let entry = entry.as_object_mut().ok_or_else(|| {
|
|
818
|
+
LifecycleError::StatePersist(format!(
|
|
819
|
+
"runtime state agent_lifecycle.{} is not an object",
|
|
820
|
+
agent_id.as_str()
|
|
821
|
+
))
|
|
822
|
+
})?;
|
|
823
|
+
entry.insert("state".to_string(), serde_json::json!("retired"));
|
|
824
|
+
entry.insert(
|
|
825
|
+
"changed_at".to_string(),
|
|
826
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
827
|
+
);
|
|
828
|
+
entry.insert("reason".to_string(), serde_json::json!("remove-agent"));
|
|
829
|
+
Ok(())
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
pub(crate) fn clear_agent_retirement_in_state(state: &mut serde_json::Value, agent_id: &AgentId) {
|
|
833
|
+
let Some(lifecycle) = state
|
|
834
|
+
.get_mut("agent_lifecycle")
|
|
835
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
836
|
+
else {
|
|
837
|
+
return;
|
|
838
|
+
};
|
|
839
|
+
let is_retired = lifecycle
|
|
840
|
+
.get(agent_id.as_str())
|
|
841
|
+
.and_then(|entry| entry.get("state"))
|
|
842
|
+
.and_then(serde_json::Value::as_str)
|
|
843
|
+
== Some("retired");
|
|
844
|
+
if is_retired {
|
|
845
|
+
lifecycle.remove(agent_id.as_str());
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
786
849
|
/// Build the persisted spec after removing one worker. Besides deleting the worker and startup entry,
|
|
787
850
|
/// prune routing references that would otherwise point at the removed worker.
|
|
788
851
|
pub(crate) fn spec_without_agent(spec: &YamlValue, agent_id: &AgentId) -> YamlValue {
|
|
@@ -16,6 +16,29 @@ use super::*;
|
|
|
16
16
|
struct LifecyclePaths {
|
|
17
17
|
run_workspace: std::path::PathBuf,
|
|
18
18
|
spec_workspace: std::path::PathBuf,
|
|
19
|
+
selected: Option<crate::state::selector::SelectedTeam>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl LifecyclePaths {
|
|
23
|
+
fn canonical_team<'a>(&'a self, requested: Option<&'a str>) -> Option<&'a str> {
|
|
24
|
+
self.selected
|
|
25
|
+
.as_ref()
|
|
26
|
+
.map(|selected| selected.team_key.as_str())
|
|
27
|
+
.or(requested)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn tmux_backend(&self) -> Result<crate::tmux_backend::TmuxBackend, LifecycleError> {
|
|
31
|
+
match self.selected.as_ref() {
|
|
32
|
+
Some(selected) => common::lifecycle_worker_tmux_backend_selection_for_state(
|
|
33
|
+
&selected.run_workspace,
|
|
34
|
+
&selected.state,
|
|
35
|
+
)
|
|
36
|
+
.map(|selection| selection.backend),
|
|
37
|
+
None => Ok(crate::tmux_backend::TmuxBackend::for_workspace(
|
|
38
|
+
&self.run_workspace,
|
|
39
|
+
)),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
}
|
|
20
43
|
|
|
21
44
|
struct LifecyclePathRefs<'a> {
|
|
@@ -115,8 +138,9 @@ fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePath
|
|
|
115
138
|
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
116
139
|
})?;
|
|
117
140
|
Ok(LifecyclePaths {
|
|
118
|
-
run_workspace: selected.run_workspace,
|
|
141
|
+
run_workspace: selected.run_workspace.clone(),
|
|
119
142
|
spec_workspace,
|
|
143
|
+
selected: Some(selected),
|
|
120
144
|
})
|
|
121
145
|
}
|
|
122
146
|
|