@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.
@@ -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
- .spec_path
121
- .clone()
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 = rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
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 = crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
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(Some(&selected.run_workspace), &state, allow_fresh)?;
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. Do not reintroduce `?`, `break`, or `return` inside this loop.
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(&selected.run_workspace, decision, "spawn", &error);
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 _ = write_restart_agent_failed_event(&selected.run_workspace, decision, "spawn", &error);
273
- failed_agents.push(restart_failed_agent(decision, "spawn", error));
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 _ = 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));
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.map(std::time::Duration::from_millis).unwrap_or_else(|| {
559
- env_duration_ms(&["TEAM_AGENT_RESTART_READINESS_DEADLINE_MS"], 30_000)
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(restart_readiness_timeout_message(
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(poll_interval, deadline.saturating_sub(elapsed)));
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 { session_created, worker_pane_addressable, coordinator_alive }
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 { "yes" } else { "no" }
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.flatten().any(|e| {
1161
- e.path().extension().and_then(|x| x.to_str()) == Some("md")
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!("{}/*.md (at least one role doc)", agents_dir.display()));
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 = resumable_provider_requires_backing(provider) && !rollout_exists;
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 resume_backing_exists = match (workspace, session_id.as_ref()) {
192
- (Some(workspace), Some(session)) => resume_backing_exists_for_agent(
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