@team-agent/installer 0.3.12 → 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/adapters.rs +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +258 -79
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +279 -24
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +343 -49
- 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 +390 -61
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- 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,
|
|
@@ -212,35 +223,65 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
212
223
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
213
224
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
214
225
|
}
|
|
215
|
-
let mut
|
|
226
|
+
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
227
|
+
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
228
|
+
let mut fatal_resume_failure = false;
|
|
229
|
+
// B5 restart isolation loop: per-agent spawn failures must be recorded and
|
|
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.
|
|
232
|
+
// BEGIN_B5_RESTART_ISOLATION_LOOP
|
|
216
233
|
for decision in &plan.decisions {
|
|
217
|
-
|
|
234
|
+
if fatal_resume_failure {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let Some(agent) = state
|
|
218
238
|
.get("agents")
|
|
219
239
|
.and_then(|v| v.get(decision.agent_id.as_str()))
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
.cloned()
|
|
241
|
+
else {
|
|
242
|
+
let error = format!("agent {} not found for restart", decision.agent_id);
|
|
243
|
+
mark_agent_restart_failed(&mut state, decision, &error);
|
|
244
|
+
let _ = write_restart_agent_failed_event(
|
|
245
|
+
&selected.run_workspace,
|
|
246
|
+
decision,
|
|
247
|
+
"spawn",
|
|
248
|
+
&error,
|
|
249
|
+
);
|
|
250
|
+
failed_agents.push(restart_failed_agent(decision, "spawn", error));
|
|
251
|
+
continue;
|
|
252
|
+
};
|
|
227
253
|
let session_id = if matches!(decision.restart_mode, StartMode::Resumed) {
|
|
228
254
|
decision.session_id.as_ref()
|
|
229
255
|
} else {
|
|
230
256
|
None
|
|
231
257
|
};
|
|
232
|
-
let session_live = session_live_or_default(transport, &session_name, false);
|
|
258
|
+
let mut session_live = session_live_or_default(transport, &session_name, false);
|
|
233
259
|
if !session_live {
|
|
234
|
-
if let Some(previous) =
|
|
235
|
-
|
|
260
|
+
if let Some(previous) = successful_agents.pop() {
|
|
261
|
+
let error = format!(
|
|
236
262
|
"session_disappeared_after_spawn: provider_resume_exited for {}; session {} disappeared before spawning {}",
|
|
237
|
-
previous,
|
|
263
|
+
previous.agent_id,
|
|
238
264
|
session_name.as_str(),
|
|
239
265
|
decision.agent_id
|
|
240
|
-
)
|
|
266
|
+
);
|
|
267
|
+
mark_agent_restart_failed(&mut state, &previous, &error);
|
|
268
|
+
let _ = write_restart_agent_failed_event(
|
|
269
|
+
&selected.run_workspace,
|
|
270
|
+
&previous,
|
|
271
|
+
"resume",
|
|
272
|
+
&error,
|
|
273
|
+
);
|
|
274
|
+
failed_agents.push(restart_failed_agent(&previous, "resume", error));
|
|
275
|
+
if is_resume_integrity_failure(&previous, "resume", "") {
|
|
276
|
+
fatal_resume_failure = true;
|
|
277
|
+
}
|
|
278
|
+
session_live = false;
|
|
241
279
|
}
|
|
242
280
|
}
|
|
243
|
-
|
|
281
|
+
if fatal_resume_failure {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
let spawn = match spawn_agent_window(
|
|
244
285
|
&selected.run_workspace,
|
|
245
286
|
&session_name,
|
|
246
287
|
&decision.agent_id,
|
|
@@ -250,10 +291,42 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
250
291
|
transport,
|
|
251
292
|
Some(&safety),
|
|
252
293
|
Some(spec_workspace),
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
294
|
+
) {
|
|
295
|
+
Ok(spawn) => spawn,
|
|
296
|
+
Err(error) => {
|
|
297
|
+
let error = error.to_string();
|
|
298
|
+
mark_agent_restart_failed(&mut state, decision, &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
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
|
|
314
|
+
.and_then(|_| {
|
|
315
|
+
mark_agent_respawned(&mut state, &decision.agent_id, &spawn, transport, &safety)
|
|
316
|
+
})
|
|
317
|
+
{
|
|
318
|
+
let error = error.to_string();
|
|
319
|
+
mark_agent_restart_failed(&mut state, decision, &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
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
successful_agents.push(decision.clone());
|
|
257
330
|
if let Some(agent) = state
|
|
258
331
|
.get_mut("agents")
|
|
259
332
|
.and_then(serde_json::Value::as_object_mut)
|
|
@@ -263,14 +336,47 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
263
336
|
persist_effective_approval_policy_for_restart(agent, &safety);
|
|
264
337
|
}
|
|
265
338
|
}
|
|
339
|
+
// END_B5_RESTART_ISOLATION_LOOP
|
|
266
340
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
267
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
|
+
}
|
|
358
|
+
if successful_agents.is_empty() && !failed_agents.is_empty() {
|
|
359
|
+
let attach_commands = Vec::new();
|
|
360
|
+
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
361
|
+
write_restart_completed_event(
|
|
362
|
+
&selected.run_workspace,
|
|
363
|
+
&successful_agents,
|
|
364
|
+
&failed_agents,
|
|
365
|
+
"fail",
|
|
366
|
+
)?;
|
|
367
|
+
return Ok(RestartReport::Failed {
|
|
368
|
+
session_name,
|
|
369
|
+
failed_agents,
|
|
370
|
+
next_actions,
|
|
371
|
+
attach_commands,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
268
374
|
let coordinator_started = start_coordinator_for_workspace(&selected.run_workspace)?;
|
|
269
375
|
wait_restart_readiness_or_timeout(
|
|
270
376
|
&selected.run_workspace,
|
|
271
377
|
&state,
|
|
272
378
|
&session_name,
|
|
273
|
-
&
|
|
379
|
+
&successful_agents,
|
|
274
380
|
transport,
|
|
275
381
|
restart_readiness_deadline(readiness_deadline_ms),
|
|
276
382
|
restart_readiness_poll_interval(),
|
|
@@ -278,14 +384,37 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
278
384
|
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
279
385
|
&selected.run_workspace,
|
|
280
386
|
&session_name,
|
|
281
|
-
|
|
387
|
+
successful_agents
|
|
282
388
|
.iter()
|
|
283
389
|
.map(|decision| decision.agent_id.as_str()),
|
|
284
390
|
);
|
|
285
|
-
let next_actions = attach_commands.clone();
|
|
391
|
+
let mut next_actions = attach_commands.clone();
|
|
392
|
+
if !failed_agents.is_empty() {
|
|
393
|
+
next_actions.extend(restart_failure_next_actions(&failed_agents));
|
|
394
|
+
write_restart_completed_event(
|
|
395
|
+
&selected.run_workspace,
|
|
396
|
+
&successful_agents,
|
|
397
|
+
&failed_agents,
|
|
398
|
+
"partial",
|
|
399
|
+
)?;
|
|
400
|
+
return Ok(RestartReport::Partial {
|
|
401
|
+
session_name,
|
|
402
|
+
agents: successful_agents,
|
|
403
|
+
failed_agents,
|
|
404
|
+
coordinator_started,
|
|
405
|
+
next_actions,
|
|
406
|
+
attach_commands,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
write_restart_completed_event(
|
|
410
|
+
&selected.run_workspace,
|
|
411
|
+
&successful_agents,
|
|
412
|
+
&failed_agents,
|
|
413
|
+
"ok",
|
|
414
|
+
)?;
|
|
286
415
|
Ok(RestartReport::Restarted {
|
|
287
416
|
session_name,
|
|
288
|
-
agents:
|
|
417
|
+
agents: successful_agents,
|
|
289
418
|
coordinator_started,
|
|
290
419
|
next_actions,
|
|
291
420
|
attach_commands,
|
|
@@ -478,9 +607,9 @@ fn parse_duration_value_seconds_ms(value: &str) -> Option<u64> {
|
|
|
478
607
|
}
|
|
479
608
|
|
|
480
609
|
fn restart_readiness_deadline(requested_ms: Option<u64>) -> std::time::Duration {
|
|
481
|
-
requested_ms
|
|
482
|
-
|
|
483
|
-
|
|
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))
|
|
484
613
|
}
|
|
485
614
|
|
|
486
615
|
fn restart_readiness_poll_interval() -> std::time::Duration {
|
|
@@ -518,11 +647,14 @@ fn wait_restart_readiness_or_timeout(
|
|
|
518
647
|
let elapsed = started.elapsed();
|
|
519
648
|
if elapsed >= deadline {
|
|
520
649
|
write_restart_readiness_timeout_event(workspace, readiness, deadline, elapsed)?;
|
|
521
|
-
return Err(LifecycleError::RequirementUnmet(
|
|
522
|
-
workspace, readiness, deadline,
|
|
523
|
-
))
|
|
650
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
651
|
+
restart_readiness_timeout_message(workspace, readiness, deadline),
|
|
652
|
+
));
|
|
524
653
|
}
|
|
525
|
-
std::thread::sleep(std::cmp::min(
|
|
654
|
+
std::thread::sleep(std::cmp::min(
|
|
655
|
+
poll_interval,
|
|
656
|
+
deadline.saturating_sub(elapsed),
|
|
657
|
+
));
|
|
526
658
|
}
|
|
527
659
|
}
|
|
528
660
|
|
|
@@ -538,7 +670,11 @@ fn restart_readiness(
|
|
|
538
670
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
539
671
|
let coordinator_alive =
|
|
540
672
|
crate::coordinator::coordinator_health(&coordinator_workspace).ok && session_created;
|
|
541
|
-
RestartReadiness {
|
|
673
|
+
RestartReadiness {
|
|
674
|
+
session_created,
|
|
675
|
+
worker_pane_addressable,
|
|
676
|
+
coordinator_alive,
|
|
677
|
+
}
|
|
542
678
|
}
|
|
543
679
|
|
|
544
680
|
fn restart_worker_panes_addressable(
|
|
@@ -650,7 +786,11 @@ fn restart_readiness_missing_summary(readiness: RestartReadiness) -> String {
|
|
|
650
786
|
}
|
|
651
787
|
|
|
652
788
|
fn yes_no(value: bool) -> &'static str {
|
|
653
|
-
if value {
|
|
789
|
+
if value {
|
|
790
|
+
"yes"
|
|
791
|
+
} else {
|
|
792
|
+
"no"
|
|
793
|
+
}
|
|
654
794
|
}
|
|
655
795
|
|
|
656
796
|
fn verify_spawned_agent_live(
|
|
@@ -749,6 +889,161 @@ fn mark_agent_respawned(
|
|
|
749
889
|
Ok(())
|
|
750
890
|
}
|
|
751
891
|
|
|
892
|
+
fn restart_failed_agent(
|
|
893
|
+
decision: &RestartedAgent,
|
|
894
|
+
phase: impl Into<String>,
|
|
895
|
+
error: String,
|
|
896
|
+
) -> RestartFailedAgent {
|
|
897
|
+
RestartFailedAgent {
|
|
898
|
+
agent_id: decision.agent_id.clone(),
|
|
899
|
+
restart_mode: decision.restart_mode,
|
|
900
|
+
decision: decision.decision,
|
|
901
|
+
session_id: decision.session_id.clone(),
|
|
902
|
+
phase: phase.into(),
|
|
903
|
+
error,
|
|
904
|
+
}
|
|
905
|
+
}
|
|
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
|
+
|
|
934
|
+
fn mark_agent_restart_failed(
|
|
935
|
+
state: &mut serde_json::Value,
|
|
936
|
+
decision: &RestartedAgent,
|
|
937
|
+
error: &str,
|
|
938
|
+
) {
|
|
939
|
+
let Some(agent) = state
|
|
940
|
+
.get_mut("agents")
|
|
941
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
942
|
+
.and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
|
|
943
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
944
|
+
else {
|
|
945
|
+
return;
|
|
946
|
+
};
|
|
947
|
+
agent.insert("status".to_string(), serde_json::json!("failed"));
|
|
948
|
+
agent.insert("restart_error".to_string(), serde_json::json!(error));
|
|
949
|
+
agent.insert(
|
|
950
|
+
"restart_failed_at".to_string(),
|
|
951
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
952
|
+
);
|
|
953
|
+
agent.remove("pane_id");
|
|
954
|
+
agent.remove("pane_pid");
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
fn write_restart_agent_failed_event(
|
|
958
|
+
workspace: &Path,
|
|
959
|
+
decision: &RestartedAgent,
|
|
960
|
+
phase: &str,
|
|
961
|
+
error: &str,
|
|
962
|
+
) -> Result<(), LifecycleError> {
|
|
963
|
+
crate::event_log::EventLog::new(workspace)
|
|
964
|
+
.write(
|
|
965
|
+
"restart.agent_failed",
|
|
966
|
+
serde_json::json!({
|
|
967
|
+
"agent_id": decision.agent_id.as_str(),
|
|
968
|
+
"restart_mode": start_mode_wire(decision.restart_mode),
|
|
969
|
+
"decision": resume_decision_wire(decision.decision),
|
|
970
|
+
"session_id": decision.session_id.as_ref().map(|session| session.as_str()),
|
|
971
|
+
"phase": phase,
|
|
972
|
+
"error": error,
|
|
973
|
+
"action": format!(
|
|
974
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
975
|
+
decision.agent_id,
|
|
976
|
+
decision.agent_id
|
|
977
|
+
),
|
|
978
|
+
"log": format!(
|
|
979
|
+
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
980
|
+
decision.agent_id
|
|
981
|
+
),
|
|
982
|
+
}),
|
|
983
|
+
)
|
|
984
|
+
.map(|_| ())
|
|
985
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
fn write_restart_completed_event(
|
|
989
|
+
workspace: &Path,
|
|
990
|
+
successful_agents: &[RestartedAgent],
|
|
991
|
+
failed_agents: &[RestartFailedAgent],
|
|
992
|
+
rc: &str,
|
|
993
|
+
) -> Result<(), LifecycleError> {
|
|
994
|
+
crate::event_log::EventLog::new(workspace)
|
|
995
|
+
.write(
|
|
996
|
+
"restart.completed",
|
|
997
|
+
serde_json::json!({
|
|
998
|
+
"rc": rc,
|
|
999
|
+
"status": rc,
|
|
1000
|
+
"successful_agents": successful_agents
|
|
1001
|
+
.iter()
|
|
1002
|
+
.map(|agent| agent.agent_id.as_str())
|
|
1003
|
+
.collect::<Vec<_>>(),
|
|
1004
|
+
"failed_agents": failed_agents
|
|
1005
|
+
.iter()
|
|
1006
|
+
.map(|failure| serde_json::json!({
|
|
1007
|
+
"agent_id": failure.agent_id.as_str(),
|
|
1008
|
+
"phase": failure.phase,
|
|
1009
|
+
"error": failure.error,
|
|
1010
|
+
}))
|
|
1011
|
+
.collect::<Vec<_>>(),
|
|
1012
|
+
}),
|
|
1013
|
+
)
|
|
1014
|
+
.map(|_| ())
|
|
1015
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<String> {
|
|
1019
|
+
failed_agents
|
|
1020
|
+
.iter()
|
|
1021
|
+
.map(|failure| {
|
|
1022
|
+
format!(
|
|
1023
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
1024
|
+
failure.agent_id, failure.agent_id
|
|
1025
|
+
)
|
|
1026
|
+
})
|
|
1027
|
+
.collect()
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn start_mode_wire(mode: StartMode) -> &'static str {
|
|
1031
|
+
match mode {
|
|
1032
|
+
StartMode::Resumed => "resumed",
|
|
1033
|
+
StartMode::Fresh => "fresh",
|
|
1034
|
+
StartMode::FreshAfterMissingRollout => "fresh_after_missing_rollout",
|
|
1035
|
+
StartMode::Noop => "noop",
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
fn resume_decision_wire(decision: ResumeDecision) -> &'static str {
|
|
1040
|
+
match decision {
|
|
1041
|
+
ResumeDecision::Resume => "resume",
|
|
1042
|
+
ResumeDecision::FreshStart => "fresh_start",
|
|
1043
|
+
ResumeDecision::Refuse => "refuse",
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
752
1047
|
fn write_restart_resume_decision_events(
|
|
753
1048
|
workspace: &Path,
|
|
754
1049
|
state: &serde_json::Value,
|
|
@@ -768,11 +1063,7 @@ fn write_restart_resume_decision_events(
|
|
|
768
1063
|
.filter(|s| !s.is_empty())
|
|
769
1064
|
.map(|s| s.to_string());
|
|
770
1065
|
let session_id = decision.session_id.as_ref().map(|s| s.as_str().to_string());
|
|
771
|
-
let decision_wire =
|
|
772
|
-
ResumeDecision::Resume => "resume",
|
|
773
|
-
ResumeDecision::FreshStart => "fresh_start",
|
|
774
|
-
ResumeDecision::Refuse => "refuse",
|
|
775
|
-
};
|
|
1066
|
+
let decision_wire = resume_decision_wire(decision.decision);
|
|
776
1067
|
write_restart_resume_decision_event(
|
|
777
1068
|
workspace,
|
|
778
1069
|
decision.agent_id.as_str(),
|
|
@@ -956,13 +1247,16 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
956
1247
|
let agents_dir = team_dir.join("agents");
|
|
957
1248
|
let has_role_doc = std::fs::read_dir(&agents_dir)
|
|
958
1249
|
.map(|entries| {
|
|
959
|
-
entries
|
|
960
|
-
|
|
961
|
-
|
|
1250
|
+
entries
|
|
1251
|
+
.flatten()
|
|
1252
|
+
.any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("md"))
|
|
962
1253
|
})
|
|
963
1254
|
.unwrap_or(false);
|
|
964
1255
|
if !has_role_doc {
|
|
965
|
-
missing.push(format!(
|
|
1256
|
+
missing.push(format!(
|
|
1257
|
+
"{}/*.md (at least one role doc)",
|
|
1258
|
+
agents_dir.display()
|
|
1259
|
+
));
|
|
966
1260
|
}
|
|
967
1261
|
if !missing.is_empty() {
|
|
968
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
|