@team-agent/installer 0.3.15 → 0.3.16
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/leader.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +198 -30
- package/crates/team-agent/src/cli/status.rs +5 -1
- package/crates/team-agent/src/cli/status_port.rs +24 -0
- package/crates/team-agent/src/cli/tests/base.rs +21 -3
- package/crates/team-agent/src/cli/tests/divergence.rs +6 -6
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +127 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +25 -0
- package/crates/team-agent/src/cli/types.rs +2 -0
- package/crates/team-agent/src/leader/start.rs +272 -8
- package/crates/team-agent/src/leader/tests/basics.rs +1 -0
- package/crates/team-agent/src/leader/tests/identity.rs +121 -25
- package/crates/team-agent/src/leader/types.rs +9 -0
- package/crates/team-agent/src/lifecycle/launch.rs +72 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +7 -3
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +63 -0
- package/package.json +4 -4
|
@@ -858,6 +858,7 @@ fn save_launched_team_state_for_key(
|
|
|
858
858
|
);
|
|
859
859
|
}
|
|
860
860
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
861
|
+
preserve_existing_leader_topology(&existing, &launched_key, &mut launched);
|
|
861
862
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
862
863
|
drop_bare_worker_seeded_owner(&mut launched, &launched_key);
|
|
863
864
|
let merged = if team_key.is_some() {
|
|
@@ -871,6 +872,28 @@ fn save_launched_team_state_for_key(
|
|
|
871
872
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
872
873
|
}
|
|
873
874
|
|
|
875
|
+
fn preserve_existing_leader_topology(
|
|
876
|
+
existing: &serde_json::Value,
|
|
877
|
+
launched_key: &str,
|
|
878
|
+
launched: &mut serde_json::Value,
|
|
879
|
+
) {
|
|
880
|
+
let Some(obj) = launched.as_object_mut() else {
|
|
881
|
+
return;
|
|
882
|
+
};
|
|
883
|
+
let existing_team = existing
|
|
884
|
+
.get("teams")
|
|
885
|
+
.and_then(serde_json::Value::as_object)
|
|
886
|
+
.and_then(|teams| teams.get(launched_key))
|
|
887
|
+
.unwrap_or(existing);
|
|
888
|
+
for key in ["is_external_leader", "leader_client"] {
|
|
889
|
+
if !obj.contains_key(key) {
|
|
890
|
+
if let Some(value) = existing_team.get(key).or_else(|| existing.get(key)) {
|
|
891
|
+
obj.insert(key.to_string(), value.clone());
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
874
897
|
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
875
898
|
if has_positive_caller_leader_env() {
|
|
876
899
|
return;
|
|
@@ -2834,7 +2857,12 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
2834
2857
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
2835
2858
|
// bare Ready.
|
|
2836
2859
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
2837
|
-
let attach_windows =
|
|
2860
|
+
let attach_windows = load_runtime_state(&workspace)
|
|
2861
|
+
.ok()
|
|
2862
|
+
.map(|state| {
|
|
2863
|
+
attach_window_names_with_managed_leader(&state, started_attach_window_names(&launch.started))
|
|
2864
|
+
})
|
|
2865
|
+
.unwrap_or_else(|| started_attach_window_names(&launch.started));
|
|
2838
2866
|
let attach_commands = attach_commands_for_runtime_windows(
|
|
2839
2867
|
launch.tmux_endpoint.as_deref(),
|
|
2840
2868
|
&workspace,
|
|
@@ -2934,8 +2962,33 @@ fn started_attach_window_names(started: &[StartedAgent]) -> Vec<String> {
|
|
|
2934
2962
|
windows
|
|
2935
2963
|
}
|
|
2936
2964
|
|
|
2965
|
+
pub(crate) fn attach_window_names_for_state_agents<'a>(
|
|
2966
|
+
state: &serde_json::Value,
|
|
2967
|
+
agent_ids: impl IntoIterator<Item = &'a str>,
|
|
2968
|
+
) -> Vec<String> {
|
|
2969
|
+
let windows = agent_ids
|
|
2970
|
+
.into_iter()
|
|
2971
|
+
.map(|agent_id| {
|
|
2972
|
+
state
|
|
2973
|
+
.get("agents")
|
|
2974
|
+
.and_then(serde_json::Value::as_object)
|
|
2975
|
+
.and_then(|agents| agents.get(agent_id))
|
|
2976
|
+
.and_then(|agent| {
|
|
2977
|
+
agent
|
|
2978
|
+
.get("layout_window")
|
|
2979
|
+
.or_else(|| agent.get("window"))
|
|
2980
|
+
.and_then(serde_json::Value::as_str)
|
|
2981
|
+
.filter(|window| !window.is_empty())
|
|
2982
|
+
})
|
|
2983
|
+
.unwrap_or(agent_id)
|
|
2984
|
+
.to_string()
|
|
2985
|
+
})
|
|
2986
|
+
.collect::<Vec<_>>();
|
|
2987
|
+
attach_window_names_with_managed_leader(state, windows)
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2937
2990
|
fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
2938
|
-
let
|
|
2991
|
+
let windows = state
|
|
2939
2992
|
.get("agents")
|
|
2940
2993
|
.and_then(serde_json::Value::as_object)
|
|
2941
2994
|
.map(|agents| {
|
|
@@ -2952,11 +3005,28 @@ fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
|
|
|
2952
3005
|
.collect::<Vec<_>>()
|
|
2953
3006
|
})
|
|
2954
3007
|
.unwrap_or_default();
|
|
3008
|
+
attach_window_names_with_managed_leader(state, windows)
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
fn attach_window_names_with_managed_leader(
|
|
3012
|
+
state: &serde_json::Value,
|
|
3013
|
+
mut windows: Vec<String>,
|
|
3014
|
+
) -> Vec<String> {
|
|
3015
|
+
if state_uses_managed_leader(state) {
|
|
3016
|
+
windows.push("leader".to_string());
|
|
3017
|
+
}
|
|
2955
3018
|
windows.sort();
|
|
2956
3019
|
windows.dedup();
|
|
2957
3020
|
windows
|
|
2958
3021
|
}
|
|
2959
3022
|
|
|
3023
|
+
fn state_uses_managed_leader(state: &serde_json::Value) -> bool {
|
|
3024
|
+
state
|
|
3025
|
+
.get("is_external_leader")
|
|
3026
|
+
.and_then(serde_json::Value::as_bool)
|
|
3027
|
+
.is_some_and(|external| !external)
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2960
3030
|
/// BUG-7 helper: derive a [`QuickStartReadiness`] verdict from the just-written
|
|
2961
3031
|
/// runtime state. Reads `agents[*].status`; any non-`running` agent flips the
|
|
2962
3032
|
/// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
|
|
@@ -395,13 +395,17 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
395
395
|
restart_readiness_deadline(readiness_deadline_ms),
|
|
396
396
|
restart_readiness_poll_interval(),
|
|
397
397
|
)?;
|
|
398
|
-
let
|
|
399
|
-
&
|
|
400
|
-
&session_name,
|
|
398
|
+
let attach_windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
|
|
399
|
+
&state,
|
|
401
400
|
successful_agents
|
|
402
401
|
.iter()
|
|
403
402
|
.map(|decision| decision.agent_id.as_str()),
|
|
404
403
|
);
|
|
404
|
+
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
405
|
+
&selected.run_workspace,
|
|
406
|
+
&session_name,
|
|
407
|
+
attach_windows.iter().map(String::as_str),
|
|
408
|
+
);
|
|
405
409
|
let mut next_actions = attach_commands.clone();
|
|
406
410
|
if !failed_agents.is_empty() {
|
|
407
411
|
next_actions.extend(restart_failure_next_actions(&failed_agents));
|
|
@@ -1205,6 +1205,69 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
|
1205
1205
|
assert_eq!(state["tmux_socket"], json!(endpoint));
|
|
1206
1206
|
}
|
|
1207
1207
|
|
|
1208
|
+
#[test]
|
|
1209
|
+
fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command() {
|
|
1210
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1211
|
+
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1212
|
+
seed_healthy_coordinator(workspace);
|
|
1213
|
+
crate::state::persist::save_runtime_state(
|
|
1214
|
+
workspace,
|
|
1215
|
+
&json!({
|
|
1216
|
+
"active_team_key": "teamdir",
|
|
1217
|
+
"session_name": "team-teamdir",
|
|
1218
|
+
"is_external_leader": false,
|
|
1219
|
+
"leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
|
|
1220
|
+
"leader_receiver": {"mode": "direct_tmux", "status": "attached", "pane_id": "%42"},
|
|
1221
|
+
"team_owner": {"pane_id": "%42", "owner_epoch": 1},
|
|
1222
|
+
"agents": {},
|
|
1223
|
+
"teams": {
|
|
1224
|
+
"teamdir": {
|
|
1225
|
+
"session_name": "team-teamdir",
|
|
1226
|
+
"is_external_leader": false,
|
|
1227
|
+
"leader_client": {"diagnostic_only": true, "attach_mode": "attach-session"},
|
|
1228
|
+
"agents": {}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}),
|
|
1232
|
+
)
|
|
1233
|
+
.expect("seed managed leader state");
|
|
1234
|
+
let transport = OfflineTransport::new();
|
|
1235
|
+
|
|
1236
|
+
let report = quick_start_with_transport(&team, None, true, true, None, &transport)
|
|
1237
|
+
.expect("quick_start_with_transport must reach Ready");
|
|
1238
|
+
let attach_commands = match report {
|
|
1239
|
+
QuickStartReport::Ready { attach_commands, .. } => attach_commands,
|
|
1240
|
+
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
assert!(
|
|
1244
|
+
attach_commands.iter().any(|cmd| cmd.contains(":leader")),
|
|
1245
|
+
"managed topology quick-start output must include the leader window attach command: {attach_commands:?}"
|
|
1246
|
+
);
|
|
1247
|
+
let (_raw, state) = raw_runtime_state(workspace);
|
|
1248
|
+
assert_eq!(state["is_external_leader"], json!(false));
|
|
1249
|
+
assert_eq!(state["leader_client"]["diagnostic_only"], json!(true));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
#[test]
|
|
1253
|
+
fn attach_window_names_for_state_agents_include_managed_leader_and_layout_windows() {
|
|
1254
|
+
let state = json!({
|
|
1255
|
+
"is_external_leader": false,
|
|
1256
|
+
"agents": {
|
|
1257
|
+
"w1": {"layout_window": "team-w1", "window": "w1"},
|
|
1258
|
+
"w2": {"layout_window": "team-w1", "window": "w2"},
|
|
1259
|
+
"w4": {"layout_window": "team-w2", "window": "w4"}
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
let windows = crate::lifecycle::launch::attach_window_names_for_state_agents(
|
|
1264
|
+
&state,
|
|
1265
|
+
["w1", "w2", "w4"].into_iter(),
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
assert_eq!(windows, vec!["leader", "team-w1", "team-w2"]);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1208
1271
|
#[test]
|
|
1209
1272
|
fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
1210
1273
|
let roles = ["w1", "w2"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.16",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.16",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.16"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|