@team-agent/installer 0.3.1 → 0.3.2

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.
@@ -31,6 +31,12 @@ pub fn restart_with_transport(
31
31
  team: Option<&str>,
32
32
  transport: &dyn crate::transport::Transport,
33
33
  ) -> Result<RestartReport, LifecycleError> {
34
+ if crate::lifecycle::restart::input_has_no_local_team_context(workspace) {
35
+ return Err(LifecycleError::TeamSelect(format!(
36
+ "missing spec for restart: {}",
37
+ workspace.join("team.spec.yaml").display()
38
+ )));
39
+ }
34
40
  let run_candidate = crate::model::paths::canonical_run_workspace(workspace)
35
41
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
36
42
  if !workspace.join("team.spec.yaml").exists()
@@ -47,7 +53,7 @@ pub fn restart_with_transport(
47
53
  crate::state::selector::SelectorMode::RequireSpec,
48
54
  )
49
55
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
50
- let state = selected.state;
56
+ let mut state = selected.state;
51
57
  crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
52
58
  let spec_workspace = selected
53
59
  .spec_workspace
@@ -55,6 +61,10 @@ pub fn restart_with_transport(
55
61
  .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
56
62
  let spec = load_team_spec(spec_workspace)?;
57
63
  let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
64
+ if refresh_missing_provider_sessions(&mut state)? {
65
+ crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
66
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
67
+ }
58
68
  let plan = classify_restart_plan(&state, allow_fresh)?;
59
69
  write_restart_resume_decision_events(&selected.run_workspace, &state, allow_fresh, &plan.decisions)?;
60
70
  if !plan.corrupt_entries.is_empty() {
@@ -117,7 +127,6 @@ fn write_restart_resume_decision_events(
117
127
  allow_fresh: bool,
118
128
  decisions: &[RestartedAgent],
119
129
  ) -> Result<(), LifecycleError> {
120
- let log = crate::event_log::EventLog::new(workspace);
121
130
  for decision in decisions {
122
131
  let agent = state
123
132
  .get("agents")
@@ -134,23 +143,56 @@ fn write_restart_resume_decision_events(
134
143
  ResumeDecision::FreshStart => "fresh_start",
135
144
  ResumeDecision::Refuse => "refuse",
136
145
  };
137
- log.write(
138
- crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
139
- serde_json::json!({
140
- "worker_id": decision.agent_id.as_str(),
141
- "has_first_send_at": first_send_at.is_some(),
142
- "has_session_id": session_id.is_some(),
143
- "allow_fresh": allow_fresh,
144
- "decision": decision_wire,
145
- "first_send_at": first_send_at,
146
- "session_id": session_id,
147
- }),
148
- )
149
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
146
+ write_restart_resume_decision_event(
147
+ workspace,
148
+ decision.agent_id.as_str(),
149
+ first_send_at,
150
+ session_id,
151
+ allow_fresh,
152
+ decision_wire,
153
+ )?;
150
154
  }
151
155
  Ok(())
152
156
  }
153
157
 
158
+ fn write_restart_resume_decision_event(
159
+ workspace: &Path,
160
+ worker_id: &str,
161
+ first_send_at: Option<String>,
162
+ session_id: Option<String>,
163
+ allow_fresh: bool,
164
+ decision: &str,
165
+ ) -> Result<(), LifecycleError> {
166
+ use std::io::Write as _;
167
+
168
+ let path = workspace.join(".team").join("logs").join("events.jsonl");
169
+ if let Some(parent) = path.parent() {
170
+ std::fs::create_dir_all(parent)
171
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
172
+ }
173
+ let event = serde_json::json!({
174
+ "ts": chrono::Utc::now().to_rfc3339(),
175
+ "event": crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
176
+ "worker_id": worker_id,
177
+ "has_first_send_at": first_send_at.is_some(),
178
+ "has_session_id": session_id.is_some(),
179
+ "allow_fresh": allow_fresh,
180
+ "decision": decision,
181
+ "first_send_at": first_send_at,
182
+ "session_id": session_id,
183
+ });
184
+ let line = serde_json::to_string(&event)
185
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
186
+ let mut file = std::fs::OpenOptions::new()
187
+ .create(true)
188
+ .append(true)
189
+ .open(&path)
190
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
191
+ file.write_all(line.as_bytes())
192
+ .and_then(|_| file.write_all(b"\n"))
193
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))
194
+ }
195
+
154
196
  /// `restart_candidates(workspace)`(`restart/selection.py:12`)。从 snapshot + active
155
197
  /// state 收集可重启 team。
156
198
  pub fn restart_candidates(workspace: &Path) -> Result<Vec<RestartCandidate>, LifecycleError> {
@@ -169,7 +169,11 @@ fn remove_agent_inner(
169
169
  // (team projection) — NOT a raw save, so other teams in a multi-team workspace are preserved.
170
170
  let mut removed_state = working_state;
171
171
  remove_agent_from_state(&mut removed_state, agent_id)?;
172
- crate::state::projection::save_team_scoped_state(paths.run_workspace, &removed_state)
172
+ crate::state::projection::save_team_scoped_state_with_deleted_agents(
173
+ paths.run_workspace,
174
+ &removed_state,
175
+ &[agent_id.as_str()],
176
+ )
173
177
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
174
178
  cleared_locations.push(serde_json::json!("state.json:agents"));
175
179
  write_remove_step_event(
@@ -33,6 +33,7 @@ mod team_state;
33
33
 
34
34
  pub use agent::{reset_agent, reset_agent_with_transport, start_agent, start_agent_with_transport, stop_agent, stop_agent_with_transport};
35
35
  pub(crate) use agent::start_agent_at_paths;
36
+ pub(crate) use common::refresh_missing_provider_sessions;
36
37
  pub use orchestrator::{halt_plan, plan_status};
37
38
  pub use rebuild::{restart, restart_candidates, restart_with_transport, select_restart_state};
38
39
  pub use remove::{remove_agent, remove_agent_with_transport};
@@ -45,6 +46,13 @@ pub(crate) fn lifecycle_run_workspace(workspace: &Path) -> Result<std::path::Pat
45
46
  }
46
47
 
47
48
  fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePaths, LifecycleError> {
49
+ if input_has_no_local_team_context(workspace) {
50
+ return Err(LifecycleError::TeamSelect(format!(
51
+ "active team spec not found: input_workspace={} expected_spec_path={}",
52
+ workspace.display(),
53
+ workspace.join("team.spec.yaml").display()
54
+ )));
55
+ }
48
56
  let selected = crate::state::selector::resolve_active_team(
49
57
  workspace,
50
58
  team,
@@ -60,6 +68,18 @@ fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePath
60
68
  })
61
69
  }
62
70
 
71
+ pub(crate) fn input_has_no_local_team_context(workspace: &Path) -> bool {
72
+ !workspace.join("team.spec.yaml").exists()
73
+ && !workspace.join(".team").exists()
74
+ && !crate::state::persist::runtime_state_path(workspace).exists()
75
+ && workspace.file_name().and_then(|s| s.to_str()) != Some(".team")
76
+ && workspace
77
+ .parent()
78
+ .and_then(|p| p.file_name())
79
+ .and_then(|s| s.to_str())
80
+ != Some(".team")
81
+ }
82
+
63
83
  fn selected_state_spec_workspace(state: &serde_json::Value) -> Option<std::path::PathBuf> {
64
84
  state
65
85
  .get("spec_path")