@team-agent/installer 0.3.33 → 0.3.35
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/mod.rs +13 -7
- package/crates/team-agent/src/cli/status_port.rs +5 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +53 -0
- package/crates/team-agent/src/leader/start.rs +129 -5
- package/crates/team-agent/src/lifecycle/restart/common.rs +103 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1 -7
- package/crates/team-agent/src/lifecycle/restart.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +101 -0
- package/crates/team-agent/src/provider/adapter.rs +0 -1
- package/crates/team-agent/src/provider/tests/adapter.rs +25 -1
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -158,13 +158,7 @@ pub mod lifecycle_port {
|
|
|
158
158
|
crate::leader::LeaderLaunchStatus::Detached => true,
|
|
159
159
|
crate::leader::LeaderLaunchStatus::NotStarted => false,
|
|
160
160
|
};
|
|
161
|
-
let leader_attach_command =
|
|
162
|
-
None
|
|
163
|
-
} else {
|
|
164
|
-
plan.session_name.as_ref().and_then(|session| {
|
|
165
|
-
crate::tmux_backend::attach_command_for_workspace(cwd, session, "leader")
|
|
166
|
-
})
|
|
167
|
-
};
|
|
161
|
+
let leader_attach_command = leader_attach_command_for_plan(cwd, &plan);
|
|
168
162
|
Ok(json!({
|
|
169
163
|
"ok": ok,
|
|
170
164
|
"provider": provider,
|
|
@@ -182,6 +176,18 @@ pub mod lifecycle_port {
|
|
|
182
176
|
"session_name": plan.session_name.as_ref().map(|session| session.as_str().to_string()),
|
|
183
177
|
}))
|
|
184
178
|
}
|
|
179
|
+
|
|
180
|
+
pub(crate) fn leader_attach_command_for_plan(
|
|
181
|
+
cwd: &Path,
|
|
182
|
+
plan: &crate::leader::LeaderStartPlan,
|
|
183
|
+
) -> Option<String> {
|
|
184
|
+
if plan.is_external_leader {
|
|
185
|
+
return None;
|
|
186
|
+
}
|
|
187
|
+
let session = plan.session_name.as_ref()?;
|
|
188
|
+
let window = plan.leader_window.as_ref()?;
|
|
189
|
+
crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
|
|
190
|
+
}
|
|
185
191
|
/// `runtime.shutdown`(`cmd_shutdown`)。
|
|
186
192
|
pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
|
|
187
193
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
@@ -53,11 +53,15 @@ use rusqlite::params;
|
|
|
53
53
|
let leader_attach_command = if is_external_leader {
|
|
54
54
|
None
|
|
55
55
|
} else {
|
|
56
|
+
let window_name = state
|
|
57
|
+
.pointer("/leader_receiver/window_name")
|
|
58
|
+
.and_then(Value::as_str)
|
|
59
|
+
.unwrap_or("leader");
|
|
56
60
|
session_name.as_str().and_then(|session| {
|
|
57
61
|
crate::tmux_backend::attach_command_for_workspace(
|
|
58
62
|
workspace,
|
|
59
63
|
&crate::transport::SessionName::new(session.to_string()),
|
|
60
|
-
|
|
64
|
+
window_name,
|
|
61
65
|
)
|
|
62
66
|
})
|
|
63
67
|
};
|
|
@@ -86,6 +86,59 @@ use super::*;
|
|
|
86
86
|
let _ = std::fs::remove_dir_all(&ws);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
#[test]
|
|
90
|
+
fn status_port_managed_attach_command_uses_receiver_window_name() {
|
|
91
|
+
let ws = seed_status_workspace();
|
|
92
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
93
|
+
if let Some(obj) = state.as_object_mut() {
|
|
94
|
+
obj.insert("is_external_leader".to_string(), json!(false));
|
|
95
|
+
obj.insert("session_name".to_string(), json!("team-current"));
|
|
96
|
+
obj.insert(
|
|
97
|
+
"leader_receiver".to_string(),
|
|
98
|
+
json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
102
|
+
|
|
103
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
104
|
+
|
|
105
|
+
let attach = v["leader_attach_command"]
|
|
106
|
+
.as_str()
|
|
107
|
+
.expect("managed status includes attach command");
|
|
108
|
+
assert!(attach.contains("attach -t team-current:claude_code"), "{attach}");
|
|
109
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn leader_attach_command_for_plan_uses_plan_leader_window() {
|
|
114
|
+
let ws = tmp_workspace();
|
|
115
|
+
let plan = crate::leader::LeaderStartPlan {
|
|
116
|
+
mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
|
|
117
|
+
provider: crate::provider::Provider::ClaudeCode,
|
|
118
|
+
workspace: ws.clone(),
|
|
119
|
+
socket: crate::leader::LeaderLaunchSocket::Workspace,
|
|
120
|
+
session_name: Some(crate::transport::SessionName::new(
|
|
121
|
+
"team-agent-leader-claude_code-demo".to_string(),
|
|
122
|
+
)),
|
|
123
|
+
argv: Vec::new(),
|
|
124
|
+
provider_argv: Vec::new(),
|
|
125
|
+
leader_window: Some(crate::transport::WindowName::new("claude_code")),
|
|
126
|
+
is_external_leader: false,
|
|
127
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
128
|
+
identity: None,
|
|
129
|
+
detached: false,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
|
|
133
|
+
.expect("managed plan has attach command");
|
|
134
|
+
|
|
135
|
+
assert!(
|
|
136
|
+
attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
|
|
137
|
+
"{attach}"
|
|
138
|
+
);
|
|
139
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
140
|
+
}
|
|
141
|
+
|
|
89
142
|
#[test]
|
|
90
143
|
fn status_port_missing_topology_marker_defaults_to_managed() {
|
|
91
144
|
let ws = seed_status_workspace();
|
|
@@ -7,7 +7,9 @@ use std::process::{Command, Stdio};
|
|
|
7
7
|
|
|
8
8
|
use crate::provider::{get_adapter, Provider};
|
|
9
9
|
use crate::tmux_backend::TmuxBackend;
|
|
10
|
-
use crate::transport::{
|
|
10
|
+
use crate::transport::{
|
|
11
|
+
PaneId, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
|
|
12
|
+
};
|
|
11
13
|
|
|
12
14
|
use super::helpers::{
|
|
13
15
|
provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
|
|
@@ -405,6 +407,7 @@ fn execute_managed_leader_plan(
|
|
|
405
407
|
.unwrap_or_else(|| "signal".to_string())
|
|
406
408
|
)));
|
|
407
409
|
}
|
|
410
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)?;
|
|
408
411
|
Ok(LeaderLaunchOutcome {
|
|
409
412
|
status: LeaderLaunchStatus::Exited,
|
|
410
413
|
exit_code: code,
|
|
@@ -414,7 +417,7 @@ fn execute_managed_leader_plan(
|
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
fn ensure_managed_leader_pane(
|
|
417
|
-
transport: &
|
|
420
|
+
transport: &dyn Transport,
|
|
418
421
|
session: &SessionName,
|
|
419
422
|
window: &WindowName,
|
|
420
423
|
plan: &LeaderStartPlan,
|
|
@@ -455,6 +458,39 @@ fn ensure_managed_leader_pane(
|
|
|
455
458
|
}
|
|
456
459
|
}
|
|
457
460
|
|
|
461
|
+
fn ensure_managed_provider_live_after_attach(
|
|
462
|
+
transport: &dyn Transport,
|
|
463
|
+
spawned: &SpawnResult,
|
|
464
|
+
) -> Result<(), LeaderError> {
|
|
465
|
+
let live = match transport.liveness(&spawned.pane_id) {
|
|
466
|
+
Ok(PaneLiveness::Live) => true,
|
|
467
|
+
Ok(PaneLiveness::Dead) => false,
|
|
468
|
+
Ok(PaneLiveness::Unknown) | Err(_) => managed_spawned_pane_in_targets(transport, spawned),
|
|
469
|
+
};
|
|
470
|
+
if live {
|
|
471
|
+
return Ok(());
|
|
472
|
+
}
|
|
473
|
+
Err(LeaderError::Start(format!(
|
|
474
|
+
"managed leader provider pane is not running after tmux client returned: {} {}:{}",
|
|
475
|
+
spawned.pane_id.as_str(),
|
|
476
|
+
spawned.session.as_str(),
|
|
477
|
+
spawned.window.as_str()
|
|
478
|
+
)))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnResult) -> bool {
|
|
482
|
+
transport
|
|
483
|
+
.list_targets()
|
|
484
|
+
.unwrap_or_default()
|
|
485
|
+
.iter()
|
|
486
|
+
.any(|pane| {
|
|
487
|
+
pane.pane_id.as_str() == spawned.pane_id.as_str()
|
|
488
|
+
&& pane.session.as_str() == spawned.session.as_str()
|
|
489
|
+
&& pane.window_name.as_ref().map(WindowName::as_str)
|
|
490
|
+
== Some(spawned.window.as_str())
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
458
494
|
fn persist_managed_leader_binding(
|
|
459
495
|
plan: &LeaderStartPlan,
|
|
460
496
|
workspace: &Path,
|
|
@@ -911,11 +947,16 @@ mod tests {
|
|
|
911
947
|
TurnVerification, WindowName,
|
|
912
948
|
};
|
|
913
949
|
|
|
914
|
-
use super::{
|
|
950
|
+
use super::{
|
|
951
|
+
ensure_managed_provider_live_after_attach, execute_leader_plan,
|
|
952
|
+
handle_exec_provider_startup_prompts, shlex_quote,
|
|
953
|
+
};
|
|
915
954
|
|
|
916
955
|
struct ScriptedTransport {
|
|
917
956
|
screens: Mutex<Vec<String>>,
|
|
918
957
|
sent: Mutex<Vec<(Target, Vec<Key>)>>,
|
|
958
|
+
liveness: PaneLiveness,
|
|
959
|
+
targets: Vec<PaneInfo>,
|
|
919
960
|
}
|
|
920
961
|
|
|
921
962
|
impl ScriptedTransport {
|
|
@@ -923,6 +964,26 @@ mod tests {
|
|
|
923
964
|
Self {
|
|
924
965
|
screens: Mutex::new(screens),
|
|
925
966
|
sent: Mutex::new(Vec::new()),
|
|
967
|
+
liveness: PaneLiveness::Unknown,
|
|
968
|
+
targets: Vec::new(),
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
fn with_liveness(liveness: PaneLiveness) -> Self {
|
|
973
|
+
Self {
|
|
974
|
+
screens: Mutex::new(Vec::new()),
|
|
975
|
+
sent: Mutex::new(Vec::new()),
|
|
976
|
+
liveness,
|
|
977
|
+
targets: Vec::new(),
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
fn with_liveness_and_targets(liveness: PaneLiveness, targets: Vec<PaneInfo>) -> Self {
|
|
982
|
+
Self {
|
|
983
|
+
screens: Mutex::new(Vec::new()),
|
|
984
|
+
sent: Mutex::new(Vec::new()),
|
|
985
|
+
liveness,
|
|
986
|
+
targets,
|
|
926
987
|
}
|
|
927
988
|
}
|
|
928
989
|
|
|
@@ -1024,11 +1085,11 @@ mod tests {
|
|
|
1024
1085
|
}
|
|
1025
1086
|
|
|
1026
1087
|
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
1027
|
-
Ok(
|
|
1088
|
+
Ok(self.liveness)
|
|
1028
1089
|
}
|
|
1029
1090
|
|
|
1030
1091
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1031
|
-
Ok(
|
|
1092
|
+
Ok(self.targets.clone())
|
|
1032
1093
|
}
|
|
1033
1094
|
|
|
1034
1095
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -1063,6 +1124,69 @@ mod tests {
|
|
|
1063
1124
|
}
|
|
1064
1125
|
}
|
|
1065
1126
|
|
|
1127
|
+
fn managed_spawn_result() -> SpawnResult {
|
|
1128
|
+
SpawnResult {
|
|
1129
|
+
pane_id: PaneId::new("%42"),
|
|
1130
|
+
session: SessionName::new("team-agent-leader-claude_code-demo"),
|
|
1131
|
+
window: WindowName::new("claude_code"),
|
|
1132
|
+
child_pid: Some(1234),
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
fn managed_pane_info(spawned: &SpawnResult) -> PaneInfo {
|
|
1137
|
+
PaneInfo {
|
|
1138
|
+
pane_id: spawned.pane_id.clone(),
|
|
1139
|
+
session: spawned.session.clone(),
|
|
1140
|
+
window_index: Some(0),
|
|
1141
|
+
window_name: Some(spawned.window.clone()),
|
|
1142
|
+
pane_index: Some(0),
|
|
1143
|
+
tty: None,
|
|
1144
|
+
current_command: Some("claude".to_string()),
|
|
1145
|
+
current_path: None,
|
|
1146
|
+
active: true,
|
|
1147
|
+
pane_pid: spawned.child_pid,
|
|
1148
|
+
leader_env: BTreeMap::new(),
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn managed_attach_success_requires_live_provider_pane() {
|
|
1154
|
+
let spawned = managed_spawn_result();
|
|
1155
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Dead);
|
|
1156
|
+
|
|
1157
|
+
let err = ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1158
|
+
.expect_err("dead provider pane must fail managed launch");
|
|
1159
|
+
|
|
1160
|
+
let text = err.to_string();
|
|
1161
|
+
assert!(
|
|
1162
|
+
text.contains("managed leader provider pane is not running"),
|
|
1163
|
+
"{text}"
|
|
1164
|
+
);
|
|
1165
|
+
assert!(text.contains("%42"), "{text}");
|
|
1166
|
+
assert!(text.contains("claude_code"), "{text}");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
#[test]
|
|
1170
|
+
fn managed_attach_success_accepts_live_provider_pane() {
|
|
1171
|
+
let spawned = managed_spawn_result();
|
|
1172
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
|
|
1173
|
+
|
|
1174
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1175
|
+
.expect("live provider pane keeps managed launch successful");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
#[test]
|
|
1179
|
+
fn managed_attach_success_uses_target_scan_when_liveness_unknown() {
|
|
1180
|
+
let spawned = managed_spawn_result();
|
|
1181
|
+
let transport = ScriptedTransport::with_liveness_and_targets(
|
|
1182
|
+
PaneLiveness::Unknown,
|
|
1183
|
+
vec![managed_pane_info(&spawned)],
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1187
|
+
.expect("target scan can prove provider pane is still live");
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1066
1190
|
struct EnvGuard {
|
|
1067
1191
|
saved: Vec<(&'static str, Option<String>)>,
|
|
1068
1192
|
}
|
|
@@ -471,7 +471,10 @@ pub(super) fn resume_backing_exists_for_agent(
|
|
|
471
471
|
let _ = (workspace, agent_id, agent, session_id, rollout_path);
|
|
472
472
|
false
|
|
473
473
|
}
|
|
474
|
-
Provider::Codex =>
|
|
474
|
+
Provider::Codex => {
|
|
475
|
+
rollout_path_exists(rollout_path)
|
|
476
|
+
|| codex_session_transcript_exists(agent, session_id.as_str(), rollout_path)
|
|
477
|
+
}
|
|
475
478
|
Provider::Claude | Provider::ClaudeCode => {
|
|
476
479
|
rollout_path_exists(rollout_path)
|
|
477
480
|
|| event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
|
|
@@ -557,6 +560,68 @@ fn claude_project_transcript_exists(agent: &serde_json::Value, session_id: &str)
|
|
|
557
560
|
.any(|entry| entry.path().join(&transcript_name).is_file())
|
|
558
561
|
}
|
|
559
562
|
|
|
563
|
+
fn codex_session_transcript_exists(
|
|
564
|
+
agent: &serde_json::Value,
|
|
565
|
+
session_id: &str,
|
|
566
|
+
rollout_path: Option<&RolloutPath>,
|
|
567
|
+
) -> bool {
|
|
568
|
+
if session_id.is_empty() {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
let mut roots = Vec::new();
|
|
572
|
+
if let Some(parent) = rollout_path
|
|
573
|
+
.map(RolloutPath::as_path)
|
|
574
|
+
.and_then(Path::parent)
|
|
575
|
+
.filter(|path| path.is_dir())
|
|
576
|
+
{
|
|
577
|
+
roots.push(parent.to_path_buf());
|
|
578
|
+
}
|
|
579
|
+
if let Some(root) = agent
|
|
580
|
+
.get("codex_sessions_root")
|
|
581
|
+
.and_then(serde_json::Value::as_str)
|
|
582
|
+
.filter(|path| !path.is_empty())
|
|
583
|
+
.map(PathBuf::from)
|
|
584
|
+
.filter(|path| path.is_dir())
|
|
585
|
+
{
|
|
586
|
+
roots.push(root);
|
|
587
|
+
}
|
|
588
|
+
if let Some(root) = std::env::var_os("HOME")
|
|
589
|
+
.map(PathBuf::from)
|
|
590
|
+
.map(|home| home.join(".codex").join("sessions"))
|
|
591
|
+
.filter(|path| path.is_dir())
|
|
592
|
+
{
|
|
593
|
+
roots.push(root);
|
|
594
|
+
}
|
|
595
|
+
roots.sort();
|
|
596
|
+
roots.dedup();
|
|
597
|
+
roots
|
|
598
|
+
.iter()
|
|
599
|
+
.any(|root| session_transcript_exists_under(root, session_id, 4))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fn session_transcript_exists_under(root: &Path, session_id: &str, max_depth: usize) -> bool {
|
|
603
|
+
let Ok(entries) = std::fs::read_dir(root) else {
|
|
604
|
+
return false;
|
|
605
|
+
};
|
|
606
|
+
for entry in entries.flatten() {
|
|
607
|
+
let path = entry.path();
|
|
608
|
+
if path.is_file() {
|
|
609
|
+
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
|
610
|
+
continue;
|
|
611
|
+
};
|
|
612
|
+
if name.ends_with(".jsonl") && name.contains(session_id) {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
} else if max_depth > 0
|
|
616
|
+
&& path.is_dir()
|
|
617
|
+
&& session_transcript_exists_under(&path, session_id, max_depth.saturating_sub(1))
|
|
618
|
+
{
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
false
|
|
623
|
+
}
|
|
624
|
+
|
|
560
625
|
fn copilot_session_store_has_session(session_id: &str) -> bool {
|
|
561
626
|
let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
|
|
562
627
|
return false;
|
|
@@ -1082,4 +1147,41 @@ mod e36_transcript_backing_tests {
|
|
|
1082
1147
|
let agent = serde_json::json!({});
|
|
1083
1148
|
assert!(!claude_project_transcript_exists(&agent, ""));
|
|
1084
1149
|
}
|
|
1150
|
+
|
|
1151
|
+
#[test]
|
|
1152
|
+
fn codex_session_transcript_is_recognized_when_rollout_path_is_stale() {
|
|
1153
|
+
let scratch = ScratchDir::new("codex-recognized");
|
|
1154
|
+
let sessions_root = scratch.path().join("sessions");
|
|
1155
|
+
let dated = sessions_root.join("2026").join("06").join("20");
|
|
1156
|
+
std::fs::create_dir_all(&dated).expect("mkdir dated sessions");
|
|
1157
|
+
let session_id = "019ee540-37ed-7a20-a141-1d654224d209";
|
|
1158
|
+
std::fs::write(
|
|
1159
|
+
dated.join(format!("rollout-2026-06-20T21-37-31-{session_id}.jsonl")),
|
|
1160
|
+
b"{}\n",
|
|
1161
|
+
)
|
|
1162
|
+
.expect("codex transcript");
|
|
1163
|
+
|
|
1164
|
+
let stale = RolloutPath::new(scratch.path().join("old").join("missing.jsonl"));
|
|
1165
|
+
let agent = serde_json::json!({
|
|
1166
|
+
"codex_sessions_root": sessions_root.to_string_lossy(),
|
|
1167
|
+
});
|
|
1168
|
+
assert!(
|
|
1169
|
+
codex_session_transcript_exists(&agent, session_id, Some(&stale)),
|
|
1170
|
+
"matching codex transcript under codex_sessions_root must count as resume backing"
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
#[test]
|
|
1175
|
+
fn missing_codex_session_transcript_is_not_backing() {
|
|
1176
|
+
let scratch = ScratchDir::new("codex-missing");
|
|
1177
|
+
let sessions_root = scratch.path().join("sessions");
|
|
1178
|
+
std::fs::create_dir_all(&sessions_root).expect("mkdir sessions");
|
|
1179
|
+
let agent = serde_json::json!({
|
|
1180
|
+
"codex_sessions_root": sessions_root.to_string_lossy(),
|
|
1181
|
+
});
|
|
1182
|
+
assert!(
|
|
1183
|
+
!codex_session_transcript_exists(&agent, "019ee540-ffff-7a20-a141-1d654224d209", None,),
|
|
1184
|
+
"no matching codex transcript => no backing"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1085
1187
|
}
|
|
@@ -172,7 +172,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
172
172
|
if !convergence.converged && convergence.changed {
|
|
173
173
|
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
174
174
|
}
|
|
175
|
-
let
|
|
175
|
+
let forced_fresh_missing = if convergence.converged {
|
|
176
176
|
std::collections::BTreeSet::new()
|
|
177
177
|
} else {
|
|
178
178
|
convergence.missing.iter().cloned().collect()
|
|
@@ -183,12 +183,6 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
183
183
|
&state,
|
|
184
184
|
allow_fresh,
|
|
185
185
|
)?;
|
|
186
|
-
for decision in &plan.decisions {
|
|
187
|
-
if matches!(decision.decision, ResumeDecision::FreshStart) && decision.session_id.is_some()
|
|
188
|
-
{
|
|
189
|
-
forced_fresh_missing.insert(decision.agent_id.as_str().to_string());
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
186
|
write_restart_resume_decision_events(
|
|
193
187
|
&selected.run_workspace,
|
|
194
188
|
&state,
|
|
@@ -43,6 +43,7 @@ pub use rebuild::{
|
|
|
43
43
|
restart, restart_candidates, restart_with_session_convergence_deadline, restart_with_transport,
|
|
44
44
|
restart_with_transport_with_readiness_deadline, select_restart_state,
|
|
45
45
|
};
|
|
46
|
+
pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline;
|
|
46
47
|
pub use remove::{remove_agent, remove_agent_with_transport};
|
|
47
48
|
pub use selection::{classify_first_send_at, classify_restart_plan, decide_start_mode, python_type_name};
|
|
48
49
|
pub(crate) use team_state::write_team_state;
|
|
@@ -1533,6 +1533,107 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1533
1533
|
);
|
|
1534
1534
|
}
|
|
1535
1535
|
|
|
1536
|
+
#[test]
|
|
1537
|
+
fn restart_allow_fresh_does_not_force_fresh_other_agents_when_one_session_capture_times_out() {
|
|
1538
|
+
let ws = temp_ws().join("restartatomic");
|
|
1539
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
1540
|
+
std::fs::write(
|
|
1541
|
+
ws.join("TEAM.md"),
|
|
1542
|
+
"---\nname: restartatomic\nobjective: Restart atomicity probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1543
|
+
)
|
|
1544
|
+
.unwrap();
|
|
1545
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
1546
|
+
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
1547
|
+
|
|
1548
|
+
let sessions_root = ws.join("codex-sessions");
|
|
1549
|
+
let dated = sessions_root.join("2026").join("06").join("20");
|
|
1550
|
+
std::fs::create_dir_all(&dated).unwrap();
|
|
1551
|
+
let alpha_session = "019ee540-37ed-7a20-a141-1d654224d209";
|
|
1552
|
+
std::fs::write(
|
|
1553
|
+
dated.join(format!(
|
|
1554
|
+
"rollout-2026-06-20T21-37-31-{alpha_session}.jsonl"
|
|
1555
|
+
)),
|
|
1556
|
+
"{}\n",
|
|
1557
|
+
)
|
|
1558
|
+
.unwrap();
|
|
1559
|
+
|
|
1560
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile restart team");
|
|
1561
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
1562
|
+
crate::state::persist::save_runtime_state(
|
|
1563
|
+
&ws,
|
|
1564
|
+
&json!({
|
|
1565
|
+
"session_name": "team-restartatomic",
|
|
1566
|
+
"agents": {
|
|
1567
|
+
"alpha": {
|
|
1568
|
+
"status": "running",
|
|
1569
|
+
"provider": "codex",
|
|
1570
|
+
"session_id": alpha_session,
|
|
1571
|
+
"rollout_path": ws.join("stale").join("missing-alpha.jsonl").to_string_lossy(),
|
|
1572
|
+
"codex_sessions_root": sessions_root.to_string_lossy(),
|
|
1573
|
+
"first_send_at": "2026-05-27T10:00:00+00:00"
|
|
1574
|
+
},
|
|
1575
|
+
"bravo": {
|
|
1576
|
+
"status": "running",
|
|
1577
|
+
"provider": "codex",
|
|
1578
|
+
"session_id": null,
|
|
1579
|
+
"spawn_cwd": ws.to_string_lossy(),
|
|
1580
|
+
"first_send_at": "2026-05-27T10:00:00+00:00"
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}),
|
|
1584
|
+
)
|
|
1585
|
+
.unwrap();
|
|
1586
|
+
seed_healthy_coordinator(&ws);
|
|
1587
|
+
let transport = OfflineTransport::new();
|
|
1588
|
+
|
|
1589
|
+
let result = restart_with_transport_with_session_convergence_deadline(
|
|
1590
|
+
&ws,
|
|
1591
|
+
true,
|
|
1592
|
+
None,
|
|
1593
|
+
&transport,
|
|
1594
|
+
Some(0),
|
|
1595
|
+
None,
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
assert!(
|
|
1599
|
+
matches!(result, Ok(RestartReport::Restarted { .. })),
|
|
1600
|
+
"allow-fresh restart should complete with alpha resumed and bravo fresh; got {result:?}"
|
|
1601
|
+
);
|
|
1602
|
+
let events = crate::event_log::EventLog::new(&ws).tail(80).unwrap();
|
|
1603
|
+
let decisions = events
|
|
1604
|
+
.iter()
|
|
1605
|
+
.filter(|event| {
|
|
1606
|
+
event.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision")
|
|
1607
|
+
})
|
|
1608
|
+
.collect::<Vec<_>>();
|
|
1609
|
+
let alpha = decisions
|
|
1610
|
+
.iter()
|
|
1611
|
+
.find(|event| event.get("worker_id").and_then(|v| v.as_str()) == Some("alpha"))
|
|
1612
|
+
.expect("alpha decision");
|
|
1613
|
+
assert_eq!(
|
|
1614
|
+
alpha.get("decision").and_then(|v| v.as_str()),
|
|
1615
|
+
Some("resume"),
|
|
1616
|
+
"alpha has session_id plus matching codex transcript and must not be forced fresh: {alpha}"
|
|
1617
|
+
);
|
|
1618
|
+
assert!(
|
|
1619
|
+
alpha.get("forced_fresh").is_none(),
|
|
1620
|
+
"alpha must not inherit bravo's convergence timeout: {alpha}"
|
|
1621
|
+
);
|
|
1622
|
+
let bravo = decisions
|
|
1623
|
+
.iter()
|
|
1624
|
+
.find(|event| event.get("worker_id").and_then(|v| v.as_str()) == Some("bravo"))
|
|
1625
|
+
.expect("bravo decision");
|
|
1626
|
+
assert_eq!(
|
|
1627
|
+
bravo.get("decision").and_then(|v| v.as_str()),
|
|
1628
|
+
Some("fresh_start")
|
|
1629
|
+
);
|
|
1630
|
+
assert_eq!(
|
|
1631
|
+
bravo.get("forced_fresh").and_then(|v| v.as_bool()),
|
|
1632
|
+
Some(true),
|
|
1633
|
+
"only the missing-session agent should carry forced_fresh: {bravo}"
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1536
1637
|
#[test]
|
|
1537
1638
|
fn restart_spawn_failure_isolated_to_partial_report() {
|
|
1538
1639
|
let ws = restart_ws_two_resumable_workers();
|
|
@@ -1476,7 +1476,6 @@ fn claude_base_command(
|
|
|
1476
1476
|
};
|
|
1477
1477
|
argv.push("--mcp-config".to_string());
|
|
1478
1478
|
argv.push(raw.to_string());
|
|
1479
|
-
argv.push("--strict-mcp-config".to_string());
|
|
1480
1479
|
}
|
|
1481
1480
|
for tool in claude_disallowed_tools(tools) {
|
|
1482
1481
|
argv.push("--disallowedTools".to_string());
|
|
@@ -215,6 +215,31 @@ fn build_command_includes_model_and_system_prompt() {
|
|
|
215
215
|
);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
#[test]
|
|
219
|
+
fn claude_mcp_config_does_not_enable_strict_mode() {
|
|
220
|
+
let adapter = get_adapter(Provider::ClaudeCode);
|
|
221
|
+
let config = adapter
|
|
222
|
+
.mcp_config(AuthMode::Subscription)
|
|
223
|
+
.expect("mcp config");
|
|
224
|
+
let argv = adapter
|
|
225
|
+
.build_command(
|
|
226
|
+
AuthMode::Subscription,
|
|
227
|
+
Some(&config),
|
|
228
|
+
Some("worker"),
|
|
229
|
+
Some("opus"),
|
|
230
|
+
)
|
|
231
|
+
.expect("build command with mcp config");
|
|
232
|
+
|
|
233
|
+
assert!(
|
|
234
|
+
argv_contains_adjacent(&argv, &["--mcp-config"]),
|
|
235
|
+
"Claude worker argv must still pass Team Agent MCP config: {argv:?}"
|
|
236
|
+
);
|
|
237
|
+
assert!(
|
|
238
|
+
!argv.iter().any(|arg| arg == "--strict-mcp-config"),
|
|
239
|
+
"--mcp-config must merge with user-level Claude MCP; strict mode hides user MCP: {argv:?}"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
#[test]
|
|
219
244
|
fn unsupported_provider_capability_error_message_shape() {
|
|
220
245
|
// unsupported.py:31 ProviderCapabilityError — placeholder providers reject
|
|
@@ -236,4 +261,3 @@ fn unsupported_provider_capability_error_message_shape() {
|
|
|
236
261
|
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
237
262
|
// Lock the CORRECT Python v0.2.11 behavior the strengthened contracts missed.
|
|
238
263
|
// Golden re-probed via /tmp/probe_p2_provider.py vs team-agent-public @ 439bef8.
|
|
239
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.35",
|
|
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.35",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.35",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.35"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|