@team-agent/installer 0.3.13 → 0.3.14
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 +168 -75
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +132 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +125 -32
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +267 -65
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport.rs +4 -0
- package/package.json +4 -4
|
@@ -116,10 +116,9 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
116
116
|
// 显式存在性门(下移后):selected.spec_path 经读序 B 已定位 runtime/legacy spec。
|
|
117
117
|
// 缺(空目录 restart 等)→ 报真实期望路径,不误导去用户目录找。
|
|
118
118
|
if !selected.spec_path.as_ref().is_some_and(|p| p.exists()) {
|
|
119
|
-
let expected = selected
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
.unwrap_or_else(|| crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key));
|
|
119
|
+
let expected = selected.spec_path.clone().unwrap_or_else(|| {
|
|
120
|
+
crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key)
|
|
121
|
+
});
|
|
123
122
|
return Err(LifecycleError::TeamSelect(format!(
|
|
124
123
|
"missing spec for restart: {} (run `team-agent quick-start <teamdir>` first, or restore the team's role docs)",
|
|
125
124
|
expected.display()
|
|
@@ -130,9 +129,11 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
130
129
|
// E5 task#3 / RC-A6a + E4(leader 裁定:每次 restart 都从角色定义重建 runtime spec,覆盖):
|
|
131
130
|
// 角色定义=第一真相源。角色齐 → compile_team 重建 + 保留运行期 override(session_name)+
|
|
132
131
|
// 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
|
|
133
|
-
let spec =
|
|
132
|
+
let spec =
|
|
133
|
+
rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
|
|
134
134
|
// 重建后 spec_workspace 恒为 runtime spec 的父目录(.team/runtime/<team_key>/)。
|
|
135
|
-
let runtime_spec =
|
|
135
|
+
let runtime_spec =
|
|
136
|
+
crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
|
|
136
137
|
let spec_workspace = runtime_spec.parent().ok_or_else(|| {
|
|
137
138
|
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
138
139
|
})?;
|
|
@@ -173,13 +174,23 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
173
174
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
174
175
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
175
176
|
}
|
|
176
|
-
let forced_fresh_missing = if convergence.converged {
|
|
177
|
+
let mut forced_fresh_missing = if convergence.converged {
|
|
177
178
|
std::collections::BTreeSet::new()
|
|
178
179
|
} else {
|
|
179
180
|
convergence.missing.iter().cloned().collect()
|
|
180
181
|
};
|
|
181
182
|
let forced_fresh_convergence = (!convergence.converged).then_some(convergence.clone());
|
|
182
|
-
let plan = classify_restart_plan_with_resume_validation(
|
|
183
|
+
let plan = classify_restart_plan_with_resume_validation(
|
|
184
|
+
Some(&selected.run_workspace),
|
|
185
|
+
&state,
|
|
186
|
+
allow_fresh,
|
|
187
|
+
)?;
|
|
188
|
+
for decision in &plan.decisions {
|
|
189
|
+
if matches!(decision.decision, ResumeDecision::FreshStart) && decision.session_id.is_some()
|
|
190
|
+
{
|
|
191
|
+
forced_fresh_missing.insert(decision.agent_id.as_str().to_string());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
183
194
|
write_restart_resume_decision_events(
|
|
184
195
|
&selected.run_workspace,
|
|
185
196
|
&state,
|
|
@@ -214,10 +225,15 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
214
225
|
}
|
|
215
226
|
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
216
227
|
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
228
|
+
let mut fatal_resume_failure = false;
|
|
217
229
|
// B5 restart isolation loop: per-agent spawn failures must be recorded and
|
|
218
|
-
// isolated here.
|
|
230
|
+
// isolated here. G1 resume-integrity failures set a fatal flag and skip later
|
|
231
|
+
// spawns; do not reintroduce `?`, `break`, or `return` inside this loop.
|
|
219
232
|
// BEGIN_B5_RESTART_ISOLATION_LOOP
|
|
220
233
|
for decision in &plan.decisions {
|
|
234
|
+
if fatal_resume_failure {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
221
237
|
let Some(agent) = state
|
|
222
238
|
.get("agents")
|
|
223
239
|
.and_then(|v| v.get(decision.agent_id.as_str()))
|
|
@@ -225,7 +241,12 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
225
241
|
else {
|
|
226
242
|
let error = format!("agent {} not found for restart", decision.agent_id);
|
|
227
243
|
mark_agent_restart_failed(&mut state, decision, &error);
|
|
228
|
-
let _ = write_restart_agent_failed_event(
|
|
244
|
+
let _ = write_restart_agent_failed_event(
|
|
245
|
+
&selected.run_workspace,
|
|
246
|
+
decision,
|
|
247
|
+
"spawn",
|
|
248
|
+
&error,
|
|
249
|
+
);
|
|
229
250
|
failed_agents.push(restart_failed_agent(decision, "spawn", error));
|
|
230
251
|
continue;
|
|
231
252
|
};
|
|
@@ -251,9 +272,15 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
251
272
|
&error,
|
|
252
273
|
);
|
|
253
274
|
failed_agents.push(restart_failed_agent(&previous, "resume", error));
|
|
275
|
+
if is_resume_integrity_failure(&previous, "resume", "") {
|
|
276
|
+
fatal_resume_failure = true;
|
|
277
|
+
}
|
|
254
278
|
session_live = false;
|
|
255
279
|
}
|
|
256
280
|
}
|
|
281
|
+
if fatal_resume_failure {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
257
284
|
let spawn = match spawn_agent_window(
|
|
258
285
|
&selected.run_workspace,
|
|
259
286
|
&session_name,
|
|
@@ -269,8 +296,17 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
269
296
|
Err(error) => {
|
|
270
297
|
let error = error.to_string();
|
|
271
298
|
mark_agent_restart_failed(&mut state, decision, &error);
|
|
272
|
-
let
|
|
273
|
-
|
|
299
|
+
let phase = restart_failure_phase(decision, "spawn", &error);
|
|
300
|
+
let _ = write_restart_agent_failed_event(
|
|
301
|
+
&selected.run_workspace,
|
|
302
|
+
decision,
|
|
303
|
+
phase,
|
|
304
|
+
&error,
|
|
305
|
+
);
|
|
306
|
+
failed_agents.push(restart_failed_agent(decision, phase, error));
|
|
307
|
+
if phase == "resume" {
|
|
308
|
+
fatal_resume_failure = true;
|
|
309
|
+
}
|
|
274
310
|
continue;
|
|
275
311
|
}
|
|
276
312
|
};
|
|
@@ -281,13 +317,13 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
281
317
|
{
|
|
282
318
|
let error = error.to_string();
|
|
283
319
|
mark_agent_restart_failed(&mut state, decision, &error);
|
|
284
|
-
let
|
|
285
|
-
|
|
286
|
-
decision,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
320
|
+
let phase = restart_failure_phase(decision, "readiness", &error);
|
|
321
|
+
let _ =
|
|
322
|
+
write_restart_agent_failed_event(&selected.run_workspace, decision, phase, &error);
|
|
323
|
+
failed_agents.push(restart_failed_agent(decision, phase, error));
|
|
324
|
+
if phase == "resume" {
|
|
325
|
+
fatal_resume_failure = true;
|
|
326
|
+
}
|
|
291
327
|
continue;
|
|
292
328
|
}
|
|
293
329
|
successful_agents.push(decision.clone());
|
|
@@ -303,6 +339,22 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
303
339
|
// END_B5_RESTART_ISOLATION_LOOP
|
|
304
340
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
305
341
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
342
|
+
if fatal_resume_failure {
|
|
343
|
+
let attach_commands = Vec::new();
|
|
344
|
+
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
345
|
+
write_restart_completed_event(
|
|
346
|
+
&selected.run_workspace,
|
|
347
|
+
&successful_agents,
|
|
348
|
+
&failed_agents,
|
|
349
|
+
"fail",
|
|
350
|
+
)?;
|
|
351
|
+
return Ok(RestartReport::Failed {
|
|
352
|
+
session_name,
|
|
353
|
+
failed_agents,
|
|
354
|
+
next_actions,
|
|
355
|
+
attach_commands,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
306
358
|
if successful_agents.is_empty() && !failed_agents.is_empty() {
|
|
307
359
|
let attach_commands = Vec::new();
|
|
308
360
|
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
@@ -555,9 +607,9 @@ fn parse_duration_value_seconds_ms(value: &str) -> Option<u64> {
|
|
|
555
607
|
}
|
|
556
608
|
|
|
557
609
|
fn restart_readiness_deadline(requested_ms: Option<u64>) -> std::time::Duration {
|
|
558
|
-
requested_ms
|
|
559
|
-
|
|
560
|
-
|
|
610
|
+
requested_ms
|
|
611
|
+
.map(std::time::Duration::from_millis)
|
|
612
|
+
.unwrap_or_else(|| env_duration_ms(&["TEAM_AGENT_RESTART_READINESS_DEADLINE_MS"], 30_000))
|
|
561
613
|
}
|
|
562
614
|
|
|
563
615
|
fn restart_readiness_poll_interval() -> std::time::Duration {
|
|
@@ -595,11 +647,14 @@ fn wait_restart_readiness_or_timeout(
|
|
|
595
647
|
let elapsed = started.elapsed();
|
|
596
648
|
if elapsed >= deadline {
|
|
597
649
|
write_restart_readiness_timeout_event(workspace, readiness, deadline, elapsed)?;
|
|
598
|
-
return Err(LifecycleError::RequirementUnmet(
|
|
599
|
-
workspace, readiness, deadline,
|
|
600
|
-
))
|
|
650
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
651
|
+
restart_readiness_timeout_message(workspace, readiness, deadline),
|
|
652
|
+
));
|
|
601
653
|
}
|
|
602
|
-
std::thread::sleep(std::cmp::min(
|
|
654
|
+
std::thread::sleep(std::cmp::min(
|
|
655
|
+
poll_interval,
|
|
656
|
+
deadline.saturating_sub(elapsed),
|
|
657
|
+
));
|
|
603
658
|
}
|
|
604
659
|
}
|
|
605
660
|
|
|
@@ -615,7 +670,11 @@ fn restart_readiness(
|
|
|
615
670
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
616
671
|
let coordinator_alive =
|
|
617
672
|
crate::coordinator::coordinator_health(&coordinator_workspace).ok && session_created;
|
|
618
|
-
RestartReadiness {
|
|
673
|
+
RestartReadiness {
|
|
674
|
+
session_created,
|
|
675
|
+
worker_pane_addressable,
|
|
676
|
+
coordinator_alive,
|
|
677
|
+
}
|
|
619
678
|
}
|
|
620
679
|
|
|
621
680
|
fn restart_worker_panes_addressable(
|
|
@@ -727,7 +786,11 @@ fn restart_readiness_missing_summary(readiness: RestartReadiness) -> String {
|
|
|
727
786
|
}
|
|
728
787
|
|
|
729
788
|
fn yes_no(value: bool) -> &'static str {
|
|
730
|
-
if value {
|
|
789
|
+
if value {
|
|
790
|
+
"yes"
|
|
791
|
+
} else {
|
|
792
|
+
"no"
|
|
793
|
+
}
|
|
731
794
|
}
|
|
732
795
|
|
|
733
796
|
fn verify_spawned_agent_live(
|
|
@@ -841,6 +904,33 @@ fn restart_failed_agent(
|
|
|
841
904
|
}
|
|
842
905
|
}
|
|
843
906
|
|
|
907
|
+
fn restart_failure_phase(
|
|
908
|
+
decision: &RestartedAgent,
|
|
909
|
+
phase: &'static str,
|
|
910
|
+
error: &str,
|
|
911
|
+
) -> &'static str {
|
|
912
|
+
if is_resume_integrity_failure(decision, phase, error) {
|
|
913
|
+
"resume"
|
|
914
|
+
} else {
|
|
915
|
+
phase
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
fn is_resume_integrity_failure(decision: &RestartedAgent, phase: &str, error: &str) -> bool {
|
|
920
|
+
if !matches!(decision.restart_mode, StartMode::Resumed) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
if phase == "resume" || phase == "readiness" {
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
error.contains("session_disappeared_after_spawn")
|
|
927
|
+
|| error.contains("provider_resume_exited")
|
|
928
|
+
|| error.contains("resume_not_ready")
|
|
929
|
+
|| error.contains("resume_atomicity")
|
|
930
|
+
|| error.contains("no live pane")
|
|
931
|
+
|| error.contains("no-pane")
|
|
932
|
+
}
|
|
933
|
+
|
|
844
934
|
fn mark_agent_restart_failed(
|
|
845
935
|
state: &mut serde_json::Value,
|
|
846
936
|
decision: &RestartedAgent,
|
|
@@ -1157,13 +1247,16 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
1157
1247
|
let agents_dir = team_dir.join("agents");
|
|
1158
1248
|
let has_role_doc = std::fs::read_dir(&agents_dir)
|
|
1159
1249
|
.map(|entries| {
|
|
1160
|
-
entries
|
|
1161
|
-
|
|
1162
|
-
|
|
1250
|
+
entries
|
|
1251
|
+
.flatten()
|
|
1252
|
+
.any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("md"))
|
|
1163
1253
|
})
|
|
1164
1254
|
.unwrap_or(false);
|
|
1165
1255
|
if !has_role_doc {
|
|
1166
|
-
missing.push(format!(
|
|
1256
|
+
missing.push(format!(
|
|
1257
|
+
"{}/*.md (at least one role doc)",
|
|
1258
|
+
agents_dir.display()
|
|
1259
|
+
));
|
|
1167
1260
|
}
|
|
1168
1261
|
if !missing.is_empty() {
|
|
1169
1262
|
// N38 三行式:error / action / log。旧 runtime spec 原地保留(不删不用)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
use super::*;
|
|
2
1
|
use super::common::*;
|
|
2
|
+
use super::*;
|
|
3
3
|
|
|
4
4
|
/// bug-085 四象限 `start_mode` 决策(`start.py:179-188` + `_resume_rollout_missing` `start.py:66-69`),
|
|
5
5
|
/// **从 start_agent 的整条 lock+spawn 路径里分离出的纯函数**(gate gap:porter 需要单元级 RED
|
|
@@ -19,7 +19,8 @@ pub fn decide_start_mode(
|
|
|
19
19
|
match session_id {
|
|
20
20
|
None => StartMode::Fresh,
|
|
21
21
|
Some(_) => {
|
|
22
|
-
let missing_resume_backing =
|
|
22
|
+
let missing_resume_backing = !provider_wire_supports_resume(provider)
|
|
23
|
+
|| (resumable_provider_requires_backing(provider) && !rollout_exists);
|
|
23
24
|
match (missing_resume_backing, allow_fresh) {
|
|
24
25
|
(true, true) => StartMode::FreshAfterMissingRollout,
|
|
25
26
|
(true, false) => StartMode::Noop,
|
|
@@ -188,8 +189,10 @@ pub(crate) fn classify_restart_plan_with_resume_validation(
|
|
|
188
189
|
// (leader 从未发消息 → first_send_at=null → interacted=false)会被它静默 fresh 丢上下文。
|
|
189
190
|
let provider = agent_provider(agent);
|
|
190
191
|
let provider_wire = provider_wire(provider);
|
|
191
|
-
let
|
|
192
|
-
|
|
192
|
+
let provider_can_resume = provider_supports_resume(provider);
|
|
193
|
+
let resume_backing_exists = match (workspace, session_id.as_ref(), provider_can_resume) {
|
|
194
|
+
(_, Some(_), false) => false,
|
|
195
|
+
(Some(workspace), Some(session), true) => resume_backing_exists_for_agent(
|
|
193
196
|
workspace,
|
|
194
197
|
&agent_id,
|
|
195
198
|
agent,
|
|
@@ -197,14 +200,14 @@ pub(crate) fn classify_restart_plan_with_resume_validation(
|
|
|
197
200
|
session,
|
|
198
201
|
agent_rollout_path(agent).as_ref(),
|
|
199
202
|
),
|
|
200
|
-
(None, Some(_)) if resumable_provider_requires_backing(provider_wire) => {
|
|
203
|
+
(None, Some(_), true) if resumable_provider_requires_backing(provider_wire) => {
|
|
201
204
|
agent_rollout_path(agent)
|
|
202
205
|
.as_ref()
|
|
203
206
|
.is_some_and(|path| path.as_path().exists())
|
|
204
207
|
}
|
|
205
208
|
_ => true,
|
|
206
209
|
};
|
|
207
|
-
let decision = if session_id.is_some() && resume_backing_exists {
|
|
210
|
+
let decision = if session_id.is_some() && provider_can_resume && resume_backing_exists {
|
|
208
211
|
ResumeDecision::Resume
|
|
209
212
|
} else if session_id.is_some() && allow_fresh {
|
|
210
213
|
ResumeDecision::FreshStart
|