@team-agent/installer 0.5.72 → 0.5.74
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 +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/leader/lease.rs +106 -21
- package/crates/team-agent/src/leader/registry.rs +0 -89
- package/crates/team-agent/src/leader/start.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +43 -7
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +34 -1
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/messaging/delivery.rs +26 -8
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +26 -6
- package/crates/team-agent/src/state/persist.rs +272 -3
- package/crates/team-agent/src/state/repository.rs +9 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
- package/crates/team-agent/src/tmux_backend.rs +57 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +4 -1
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
|
@@ -2,13 +2,19 @@ use std::os::unix::fs::PermissionsExt;
|
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
4
4
|
|
|
5
|
+
use crate::lifecycle::launch::launch_with_transport_in_workspace;
|
|
5
6
|
use crate::lifecycle::launch::pi_mcp::{
|
|
7
|
+
PiAdapterIdentity, PiExecutableChain, PiExecutableFileType, PiMaterializeRequest,
|
|
8
|
+
PiSessionScope, PiWrapperRequest, materialize_pi_plan, materialize_pi_resume_plan,
|
|
6
9
|
parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
|
|
7
10
|
validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
-
write_pi_wrapper, write_pi_wrapper_with_publish,
|
|
9
|
-
PiExecutableFileType, PiWrapperRequest,
|
|
11
|
+
write_pi_wrapper, write_pi_wrapper_with_publish,
|
|
10
12
|
};
|
|
11
|
-
use crate::
|
|
13
|
+
use crate::lifecycle::start_agent_with_transport;
|
|
14
|
+
use crate::model::enums::ProviderEffort;
|
|
15
|
+
use crate::provider::session_scan::{CaptureSessionContext, scan_session_candidates_once};
|
|
16
|
+
use crate::provider::{McpConfig, Provider, SessionId};
|
|
17
|
+
use crate::transport::test_support::OfflineTransport;
|
|
12
18
|
|
|
13
19
|
#[path = "../../../tests/support/hermetic.rs"]
|
|
14
20
|
mod hermetic_guard;
|
|
@@ -34,6 +40,11 @@ const PI_SYMLINK_TEST: &str = concat!(
|
|
|
34
40
|
"lifecycle::tests::pi_executable_mcp_red::",
|
|
35
41
|
"pi_standard_npm_symlink_is_a_verified_launch_entry"
|
|
36
42
|
);
|
|
43
|
+
const PI_MATERIALIZER_CHILD: &str = "TEAM_AGENT_TEST_PI_MATERIALIZER_CHILD";
|
|
44
|
+
const PI_MATERIALIZER_TEST: &str = concat!(
|
|
45
|
+
"lifecycle::tests::pi_executable_mcp_red::",
|
|
46
|
+
"pi_materializer_and_worker_routes_use_the_recorded_scope"
|
|
47
|
+
);
|
|
37
48
|
|
|
38
49
|
fn run_process_isolated(marker: &str, test_name: &str, body: impl FnOnce()) {
|
|
39
50
|
if std::env::var_os(marker).is_some() {
|
|
@@ -504,3 +515,385 @@ fn pi_wrapper_import_failure_body(hermetic: &HermeticTestEnv) {
|
|
|
504
515
|
|
|
505
516
|
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
506
517
|
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn pi_materializer_and_worker_routes_use_the_recorded_scope() {
|
|
521
|
+
run_process_isolated(PI_MATERIALIZER_CHILD, PI_MATERIALIZER_TEST, || {
|
|
522
|
+
let hermetic = HermeticTestEnv::enter("pi-materializer-scope");
|
|
523
|
+
assert_isolated_child();
|
|
524
|
+
pi_materializer_and_worker_routes_body(&hermetic);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fn pi_materializer_and_worker_routes_body(hermetic: &HermeticTestEnv) {
|
|
529
|
+
let fixture_root = hermetic.root().join("materializer-fixture");
|
|
530
|
+
let bin = fixture_root.join("bin");
|
|
531
|
+
let adapter_root = fixture_root.join("pi-mcp-adapter");
|
|
532
|
+
let candidate = fixture_root.join("candidate/team-agent");
|
|
533
|
+
let real = fixture_root.join("pi-real");
|
|
534
|
+
std::fs::create_dir_all(&bin).expect("create Pi fixture bin");
|
|
535
|
+
std::fs::create_dir_all(&adapter_root).expect("create Pi adapter fixture");
|
|
536
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
537
|
+
.expect("create candidate parent");
|
|
538
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
539
|
+
std::fs::write(
|
|
540
|
+
adapter_root.join("package.json"),
|
|
541
|
+
br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
|
|
542
|
+
)
|
|
543
|
+
.expect("write adapter package");
|
|
544
|
+
std::fs::write(
|
|
545
|
+
adapter_root.join("index.ts"),
|
|
546
|
+
b"export const createMcpAdapter = () => {};\n",
|
|
547
|
+
)
|
|
548
|
+
.expect("write adapter entry");
|
|
549
|
+
let pi_script = format!(
|
|
550
|
+
"#!/bin/sh\ncase \"${{1-}}\" in\n --version) printf '0.84.4\\n' ;;\n --list-models) printf 'provider model\\nteam-agent qwen3.8-27b\\n' ;;\n list) printf 'npm:pi-mcp-adapter\\n{}\\n' ;;\n *) exit 0 ;;\nesac\n",
|
|
551
|
+
adapter_root.display()
|
|
552
|
+
);
|
|
553
|
+
std::fs::write(&real, &pi_script).expect("write Pi real fixture");
|
|
554
|
+
std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o755))
|
|
555
|
+
.expect("make Pi real fixture executable");
|
|
556
|
+
std::os::unix::fs::symlink(&real, bin.join("pi")).expect("create Pi PATH symlink");
|
|
557
|
+
let _path = hermetic.with_env(
|
|
558
|
+
"PATH",
|
|
559
|
+
std::env::join_paths([&bin])
|
|
560
|
+
.expect("join Pi fixture PATH")
|
|
561
|
+
.to_str()
|
|
562
|
+
.expect("Pi fixture PATH is UTF-8"),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
let native_workspace = hermetic.workspace("native");
|
|
566
|
+
let native_paths = pi_seat_paths(&native_workspace, "team-a", "leader");
|
|
567
|
+
let native_config = mcp_config(&candidate, &native_workspace, "leader");
|
|
568
|
+
let native = materialize_pi_plan(PiMaterializeRequest {
|
|
569
|
+
workspace: &native_workspace,
|
|
570
|
+
team_id: "team-a",
|
|
571
|
+
agent_id: "leader",
|
|
572
|
+
model: None,
|
|
573
|
+
effort: None,
|
|
574
|
+
system_prompt: "native leader fixture",
|
|
575
|
+
tool_categories: &["mcp_team"],
|
|
576
|
+
team_mcp_tools: &["send_message"],
|
|
577
|
+
mcp_config: &native_config,
|
|
578
|
+
session_scope: PiSessionScope::NativeDefault,
|
|
579
|
+
})
|
|
580
|
+
.expect("NativeDefault materializer");
|
|
581
|
+
assert!(
|
|
582
|
+
native.provider_projects_root.is_none(),
|
|
583
|
+
"NativeDefault must not expose a provider capture root: {native:?}"
|
|
584
|
+
);
|
|
585
|
+
assert!(
|
|
586
|
+
!native_paths.sessions.exists(),
|
|
587
|
+
"NativeDefault must not create the Team Agent seat session root"
|
|
588
|
+
);
|
|
589
|
+
for forbidden in ["--session-dir", "--cwd", "--workspace", "--session"] {
|
|
590
|
+
assert!(
|
|
591
|
+
!native.argv.iter().any(|arg| arg == forbidden),
|
|
592
|
+
"NativeDefault fresh argv must omit {forbidden}: {:?}",
|
|
593
|
+
native.argv
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let isolated_workspace = hermetic.workspace("isolated");
|
|
598
|
+
let isolated_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-a");
|
|
599
|
+
let isolated_config = mcp_config(&candidate, &isolated_workspace, "worker-a");
|
|
600
|
+
let isolated = materialize_pi_plan(PiMaterializeRequest {
|
|
601
|
+
workspace: &isolated_workspace,
|
|
602
|
+
team_id: "team-a",
|
|
603
|
+
agent_id: "worker-a",
|
|
604
|
+
model: Some("team-agent/qwen3.8-27b"),
|
|
605
|
+
effort: Some(ProviderEffort::Medium),
|
|
606
|
+
system_prompt: "isolated worker fixture",
|
|
607
|
+
tool_categories: &["mcp_team"],
|
|
608
|
+
team_mcp_tools: &["send_message"],
|
|
609
|
+
mcp_config: &isolated_config,
|
|
610
|
+
session_scope: PiSessionScope::Isolated,
|
|
611
|
+
})
|
|
612
|
+
.expect("Isolated materializer");
|
|
613
|
+
assert_eq!(
|
|
614
|
+
isolated.provider_projects_root.as_deref(),
|
|
615
|
+
Some(isolated_paths.sessions.as_path())
|
|
616
|
+
);
|
|
617
|
+
assert_session_pair(&isolated.argv, &isolated_paths.sessions);
|
|
618
|
+
assert!(!isolated.argv.iter().any(|arg| arg == "--cwd"));
|
|
619
|
+
assert!(!isolated.argv.iter().any(|arg| arg == "--workspace"));
|
|
620
|
+
|
|
621
|
+
let session_id = SessionId::new("pi-materializer-resume");
|
|
622
|
+
let exact = isolated_paths.sessions.join("2026/09/exact.jsonl");
|
|
623
|
+
std::fs::create_dir_all(exact.parent().expect("exact session parent"))
|
|
624
|
+
.expect("create exact session parent");
|
|
625
|
+
std::fs::write(&exact, b"{}\n").expect("write exact session fixture");
|
|
626
|
+
let resumed = materialize_pi_resume_plan(
|
|
627
|
+
PiMaterializeRequest {
|
|
628
|
+
workspace: &isolated_workspace,
|
|
629
|
+
team_id: "team-a",
|
|
630
|
+
agent_id: "worker-a",
|
|
631
|
+
model: None,
|
|
632
|
+
effort: None,
|
|
633
|
+
system_prompt: "isolated resume fixture",
|
|
634
|
+
tool_categories: &["mcp_team"],
|
|
635
|
+
team_mcp_tools: &["send_message"],
|
|
636
|
+
mcp_config: &isolated_config,
|
|
637
|
+
session_scope: PiSessionScope::Isolated,
|
|
638
|
+
},
|
|
639
|
+
&session_id,
|
|
640
|
+
&exact,
|
|
641
|
+
&isolated_workspace,
|
|
642
|
+
)
|
|
643
|
+
.expect("Isolated exact-path resume materializer");
|
|
644
|
+
assert_eq!(
|
|
645
|
+
resumed.provider_projects_root.as_deref(),
|
|
646
|
+
Some(isolated_paths.sessions.as_path())
|
|
647
|
+
);
|
|
648
|
+
assert_session_pair(&resumed.argv, &isolated_paths.sessions);
|
|
649
|
+
assert!(
|
|
650
|
+
resumed
|
|
651
|
+
.argv
|
|
652
|
+
.windows(2)
|
|
653
|
+
.any(|pair| { pair == ["--session", exact.to_string_lossy().as_ref()] })
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
let native_resume = materialize_pi_resume_plan(
|
|
657
|
+
PiMaterializeRequest {
|
|
658
|
+
workspace: &native_workspace,
|
|
659
|
+
team_id: "team-a",
|
|
660
|
+
agent_id: "leader",
|
|
661
|
+
model: None,
|
|
662
|
+
effort: None,
|
|
663
|
+
system_prompt: "native resume fixture",
|
|
664
|
+
tool_categories: &["mcp_team"],
|
|
665
|
+
team_mcp_tools: &["send_message"],
|
|
666
|
+
mcp_config: &native_config,
|
|
667
|
+
session_scope: PiSessionScope::NativeDefault,
|
|
668
|
+
},
|
|
669
|
+
&session_id,
|
|
670
|
+
&exact,
|
|
671
|
+
&native_workspace,
|
|
672
|
+
);
|
|
673
|
+
assert!(
|
|
674
|
+
matches!(
|
|
675
|
+
native_resume,
|
|
676
|
+
Err(crate::provider::ProviderError::ResumeUnavailable(_))
|
|
677
|
+
),
|
|
678
|
+
"NativeDefault resume must fail closed before emitting a native HOME path: {native_resume:?}"
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
let home_candidate = hermetic.home().join(".pi/agent/sessions/home.jsonl");
|
|
682
|
+
std::fs::create_dir_all(home_candidate.parent().expect("home session parent"))
|
|
683
|
+
.expect("create home session parent");
|
|
684
|
+
std::fs::write(&home_candidate, b"{}\n").expect("write home candidate");
|
|
685
|
+
let other_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-b");
|
|
686
|
+
let other_candidate = other_paths.sessions.join("other.jsonl");
|
|
687
|
+
std::fs::create_dir_all(other_candidate.parent().expect("other session parent"))
|
|
688
|
+
.expect("create other session parent");
|
|
689
|
+
std::fs::write(&other_candidate, b"{}\n").expect("write other seat candidate");
|
|
690
|
+
for (label, outside) in [("HOME", &home_candidate), ("other seat", &other_candidate)] {
|
|
691
|
+
let refused = materialize_pi_resume_plan(
|
|
692
|
+
PiMaterializeRequest {
|
|
693
|
+
workspace: &isolated_workspace,
|
|
694
|
+
team_id: "team-a",
|
|
695
|
+
agent_id: "worker-a",
|
|
696
|
+
model: None,
|
|
697
|
+
effort: None,
|
|
698
|
+
system_prompt: "isolated negative resume fixture",
|
|
699
|
+
tool_categories: &["mcp_team"],
|
|
700
|
+
team_mcp_tools: &["send_message"],
|
|
701
|
+
mcp_config: &isolated_config,
|
|
702
|
+
session_scope: PiSessionScope::Isolated,
|
|
703
|
+
},
|
|
704
|
+
&session_id,
|
|
705
|
+
&outside,
|
|
706
|
+
&isolated_workspace,
|
|
707
|
+
);
|
|
708
|
+
assert!(
|
|
709
|
+
matches!(
|
|
710
|
+
refused,
|
|
711
|
+
Err(crate::provider::ProviderError::ResumeUnavailable(_))
|
|
712
|
+
),
|
|
713
|
+
"{label} candidate must be refused by the exact seat-root check: {refused:?}"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let worker_workspace = hermetic.workspace("worker-spawn");
|
|
718
|
+
let spec_path = worker_workspace
|
|
719
|
+
.join(".team")
|
|
720
|
+
.join("team-a")
|
|
721
|
+
.join("team.spec.yaml");
|
|
722
|
+
std::fs::create_dir_all(spec_path.parent().expect("worker Pi spec parent"))
|
|
723
|
+
.expect("create worker Pi spec parent");
|
|
724
|
+
let spec = crate::model::yaml::loads(
|
|
725
|
+
"team:\n name: team-a\nruntime:\n session_name: team-workers\n display_backend: tmux_attach\nagents:\n - id: worker-a\n role: Worker A\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n - id: worker-b\n role: Worker B\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n",
|
|
726
|
+
)
|
|
727
|
+
.expect("parse worker Pi spec");
|
|
728
|
+
std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).expect("write worker Pi spec");
|
|
729
|
+
let transport = OfflineTransport::new();
|
|
730
|
+
let started = launch_with_transport_in_workspace(
|
|
731
|
+
&worker_workspace,
|
|
732
|
+
&spec_path,
|
|
733
|
+
false,
|
|
734
|
+
true,
|
|
735
|
+
true,
|
|
736
|
+
&transport,
|
|
737
|
+
)
|
|
738
|
+
.expect("worker fresh spawn construction")
|
|
739
|
+
.started;
|
|
740
|
+
assert_eq!(started.len(), 2, "both Pi worker seats must be spawned");
|
|
741
|
+
for worker in ["worker-a", "worker-b"] {
|
|
742
|
+
let paths = pi_seat_paths(&worker_workspace, "team-a", worker);
|
|
743
|
+
let started_agent = started
|
|
744
|
+
.iter()
|
|
745
|
+
.find(|entry| entry.agent_id.as_str() == worker)
|
|
746
|
+
.expect("started worker entry");
|
|
747
|
+
assert_eq!(
|
|
748
|
+
started_agent.provider_projects_root.as_deref(),
|
|
749
|
+
Some(paths.sessions.as_path()),
|
|
750
|
+
"fresh worker must expose its exact seat capture root"
|
|
751
|
+
);
|
|
752
|
+
let record = transport
|
|
753
|
+
.spawn_records()
|
|
754
|
+
.into_iter()
|
|
755
|
+
.find(|(_, argv)| {
|
|
756
|
+
argv.iter()
|
|
757
|
+
.any(|arg| arg.as_str() == paths.wrapper.to_string_lossy().as_ref())
|
|
758
|
+
})
|
|
759
|
+
.expect("transport spawn record for worker seat");
|
|
760
|
+
assert_session_pair(&record.1, &paths.sessions);
|
|
761
|
+
assert!(
|
|
762
|
+
!record
|
|
763
|
+
.1
|
|
764
|
+
.iter()
|
|
765
|
+
.any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
|
|
766
|
+
);
|
|
767
|
+
assert!(
|
|
768
|
+
!record
|
|
769
|
+
.1
|
|
770
|
+
.iter()
|
|
771
|
+
.any(|arg| arg.as_str() == other_candidate.to_string_lossy().as_ref())
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
let pending_session_id = started_agent
|
|
775
|
+
.pending_session_id
|
|
776
|
+
.clone()
|
|
777
|
+
.expect("fresh worker carries a pending Pi session id");
|
|
778
|
+
write_pi_session_header(
|
|
779
|
+
&home_candidate,
|
|
780
|
+
pending_session_id.as_str(),
|
|
781
|
+
&worker_workspace,
|
|
782
|
+
"2099-01-01T00:00:00Z",
|
|
783
|
+
);
|
|
784
|
+
let other_worker = if worker == "worker-a" {
|
|
785
|
+
"worker-b"
|
|
786
|
+
} else {
|
|
787
|
+
"worker-a"
|
|
788
|
+
};
|
|
789
|
+
let cross_seat = pi_seat_paths(&worker_workspace, "team-a", other_worker)
|
|
790
|
+
.sessions
|
|
791
|
+
.join("foreign.jsonl");
|
|
792
|
+
write_pi_session_header(
|
|
793
|
+
&cross_seat,
|
|
794
|
+
pending_session_id.as_str(),
|
|
795
|
+
&worker_workspace,
|
|
796
|
+
"2099-01-01T00:00:00Z",
|
|
797
|
+
);
|
|
798
|
+
let candidates = scan_session_candidates_once(
|
|
799
|
+
Provider::Pi,
|
|
800
|
+
&CaptureSessionContext {
|
|
801
|
+
agent_id: worker.to_string(),
|
|
802
|
+
spawn_cwd: worker_workspace.clone(),
|
|
803
|
+
pane_id: None,
|
|
804
|
+
pane_pid: None,
|
|
805
|
+
spawned_at: Some(started_agent.spawned_at.clone()),
|
|
806
|
+
expected_session_id: Some(pending_session_id),
|
|
807
|
+
provider_projects_root: Some(paths.sessions.clone()),
|
|
808
|
+
},
|
|
809
|
+
)
|
|
810
|
+
.expect("scan the recorded worker seat only");
|
|
811
|
+
assert!(
|
|
812
|
+
candidates.is_empty(),
|
|
813
|
+
"HOME and another worker seat must not become capture candidates: {candidates:?}"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let restart_workspace = hermetic.workspace("worker-restart");
|
|
818
|
+
let restart_paths = pi_seat_paths(&restart_workspace, "team-a", "worker-a");
|
|
819
|
+
let restart_exact = restart_paths.sessions.join("2026/09/restart.jsonl");
|
|
820
|
+
write_pi_session_header(
|
|
821
|
+
&restart_exact,
|
|
822
|
+
"pi-restart-session",
|
|
823
|
+
&restart_workspace,
|
|
824
|
+
"2026-09-01T00:00:01Z",
|
|
825
|
+
);
|
|
826
|
+
crate::state::persist::save_runtime_state(
|
|
827
|
+
&restart_workspace,
|
|
828
|
+
&serde_json::json!({
|
|
829
|
+
"session_name": "team-workers",
|
|
830
|
+
"active_team_key": "team-a",
|
|
831
|
+
"agents": {
|
|
832
|
+
"worker-a": {
|
|
833
|
+
"id": "worker-a",
|
|
834
|
+
"status": "running",
|
|
835
|
+
"provider": "pi",
|
|
836
|
+
"role": "Worker A",
|
|
837
|
+
"model": "team-agent/qwen3.8-27b",
|
|
838
|
+
"auth_mode": "subscription",
|
|
839
|
+
"tools": ["mcp_team"],
|
|
840
|
+
"session_id": "pi-restart-session",
|
|
841
|
+
"rollout_path": restart_exact.to_string_lossy(),
|
|
842
|
+
"spawn_cwd": restart_workspace.to_string_lossy(),
|
|
843
|
+
"first_send_at": "2026-09-01T00:00:00Z"
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}),
|
|
847
|
+
)
|
|
848
|
+
.expect("write restart state");
|
|
849
|
+
let restart_transport = OfflineTransport::new();
|
|
850
|
+
let _ = start_agent_with_transport(
|
|
851
|
+
&restart_workspace,
|
|
852
|
+
&crate::model::ids::AgentId::new("worker-a"),
|
|
853
|
+
false,
|
|
854
|
+
false,
|
|
855
|
+
false,
|
|
856
|
+
None,
|
|
857
|
+
&restart_transport,
|
|
858
|
+
);
|
|
859
|
+
let restart_record = restart_transport
|
|
860
|
+
.spawn_records()
|
|
861
|
+
.into_iter()
|
|
862
|
+
.next()
|
|
863
|
+
.expect("restart must reach the worker spawn construction");
|
|
864
|
+
assert_session_pair(&restart_record.1, &restart_paths.sessions);
|
|
865
|
+
assert!(
|
|
866
|
+
restart_record
|
|
867
|
+
.1
|
|
868
|
+
.windows(2)
|
|
869
|
+
.any(|pair| { pair == ["--session", restart_exact.to_string_lossy().as_ref()] })
|
|
870
|
+
);
|
|
871
|
+
assert!(
|
|
872
|
+
!restart_record
|
|
873
|
+
.1
|
|
874
|
+
.iter()
|
|
875
|
+
.any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn assert_session_pair(argv: &[String], session_root: &Path) {
|
|
880
|
+
let expected = session_root.to_string_lossy();
|
|
881
|
+
assert!(
|
|
882
|
+
argv.windows(2)
|
|
883
|
+
.any(|pair| pair[0] == "--session-dir" && pair[1].as_str() == expected.as_ref()),
|
|
884
|
+
"argv must contain exact --session-dir pair for {}: {argv:?}",
|
|
885
|
+
session_root.display()
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
fn write_pi_session_header(path: &Path, id: &str, cwd: &Path, timestamp: &str) {
|
|
890
|
+
std::fs::create_dir_all(path.parent().expect("session parent")).expect("create session parent");
|
|
891
|
+
let header = serde_json::json!({
|
|
892
|
+
"type": "session",
|
|
893
|
+
"version": 3,
|
|
894
|
+
"id": id,
|
|
895
|
+
"cwd": cwd,
|
|
896
|
+
"timestamp": timestamp,
|
|
897
|
+
});
|
|
898
|
+
std::fs::write(path, format!("{header}\n")).expect("write Pi session header");
|
|
899
|
+
}
|
|
@@ -60,7 +60,9 @@ fn resume_argv(path: &Path) -> Result<Vec<String>, ProviderError> {
|
|
|
60
60
|
effort: Some(ProviderEffort::Medium),
|
|
61
61
|
system_prompt: "worker contract",
|
|
62
62
|
tool_categories: &["mcp_team"],
|
|
63
|
-
session_dir: Path::new(
|
|
63
|
+
session_dir: Some(Path::new(
|
|
64
|
+
"/workspace/.team/runtime/pi/team-a/worker-a/sessions",
|
|
65
|
+
)),
|
|
64
66
|
session: PiSessionSelector::Resume { path },
|
|
65
67
|
agent_id: "worker-a",
|
|
66
68
|
})
|
|
@@ -2866,7 +2866,9 @@ fn project_state_for_owner_team(
|
|
|
2866
2866
|
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
2867
2867
|
}
|
|
2868
2868
|
};
|
|
2869
|
-
if top_level_state_matches_owner_team(fallback, &canonical_team)
|
|
2869
|
+
if top_level_state_matches_owner_team(fallback, &canonical_team)
|
|
2870
|
+
&& state_has_no_team_entries(fallback)
|
|
2871
|
+
{
|
|
2870
2872
|
let mut state = fallback.clone();
|
|
2871
2873
|
carry_top_level_leader_binding(&mut state, &raw);
|
|
2872
2874
|
return Ok(OwnerTeamProjection::Projected {
|
|
@@ -2874,7 +2876,8 @@ fn project_state_for_owner_team(
|
|
|
2874
2876
|
canonical_team,
|
|
2875
2877
|
});
|
|
2876
2878
|
}
|
|
2877
|
-
if top_level_state_matches_owner_team(&raw, &canonical_team)
|
|
2879
|
+
if top_level_state_matches_owner_team(&raw, &canonical_team) && state_has_no_team_entries(&raw)
|
|
2880
|
+
{
|
|
2878
2881
|
return Ok(OwnerTeamProjection::Projected {
|
|
2879
2882
|
state: raw,
|
|
2880
2883
|
canonical_team,
|
|
@@ -2999,16 +3002,31 @@ fn project_state_for_owner_team_value(
|
|
|
2999
3002
|
None
|
|
3000
3003
|
}
|
|
3001
3004
|
|
|
3002
|
-
fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
3003
|
-
state
|
|
3005
|
+
pub(crate) fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
3006
|
+
if !state.is_object() {
|
|
3007
|
+
return false;
|
|
3008
|
+
}
|
|
3009
|
+
if state
|
|
3004
3010
|
.get("active_team_key")
|
|
3005
3011
|
.and_then(serde_json::Value::as_str)
|
|
3006
3012
|
.is_some_and(|value| value == team)
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3013
|
+
{
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
if state
|
|
3017
|
+
.get("session_name")
|
|
3018
|
+
.and_then(serde_json::Value::as_str)
|
|
3019
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
3020
|
+
{
|
|
3021
|
+
return true;
|
|
3022
|
+
}
|
|
3023
|
+
let has_explicit_key = ["team_key", "team_dir", "spec_path"].iter().any(|field| {
|
|
3024
|
+
state
|
|
3025
|
+
.get(*field)
|
|
3010
3026
|
.and_then(serde_json::Value::as_str)
|
|
3011
|
-
.is_some_and(|
|
|
3027
|
+
.is_some_and(|value| !value.is_empty())
|
|
3028
|
+
});
|
|
3029
|
+
has_explicit_key && crate::state::projection::team_state_key(state) == team
|
|
3012
3030
|
}
|
|
3013
3031
|
|
|
3014
3032
|
/// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答
|