@team-agent/installer 0.3.13 → 0.3.15

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.
Files changed (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/mod.rs +228 -85
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/leader/start.rs +279 -4
  14. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  15. package/crates/team-agent/src/lifecycle/launch.rs +692 -92
  16. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  17. package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
  18. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
  19. package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
  20. package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
  22. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  23. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  24. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  26. package/crates/team-agent/src/messaging/mod.rs +6 -3
  27. package/crates/team-agent/src/messaging/results.rs +240 -158
  28. package/crates/team-agent/src/messaging/send.rs +3 -2
  29. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  30. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  32. package/crates/team-agent/src/os_probe.rs +11 -0
  33. package/crates/team-agent/src/state/persist.rs +6 -0
  34. package/crates/team-agent/src/tmux_backend.rs +90 -0
  35. package/crates/team-agent/src/transport/test_support.rs +46 -1
  36. package/crates/team-agent/src/transport.rs +31 -0
  37. package/package.json +4 -4
  38. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -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,21 @@ 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
+ }
284
+ let layout_placement = crate::lifecycle::launch::adaptive_existing_placement_for_agent(
285
+ &state,
286
+ transport,
287
+ &session_name,
288
+ &decision.agent_id,
289
+ );
257
290
  let spawn = match spawn_agent_window(
258
291
  &selected.run_workspace,
259
292
  &session_name,
@@ -263,31 +296,48 @@ pub fn restart_with_transport_with_session_convergence_deadline(
263
296
  session_live,
264
297
  transport,
265
298
  Some(&safety),
299
+ layout_placement.as_ref(),
266
300
  Some(spec_workspace),
267
301
  ) {
268
302
  Ok(spawn) => spawn,
269
303
  Err(error) => {
270
304
  let error = error.to_string();
271
305
  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));
306
+ let phase = restart_failure_phase(decision, "spawn", &error);
307
+ let _ = write_restart_agent_failed_event(
308
+ &selected.run_workspace,
309
+ decision,
310
+ phase,
311
+ &error,
312
+ );
313
+ failed_agents.push(restart_failed_agent(decision, phase, error));
314
+ if phase == "resume" {
315
+ fatal_resume_failure = true;
316
+ }
274
317
  continue;
275
318
  }
276
319
  };
277
320
  if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
278
321
  .and_then(|_| {
279
- mark_agent_respawned(&mut state, &decision.agent_id, &spawn, transport, &safety)
322
+ mark_agent_respawned(
323
+ &mut state,
324
+ &decision.agent_id,
325
+ decision.restart_mode,
326
+ &spawn,
327
+ transport,
328
+ &safety,
329
+ )
280
330
  })
281
331
  {
282
332
  let error = error.to_string();
283
333
  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));
334
+ let phase = restart_failure_phase(decision, "readiness", &error);
335
+ let _ =
336
+ write_restart_agent_failed_event(&selected.run_workspace, decision, phase, &error);
337
+ failed_agents.push(restart_failed_agent(decision, phase, error));
338
+ if phase == "resume" {
339
+ fatal_resume_failure = true;
340
+ }
291
341
  continue;
292
342
  }
293
343
  successful_agents.push(decision.clone());
@@ -303,6 +353,22 @@ pub fn restart_with_transport_with_session_convergence_deadline(
303
353
  // END_B5_RESTART_ISOLATION_LOOP
304
354
  crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
305
355
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
356
+ if fatal_resume_failure {
357
+ let attach_commands = Vec::new();
358
+ let next_actions = restart_failure_next_actions(&failed_agents);
359
+ write_restart_completed_event(
360
+ &selected.run_workspace,
361
+ &successful_agents,
362
+ &failed_agents,
363
+ "fail",
364
+ )?;
365
+ return Ok(RestartReport::Failed {
366
+ session_name,
367
+ failed_agents,
368
+ next_actions,
369
+ attach_commands,
370
+ });
371
+ }
306
372
  if successful_agents.is_empty() && !failed_agents.is_empty() {
307
373
  let attach_commands = Vec::new();
308
374
  let next_actions = restart_failure_next_actions(&failed_agents);
@@ -555,9 +621,9 @@ fn parse_duration_value_seconds_ms(value: &str) -> Option<u64> {
555
621
  }
556
622
 
557
623
  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
- })
624
+ requested_ms
625
+ .map(std::time::Duration::from_millis)
626
+ .unwrap_or_else(|| env_duration_ms(&["TEAM_AGENT_RESTART_READINESS_DEADLINE_MS"], 30_000))
561
627
  }
562
628
 
563
629
  fn restart_readiness_poll_interval() -> std::time::Duration {
@@ -595,11 +661,14 @@ fn wait_restart_readiness_or_timeout(
595
661
  let elapsed = started.elapsed();
596
662
  if elapsed >= deadline {
597
663
  write_restart_readiness_timeout_event(workspace, readiness, deadline, elapsed)?;
598
- return Err(LifecycleError::RequirementUnmet(restart_readiness_timeout_message(
599
- workspace, readiness, deadline,
600
- )));
664
+ return Err(LifecycleError::RequirementUnmet(
665
+ restart_readiness_timeout_message(workspace, readiness, deadline),
666
+ ));
601
667
  }
602
- std::thread::sleep(std::cmp::min(poll_interval, deadline.saturating_sub(elapsed)));
668
+ std::thread::sleep(std::cmp::min(
669
+ poll_interval,
670
+ deadline.saturating_sub(elapsed),
671
+ ));
603
672
  }
604
673
  }
605
674
 
@@ -615,7 +684,11 @@ fn restart_readiness(
615
684
  let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
616
685
  let coordinator_alive =
617
686
  crate::coordinator::coordinator_health(&coordinator_workspace).ok && session_created;
618
- RestartReadiness { session_created, worker_pane_addressable, coordinator_alive }
687
+ RestartReadiness {
688
+ session_created,
689
+ worker_pane_addressable,
690
+ coordinator_alive,
691
+ }
619
692
  }
620
693
 
621
694
  fn restart_worker_panes_addressable(
@@ -727,7 +800,11 @@ fn restart_readiness_missing_summary(readiness: RestartReadiness) -> String {
727
800
  }
728
801
 
729
802
  fn yes_no(value: bool) -> &'static str {
730
- if value { "yes" } else { "no" }
803
+ if value {
804
+ "yes"
805
+ } else {
806
+ "no"
807
+ }
731
808
  }
732
809
 
733
810
  fn verify_spawned_agent_live(
@@ -788,6 +865,7 @@ fn mark_restart_targets_stopped_after_teardown(
788
865
  fn mark_agent_respawned(
789
866
  state: &mut serde_json::Value,
790
867
  agent_id: &AgentId,
868
+ restart_mode: StartMode,
791
869
  spawn: &SpawnedAgentWindow,
792
870
  transport: &dyn crate::transport::Transport,
793
871
  safety: &DangerousApproval,
@@ -804,6 +882,10 @@ fn mark_agent_respawned(
804
882
  )));
805
883
  };
806
884
  agent.insert("status".to_string(), serde_json::json!("running"));
885
+ agent.insert(
886
+ "window".to_string(),
887
+ serde_json::json!(spawn.spawn.window.as_str()),
888
+ );
807
889
  agent.insert(
808
890
  "pane_id".to_string(),
809
891
  serde_json::json!(spawn.spawn.pane_id.as_str()),
@@ -819,8 +901,60 @@ fn mark_agent_respawned(
819
901
  if let Some(pane_pid) = pane_pid {
820
902
  agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
821
903
  }
904
+ if matches!(
905
+ restart_mode,
906
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
907
+ ) {
908
+ if let Some(session_id) = spawn.plan.expected_session_id.as_ref() {
909
+ agent.insert(
910
+ "session_id".to_string(),
911
+ serde_json::json!(session_id.as_str()),
912
+ );
913
+ }
914
+ }
822
915
  crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
823
916
  persist_effective_approval_policy_for_restart(agent, safety);
917
+ if let Some(placement) = spawn.layout_placement.as_ref() {
918
+ agent.insert(
919
+ "layout_window".to_string(),
920
+ serde_json::json!(placement.layout_window.as_str()),
921
+ );
922
+ agent.insert(
923
+ "layout_index".to_string(),
924
+ serde_json::json!(placement.layout_index),
925
+ );
926
+ agent.insert("pane_index".to_string(), serde_json::json!(placement.pane_index));
927
+ agent.insert(
928
+ "display".to_string(),
929
+ serde_json::json!({
930
+ "backend": "adaptive",
931
+ "status": "opened",
932
+ "window": placement.layout_window.as_str(),
933
+ "workspace_window": null,
934
+ "pane_id": spawn.spawn.pane_id.as_str(),
935
+ "pane_title": agent_id.as_str(),
936
+ "target": spawn.spawn.pane_id.as_str(),
937
+ "target_worker_session": spawn.spawn.session.as_str(),
938
+ "linked_session": null,
939
+ "leader_session": spawn.spawn.session.as_str(),
940
+ "display_session": null,
941
+ "fallback": null,
942
+ }),
943
+ );
944
+ }
945
+ if matches!(
946
+ restart_mode,
947
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
948
+ ) && spawn.plan.expected_session_id.is_some()
949
+ {
950
+ agent.insert("rollout_path".to_string(), serde_json::Value::Null);
951
+ agent.insert("captured_at".to_string(), serde_json::Value::Null);
952
+ agent.insert("captured_via".to_string(), serde_json::Value::Null);
953
+ agent.insert(
954
+ "attribution_confidence".to_string(),
955
+ serde_json::Value::Null,
956
+ );
957
+ }
824
958
  agent.remove("startup_prompts");
825
959
  agent.remove("startup_prompt_status");
826
960
  Ok(())
@@ -841,6 +975,33 @@ fn restart_failed_agent(
841
975
  }
842
976
  }
843
977
 
978
+ fn restart_failure_phase(
979
+ decision: &RestartedAgent,
980
+ phase: &'static str,
981
+ error: &str,
982
+ ) -> &'static str {
983
+ if is_resume_integrity_failure(decision, phase, error) {
984
+ "resume"
985
+ } else {
986
+ phase
987
+ }
988
+ }
989
+
990
+ fn is_resume_integrity_failure(decision: &RestartedAgent, phase: &str, error: &str) -> bool {
991
+ if !matches!(decision.restart_mode, StartMode::Resumed) {
992
+ return false;
993
+ }
994
+ if phase == "resume" || phase == "readiness" {
995
+ return true;
996
+ }
997
+ error.contains("session_disappeared_after_spawn")
998
+ || error.contains("provider_resume_exited")
999
+ || error.contains("resume_not_ready")
1000
+ || error.contains("resume_atomicity")
1001
+ || error.contains("no live pane")
1002
+ || error.contains("no-pane")
1003
+ }
1004
+
844
1005
  fn mark_agent_restart_failed(
845
1006
  state: &mut serde_json::Value,
846
1007
  decision: &RestartedAgent,
@@ -1157,13 +1318,16 @@ fn rebuild_runtime_spec_from_roles(
1157
1318
  let agents_dir = team_dir.join("agents");
1158
1319
  let has_role_doc = std::fs::read_dir(&agents_dir)
1159
1320
  .map(|entries| {
1160
- entries.flatten().any(|e| {
1161
- e.path().extension().and_then(|x| x.to_str()) == Some("md")
1162
- })
1321
+ entries
1322
+ .flatten()
1323
+ .any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("md"))
1163
1324
  })
1164
1325
  .unwrap_or(false);
1165
1326
  if !has_role_doc {
1166
- missing.push(format!("{}/*.md (at least one role doc)", agents_dir.display()));
1327
+ missing.push(format!(
1328
+ "{}/*.md (at least one role doc)",
1329
+ agents_dir.display()
1330
+ ));
1167
1331
  }
1168
1332
  if !missing.is_empty() {
1169
1333
  // 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