@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/mod.rs +228 -85
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
- package/crates/team-agent/src/cli/types.rs +30 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +692 -92
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +90 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +31 -0
- package/package.json +4 -4
- 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
|
-
.
|
|
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,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
|
|
273
|
-
|
|
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(
|
|
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
|
|
285
|
-
|
|
286
|
-
decision,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
559
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
|
1161
|
-
|
|
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!(
|
|
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 =
|
|
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
|