@team-agent/installer 0.4.11 → 0.5.1
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/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +98 -5
- package/crates/team-agent/src/cli/mod.rs +2 -0
- package/crates/team-agent/src/cli/named_address.rs +864 -0
- package/crates/team-agent/src/cli/send.rs +104 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +4 -0
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -118,7 +118,9 @@ pub(super) fn spawn_agent_window(
|
|
|
118
118
|
// persisted agent JSON (state's agent["effort"] field, set by launch).
|
|
119
119
|
let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider);
|
|
120
120
|
if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
|
|
121
|
-
agent,
|
|
121
|
+
agent,
|
|
122
|
+
provider,
|
|
123
|
+
agent_id.as_str(),
|
|
122
124
|
) {
|
|
123
125
|
let _ = crate::event_log::EventLog::new(workspace)
|
|
124
126
|
.write("provider.effort_unsupported", event_value);
|
|
@@ -200,10 +202,8 @@ pub(super) fn spawn_agent_window(
|
|
|
200
202
|
.and_then(|i| plan.argv.get(i + 1))
|
|
201
203
|
.cloned();
|
|
202
204
|
let env_overlay_keys: Vec<&String> = env.keys().collect();
|
|
203
|
-
let tmux_start_mode_pre_spawn =
|
|
204
|
-
layout_placement,
|
|
205
|
-
into_existing_session,
|
|
206
|
-
);
|
|
205
|
+
let tmux_start_mode_pre_spawn =
|
|
206
|
+
predict_tmux_start_mode(layout_placement, into_existing_session);
|
|
207
207
|
let spawn_epoch = state_spawn_epoch_for_agent(workspace, agent_id);
|
|
208
208
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
209
209
|
let _ = event_log.write(
|
|
@@ -311,12 +311,23 @@ pub(super) fn spawn_agent_window(
|
|
|
311
311
|
agent_id.as_str(),
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
|
-
let
|
|
314
|
+
let startup_prompt = adapter.handle_startup_prompts_outcome(
|
|
315
315
|
transport,
|
|
316
316
|
&crate::transport::Target::Pane(spawn.pane_id.clone()),
|
|
317
317
|
30,
|
|
318
318
|
0.5,
|
|
319
319
|
);
|
|
320
|
+
if let Some(error) = startup_prompt.capture_error.as_deref() {
|
|
321
|
+
if is_structural_startup_prompt_error(error) {
|
|
322
|
+
return Err(LifecycleError::Transport(format!(
|
|
323
|
+
"startup prompt structural failure for {}:{} pane {}: {}",
|
|
324
|
+
session_name.as_str(),
|
|
325
|
+
window.as_str(),
|
|
326
|
+
spawn.pane_id.as_str(),
|
|
327
|
+
error
|
|
328
|
+
)));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
320
331
|
Ok(SpawnedAgentWindow {
|
|
321
332
|
spawn,
|
|
322
333
|
plan,
|
|
@@ -327,6 +338,86 @@ pub(super) fn spawn_agent_window(
|
|
|
327
338
|
})
|
|
328
339
|
}
|
|
329
340
|
|
|
341
|
+
fn is_structural_startup_prompt_error(error: &str) -> bool {
|
|
342
|
+
let lower = error.to_ascii_lowercase();
|
|
343
|
+
[
|
|
344
|
+
"can't find window",
|
|
345
|
+
"can't find pane",
|
|
346
|
+
"cannot find window",
|
|
347
|
+
"cannot find pane",
|
|
348
|
+
"target not found",
|
|
349
|
+
"no such pane",
|
|
350
|
+
"window disappeared",
|
|
351
|
+
"pane not owned",
|
|
352
|
+
"not owned by requested",
|
|
353
|
+
]
|
|
354
|
+
.iter()
|
|
355
|
+
.any(|needle| lower.contains(needle))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
pub(super) fn rehydrate_agent_command_context_from_spec(
|
|
359
|
+
spec_workspace: &Path,
|
|
360
|
+
agent_id: &AgentId,
|
|
361
|
+
agent: &serde_json::Value,
|
|
362
|
+
) -> serde_json::Value {
|
|
363
|
+
let Ok(spec) = load_team_spec(spec_workspace) else {
|
|
364
|
+
return agent.clone();
|
|
365
|
+
};
|
|
366
|
+
let Some(spec_agent) = find_spec_agent(&spec, agent_id) else {
|
|
367
|
+
return agent.clone();
|
|
368
|
+
};
|
|
369
|
+
merge_command_context_fields(agent, spec_agent)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fn merge_command_context_fields(
|
|
373
|
+
agent: &serde_json::Value,
|
|
374
|
+
spec_agent: &YamlValue,
|
|
375
|
+
) -> serde_json::Value {
|
|
376
|
+
let mut merged = agent.clone();
|
|
377
|
+
let Some(obj) = merged.as_object_mut() else {
|
|
378
|
+
return merged;
|
|
379
|
+
};
|
|
380
|
+
for field in [
|
|
381
|
+
"role",
|
|
382
|
+
"tools",
|
|
383
|
+
"system_prompt",
|
|
384
|
+
"output_contract",
|
|
385
|
+
"provider",
|
|
386
|
+
"model",
|
|
387
|
+
"auth_mode",
|
|
388
|
+
"effort",
|
|
389
|
+
"profile",
|
|
390
|
+
"permission_mode",
|
|
391
|
+
] {
|
|
392
|
+
if let Some(value) = spec_agent.get(field).and_then(yaml_value_to_json) {
|
|
393
|
+
obj.insert(field.to_string(), value);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
merged
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn yaml_value_to_json(value: &YamlValue) -> Option<serde_json::Value> {
|
|
400
|
+
match value {
|
|
401
|
+
YamlValue::Null => Some(serde_json::Value::Null),
|
|
402
|
+
YamlValue::Bool(value) => Some(serde_json::json!(value)),
|
|
403
|
+
YamlValue::Int(value) => Some(serde_json::json!(value)),
|
|
404
|
+
YamlValue::Float(value) => Some(serde_json::json!(value)),
|
|
405
|
+
YamlValue::Str(value) => Some(serde_json::json!(value)),
|
|
406
|
+
YamlValue::List(items) => Some(serde_json::Value::Array(
|
|
407
|
+
items.iter().filter_map(yaml_value_to_json).collect(),
|
|
408
|
+
)),
|
|
409
|
+
YamlValue::Map(items) => {
|
|
410
|
+
let mut map = serde_json::Map::new();
|
|
411
|
+
for (key, value) in items {
|
|
412
|
+
if let Some(value) = yaml_value_to_json(value) {
|
|
413
|
+
map.insert(key.clone(), value);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
Some(serde_json::Value::Object(map))
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
330
421
|
/// E43 Fix C helper (0.3.24 bug#3): probe live tmux for a window's existence
|
|
331
422
|
/// before issuing `split-window -t :<window>`. Uses `list_windows` first
|
|
332
423
|
/// (cheaper, authoritative when present); falls back to `list_targets` so
|
|
@@ -419,16 +510,18 @@ pub(super) fn save_restart_projected_state(
|
|
|
419
510
|
workspace: &Path,
|
|
420
511
|
state: &mut serde_json::Value,
|
|
421
512
|
team_key: &str,
|
|
513
|
+
topology_authority_agent_ids: &[&str],
|
|
422
514
|
) -> Result<(), LifecycleError> {
|
|
423
515
|
sync_restart_team_projections(state, team_key);
|
|
424
|
-
crate::state::projection::
|
|
425
|
-
|
|
516
|
+
crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority(
|
|
517
|
+
workspace,
|
|
518
|
+
state,
|
|
519
|
+
topology_authority_agent_ids,
|
|
520
|
+
)
|
|
521
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
426
522
|
}
|
|
427
523
|
|
|
428
|
-
pub(super) fn restart_projection_team_key(
|
|
429
|
-
state: &serde_json::Value,
|
|
430
|
-
team: Option<&str>,
|
|
431
|
-
) -> String {
|
|
524
|
+
pub(super) fn restart_projection_team_key(state: &serde_json::Value, team: Option<&str>) -> String {
|
|
432
525
|
team.filter(|key| !key.is_empty())
|
|
433
526
|
.map(str::to_string)
|
|
434
527
|
.or_else(|| {
|
|
@@ -605,10 +698,7 @@ pub(super) fn resume_backing_probe_for_agent(
|
|
|
605
698
|
checked_paths.push(root.clone());
|
|
606
699
|
}
|
|
607
700
|
rollout_ok
|
|
608
|
-
|| codex_session_transcript_exists_with_roots(
|
|
609
|
-
session_id.as_str(),
|
|
610
|
-
&scan_roots,
|
|
611
|
-
)
|
|
701
|
+
|| codex_session_transcript_exists_with_roots(session_id.as_str(), &scan_roots)
|
|
612
702
|
}
|
|
613
703
|
Provider::Claude | Provider::ClaudeCode => {
|
|
614
704
|
let rollout_ok = rollout_path_exists(rollout_path);
|
|
@@ -705,7 +795,8 @@ fn claude_projects_root_for_agent(agent: &serde_json::Value) -> Option<PathBuf>
|
|
|
705
795
|
.filter(|value| !value.is_empty())
|
|
706
796
|
.map(PathBuf::from)
|
|
707
797
|
.or_else(|| {
|
|
708
|
-
std::env::var_os("HOME")
|
|
798
|
+
std::env::var_os("HOME")
|
|
799
|
+
.map(|home| PathBuf::from(home).join(".claude").join("projects"))
|
|
709
800
|
})
|
|
710
801
|
}
|
|
711
802
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use super::common::*;
|
|
2
2
|
use super::selection::classify_restart_plan_with_resume_validation;
|
|
3
3
|
use super::*;
|
|
4
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
4
5
|
|
|
5
6
|
// ── lifecycle::restart —— 整队 Route B resume-or-fresh 重建 ──────────────────
|
|
6
7
|
|
|
@@ -128,6 +129,12 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
128
129
|
expected.display()
|
|
129
130
|
)));
|
|
130
131
|
}
|
|
132
|
+
let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
133
|
+
workspace: &selected.run_workspace,
|
|
134
|
+
operation: "restart",
|
|
135
|
+
team: Some(selected.team_key.as_str()),
|
|
136
|
+
agent_id: None,
|
|
137
|
+
})?;
|
|
131
138
|
let mut state = selected.state;
|
|
132
139
|
crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
|
|
133
140
|
// E5 task#3 / RC-A6a + E4(leader 裁定:每次 restart 都从角色定义重建 runtime spec,覆盖):
|
|
@@ -187,8 +194,15 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
187
194
|
if convergence.converged && convergence.changed {
|
|
188
195
|
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
189
196
|
}
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
let repaired_agent_ids =
|
|
198
|
+
repair_resume_sessions_from_event_log(&selected.run_workspace, &mut state)?;
|
|
199
|
+
if !repaired_agent_ids.is_empty() {
|
|
200
|
+
save_restart_session_repairs(
|
|
201
|
+
&selected.run_workspace,
|
|
202
|
+
&mut state,
|
|
203
|
+
&selected.team_key,
|
|
204
|
+
&repaired_agent_ids,
|
|
205
|
+
)?;
|
|
192
206
|
let missing_after_repair = restart_required_missing_session_agent_ids(&state);
|
|
193
207
|
convergence.changed = true;
|
|
194
208
|
convergence.converged = missing_after_repair.is_empty();
|
|
@@ -295,7 +309,17 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
295
309
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
296
310
|
mark_leader_receiver_rebind_required(&mut state, &session_name);
|
|
297
311
|
mark_restart_targets_stopped_after_teardown(&mut state, &plan.decisions);
|
|
298
|
-
|
|
312
|
+
let topology_authority_agent_ids = plan
|
|
313
|
+
.decisions
|
|
314
|
+
.iter()
|
|
315
|
+
.map(|decision| decision.agent_id.as_str().to_string())
|
|
316
|
+
.collect::<Vec<_>>();
|
|
317
|
+
save_restart_state_with_lifecycle_topology_authority(
|
|
318
|
+
&selected.run_workspace,
|
|
319
|
+
&mut state,
|
|
320
|
+
&selected.team_key,
|
|
321
|
+
&topology_authority_agent_ids,
|
|
322
|
+
)?;
|
|
299
323
|
}
|
|
300
324
|
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
301
325
|
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
@@ -308,7 +332,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
308
332
|
if fatal_resume_failure {
|
|
309
333
|
continue;
|
|
310
334
|
}
|
|
311
|
-
let Some(
|
|
335
|
+
let Some(raw_agent) = state
|
|
312
336
|
.get("agents")
|
|
313
337
|
.and_then(|v| v.get(decision.agent_id.as_str()))
|
|
314
338
|
.cloned()
|
|
@@ -324,6 +348,11 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
324
348
|
failed_agents.push(restart_failed_agent(decision, "spawn", error));
|
|
325
349
|
continue;
|
|
326
350
|
};
|
|
351
|
+
let agent = rehydrate_agent_command_context_from_spec(
|
|
352
|
+
spec_workspace,
|
|
353
|
+
&decision.agent_id,
|
|
354
|
+
&raw_agent,
|
|
355
|
+
);
|
|
327
356
|
let session_id = if matches!(decision.restart_mode, StartMode::Resumed) {
|
|
328
357
|
decision.session_id.as_ref()
|
|
329
358
|
} else {
|
|
@@ -429,7 +458,21 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
429
458
|
}
|
|
430
459
|
}
|
|
431
460
|
// END_B5_RESTART_ISOLATION_LOOP
|
|
432
|
-
|
|
461
|
+
let mut topology_authority_agent_ids = successful_agents
|
|
462
|
+
.iter()
|
|
463
|
+
.map(|agent| agent.agent_id.as_str().to_string())
|
|
464
|
+
.collect::<Vec<_>>();
|
|
465
|
+
topology_authority_agent_ids.extend(
|
|
466
|
+
failed_agents
|
|
467
|
+
.iter()
|
|
468
|
+
.map(|agent| agent.agent_id.as_str().to_string()),
|
|
469
|
+
);
|
|
470
|
+
save_restart_state_with_lifecycle_topology_authority(
|
|
471
|
+
&selected.run_workspace,
|
|
472
|
+
&mut state,
|
|
473
|
+
&selected.team_key,
|
|
474
|
+
&topology_authority_agent_ids,
|
|
475
|
+
)?;
|
|
433
476
|
if fatal_resume_failure {
|
|
434
477
|
let attach_commands = Vec::new();
|
|
435
478
|
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
@@ -496,7 +539,10 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
496
539
|
let convergence_deadline = session_convergence_deadline(session_converge_deadline_ms);
|
|
497
540
|
let convergence_poll = session_convergence_poll_interval();
|
|
498
541
|
for decision in successful_agents.drain(..) {
|
|
499
|
-
if !matches!(
|
|
542
|
+
if !matches!(
|
|
543
|
+
decision.restart_mode,
|
|
544
|
+
crate::lifecycle::types::StartMode::Resumed
|
|
545
|
+
) {
|
|
500
546
|
survivors.push(decision);
|
|
501
547
|
continue;
|
|
502
548
|
}
|
|
@@ -610,18 +656,23 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
610
656
|
let phase = "resume_postflight";
|
|
611
657
|
let error = "session_backing_store_missing_after_restart".to_string();
|
|
612
658
|
mark_agent_restart_failed(&mut state, &decision, &error);
|
|
613
|
-
let _ =
|
|
614
|
-
&selected.run_workspace,
|
|
615
|
-
&decision,
|
|
616
|
-
phase,
|
|
617
|
-
&error,
|
|
618
|
-
);
|
|
659
|
+
let _ =
|
|
660
|
+
write_restart_agent_failed_event(&selected.run_workspace, &decision, phase, &error);
|
|
619
661
|
postflight_failed.push(restart_failed_agent(&decision, phase, error));
|
|
620
662
|
}
|
|
621
663
|
successful_agents = survivors;
|
|
622
664
|
failed_agents.extend(postflight_failed);
|
|
623
665
|
if !failed_agents.is_empty() {
|
|
624
|
-
|
|
666
|
+
let topology_authority_agent_ids = failed_agents
|
|
667
|
+
.iter()
|
|
668
|
+
.map(|agent| agent.agent_id.as_str().to_string())
|
|
669
|
+
.collect::<Vec<_>>();
|
|
670
|
+
save_restart_state_with_lifecycle_topology_authority(
|
|
671
|
+
&selected.run_workspace,
|
|
672
|
+
&mut state,
|
|
673
|
+
&selected.team_key,
|
|
674
|
+
&topology_authority_agent_ids,
|
|
675
|
+
)?;
|
|
625
676
|
}
|
|
626
677
|
}
|
|
627
678
|
if successful_agents.is_empty() && !failed_agents.is_empty() {
|
|
@@ -641,6 +692,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
641
692
|
attach_commands,
|
|
642
693
|
});
|
|
643
694
|
}
|
|
695
|
+
drop(lifecycle_lock);
|
|
644
696
|
let coordinator_started = start_coordinator_for_workspace(&selected.run_workspace)?;
|
|
645
697
|
wait_restart_readiness_or_timeout(
|
|
646
698
|
&selected.run_workspace,
|
|
@@ -691,11 +743,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
691
743
|
// invoked from a tmux pane should bind that pane as leader_receiver,
|
|
692
744
|
// restoring the worker→leader delivery path. Failure is non-fatal — the
|
|
693
745
|
// user can still run `team-agent attach-leader` manually.
|
|
694
|
-
try_autobind_leader_after_restart(
|
|
695
|
-
&selected.run_workspace,
|
|
696
|
-
Some(&selected.team_key),
|
|
697
|
-
&state,
|
|
698
|
-
);
|
|
746
|
+
try_autobind_leader_after_restart(&selected.run_workspace, Some(&selected.team_key), &state);
|
|
699
747
|
// 0.3.28 Step 1: topology invariant guard (warn-only). Same pattern as
|
|
700
748
|
// `lifecycle::launch::launch_with_transport_in_workspace` — logs to stderr,
|
|
701
749
|
// never panics. Hard error path is deferred to Step 10.
|
|
@@ -713,13 +761,13 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
713
761
|
fn repair_resume_sessions_from_event_log(
|
|
714
762
|
workspace: &Path,
|
|
715
763
|
state: &mut serde_json::Value,
|
|
716
|
-
) -> Result<
|
|
764
|
+
) -> Result<Vec<String>, LifecycleError> {
|
|
717
765
|
let agent_ids = state
|
|
718
766
|
.get("agents")
|
|
719
767
|
.and_then(serde_json::Value::as_object)
|
|
720
768
|
.map(|agents| agents.keys().cloned().collect::<Vec<_>>())
|
|
721
769
|
.unwrap_or_default();
|
|
722
|
-
let mut
|
|
770
|
+
let mut repaired_agent_ids = Vec::new();
|
|
723
771
|
for agent_id in agent_ids {
|
|
724
772
|
let previous = state
|
|
725
773
|
.get("agents")
|
|
@@ -824,9 +872,9 @@ fn repair_resume_sessions_from_event_log(
|
|
|
824
872
|
}),
|
|
825
873
|
)
|
|
826
874
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
827
|
-
|
|
875
|
+
repaired_agent_ids.push(agent_id);
|
|
828
876
|
}
|
|
829
|
-
Ok(
|
|
877
|
+
Ok(repaired_agent_ids)
|
|
830
878
|
}
|
|
831
879
|
|
|
832
880
|
fn claimed_session_ids_except(
|
|
@@ -1189,9 +1237,7 @@ fn try_autobind_leader_after_restart(
|
|
|
1189
1237
|
// the same attach target). Wire-format `parse_provider` keeps
|
|
1190
1238
|
// them distinct everywhere else.
|
|
1191
1239
|
crate::provider::wire::parse_provider(s).map(|p| match p {
|
|
1192
|
-
crate::model::enums::Provider::Claude =>
|
|
1193
|
-
crate::model::enums::Provider::ClaudeCode
|
|
1194
|
-
}
|
|
1240
|
+
crate::model::enums::Provider::Claude => crate::model::enums::Provider::ClaudeCode,
|
|
1195
1241
|
other => other,
|
|
1196
1242
|
})
|
|
1197
1243
|
})
|
|
@@ -1272,7 +1318,92 @@ fn save_restart_state(
|
|
|
1272
1318
|
state: &mut serde_json::Value,
|
|
1273
1319
|
team_key: &str,
|
|
1274
1320
|
) -> Result<(), LifecycleError> {
|
|
1275
|
-
save_restart_projected_state(workspace, state, team_key)
|
|
1321
|
+
save_restart_projected_state(workspace, state, team_key, &[])
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
fn save_restart_state_with_lifecycle_topology_authority(
|
|
1325
|
+
workspace: &Path,
|
|
1326
|
+
state: &mut serde_json::Value,
|
|
1327
|
+
team_key: &str,
|
|
1328
|
+
agent_ids: &[String],
|
|
1329
|
+
) -> Result<(), LifecycleError> {
|
|
1330
|
+
let agent_ids = agent_ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
1331
|
+
save_restart_projected_state(workspace, state, team_key, &agent_ids)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
fn save_restart_session_repairs(
|
|
1335
|
+
workspace: &Path,
|
|
1336
|
+
state: &mut serde_json::Value,
|
|
1337
|
+
team_key: &str,
|
|
1338
|
+
agent_ids: &[String],
|
|
1339
|
+
) -> Result<(), LifecycleError> {
|
|
1340
|
+
let repairs = collect_session_repair_fields(state, agent_ids);
|
|
1341
|
+
sync_restart_team_projections(state, team_key);
|
|
1342
|
+
match crate::state::projection::save_team_scoped_state(workspace, state) {
|
|
1343
|
+
Ok(()) => Ok(()),
|
|
1344
|
+
Err(crate::state::StateError::SaveConflict(_)) => {
|
|
1345
|
+
let mut latest =
|
|
1346
|
+
crate::state::projection::select_runtime_state(workspace, Some(team_key))
|
|
1347
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1348
|
+
for (agent_id, repair) in &repairs {
|
|
1349
|
+
apply_session_repair_fields(&mut latest, agent_id, repair);
|
|
1350
|
+
}
|
|
1351
|
+
sync_restart_team_projections(&mut latest, team_key);
|
|
1352
|
+
crate::state::projection::save_team_scoped_state(workspace, &latest)
|
|
1353
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1354
|
+
*state = latest;
|
|
1355
|
+
Ok(())
|
|
1356
|
+
}
|
|
1357
|
+
Err(error) => Err(LifecycleError::StatePersist(error.to_string())),
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
fn collect_session_repair_fields(
|
|
1362
|
+
state: &serde_json::Value,
|
|
1363
|
+
agent_ids: &[String],
|
|
1364
|
+
) -> Vec<(String, serde_json::Value)> {
|
|
1365
|
+
agent_ids
|
|
1366
|
+
.iter()
|
|
1367
|
+
.filter_map(|agent_id| {
|
|
1368
|
+
state
|
|
1369
|
+
.get("agents")
|
|
1370
|
+
.and_then(|agents| agents.get(agent_id))
|
|
1371
|
+
.cloned()
|
|
1372
|
+
.map(|agent| (agent_id.clone(), agent))
|
|
1373
|
+
})
|
|
1374
|
+
.collect()
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
fn apply_session_repair_fields(
|
|
1378
|
+
state: &mut serde_json::Value,
|
|
1379
|
+
agent_id: &str,
|
|
1380
|
+
repair: &serde_json::Value,
|
|
1381
|
+
) {
|
|
1382
|
+
let Some(target) = state
|
|
1383
|
+
.get_mut("agents")
|
|
1384
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1385
|
+
.and_then(|agents| agents.get_mut(agent_id))
|
|
1386
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1387
|
+
else {
|
|
1388
|
+
return;
|
|
1389
|
+
};
|
|
1390
|
+
let Some(repair) = repair.as_object() else {
|
|
1391
|
+
return;
|
|
1392
|
+
};
|
|
1393
|
+
for field in [
|
|
1394
|
+
"session_id",
|
|
1395
|
+
"rollout_path",
|
|
1396
|
+
"captured_at",
|
|
1397
|
+
"captured_via",
|
|
1398
|
+
"attribution_confidence",
|
|
1399
|
+
] {
|
|
1400
|
+
if let Some(value) = repair.get(field) {
|
|
1401
|
+
target.insert(field.to_string(), value.clone());
|
|
1402
|
+
} else {
|
|
1403
|
+
target.remove(field);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
target.remove("attribution_ambiguous");
|
|
1276
1407
|
}
|
|
1277
1408
|
|
|
1278
1409
|
fn mark_agent_respawned(
|
|
@@ -1330,10 +1461,7 @@ fn mark_agent_respawned(
|
|
|
1330
1461
|
// active_team_key (priority 3).
|
|
1331
1462
|
if let Some(ref team_id) = spawn.owner_team_id {
|
|
1332
1463
|
if !team_id.is_empty() {
|
|
1333
|
-
agent.insert(
|
|
1334
|
-
"owner_team_id".to_string(),
|
|
1335
|
-
serde_json::json!(team_id),
|
|
1336
|
-
);
|
|
1464
|
+
agent.insert("owner_team_id".to_string(), serde_json::json!(team_id));
|
|
1337
1465
|
}
|
|
1338
1466
|
}
|
|
1339
1467
|
// Bug 2 (0.3.32): clear stale `attribution_ambiguous` whenever a new
|
|
@@ -1638,8 +1766,13 @@ fn write_restart_resume_decision_events(
|
|
|
1638
1766
|
// Layer 2 (leader directive 2026-06-22): every restart.resume_decision
|
|
1639
1767
|
// event for a Refuse decision must carry the structured refusal_reason
|
|
1640
1768
|
// wire string so operators can see WHY without crawling state.json.
|
|
1641
|
-
let refusal_index: std::collections::BTreeMap
|
|
1642
|
-
|
|
1769
|
+
let refusal_index: std::collections::BTreeMap<
|
|
1770
|
+
&str,
|
|
1771
|
+
&crate::lifecycle::types::UnresumableWorker,
|
|
1772
|
+
> = unresumable
|
|
1773
|
+
.iter()
|
|
1774
|
+
.map(|u| (u.agent_id.as_str(), u))
|
|
1775
|
+
.collect();
|
|
1643
1776
|
for decision in decisions {
|
|
1644
1777
|
let agent = state
|
|
1645
1778
|
.get("agents")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
use super::*;
|
|
2
|
-
use super::agent::{resolve_team_scoped_state_or_refuse,
|
|
2
|
+
use super::agent::{resolve_team_scoped_state_or_refuse, start_agent_at_paths, stop_agent_at_paths};
|
|
3
3
|
use super::common::*;
|
|
4
4
|
use super::team_state::write_team_state;
|
|
5
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
5
6
|
|
|
6
7
|
/// `remove_agent(workspace, agent_id, from_spec, force, team)`(`lifecycle/agents.py:22`)。
|
|
7
8
|
/// 从 spec/state/team_state/role-file/agent_health 原子摘除;`_RemoveRollback` 字节级快照
|
|
@@ -14,6 +15,12 @@ pub fn remove_agent(
|
|
|
14
15
|
team: Option<&str>,
|
|
15
16
|
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
16
17
|
let paths = lifecycle_paths(workspace, team)?;
|
|
18
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
19
|
+
workspace: &paths.run_workspace,
|
|
20
|
+
operation: "remove-agent",
|
|
21
|
+
team,
|
|
22
|
+
agent_id: Some(agent_id),
|
|
23
|
+
})?;
|
|
17
24
|
let transport =
|
|
18
25
|
lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
19
26
|
remove_agent_at_paths(
|
|
@@ -36,6 +43,12 @@ pub fn remove_agent_with_transport(
|
|
|
36
43
|
transport: &dyn crate::transport::Transport,
|
|
37
44
|
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
38
45
|
let paths = lifecycle_paths(workspace, team)?;
|
|
46
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
47
|
+
workspace: &paths.run_workspace,
|
|
48
|
+
operation: "remove-agent",
|
|
49
|
+
team,
|
|
50
|
+
agent_id: Some(agent_id),
|
|
51
|
+
})?;
|
|
39
52
|
remove_agent_at_paths(
|
|
40
53
|
&paths.run_workspace,
|
|
41
54
|
&paths.spec_workspace,
|
|
@@ -787,8 +800,9 @@ impl RemoveRollback {
|
|
|
787
800
|
}
|
|
788
801
|
|
|
789
802
|
if self.restore_running && errors.is_empty() {
|
|
790
|
-
if let Err(e) =
|
|
803
|
+
if let Err(e) = start_agent_at_paths(
|
|
791
804
|
workspace,
|
|
805
|
+
spec_workspace,
|
|
792
806
|
&self.agent_id,
|
|
793
807
|
true,
|
|
794
808
|
false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use super::common::*;
|
|
2
2
|
use super::*;
|
|
3
|
+
use crate::provider::wire::{parse_canonical_provider, requires_resume_backing};
|
|
3
4
|
|
|
4
5
|
/// bug-085 四象限 `start_mode` 决策(`start.py:179-188` + `_resume_rollout_missing` `start.py:66-69`),
|
|
5
6
|
/// **从 start_agent 的整条 lock+spawn 路径里分离出的纯函数**(gate gap:porter 需要单元级 RED
|
|
@@ -31,7 +32,7 @@ pub fn decide_start_mode(
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
pub(crate) fn resumable_provider_requires_backing(provider: &str) -> bool {
|
|
34
|
-
|
|
35
|
+
parse_canonical_provider(provider).is_some_and(requires_resume_backing)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/// `first_send_at` 严格分类(`_classify_first_send_at`,`orchestration.py:399`)。
|
|
@@ -323,6 +323,7 @@ expected="${TEAM_AGENT_E27_EXPECTED_ENDPOINT}"
|
|
|
323
323
|
session="${TEAM_AGENT_E27_SESSION_NAME}"
|
|
324
324
|
pane="${TEAM_AGENT_E27_PANE_ID}"
|
|
325
325
|
killed_marker="${log}.killed"
|
|
326
|
+
spawned_marker="${log}.spawned"
|
|
326
327
|
track=0
|
|
327
328
|
case "$*" in
|
|
328
329
|
*"$expected"*|*"$session"*|*"$pane"*) track=1 ;;
|
|
@@ -361,6 +362,7 @@ case "$*" in
|
|
|
361
362
|
exit 0
|
|
362
363
|
;;
|
|
363
364
|
*"display-message -p -t $session:alpha #{pane_id}"*)
|
|
365
|
+
: > "$spawned_marker"
|
|
364
366
|
printf '%%9288\n'
|
|
365
367
|
exit 0
|
|
366
368
|
;;
|
|
@@ -368,6 +370,10 @@ case "$*" in
|
|
|
368
370
|
exit 0
|
|
369
371
|
;;
|
|
370
372
|
*"list-panes -a -F"*)
|
|
373
|
+
if [ -f "$spawned_marker" ]; then
|
|
374
|
+
printf '%%9288\t%s\t0\talpha\t0\t/dev/ttys099\tnode\t1\t/tmp\t1\t0\t9288\n' "$session"
|
|
375
|
+
exit 0
|
|
376
|
+
fi
|
|
371
377
|
# Hard-gate post-stop snapshot: no residual same-role panes after kill.
|
|
372
378
|
if [ -f "$killed_marker" ]; then
|
|
373
379
|
exit 0
|
|
@@ -27,10 +27,10 @@ impl LaneTransport {
|
|
|
27
27
|
spawns: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
fn killed(&self) -> Vec<String> {
|
|
30
|
+
pub(super) fn killed(&self) -> Vec<String> {
|
|
31
31
|
self.killed.lock().unwrap().clone()
|
|
32
32
|
}
|
|
33
|
-
fn spawns(&self) -> Vec<(String, Vec<String>)> {
|
|
33
|
+
pub(super) fn spawns(&self) -> Vec<(String, Vec<String>)> {
|
|
34
34
|
self.spawns.lock().unwrap().clone()
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -164,7 +164,7 @@ fn set_context_state_file(ws: &std::path::Path, name: &str) {
|
|
|
164
164
|
/// Fork workspace: source `alpha` (role doc = `alpha_role`) + `bravo`, alpha seeded RUNNING with a
|
|
165
165
|
/// source `session_id` (so fork reaches the spec-mutation / native-fork gate). Seeded-healthy coordinator
|
|
166
166
|
/// (fork's start_coordinator -> AlreadyRunning, no real daemon). session_name = "team-laneateam".
|
|
167
|
-
fn fork_ws(alpha_role: &str) -> PathBuf {
|
|
167
|
+
pub(super) fn fork_ws(alpha_role: &str) -> PathBuf {
|
|
168
168
|
let ws = temp_ws().join("forkv2");
|
|
169
169
|
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
170
170
|
std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Fork v2 probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|