@team-agent/installer 0.3.12 → 0.3.13
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 +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +90 -4
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/lifecycle/launch.rs +158 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +229 -28
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +135 -8
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- package/package.json +4 -4
|
@@ -383,6 +383,7 @@ struct LeaderTarget {
|
|
|
383
383
|
current_command: Option<String>,
|
|
384
384
|
current_path: Option<PathBuf>,
|
|
385
385
|
active: bool,
|
|
386
|
+
pane_pid: Option<u32>,
|
|
386
387
|
leader_env: BTreeMap<String, String>,
|
|
387
388
|
provider: Option<Provider>,
|
|
388
389
|
leader_session_uuid: Option<LeaderSessionUuid>,
|
|
@@ -394,7 +395,7 @@ impl LeaderTarget {
|
|
|
394
395
|
if !info.active {
|
|
395
396
|
return None;
|
|
396
397
|
}
|
|
397
|
-
let provider =
|
|
398
|
+
let provider = super::attribute_pane_provider(info);
|
|
398
399
|
let leader_session_uuid = info
|
|
399
400
|
.leader_env
|
|
400
401
|
.get("TEAM_AGENT_LEADER_SESSION_UUID")
|
|
@@ -411,6 +412,7 @@ impl LeaderTarget {
|
|
|
411
412
|
current_command: info.current_command.clone(),
|
|
412
413
|
current_path: info.current_path.clone(),
|
|
413
414
|
active: info.active,
|
|
415
|
+
pane_pid: info.pane_pid,
|
|
414
416
|
leader_env: info.leader_env.clone(),
|
|
415
417
|
provider,
|
|
416
418
|
leader_session_uuid,
|
|
@@ -508,8 +510,26 @@ impl OwnerIdentity {
|
|
|
508
510
|
fn target_from_value(value: &Value) -> Option<LeaderTarget> {
|
|
509
511
|
let pane_id = get_str(value, "pane_id").or_else(|| get_str(value, "pane"))?;
|
|
510
512
|
let current_command = get_str(value, "pane_current_command").or_else(|| get_str(value, "current_command"));
|
|
511
|
-
let provider = current_command.as_deref().and_then(leader_command_provider);
|
|
512
513
|
let leader_env = map_env(value.get("leader_env").or_else(|| value.get("env")));
|
|
514
|
+
let pane_pid = get_u32(value, "pane_pid");
|
|
515
|
+
let provider = super::attribute_pane_provider(&PaneInfo {
|
|
516
|
+
pane_id: PaneId::new(pane_id.clone()),
|
|
517
|
+
session: get_str(value, "session_name")
|
|
518
|
+
.or_else(|| get_str(value, "session"))
|
|
519
|
+
.map(SessionName::new)
|
|
520
|
+
.unwrap_or_else(|| SessionName::new("")),
|
|
521
|
+
window_index: get_u32(value, "window_index"),
|
|
522
|
+
window_name: get_str(value, "window_name").map(WindowName::new),
|
|
523
|
+
pane_index: get_u32(value, "pane_index"),
|
|
524
|
+
tty: get_str(value, "pane_tty").or_else(|| get_str(value, "tty")),
|
|
525
|
+
current_command: current_command.clone(),
|
|
526
|
+
current_path: get_str(value, "pane_current_path")
|
|
527
|
+
.or_else(|| get_str(value, "current_path"))
|
|
528
|
+
.map(PathBuf::from),
|
|
529
|
+
active: value.get("active").and_then(Value::as_bool).unwrap_or(true),
|
|
530
|
+
pane_pid,
|
|
531
|
+
leader_env: leader_env.clone(),
|
|
532
|
+
});
|
|
513
533
|
let leader_session_uuid = get_str(value, "leader_session_uuid")
|
|
514
534
|
.or_else(|| leader_env.get("TEAM_AGENT_LEADER_SESSION_UUID").cloned())
|
|
515
535
|
.and_then(|raw| serde_json::from_value(Value::String(raw)).ok());
|
|
@@ -528,6 +548,7 @@ fn target_from_value(value: &Value) -> Option<LeaderTarget> {
|
|
|
528
548
|
.or_else(|| get_str(value, "current_path"))
|
|
529
549
|
.map(PathBuf::from),
|
|
530
550
|
active: value.get("active").and_then(Value::as_bool).unwrap_or(true),
|
|
551
|
+
pane_pid,
|
|
531
552
|
leader_env,
|
|
532
553
|
provider,
|
|
533
554
|
leader_session_uuid,
|
|
@@ -584,21 +605,6 @@ fn path_in_workspace(path: &Path, workspace: &Path) -> bool {
|
|
|
584
605
|
cwd == root || cwd.starts_with(root)
|
|
585
606
|
}
|
|
586
607
|
|
|
587
|
-
fn leader_command_provider(command: &str) -> Option<Provider> {
|
|
588
|
-
let lower = command.to_ascii_lowercase();
|
|
589
|
-
if lower.contains("claude") {
|
|
590
|
-
Some(Provider::ClaudeCode)
|
|
591
|
-
} else if lower.contains("codex") {
|
|
592
|
-
Some(Provider::Codex)
|
|
593
|
-
} else if lower.contains("copilot") {
|
|
594
|
-
Some(Provider::Copilot)
|
|
595
|
-
} else if lower.contains("fake") {
|
|
596
|
-
Some(Provider::Fake)
|
|
597
|
-
} else {
|
|
598
|
-
None
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
608
|
fn target_matches_owner_identity(target: &LeaderTarget, identity: &OwnerIdentity) -> bool {
|
|
603
609
|
identity
|
|
604
610
|
.pane_id
|
|
@@ -624,6 +630,7 @@ fn pane_info_value(
|
|
|
624
630
|
"pane_index": info.pane_index,
|
|
625
631
|
"pane_tty": info.tty,
|
|
626
632
|
"pane_current_command": info.current_command,
|
|
633
|
+
"pane_pid": info.pane_pid,
|
|
627
634
|
"pane_current_path": info.current_path.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
628
635
|
"fingerprint": info.leader_env.get("TEAM_AGENT_MACHINE_FINGERPRINT"),
|
|
629
636
|
"provider": provider.map(provider_wire),
|
|
@@ -1100,5 +1107,15 @@ fn get_str(value: &Value, key: &str) -> Option<String> {
|
|
|
1100
1107
|
.filter(|s| !s.is_empty())
|
|
1101
1108
|
}
|
|
1102
1109
|
|
|
1110
|
+
fn get_u32(value: &Value, key: &str) -> Option<u32> {
|
|
1111
|
+
value
|
|
1112
|
+
.get(key)
|
|
1113
|
+
.and_then(|raw| {
|
|
1114
|
+
raw.as_u64()
|
|
1115
|
+
.or_else(|| raw.as_i64().and_then(|n| u64::try_from(n).ok()))
|
|
1116
|
+
})
|
|
1117
|
+
.and_then(|n| u32::try_from(n).ok())
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1103
1120
|
#[cfg(test)]
|
|
1104
1121
|
mod tests;
|
|
@@ -9,7 +9,7 @@ use crate::model::ids::AgentId;
|
|
|
9
9
|
use crate::model::permissions::{self, AgentPermissionInput};
|
|
10
10
|
use crate::model::yaml::{self, Value};
|
|
11
11
|
use crate::state::persist::{load_runtime_state, save_runtime_state};
|
|
12
|
-
use crate::transport::{SessionName, Target, Transport, WindowName};
|
|
12
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
13
13
|
|
|
14
14
|
use super::*;
|
|
15
15
|
|
|
@@ -595,7 +595,7 @@ fn merge_workspace_team_state_with_key(
|
|
|
595
595
|
launched_key: &str,
|
|
596
596
|
) -> serde_json::Value {
|
|
597
597
|
let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
|
|
598
|
-
let mut teams =
|
|
598
|
+
let mut teams = existing
|
|
599
599
|
.get("teams")
|
|
600
600
|
.and_then(serde_json::Value::as_object)
|
|
601
601
|
.cloned()
|
|
@@ -638,6 +638,41 @@ fn merge_workspace_team_state_with_key(
|
|
|
638
638
|
serde_json::Value::Object(merged)
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
#[cfg(test)]
|
|
642
|
+
mod merge_workspace_team_state_with_key_tests {
|
|
643
|
+
use super::*;
|
|
644
|
+
use serde_json::json;
|
|
645
|
+
|
|
646
|
+
#[test]
|
|
647
|
+
fn empty_top_level_existing_session_preserves_existing_teams() {
|
|
648
|
+
let existing = json!({
|
|
649
|
+
"session_name": "",
|
|
650
|
+
"active_team_key": "parent",
|
|
651
|
+
"teams": {
|
|
652
|
+
"parent": {
|
|
653
|
+
"session_name": "team-parent",
|
|
654
|
+
"agents": {"parent_worker": {"status": "running"}}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
let launched = json!({
|
|
659
|
+
"session_name": "team-child",
|
|
660
|
+
"agents": {"child_worker": {"status": "running"}}
|
|
661
|
+
});
|
|
662
|
+
let merged = merge_workspace_team_state_with_key(&existing, &launched, "child");
|
|
663
|
+
assert_eq!(
|
|
664
|
+
merged.pointer("/teams/parent/session_name"),
|
|
665
|
+
Some(&json!("team-parent")),
|
|
666
|
+
"existing.teams must survive even when existing.session_name is empty: {merged}"
|
|
667
|
+
);
|
|
668
|
+
assert_eq!(
|
|
669
|
+
merged.pointer("/teams/child/session_name"),
|
|
670
|
+
Some(&json!("team-child")),
|
|
671
|
+
"launched team must still be inserted under its runtime key: {merged}"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
641
676
|
fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
|
|
642
677
|
let entry = launched
|
|
643
678
|
.get("teams")
|
|
@@ -849,6 +884,29 @@ fn write_invalid_leader_pane_env_warning(workspaces: &[&Path], pane_id_raw: &str
|
|
|
849
884
|
}
|
|
850
885
|
}
|
|
851
886
|
|
|
887
|
+
fn warn_ignored_owner_team_id(workspace: &Path, team_dir: &Path, runtime_team_key: &str) {
|
|
888
|
+
let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
|
|
889
|
+
return;
|
|
890
|
+
};
|
|
891
|
+
eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
|
|
892
|
+
eprintln!("Reason: owner identity is the canonical runtime team key ({runtime_team_key}), not TEAM.md front matter");
|
|
893
|
+
eprintln!("Action: remove {} from TEAM.md", ignored.field);
|
|
894
|
+
if let Err(err) = crate::event_log::EventLog::new(workspace).write(
|
|
895
|
+
"spec.field_ignored",
|
|
896
|
+
serde_json::json!({
|
|
897
|
+
"field": ignored.field,
|
|
898
|
+
"source": team_dir.join("TEAM.md").to_string_lossy().to_string(),
|
|
899
|
+
"value": ignored.value,
|
|
900
|
+
"warning": "ignored user-set owner_team_id",
|
|
901
|
+
"reason": "owner identity is derived from the canonical runtime team key",
|
|
902
|
+
"action": "remove owner_team_id from TEAM.md",
|
|
903
|
+
"runtime_team_key": runtime_team_key,
|
|
904
|
+
}),
|
|
905
|
+
) {
|
|
906
|
+
eprintln!("Warning: spec.field_ignored event write failed: {err}");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
852
910
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
853
911
|
pub(crate) enum LeaderPaneEnvState {
|
|
854
912
|
Live,
|
|
@@ -932,12 +990,13 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
932
990
|
let Some(owner) = unbound_launched_owner(launched, launched_key) else {
|
|
933
991
|
return;
|
|
934
992
|
};
|
|
935
|
-
let provider =
|
|
936
|
-
.get("
|
|
937
|
-
.and_then(|owner| owner.get("provider"))
|
|
993
|
+
let Some(provider) = owner
|
|
994
|
+
.get("provider")
|
|
938
995
|
.and_then(serde_json::Value::as_str)
|
|
939
996
|
.filter(|provider| !provider.is_empty())
|
|
940
|
-
|
|
997
|
+
else {
|
|
998
|
+
return;
|
|
999
|
+
};
|
|
941
1000
|
let owner_epoch = 1u64;
|
|
942
1001
|
let receiver = serde_json::json!({
|
|
943
1002
|
"mode": "direct_tmux",
|
|
@@ -958,12 +1017,7 @@ fn unbound_launched_owner(
|
|
|
958
1017
|
launched: &serde_json::Value,
|
|
959
1018
|
launched_key: &str,
|
|
960
1019
|
) -> Option<serde_json::Value> {
|
|
961
|
-
let provider = launched
|
|
962
|
-
.get("team_owner")
|
|
963
|
-
.and_then(|owner| owner.get("provider"))
|
|
964
|
-
.and_then(serde_json::Value::as_str)
|
|
965
|
-
.filter(|provider| !provider.is_empty())
|
|
966
|
-
.unwrap_or("codex");
|
|
1020
|
+
let provider = unbound_launched_provider(launched)?;
|
|
967
1021
|
let machine_fingerprint = launched
|
|
968
1022
|
.get("team_owner")
|
|
969
1023
|
.and_then(|owner| owner.get("machine_fingerprint"))
|
|
@@ -994,6 +1048,97 @@ fn unbound_launched_owner(
|
|
|
994
1048
|
}))
|
|
995
1049
|
}
|
|
996
1050
|
|
|
1051
|
+
fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
|
|
1052
|
+
if let Some(provider) = launched
|
|
1053
|
+
.get("team_owner")
|
|
1054
|
+
.and_then(|owner| owner.get("provider"))
|
|
1055
|
+
.and_then(serde_json::Value::as_str)
|
|
1056
|
+
.filter(|provider| !provider.is_empty())
|
|
1057
|
+
.and_then(parse_provider)
|
|
1058
|
+
.and_then(provider_wire_string)
|
|
1059
|
+
{
|
|
1060
|
+
return Some(provider);
|
|
1061
|
+
}
|
|
1062
|
+
let workspace = launched
|
|
1063
|
+
.get("workspace")
|
|
1064
|
+
.and_then(serde_json::Value::as_str)
|
|
1065
|
+
.filter(|workspace| !workspace.is_empty())?;
|
|
1066
|
+
let pane = launched
|
|
1067
|
+
.get("team_owner")
|
|
1068
|
+
.and_then(|owner| owner.get("pane_id"))
|
|
1069
|
+
.and_then(serde_json::Value::as_str)
|
|
1070
|
+
.filter(|pane| !pane.is_empty())?;
|
|
1071
|
+
let target = PaneId::new(pane);
|
|
1072
|
+
crate::tmux_backend::TmuxBackend::for_workspace(Path::new(workspace))
|
|
1073
|
+
.list_targets()
|
|
1074
|
+
.ok()?
|
|
1075
|
+
.into_iter()
|
|
1076
|
+
.find(|info| info.pane_id == target)
|
|
1077
|
+
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1078
|
+
.and_then(provider_wire_string)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
1082
|
+
serde_json::to_value(provider)
|
|
1083
|
+
.ok()
|
|
1084
|
+
.and_then(|value| value.as_str().map(str::to_string))
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#[cfg(test)]
|
|
1088
|
+
mod e22_unbound_owner_provider_tests {
|
|
1089
|
+
use super::*;
|
|
1090
|
+
|
|
1091
|
+
#[test]
|
|
1092
|
+
fn unbound_owner_preserves_explicit_copilot_provider() {
|
|
1093
|
+
let mut launched = serde_json::json!({
|
|
1094
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1095
|
+
"team_owner": {
|
|
1096
|
+
"provider": "copilot",
|
|
1097
|
+
"machine_fingerprint": "machine"
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1102
|
+
|
|
1103
|
+
assert_eq!(
|
|
1104
|
+
launched
|
|
1105
|
+
.pointer("/team_owner/provider")
|
|
1106
|
+
.and_then(serde_json::Value::as_str),
|
|
1107
|
+
Some("copilot")
|
|
1108
|
+
);
|
|
1109
|
+
assert_eq!(
|
|
1110
|
+
launched
|
|
1111
|
+
.pointer("/leader_receiver/provider")
|
|
1112
|
+
.and_then(serde_json::Value::as_str),
|
|
1113
|
+
Some("copilot")
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
#[test]
|
|
1118
|
+
fn unbound_owner_without_attributed_provider_does_not_default_codex() {
|
|
1119
|
+
let mut launched = serde_json::json!({
|
|
1120
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1121
|
+
"team_owner": {
|
|
1122
|
+
"machine_fingerprint": "machine"
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1127
|
+
|
|
1128
|
+
assert!(
|
|
1129
|
+
launched.get("leader_receiver").is_none(),
|
|
1130
|
+
"unattributed unbound owner must not seed a codex receiver: {launched}"
|
|
1131
|
+
);
|
|
1132
|
+
assert!(
|
|
1133
|
+
launched
|
|
1134
|
+
.pointer("/team_owner/provider")
|
|
1135
|
+
.and_then(serde_json::Value::as_str)
|
|
1136
|
+
!= Some("codex"),
|
|
1137
|
+
"unattributed unbound owner must not silently become codex: {launched}"
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
997
1142
|
fn owner_pane_belongs_to_other_team(
|
|
998
1143
|
existing: &serde_json::Value,
|
|
999
1144
|
launched_key: &str,
|
|
@@ -2144,6 +2289,7 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2144
2289
|
let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
|
|
2145
2290
|
runtime_team_key_for_spec(&agents_dir.join("team.spec.yaml"), &spec, &session_name)
|
|
2146
2291
|
});
|
|
2292
|
+
warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
|
|
2147
2293
|
// E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
|
|
2148
2294
|
// Bug2:原子写(tmp+rename),避免半截 spec。
|
|
2149
2295
|
let spec_path = crate::model::paths::runtime_spec_path(&workspace, &state_team_key);
|
|
@@ -212,35 +212,49 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
212
212
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
213
213
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
214
214
|
}
|
|
215
|
-
let mut
|
|
215
|
+
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
216
|
+
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
217
|
+
// B5 restart isolation loop: per-agent spawn failures must be recorded and
|
|
218
|
+
// isolated here. Do not reintroduce `?`, `break`, or `return` inside this loop.
|
|
219
|
+
// BEGIN_B5_RESTART_ISOLATION_LOOP
|
|
216
220
|
for decision in &plan.decisions {
|
|
217
|
-
let agent = state
|
|
221
|
+
let Some(agent) = state
|
|
218
222
|
.get("agents")
|
|
219
223
|
.and_then(|v| v.get(decision.agent_id.as_str()))
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
.cloned()
|
|
225
|
+
else {
|
|
226
|
+
let error = format!("agent {} not found for restart", decision.agent_id);
|
|
227
|
+
mark_agent_restart_failed(&mut state, decision, &error);
|
|
228
|
+
let _ = write_restart_agent_failed_event(&selected.run_workspace, decision, "spawn", &error);
|
|
229
|
+
failed_agents.push(restart_failed_agent(decision, "spawn", error));
|
|
230
|
+
continue;
|
|
231
|
+
};
|
|
227
232
|
let session_id = if matches!(decision.restart_mode, StartMode::Resumed) {
|
|
228
233
|
decision.session_id.as_ref()
|
|
229
234
|
} else {
|
|
230
235
|
None
|
|
231
236
|
};
|
|
232
|
-
let session_live = session_live_or_default(transport, &session_name, false);
|
|
237
|
+
let mut session_live = session_live_or_default(transport, &session_name, false);
|
|
233
238
|
if !session_live {
|
|
234
|
-
if let Some(previous) =
|
|
235
|
-
|
|
239
|
+
if let Some(previous) = successful_agents.pop() {
|
|
240
|
+
let error = format!(
|
|
236
241
|
"session_disappeared_after_spawn: provider_resume_exited for {}; session {} disappeared before spawning {}",
|
|
237
|
-
previous,
|
|
242
|
+
previous.agent_id,
|
|
238
243
|
session_name.as_str(),
|
|
239
244
|
decision.agent_id
|
|
240
|
-
)
|
|
245
|
+
);
|
|
246
|
+
mark_agent_restart_failed(&mut state, &previous, &error);
|
|
247
|
+
let _ = write_restart_agent_failed_event(
|
|
248
|
+
&selected.run_workspace,
|
|
249
|
+
&previous,
|
|
250
|
+
"resume",
|
|
251
|
+
&error,
|
|
252
|
+
);
|
|
253
|
+
failed_agents.push(restart_failed_agent(&previous, "resume", error));
|
|
254
|
+
session_live = false;
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
|
-
let spawn = spawn_agent_window(
|
|
257
|
+
let spawn = match spawn_agent_window(
|
|
244
258
|
&selected.run_workspace,
|
|
245
259
|
&session_name,
|
|
246
260
|
&decision.agent_id,
|
|
@@ -250,10 +264,33 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
250
264
|
transport,
|
|
251
265
|
Some(&safety),
|
|
252
266
|
Some(spec_workspace),
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
) {
|
|
268
|
+
Ok(spawn) => spawn,
|
|
269
|
+
Err(error) => {
|
|
270
|
+
let error = error.to_string();
|
|
271
|
+
mark_agent_restart_failed(&mut state, decision, &error);
|
|
272
|
+
let _ = write_restart_agent_failed_event(&selected.run_workspace, decision, "spawn", &error);
|
|
273
|
+
failed_agents.push(restart_failed_agent(decision, "spawn", error));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
|
|
278
|
+
.and_then(|_| {
|
|
279
|
+
mark_agent_respawned(&mut state, &decision.agent_id, &spawn, transport, &safety)
|
|
280
|
+
})
|
|
281
|
+
{
|
|
282
|
+
let error = error.to_string();
|
|
283
|
+
mark_agent_restart_failed(&mut state, decision, &error);
|
|
284
|
+
let _ = write_restart_agent_failed_event(
|
|
285
|
+
&selected.run_workspace,
|
|
286
|
+
decision,
|
|
287
|
+
"readiness",
|
|
288
|
+
&error,
|
|
289
|
+
);
|
|
290
|
+
failed_agents.push(restart_failed_agent(decision, "readiness", error));
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
successful_agents.push(decision.clone());
|
|
257
294
|
if let Some(agent) = state
|
|
258
295
|
.get_mut("agents")
|
|
259
296
|
.and_then(serde_json::Value::as_object_mut)
|
|
@@ -263,14 +300,31 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
263
300
|
persist_effective_approval_policy_for_restart(agent, &safety);
|
|
264
301
|
}
|
|
265
302
|
}
|
|
303
|
+
// END_B5_RESTART_ISOLATION_LOOP
|
|
266
304
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
267
305
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
306
|
+
if successful_agents.is_empty() && !failed_agents.is_empty() {
|
|
307
|
+
let attach_commands = Vec::new();
|
|
308
|
+
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
309
|
+
write_restart_completed_event(
|
|
310
|
+
&selected.run_workspace,
|
|
311
|
+
&successful_agents,
|
|
312
|
+
&failed_agents,
|
|
313
|
+
"fail",
|
|
314
|
+
)?;
|
|
315
|
+
return Ok(RestartReport::Failed {
|
|
316
|
+
session_name,
|
|
317
|
+
failed_agents,
|
|
318
|
+
next_actions,
|
|
319
|
+
attach_commands,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
268
322
|
let coordinator_started = start_coordinator_for_workspace(&selected.run_workspace)?;
|
|
269
323
|
wait_restart_readiness_or_timeout(
|
|
270
324
|
&selected.run_workspace,
|
|
271
325
|
&state,
|
|
272
326
|
&session_name,
|
|
273
|
-
&
|
|
327
|
+
&successful_agents,
|
|
274
328
|
transport,
|
|
275
329
|
restart_readiness_deadline(readiness_deadline_ms),
|
|
276
330
|
restart_readiness_poll_interval(),
|
|
@@ -278,14 +332,37 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
278
332
|
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
279
333
|
&selected.run_workspace,
|
|
280
334
|
&session_name,
|
|
281
|
-
|
|
335
|
+
successful_agents
|
|
282
336
|
.iter()
|
|
283
337
|
.map(|decision| decision.agent_id.as_str()),
|
|
284
338
|
);
|
|
285
|
-
let next_actions = attach_commands.clone();
|
|
339
|
+
let mut next_actions = attach_commands.clone();
|
|
340
|
+
if !failed_agents.is_empty() {
|
|
341
|
+
next_actions.extend(restart_failure_next_actions(&failed_agents));
|
|
342
|
+
write_restart_completed_event(
|
|
343
|
+
&selected.run_workspace,
|
|
344
|
+
&successful_agents,
|
|
345
|
+
&failed_agents,
|
|
346
|
+
"partial",
|
|
347
|
+
)?;
|
|
348
|
+
return Ok(RestartReport::Partial {
|
|
349
|
+
session_name,
|
|
350
|
+
agents: successful_agents,
|
|
351
|
+
failed_agents,
|
|
352
|
+
coordinator_started,
|
|
353
|
+
next_actions,
|
|
354
|
+
attach_commands,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
write_restart_completed_event(
|
|
358
|
+
&selected.run_workspace,
|
|
359
|
+
&successful_agents,
|
|
360
|
+
&failed_agents,
|
|
361
|
+
"ok",
|
|
362
|
+
)?;
|
|
286
363
|
Ok(RestartReport::Restarted {
|
|
287
364
|
session_name,
|
|
288
|
-
agents:
|
|
365
|
+
agents: successful_agents,
|
|
289
366
|
coordinator_started,
|
|
290
367
|
next_actions,
|
|
291
368
|
attach_commands,
|
|
@@ -749,6 +826,134 @@ fn mark_agent_respawned(
|
|
|
749
826
|
Ok(())
|
|
750
827
|
}
|
|
751
828
|
|
|
829
|
+
fn restart_failed_agent(
|
|
830
|
+
decision: &RestartedAgent,
|
|
831
|
+
phase: impl Into<String>,
|
|
832
|
+
error: String,
|
|
833
|
+
) -> RestartFailedAgent {
|
|
834
|
+
RestartFailedAgent {
|
|
835
|
+
agent_id: decision.agent_id.clone(),
|
|
836
|
+
restart_mode: decision.restart_mode,
|
|
837
|
+
decision: decision.decision,
|
|
838
|
+
session_id: decision.session_id.clone(),
|
|
839
|
+
phase: phase.into(),
|
|
840
|
+
error,
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
fn mark_agent_restart_failed(
|
|
845
|
+
state: &mut serde_json::Value,
|
|
846
|
+
decision: &RestartedAgent,
|
|
847
|
+
error: &str,
|
|
848
|
+
) {
|
|
849
|
+
let Some(agent) = state
|
|
850
|
+
.get_mut("agents")
|
|
851
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
852
|
+
.and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
|
|
853
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
854
|
+
else {
|
|
855
|
+
return;
|
|
856
|
+
};
|
|
857
|
+
agent.insert("status".to_string(), serde_json::json!("failed"));
|
|
858
|
+
agent.insert("restart_error".to_string(), serde_json::json!(error));
|
|
859
|
+
agent.insert(
|
|
860
|
+
"restart_failed_at".to_string(),
|
|
861
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
862
|
+
);
|
|
863
|
+
agent.remove("pane_id");
|
|
864
|
+
agent.remove("pane_pid");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
fn write_restart_agent_failed_event(
|
|
868
|
+
workspace: &Path,
|
|
869
|
+
decision: &RestartedAgent,
|
|
870
|
+
phase: &str,
|
|
871
|
+
error: &str,
|
|
872
|
+
) -> Result<(), LifecycleError> {
|
|
873
|
+
crate::event_log::EventLog::new(workspace)
|
|
874
|
+
.write(
|
|
875
|
+
"restart.agent_failed",
|
|
876
|
+
serde_json::json!({
|
|
877
|
+
"agent_id": decision.agent_id.as_str(),
|
|
878
|
+
"restart_mode": start_mode_wire(decision.restart_mode),
|
|
879
|
+
"decision": resume_decision_wire(decision.decision),
|
|
880
|
+
"session_id": decision.session_id.as_ref().map(|session| session.as_str()),
|
|
881
|
+
"phase": phase,
|
|
882
|
+
"error": error,
|
|
883
|
+
"action": format!(
|
|
884
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
885
|
+
decision.agent_id,
|
|
886
|
+
decision.agent_id
|
|
887
|
+
),
|
|
888
|
+
"log": format!(
|
|
889
|
+
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
890
|
+
decision.agent_id
|
|
891
|
+
),
|
|
892
|
+
}),
|
|
893
|
+
)
|
|
894
|
+
.map(|_| ())
|
|
895
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
fn write_restart_completed_event(
|
|
899
|
+
workspace: &Path,
|
|
900
|
+
successful_agents: &[RestartedAgent],
|
|
901
|
+
failed_agents: &[RestartFailedAgent],
|
|
902
|
+
rc: &str,
|
|
903
|
+
) -> Result<(), LifecycleError> {
|
|
904
|
+
crate::event_log::EventLog::new(workspace)
|
|
905
|
+
.write(
|
|
906
|
+
"restart.completed",
|
|
907
|
+
serde_json::json!({
|
|
908
|
+
"rc": rc,
|
|
909
|
+
"status": rc,
|
|
910
|
+
"successful_agents": successful_agents
|
|
911
|
+
.iter()
|
|
912
|
+
.map(|agent| agent.agent_id.as_str())
|
|
913
|
+
.collect::<Vec<_>>(),
|
|
914
|
+
"failed_agents": failed_agents
|
|
915
|
+
.iter()
|
|
916
|
+
.map(|failure| serde_json::json!({
|
|
917
|
+
"agent_id": failure.agent_id.as_str(),
|
|
918
|
+
"phase": failure.phase,
|
|
919
|
+
"error": failure.error,
|
|
920
|
+
}))
|
|
921
|
+
.collect::<Vec<_>>(),
|
|
922
|
+
}),
|
|
923
|
+
)
|
|
924
|
+
.map(|_| ())
|
|
925
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<String> {
|
|
929
|
+
failed_agents
|
|
930
|
+
.iter()
|
|
931
|
+
.map(|failure| {
|
|
932
|
+
format!(
|
|
933
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
934
|
+
failure.agent_id, failure.agent_id
|
|
935
|
+
)
|
|
936
|
+
})
|
|
937
|
+
.collect()
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
fn start_mode_wire(mode: StartMode) -> &'static str {
|
|
941
|
+
match mode {
|
|
942
|
+
StartMode::Resumed => "resumed",
|
|
943
|
+
StartMode::Fresh => "fresh",
|
|
944
|
+
StartMode::FreshAfterMissingRollout => "fresh_after_missing_rollout",
|
|
945
|
+
StartMode::Noop => "noop",
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
fn resume_decision_wire(decision: ResumeDecision) -> &'static str {
|
|
950
|
+
match decision {
|
|
951
|
+
ResumeDecision::Resume => "resume",
|
|
952
|
+
ResumeDecision::FreshStart => "fresh_start",
|
|
953
|
+
ResumeDecision::Refuse => "refuse",
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
752
957
|
fn write_restart_resume_decision_events(
|
|
753
958
|
workspace: &Path,
|
|
754
959
|
state: &serde_json::Value,
|
|
@@ -768,11 +973,7 @@ fn write_restart_resume_decision_events(
|
|
|
768
973
|
.filter(|s| !s.is_empty())
|
|
769
974
|
.map(|s| s.to_string());
|
|
770
975
|
let session_id = decision.session_id.as_ref().map(|s| s.as_str().to_string());
|
|
771
|
-
let decision_wire =
|
|
772
|
-
ResumeDecision::Resume => "resume",
|
|
773
|
-
ResumeDecision::FreshStart => "fresh_start",
|
|
774
|
-
ResumeDecision::Refuse => "refuse",
|
|
775
|
-
};
|
|
976
|
+
let decision_wire = resume_decision_wire(decision.decision);
|
|
776
977
|
write_restart_resume_decision_event(
|
|
777
978
|
workspace,
|
|
778
979
|
decision.agent_id.as_str(),
|