@team-agent/installer 0.5.50 → 0.5.52
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +46 -115
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +284 -21
- 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/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use super::{reapply_scope, ReapplyScope, StateWriteIntent};
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn mcp_reapply_scope_matches_legacy_helpers() {
|
|
5
|
+
assert!(
|
|
6
|
+
reapply_scope(&StateWriteIntent::McpUpdateStateNote {
|
|
7
|
+
team_key: Some("team-a"),
|
|
8
|
+
}) == ReapplyScope::Team
|
|
9
|
+
);
|
|
10
|
+
assert!(
|
|
11
|
+
reapply_scope(&StateWriteIntent::McpAssignTask {
|
|
12
|
+
team_key: Some("team-a"),
|
|
13
|
+
task_id: "task-a",
|
|
14
|
+
}) == ReapplyScope::Root
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn optional_unmigrated_read_preserves_missing_corrupt_and_valid_shapes(
|
|
20
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
21
|
+
let nonce = std::time::SystemTime::now()
|
|
22
|
+
.duration_since(std::time::UNIX_EPOCH)?
|
|
23
|
+
.as_nanos();
|
|
24
|
+
let workspace = std::env::temp_dir().join(format!("state-repository-read-{nonce}"));
|
|
25
|
+
let repository = super::StateRepository::new(&workspace);
|
|
26
|
+
assert!(repository
|
|
27
|
+
.load_workspace_if_exists_without_migrations()?
|
|
28
|
+
.is_none());
|
|
29
|
+
|
|
30
|
+
let path = super::helper_workspace_path(&workspace);
|
|
31
|
+
if let Some(parent) = path.parent() {
|
|
32
|
+
std::fs::create_dir_all(parent)?;
|
|
33
|
+
}
|
|
34
|
+
std::fs::write(&path, b"{not-json")?;
|
|
35
|
+
assert!(matches!(
|
|
36
|
+
repository.load_workspace_if_exists_without_migrations(),
|
|
37
|
+
Err(super::StateError::Json(_))
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
std::fs::write(&path, br#"{"legacy":true}"#)?;
|
|
41
|
+
assert_eq!(
|
|
42
|
+
repository.load_workspace_if_exists_without_migrations()?,
|
|
43
|
+
Some(serde_json::json!({"legacy": true}))
|
|
44
|
+
);
|
|
45
|
+
std::fs::remove_dir_all(workspace)?;
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|