@team-agent/installer 0.5.51 → 0.5.53
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/named_address.rs +35 -67
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +526 -73
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +20 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
|
@@ -22,6 +22,8 @@ use crate::state::persist::{
|
|
|
22
22
|
save_runtime_state_with_team_tombstoned_agents,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
pub(crate) const CURRENT_TEAM_ALIAS: &str = "current";
|
|
26
|
+
|
|
25
27
|
/// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
|
|
26
28
|
/// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
|
|
27
29
|
pub fn team_state_key(state: &Value) -> String {
|
|
@@ -283,6 +285,14 @@ pub(crate) fn team_is_alive_candidate(team: &Value) -> bool {
|
|
|
283
285
|
if let Some(status) = status_raw {
|
|
284
286
|
return status.eq_ignore_ascii_case("alive");
|
|
285
287
|
}
|
|
288
|
+
if !team
|
|
289
|
+
.get("agents")
|
|
290
|
+
.and_then(Value::as_object)
|
|
291
|
+
.is_some_and(|agents| !agents.is_empty())
|
|
292
|
+
&& team_has_live_leader_binding(team)
|
|
293
|
+
{
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
286
296
|
if all_present_agents_terminal(team) && !team_has_live_leader_binding(team) {
|
|
287
297
|
return false;
|
|
288
298
|
}
|
|
@@ -302,6 +312,20 @@ fn team_has_live_leader_binding(team: &Value) -> bool {
|
|
|
302
312
|
is_non_null_object("leader_receiver") || is_non_null_object("team_owner")
|
|
303
313
|
}
|
|
304
314
|
|
|
315
|
+
fn team_has_attached_leader_receiver(team: &Value) -> bool {
|
|
316
|
+
let Some(receiver) = team.get("leader_receiver").and_then(Value::as_object) else {
|
|
317
|
+
return false;
|
|
318
|
+
};
|
|
319
|
+
receiver
|
|
320
|
+
.get("status")
|
|
321
|
+
.and_then(Value::as_str)
|
|
322
|
+
.is_some_and(|status| status.eq_ignore_ascii_case("attached"))
|
|
323
|
+
&& receiver
|
|
324
|
+
.get("pane_id")
|
|
325
|
+
.and_then(Value::as_str)
|
|
326
|
+
.is_some_and(|pane| !pane.is_empty())
|
|
327
|
+
}
|
|
328
|
+
|
|
305
329
|
fn all_present_agents_terminal(team: &Value) -> bool {
|
|
306
330
|
let Some(agents) = team.get("agents").and_then(Value::as_object) else {
|
|
307
331
|
return false;
|
|
@@ -458,15 +482,71 @@ pub fn project_top_level_view(state: &Value, team_key: &str) -> Value {
|
|
|
458
482
|
Value::Object(p)
|
|
459
483
|
}
|
|
460
484
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
pub
|
|
485
|
+
#[derive(Debug, Clone)]
|
|
486
|
+
pub struct TeamScopeResolution {
|
|
487
|
+
pub canonical_team_key: String,
|
|
488
|
+
pub state: Value,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// Resolve a requested team selector once, returning both the canonical map key and its
|
|
492
|
+
/// projected state. Callers must carry `canonical_team_key` forward rather than deriving team
|
|
493
|
+
/// identity again from the projection or the original alias.
|
|
494
|
+
pub fn resolve_runtime_team_scope(
|
|
495
|
+
workspace: &Path,
|
|
496
|
+
team: Option<&str>,
|
|
497
|
+
) -> Result<TeamScopeResolution, StateError> {
|
|
464
498
|
let state = load_runtime_state(workspace)?;
|
|
465
499
|
let alive = team_state_candidates(&state);
|
|
466
500
|
// Python `if team:` —— 空串 falsy,等同无 team(对抗 P1:此前 Some("") 误入 team 分支,空
|
|
467
501
|
// team_dir 匹配致歧义/未找到错误串漂移)。
|
|
468
502
|
let team = team.filter(|t| !t.is_empty());
|
|
469
503
|
if let Some(team) = team {
|
|
504
|
+
// Intentional semantic split with `persist::apply_persist_merge_contract`:
|
|
505
|
+
// selector resolution validates the active team against the alive/terminal
|
|
506
|
+
// view, while the lock-held merge cannot depend on that mutable view. A
|
|
507
|
+
// change to either CURRENT_TEAM_ALIAS mapping must evaluate the other.
|
|
508
|
+
let canonical_request = if team.eq_ignore_ascii_case(CURRENT_TEAM_ALIAS) {
|
|
509
|
+
let teams = state.get("teams").and_then(Value::as_object);
|
|
510
|
+
state
|
|
511
|
+
.get("active_team_key")
|
|
512
|
+
.and_then(Value::as_str)
|
|
513
|
+
.filter(|active| {
|
|
514
|
+
alive.contains_key(*active)
|
|
515
|
+
|| teams.is_none_or(Map::is_empty)
|
|
516
|
+
|| teams
|
|
517
|
+
.and_then(|teams| teams.get(*active))
|
|
518
|
+
.is_some_and(team_has_attached_leader_receiver)
|
|
519
|
+
})
|
|
520
|
+
.map(str::to_string)
|
|
521
|
+
.or_else(|| {
|
|
522
|
+
teams
|
|
523
|
+
.is_none_or(Map::is_empty)
|
|
524
|
+
.then(|| team_state_key(&state))
|
|
525
|
+
.filter(|derived| derived != "current")
|
|
526
|
+
})
|
|
527
|
+
.ok_or_else(|| {
|
|
528
|
+
StateError::TeamSelect(format!(
|
|
529
|
+
"team 'current' not found among alive teams. Use --team <team-key> with an explicit team key to address a terminal team. {}",
|
|
530
|
+
format_team_candidates(&alive)
|
|
531
|
+
))
|
|
532
|
+
})?
|
|
533
|
+
} else {
|
|
534
|
+
team.to_string()
|
|
535
|
+
};
|
|
536
|
+
let team = canonical_request.as_str();
|
|
537
|
+
// An exact canonical key is authoritative even when the team is terminal or excluded from
|
|
538
|
+
// the default alive-candidate set. Explicit lifecycle operations must still be able to
|
|
539
|
+
// address a shutdown team without falling back to the raw root projection.
|
|
540
|
+
if state
|
|
541
|
+
.get("teams")
|
|
542
|
+
.and_then(Value::as_object)
|
|
543
|
+
.is_some_and(|teams| teams.contains_key(team))
|
|
544
|
+
{
|
|
545
|
+
return Ok(TeamScopeResolution {
|
|
546
|
+
canonical_team_key: team.to_string(),
|
|
547
|
+
state: project_top_level_view(&state, team),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
470
550
|
// 无 alive 但 team 命中 active_team_key / 派生 key → 直接以全量 state 投影。
|
|
471
551
|
if alive.is_empty() {
|
|
472
552
|
let active = state
|
|
@@ -484,32 +564,22 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
484
564
|
}
|
|
485
565
|
None => projection = json!({ "active_team_key": team }),
|
|
486
566
|
}
|
|
487
|
-
return Ok(
|
|
567
|
+
return Ok(TeamScopeResolution {
|
|
568
|
+
canonical_team_key: team.to_string(),
|
|
569
|
+
state: projection,
|
|
570
|
+
});
|
|
488
571
|
}
|
|
489
572
|
}
|
|
490
|
-
if alive.contains_key(team) {
|
|
491
|
-
return Ok(project_top_level_view(&state, team));
|
|
492
|
-
}
|
|
493
|
-
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.1):
|
|
494
|
-
// 显式 `--team X` 命中 `state.teams` 里的键(即使被标 shutdown/终态)
|
|
495
|
-
// 仍应投影出来,支持「shutdown 后再 restart --team X」多队场景
|
|
496
|
-
// (E2E-REST-014 家族)。走 project_top_level_view 保证顶层 agents
|
|
497
|
-
// 与 teams[key].agents 收敛(E2E-REST-002 依赖 projection clobber
|
|
498
|
-
// 顶层注入的 session_id)。
|
|
499
|
-
if state
|
|
500
|
-
.get("teams")
|
|
501
|
-
.and_then(Value::as_object)
|
|
502
|
-
.is_some_and(|teams| teams.contains_key(team))
|
|
503
|
-
{
|
|
504
|
-
return Ok(project_top_level_view(&state, team));
|
|
505
|
-
}
|
|
506
573
|
let matches: Vec<&String> = alive
|
|
507
574
|
.iter()
|
|
508
575
|
.filter(|(key, value)| team_selector_matches(team, key, value))
|
|
509
576
|
.map(|(k, _)| k)
|
|
510
577
|
.collect();
|
|
511
578
|
if matches.len() == 1 {
|
|
512
|
-
return Ok(
|
|
579
|
+
return Ok(TeamScopeResolution {
|
|
580
|
+
canonical_team_key: matches[0].clone(),
|
|
581
|
+
state: project_top_level_view(&state, matches[0]),
|
|
582
|
+
});
|
|
513
583
|
}
|
|
514
584
|
if matches.len() > 1 {
|
|
515
585
|
return Err(StateError::TeamSelect(
|
|
@@ -529,12 +599,18 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
529
599
|
.filter(|s| !s.is_empty());
|
|
530
600
|
if let Some(active) = active {
|
|
531
601
|
if alive.contains_key(active) {
|
|
532
|
-
return Ok(
|
|
602
|
+
return Ok(TeamScopeResolution {
|
|
603
|
+
canonical_team_key: active.to_string(),
|
|
604
|
+
state: project_top_level_view(&state, active),
|
|
605
|
+
});
|
|
533
606
|
}
|
|
534
607
|
}
|
|
535
608
|
if alive.len() == 1 {
|
|
536
609
|
let only = alive.keys().next().cloned().unwrap_or_default();
|
|
537
|
-
return Ok(
|
|
610
|
+
return Ok(TeamScopeResolution {
|
|
611
|
+
canonical_team_key: only.clone(),
|
|
612
|
+
state: project_top_level_view(&state, &only),
|
|
613
|
+
});
|
|
538
614
|
}
|
|
539
615
|
if alive.is_empty() {
|
|
540
616
|
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.1):
|
|
@@ -547,10 +623,22 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
547
623
|
.and_then(Value::as_object)
|
|
548
624
|
.is_some_and(|teams| teams.contains_key(active_key))
|
|
549
625
|
{
|
|
550
|
-
return Ok(
|
|
626
|
+
return Ok(TeamScopeResolution {
|
|
627
|
+
canonical_team_key: active_key.to_string(),
|
|
628
|
+
state: project_top_level_view(&state, active_key),
|
|
629
|
+
});
|
|
551
630
|
}
|
|
552
631
|
}
|
|
553
|
-
|
|
632
|
+
let canonical_team_key = state
|
|
633
|
+
.get("active_team_key")
|
|
634
|
+
.and_then(Value::as_str)
|
|
635
|
+
.filter(|key| !key.is_empty())
|
|
636
|
+
.map(str::to_string)
|
|
637
|
+
.unwrap_or_else(|| team_state_key(&state));
|
|
638
|
+
return Ok(TeamScopeResolution {
|
|
639
|
+
canonical_team_key,
|
|
640
|
+
state,
|
|
641
|
+
});
|
|
554
642
|
}
|
|
555
643
|
Err(StateError::TeamSelect(
|
|
556
644
|
"multiple teams found in this workspace; pass --team <team> to choose. ".to_string()
|
|
@@ -558,6 +646,11 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
558
646
|
))
|
|
559
647
|
}
|
|
560
648
|
|
|
649
|
+
/// `select_runtime_state`(`state.py:193`):compatibility projection wrapper.
|
|
650
|
+
pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Value, StateError> {
|
|
651
|
+
resolve_runtime_team_scope(workspace, team).map(|resolved| resolved.state)
|
|
652
|
+
}
|
|
653
|
+
|
|
561
654
|
fn team_selector_matches(team: &str, key: &str, value: &Value) -> bool {
|
|
562
655
|
if team == key {
|
|
563
656
|
return true;
|
|
@@ -678,22 +771,41 @@ pub(crate) fn save_team_scoped_state_with_deleted_agents(
|
|
|
678
771
|
pub(crate) fn save_team_scoped_state_with_tombstone_lifecycle_topology_authority(
|
|
679
772
|
workspace: &Path,
|
|
680
773
|
team_state: &Value,
|
|
774
|
+
topology_team_key: &str,
|
|
681
775
|
agent_ids: &[&str],
|
|
682
776
|
) -> Result<(), StateError> {
|
|
683
|
-
save_team_scoped_state_with_merge_options(
|
|
777
|
+
save_team_scoped_state_with_merge_options(
|
|
778
|
+
workspace,
|
|
779
|
+
team_state,
|
|
780
|
+
&[],
|
|
781
|
+
agent_ids,
|
|
782
|
+
&[],
|
|
783
|
+
Some(topology_team_key),
|
|
784
|
+
agent_ids,
|
|
785
|
+
)
|
|
684
786
|
}
|
|
685
787
|
|
|
686
788
|
pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority(
|
|
687
789
|
workspace: &Path,
|
|
688
790
|
team_state: &Value,
|
|
791
|
+
topology_team_key: &str,
|
|
689
792
|
agent_ids: &[&str],
|
|
690
793
|
) -> Result<(), StateError> {
|
|
691
|
-
save_team_scoped_state_with_merge_options(
|
|
794
|
+
save_team_scoped_state_with_merge_options(
|
|
795
|
+
workspace,
|
|
796
|
+
team_state,
|
|
797
|
+
&[],
|
|
798
|
+
&[],
|
|
799
|
+
&[],
|
|
800
|
+
Some(topology_team_key),
|
|
801
|
+
agent_ids,
|
|
802
|
+
)
|
|
692
803
|
}
|
|
693
804
|
|
|
694
805
|
pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
695
806
|
workspace: &Path,
|
|
696
807
|
team_state: &Value,
|
|
808
|
+
topology_team_key: &str,
|
|
697
809
|
skip_capture_backfill_agent_ids: &[&str],
|
|
698
810
|
topology_agent_ids: &[&str],
|
|
699
811
|
) -> Result<(), StateError> {
|
|
@@ -703,6 +815,7 @@ pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority_and_captu
|
|
|
703
815
|
&[],
|
|
704
816
|
&[],
|
|
705
817
|
skip_capture_backfill_agent_ids,
|
|
818
|
+
Some(topology_team_key),
|
|
706
819
|
topology_agent_ids,
|
|
707
820
|
)
|
|
708
821
|
}
|
|
@@ -719,6 +832,7 @@ fn save_team_scoped_state_with_merge_exceptions(
|
|
|
719
832
|
deleted_agent_ids,
|
|
720
833
|
tombstoned_agent_ids,
|
|
721
834
|
&[],
|
|
835
|
+
None,
|
|
722
836
|
&[],
|
|
723
837
|
)
|
|
724
838
|
}
|
|
@@ -729,6 +843,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
729
843
|
deleted_agent_ids: &[&str],
|
|
730
844
|
tombstoned_agent_ids: &[&str],
|
|
731
845
|
skip_capture_backfill_agent_ids: &[&str],
|
|
846
|
+
topology_team_key: Option<&str>,
|
|
732
847
|
topology_agent_ids: &[&str],
|
|
733
848
|
) -> Result<(), StateError> {
|
|
734
849
|
let target_key = team_state_key(team_state);
|
|
@@ -768,7 +883,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
768
883
|
return save_runtime_state_with_team_tombstone_lifecycle_topology_authority(
|
|
769
884
|
workspace,
|
|
770
885
|
&merged,
|
|
771
|
-
&target_key,
|
|
886
|
+
topology_team_key.unwrap_or(&target_key),
|
|
772
887
|
topology_agent_ids,
|
|
773
888
|
);
|
|
774
889
|
}
|
|
@@ -776,7 +891,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
776
891
|
return save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
777
892
|
workspace,
|
|
778
893
|
&merged,
|
|
779
|
-
&target_key,
|
|
894
|
+
topology_team_key.unwrap_or(&target_key),
|
|
780
895
|
skip_capture_backfill_agent_ids,
|
|
781
896
|
topology_agent_ids,
|
|
782
897
|
);
|
|
@@ -784,6 +899,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
784
899
|
return save_runtime_state_with_lifecycle_topology_authority(
|
|
785
900
|
workspace,
|
|
786
901
|
&merged,
|
|
902
|
+
topology_team_key.unwrap_or(&target_key),
|
|
787
903
|
topology_agent_ids,
|
|
788
904
|
);
|
|
789
905
|
}
|
|
@@ -834,7 +950,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
834
950
|
save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
835
951
|
workspace,
|
|
836
952
|
&Value::Object(merged),
|
|
837
|
-
&target_key,
|
|
953
|
+
topology_team_key.unwrap_or(&target_key),
|
|
838
954
|
skip_capture_backfill_agent_ids,
|
|
839
955
|
topology_agent_ids,
|
|
840
956
|
)
|
|
@@ -842,6 +958,7 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
842
958
|
save_runtime_state_with_lifecycle_topology_authority(
|
|
843
959
|
workspace,
|
|
844
960
|
&Value::Object(merged),
|
|
961
|
+
topology_team_key.unwrap_or(&target_key),
|
|
845
962
|
topology_agent_ids,
|
|
846
963
|
)
|
|
847
964
|
}
|
|
@@ -1113,6 +1230,104 @@ mod tests {
|
|
|
1113
1230
|
);
|
|
1114
1231
|
}
|
|
1115
1232
|
|
|
1233
|
+
#[test]
|
|
1234
|
+
fn resolve_runtime_team_scope_current_uses_valid_active_key() {
|
|
1235
|
+
let ws = temp_ws_with_state(&json!({
|
|
1236
|
+
"active_team_key": "alpha",
|
|
1237
|
+
"teams": {
|
|
1238
|
+
"alpha": {"status": "alive", "session_name": "team-alpha", "agents": {"worker": {}}},
|
|
1239
|
+
"old-team": {"team_owner": {"pane_id": "%9"}}
|
|
1240
|
+
}
|
|
1241
|
+
}));
|
|
1242
|
+
let resolved = resolve_runtime_team_scope(&ws, Some("current")).unwrap();
|
|
1243
|
+
assert_eq!(resolved.canonical_team_key, "alpha");
|
|
1244
|
+
assert_eq!(
|
|
1245
|
+
resolved.state.get("active_team_key").and_then(Value::as_str),
|
|
1246
|
+
Some("alpha")
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
#[test]
|
|
1251
|
+
fn resolve_runtime_team_scope_current_accepts_attached_receiver_recovery() {
|
|
1252
|
+
let ws = temp_ws_with_state(&json!({
|
|
1253
|
+
"active_team_key": "alpha",
|
|
1254
|
+
"teams": {
|
|
1255
|
+
"alpha": {
|
|
1256
|
+
"agents": {},
|
|
1257
|
+
"leader_receiver": {"status": "attached", "pane_id": "%9"}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}));
|
|
1261
|
+
let resolved = resolve_runtime_team_scope(&ws, Some("current")).unwrap();
|
|
1262
|
+
assert_eq!(resolved.canonical_team_key, "alpha");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
#[test]
|
|
1266
|
+
fn resolve_runtime_team_scope_current_accepts_shutdown_receiver_recovery() {
|
|
1267
|
+
let ws = temp_ws_with_state(&json!({
|
|
1268
|
+
"active_team_key": "alpha",
|
|
1269
|
+
"teams": {
|
|
1270
|
+
"alpha": {
|
|
1271
|
+
"status": "shutdown",
|
|
1272
|
+
"agents": {"worker": {"status": "stopped"}},
|
|
1273
|
+
"leader_receiver": {"status": "attached", "pane_id": "%9"}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}));
|
|
1277
|
+
let resolved = resolve_runtime_team_scope(&ws, Some("current")).unwrap();
|
|
1278
|
+
assert_eq!(resolved.canonical_team_key, "alpha");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
#[test]
|
|
1282
|
+
fn resolve_runtime_team_scope_current_rejects_owner_only_stub() {
|
|
1283
|
+
let ws = temp_ws_with_state(&json!({
|
|
1284
|
+
"active_team_key": "old-team",
|
|
1285
|
+
"teams": {
|
|
1286
|
+
"old-team": {"agents": {}, "team_owner": {"pane_id": "%9"}}
|
|
1287
|
+
}
|
|
1288
|
+
}));
|
|
1289
|
+
let error = resolve_runtime_team_scope(&ws, Some("current")).unwrap_err();
|
|
1290
|
+
assert!(error.to_string().contains("team 'current' not found"));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
#[test]
|
|
1294
|
+
fn resolve_runtime_team_scope_current_fails_without_valid_active_key() {
|
|
1295
|
+
let ws = temp_ws_with_state(&json!({
|
|
1296
|
+
"active_team_key": "missing",
|
|
1297
|
+
"teams": {
|
|
1298
|
+
"alpha": {"status": "alive", "session_name": "team-alpha", "agents": {"worker": {}}}
|
|
1299
|
+
}
|
|
1300
|
+
}));
|
|
1301
|
+
let error = resolve_runtime_team_scope(&ws, Some("current")).unwrap_err();
|
|
1302
|
+
assert!(error.to_string().contains("team 'current' not found"));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
#[test]
|
|
1306
|
+
fn resolve_runtime_team_scope_current_keeps_legacy_single_team_state() {
|
|
1307
|
+
let ws = temp_ws_with_state(&json!({
|
|
1308
|
+
"active_team_key": "legacy",
|
|
1309
|
+
"team_key": "legacy",
|
|
1310
|
+
"session_name": "team-legacy",
|
|
1311
|
+
"agents": {"worker": {"status": "running"}}
|
|
1312
|
+
}));
|
|
1313
|
+
let resolved = resolve_runtime_team_scope(&ws, Some("current")).unwrap();
|
|
1314
|
+
assert_eq!(resolved.canonical_team_key, "legacy");
|
|
1315
|
+
assert_eq!(resolved.state["active_team_key"], json!("legacy"));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
#[test]
|
|
1319
|
+
fn resolve_runtime_team_scope_current_rejects_empty_workspace_bootstrap() {
|
|
1320
|
+
let ws = temp_ws_with_state(&json!({}));
|
|
1321
|
+
let error = resolve_runtime_team_scope(&ws, Some("current")).unwrap_err();
|
|
1322
|
+
assert!(error.to_string().contains("team 'current' not found"));
|
|
1323
|
+
let saved = load_runtime_state(&ws).unwrap();
|
|
1324
|
+
assert_ne!(
|
|
1325
|
+
saved.get("active_team_key").and_then(Value::as_str),
|
|
1326
|
+
Some("current")
|
|
1327
|
+
);
|
|
1328
|
+
assert!(!crate::model::paths::runtime_spec_path(&ws, "current").exists());
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1116
1331
|
// 对抗 P1:空串 team 必须走「无 team」分支(Python `if team:` falsy),报「multiple teams」
|
|
1117
1332
|
// 而非「team selector is ambiguous」(后者是非空 team 多匹配)。
|
|
1118
1333
|
#[test]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
use super::StateWriteIntent;
|
|
2
|
+
|
|
3
|
+
impl<'a> StateWriteIntent<'a> {
|
|
4
|
+
pub(crate) fn launch_team_or_add_agent(
|
|
5
|
+
team_key: &'a str,
|
|
6
|
+
added_agent_id: Option<&'a str>,
|
|
7
|
+
) -> Self {
|
|
8
|
+
match added_agent_id {
|
|
9
|
+
Some(agent_id) => Self::AddAgent { team_key, agent_id },
|
|
10
|
+
None => Self::LaunchTeam { team_key },
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -46,6 +46,7 @@ use super::persist::{
|
|
|
46
46
|
save_runtime_state_with_deleted_agents as helper_write_root_with_deleted_agents,
|
|
47
47
|
save_runtime_state_with_lifecycle_topology_authority as helper_write_root_with_lifecycle_topology_authority,
|
|
48
48
|
save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip as helper_write_root_with_lifecycle_topology_authority_and_capture_backfill_skip,
|
|
49
|
+
save_runtime_state_with_receiver_authority as helper_write_root_with_receiver_authority,
|
|
49
50
|
save_runtime_state_with_team_tombstone_lifecycle_topology_authority as helper_write_root_with_team_tombstone_lifecycle_topology_authority,
|
|
50
51
|
save_runtime_state_with_team_tombstoned_agents as helper_write_root_with_team_tombstoned_agents,
|
|
51
52
|
save_runtime_state_without_migrations as helper_write_root_without_migrations,
|
|
@@ -60,6 +61,8 @@ use super::projection::{
|
|
|
60
61
|
save_team_scoped_state_with_tombstone_lifecycle_topology_authority as helper_write_team_scoped_with_tombstone_lifecycle_topology_authority,
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
mod intent;
|
|
65
|
+
|
|
63
66
|
/// Repository facade for workspace state reads and writes.
|
|
64
67
|
///
|
|
65
68
|
/// S1a scope: hold the workspace path, expose `load_workspace`/`load_team`
|
|
@@ -141,6 +144,10 @@ pub enum StateWriteIntent<'a> {
|
|
|
141
144
|
LaunchTeam {
|
|
142
145
|
team_key: &'a str,
|
|
143
146
|
},
|
|
147
|
+
AddAgent {
|
|
148
|
+
team_key: &'a str,
|
|
149
|
+
agent_id: &'a str,
|
|
150
|
+
},
|
|
144
151
|
AnnotateTeamDepth {
|
|
145
152
|
team_key: &'a str,
|
|
146
153
|
},
|
|
@@ -245,9 +252,8 @@ pub enum StateWriteIntent<'a> {
|
|
|
245
252
|
SelfMigration,
|
|
246
253
|
}
|
|
247
254
|
|
|
248
|
-
// S1a dispatch: every intent forwards to
|
|
249
|
-
//
|
|
250
|
-
// shifted. When S1b migrates a writer cluster, only the arms below change;
|
|
255
|
+
// S1a dispatch: every intent forwards to the same pre-S1a helper family; only naming shifted.
|
|
256
|
+
// When S1b migrates a writer cluster, only the arms below change;
|
|
251
257
|
// the callsites and tests keep talking to `StateRepository`.
|
|
252
258
|
fn route_direct(
|
|
253
259
|
workspace: &Path,
|
|
@@ -258,6 +264,14 @@ fn route_direct(
|
|
|
258
264
|
// LaunchTeam -> the launch writer path uses the root helper today
|
|
259
265
|
// (`save_runtime_state` at lifecycle/launch.rs:996).
|
|
260
266
|
StateWriteIntent::LaunchTeam { .. } => helper_write_root(workspace, state),
|
|
267
|
+
StateWriteIntent::AddAgent { team_key, agent_id } => {
|
|
268
|
+
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
269
|
+
workspace,
|
|
270
|
+
state,
|
|
271
|
+
team_key,
|
|
272
|
+
&[agent_id],
|
|
273
|
+
)
|
|
274
|
+
}
|
|
261
275
|
// AnnotateTeamDepth also lives on the root save path.
|
|
262
276
|
StateWriteIntent::AnnotateTeamDepth { .. } => helper_write_root(workspace, state),
|
|
263
277
|
// ShutdownTeam: scoped clean shutdown uses `save_team_scoped_state`;
|
|
@@ -277,12 +291,13 @@ fn route_direct(
|
|
|
277
291
|
// `save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip`
|
|
278
292
|
// at lifecycle/restart/common.rs:633.
|
|
279
293
|
StateWriteIntent::RestartTeam {
|
|
294
|
+
team_key,
|
|
280
295
|
topology_authority_agent_ids,
|
|
281
296
|
skip_capture_backfill_agent_ids,
|
|
282
|
-
..
|
|
283
297
|
} => helper_write_team_scoped_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
284
298
|
workspace,
|
|
285
299
|
state,
|
|
300
|
+
team_key,
|
|
286
301
|
skip_capture_backfill_agent_ids,
|
|
287
302
|
topology_authority_agent_ids,
|
|
288
303
|
),
|
|
@@ -292,16 +307,17 @@ fn route_direct(
|
|
|
292
307
|
StateWriteIntent::RestartSessionRepair { .. } => helper_write_team_scoped(workspace, state),
|
|
293
308
|
// StartAgent - the launch add-agent tail lands via
|
|
294
309
|
// `save_team_scoped_state_with_lifecycle_topology_authority` today.
|
|
295
|
-
StateWriteIntent::StartAgent { .. } => {
|
|
296
|
-
helper_write_team_scoped_with_lifecycle_topology_authority(workspace, state, &[])
|
|
310
|
+
StateWriteIntent::StartAgent { team_key, .. } => {
|
|
311
|
+
helper_write_team_scoped_with_lifecycle_topology_authority(workspace, state, team_key, &[])
|
|
297
312
|
}
|
|
298
313
|
// StopAgent uses the legacy helper family
|
|
299
314
|
// `save_team_scoped_state_with_lifecycle_topology_authority`,
|
|
300
315
|
// matching lifecycle/restart/agent.rs:911.
|
|
301
|
-
StateWriteIntent::StopAgent {
|
|
316
|
+
StateWriteIntent::StopAgent { team_key, agent_id } => {
|
|
302
317
|
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
303
318
|
workspace,
|
|
304
319
|
state,
|
|
320
|
+
team_key,
|
|
305
321
|
&[agent_id],
|
|
306
322
|
)
|
|
307
323
|
}
|
|
@@ -310,20 +326,22 @@ fn route_direct(
|
|
|
310
326
|
// matching lifecycle/restart/agent.rs:1355. Non-discard falls back to
|
|
311
327
|
// the same lifecycle topology-authority helper as StopAgent.
|
|
312
328
|
StateWriteIntent::ResetAgent {
|
|
329
|
+
team_key,
|
|
313
330
|
agent_id,
|
|
314
331
|
discard_session,
|
|
315
|
-
..
|
|
316
332
|
} => {
|
|
317
333
|
if discard_session {
|
|
318
334
|
helper_write_team_scoped_with_tombstone_lifecycle_topology_authority(
|
|
319
335
|
workspace,
|
|
320
336
|
state,
|
|
337
|
+
team_key,
|
|
321
338
|
&[agent_id],
|
|
322
339
|
)
|
|
323
340
|
} else {
|
|
324
341
|
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
325
342
|
workspace,
|
|
326
343
|
state,
|
|
344
|
+
team_key,
|
|
327
345
|
&[agent_id],
|
|
328
346
|
)
|
|
329
347
|
}
|
|
@@ -335,10 +353,11 @@ fn route_direct(
|
|
|
335
353
|
}
|
|
336
354
|
// ForkAgent mutates a selected team projection, so persist it back to
|
|
337
355
|
// that projection with the forked row as lifecycle topology authority.
|
|
338
|
-
StateWriteIntent::ForkAgent {
|
|
356
|
+
StateWriteIntent::ForkAgent { team_key, agent_id } => {
|
|
339
357
|
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
340
358
|
workspace,
|
|
341
359
|
state,
|
|
360
|
+
team_key,
|
|
342
361
|
&[agent_id],
|
|
343
362
|
)
|
|
344
363
|
}
|
|
@@ -352,23 +371,28 @@ fn route_direct(
|
|
|
352
371
|
// Force-recreate rollback restores an existing row after the
|
|
353
372
|
// replacement spawn has advanced its lifecycle tuple. The selected
|
|
354
373
|
// row is therefore the explicit topology authority.
|
|
355
|
-
StateWriteIntent::ForceRecreateRollback {
|
|
374
|
+
StateWriteIntent::ForceRecreateRollback { team_key, agent_id } => {
|
|
356
375
|
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
357
376
|
workspace,
|
|
358
377
|
state,
|
|
378
|
+
team_key,
|
|
359
379
|
&[agent_id],
|
|
360
380
|
)
|
|
361
381
|
}
|
|
362
382
|
// ClaimLeader -> leader/lease.rs:1625 uses the root helper, and the
|
|
363
383
|
// scoped preserve-claim-fields variant at :1702 uses the team-tombstoned
|
|
364
384
|
// agents helper.
|
|
365
|
-
StateWriteIntent::ClaimLeader {
|
|
385
|
+
StateWriteIntent::ClaimLeader { team_key } => {
|
|
386
|
+
helper_write_root_with_receiver_authority(workspace, state, team_key)
|
|
387
|
+
}
|
|
366
388
|
StateWriteIntent::LeaderBindingRestoreNonTargetTeams { .. } => {
|
|
367
389
|
helper_write_root_without_migrations(workspace, state)
|
|
368
390
|
}
|
|
369
391
|
// LeaderStartBinding -> managed/exec/external all root-save at
|
|
370
392
|
// leader/start.rs:795/903/946.
|
|
371
|
-
StateWriteIntent::LeaderStartBinding { .. } =>
|
|
393
|
+
StateWriteIntent::LeaderStartBinding { team_key, .. } => {
|
|
394
|
+
helper_write_root_with_receiver_authority(workspace, state, team_key)
|
|
395
|
+
}
|
|
372
396
|
// CoordinatorTick dispatches to the existing scoped helper
|
|
373
397
|
// (`save_team_scoped_state`) preserving coordinator/tick.rs:427.
|
|
374
398
|
StateWriteIntent::CoordinatorTick { .. } => helper_write_team_scoped(workspace, state),
|
|
@@ -5,8 +5,8 @@ use std::path::{Path, PathBuf};
|
|
|
5
5
|
use serde_json::Value;
|
|
6
6
|
|
|
7
7
|
use crate::model::paths::{canonical_run_workspace, runtime_spec_path, team_workspace};
|
|
8
|
-
use crate::state::persist::
|
|
9
|
-
use crate::state::projection::
|
|
8
|
+
use crate::state::persist::runtime_state_path;
|
|
9
|
+
use crate::state::projection::resolve_runtime_team_scope;
|
|
10
10
|
use crate::state::StateError;
|
|
11
11
|
|
|
12
12
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
@@ -36,15 +36,15 @@ pub fn resolve_active_team(
|
|
|
36
36
|
mode: SelectorMode,
|
|
37
37
|
) -> Result<SelectedTeam, StateError> {
|
|
38
38
|
let explicit_spec = input.join("team.spec.yaml");
|
|
39
|
-
let (run_workspace,
|
|
39
|
+
let (run_workspace, resolved) = if explicit_spec.exists() {
|
|
40
40
|
let team_run = team_workspace(input).map_err(|e| StateError::TeamSelect(e.to_string()))?;
|
|
41
41
|
let run = if runtime_state_path(input).exists() || !runtime_state_path(&team_run).exists() {
|
|
42
42
|
input.to_path_buf()
|
|
43
43
|
} else {
|
|
44
44
|
team_run
|
|
45
45
|
};
|
|
46
|
-
let
|
|
47
|
-
(run,
|
|
46
|
+
let resolved = resolve_runtime_team_scope(&run, team)?;
|
|
47
|
+
(run, resolved)
|
|
48
48
|
} else {
|
|
49
49
|
let run =
|
|
50
50
|
canonical_run_workspace(input).map_err(|e| StateError::TeamSelect(e.to_string()))?;
|
|
@@ -58,15 +58,16 @@ pub fn resolve_active_team(
|
|
|
58
58
|
input.display()
|
|
59
59
|
)));
|
|
60
60
|
}
|
|
61
|
-
let
|
|
62
|
-
(run,
|
|
61
|
+
let resolved = resolve_runtime_team_scope(&run, team)?;
|
|
62
|
+
(run, resolved)
|
|
63
63
|
};
|
|
64
|
+
let state = resolved.state;
|
|
64
65
|
|
|
65
66
|
// E5 spec 迁移·读序 B(architect+leader 裁定):
|
|
66
67
|
// 1) runtime spec 优先严格:<run_ws>/.team/runtime/<team_key>/team.spec.yaml 存在即必用。
|
|
67
68
|
// 2) 缺失才**只读回落**用户目录旧 spec(过渡腿;绝不在此写/迁移——迁移+清理只属启动重建)。
|
|
68
69
|
// TODO(E5 后续版本):新 team 永不写用户目录(G1),回落腿可在 legacy 清零后移除。
|
|
69
|
-
let team_key =
|
|
70
|
+
let team_key = resolved.canonical_team_key;
|
|
70
71
|
let runtime_spec = runtime_spec_path(&run_workspace, &team_key);
|
|
71
72
|
let (spec_workspace, spec_path) = if runtime_spec.exists() {
|
|
72
73
|
(
|
|
@@ -136,13 +137,3 @@ fn spec_workspace_from_state(state: &Value) -> Option<PathBuf> {
|
|
|
136
137
|
.map(PathBuf::from)
|
|
137
138
|
})
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
fn selected_team_key(state: &Value, team: Option<&str>) -> String {
|
|
141
|
-
state
|
|
142
|
-
.get("active_team_key")
|
|
143
|
-
.and_then(Value::as_str)
|
|
144
|
-
.filter(|s| !s.is_empty())
|
|
145
|
-
.map(ToString::to_string)
|
|
146
|
-
.or_else(|| team.filter(|s| !s.is_empty()).map(ToString::to_string))
|
|
147
|
-
.unwrap_or_else(|| team_state_key(state))
|
|
148
|
-
}
|