@team-agent/installer 0.5.72 → 0.5.73

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 CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.72"
584
+ version = "0.5.73"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.72"
12
+ version = "0.5.73"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -162,6 +162,7 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
162
162
  std::env::var_os("TMUX").is_some(),
163
163
  managed_client_attach_mode,
164
164
  managed_exec_provider,
165
+ allow_nested_attach,
165
166
  )?;
166
167
  Ok(PreparedLeaderStart {
167
168
  plan,
@@ -196,6 +197,7 @@ pub(crate) fn leader_start_plan_after_ambient_authority_check(
196
197
  in_tmux,
197
198
  managed_client_attach_mode,
198
199
  false,
200
+ false,
199
201
  )
200
202
  }
201
203
 
@@ -210,6 +212,7 @@ fn leader_start_plan_with_ambient_authority(
210
212
  in_tmux: bool,
211
213
  managed_client_attach_mode: Option<ManagedClientAttachMode>,
212
214
  managed_exec_provider: bool,
215
+ allow_nested_attach: bool,
213
216
  ) -> Result<LeaderStartPlan, LeaderError> {
214
217
  if attach_session.is_some() && !confirm_attach {
215
218
  return Err(LeaderError::Start(
@@ -327,6 +330,11 @@ fn leader_start_plan_with_ambient_authority(
327
330
  tool_categories: &tools,
328
331
  team_mcp_tools: &team_mcp_tools,
329
332
  mcp_config: &mcp_config,
333
+ session_scope: crate::lifecycle::launch::pi_mcp::pi_leader_session_scope(
334
+ provider_args,
335
+ external_path,
336
+ allow_nested_attach,
337
+ ),
330
338
  },
331
339
  )
332
340
  .map_err(|error| LeaderError::Start(error.to_string()))?
@@ -816,6 +816,27 @@ pub(crate) fn resolve_pi_executable_chain(
816
816
  Ok((chain, models))
817
817
  }
818
818
 
819
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
820
+ pub(crate) enum PiSessionScope {
821
+ Isolated,
822
+ NativeDefault,
823
+ }
824
+
825
+ /// Zero-argument managed Pi is the interactive leader entry. Only that route
826
+ /// may use Pi's cwd-keyed native session root; every explicit or worker route
827
+ /// remains Team Agent session-isolated.
828
+ pub(crate) fn pi_leader_session_scope(
829
+ provider_args: &[String],
830
+ external_path: bool,
831
+ allow_nested_attach: bool,
832
+ ) -> PiSessionScope {
833
+ if provider_args.is_empty() && !external_path && !allow_nested_attach {
834
+ PiSessionScope::NativeDefault
835
+ } else {
836
+ PiSessionScope::Isolated
837
+ }
838
+ }
839
+
819
840
  pub(crate) struct PiMaterializeRequest<'a> {
820
841
  pub workspace: &'a Path,
821
842
  pub team_id: &'a str,
@@ -826,11 +847,13 @@ pub(crate) struct PiMaterializeRequest<'a> {
826
847
  pub tool_categories: &'a [&'a str],
827
848
  pub team_mcp_tools: &'a [&'a str],
828
849
  pub mcp_config: &'a McpConfig,
850
+ pub session_scope: PiSessionScope,
829
851
  }
830
852
 
831
853
  /// ---
832
854
  /// purpose: 共享物化 Pi leader 与 TeamMate 的 wrapper、UUID 和 CommandPlan
833
- /// returns: fresh Pi CommandPlan,含 expected UUID 与 seat session root
855
+ /// returns: fresh Pi CommandPlan;isolated seats carry their session root, while the
856
+ /// interactive leader preserves Pi's native default root
834
857
  /// errors: resolver、exact model、wrapper 或 argv 构造失败时返回 ProviderError
835
858
  /// ---
836
859
  pub(crate) fn materialize_pi_plan(
@@ -863,8 +886,18 @@ fn materialize_pi_plan_with_session(
863
886
  .map(|model| select_exact_pi_model(&catalog, model))
864
887
  .transpose()?;
865
888
  let paths = pi_seat_paths(request.workspace, request.team_id, request.agent_id);
889
+ let session_dir = match request.session_scope {
890
+ PiSessionScope::Isolated => Some(paths.sessions.as_path()),
891
+ PiSessionScope::NativeDefault => None,
892
+ };
866
893
  let resume = if let Some((session_id, session_path, spawn_cwd)) = resume {
867
- let session_root = std::fs::canonicalize(&paths.sessions).map_err(|error| {
894
+ let session_dir = session_dir.ok_or_else(|| {
895
+ ProviderError::ResumeUnavailable(format!(
896
+ "Pi session root is unavailable {}",
897
+ paths.sessions.display()
898
+ ))
899
+ })?;
900
+ let session_root = std::fs::canonicalize(session_dir).map_err(|error| {
868
901
  ProviderError::ResumeUnavailable(format!(
869
902
  "Pi session root is unavailable {}: {error}",
870
903
  paths.sessions.display()
@@ -885,8 +918,11 @@ fn materialize_pi_plan_with_session(
885
918
  let _ = spawn_cwd;
886
919
  Some((session_id, exact_path))
887
920
  } else {
888
- std::fs::create_dir_all(&paths.sessions)
889
- .map_err(|error| ProviderError::Io(format!("{}: {error}", paths.sessions.display())))?;
921
+ if let Some(session_dir) = session_dir {
922
+ std::fs::create_dir_all(session_dir).map_err(|error| {
923
+ ProviderError::Io(format!("{}: {error}", session_dir.display()))
924
+ })?;
925
+ }
890
926
  None
891
927
  };
892
928
  let candidate = candidate_from_mcp_config(request.mcp_config)?;
@@ -908,7 +944,7 @@ fn materialize_pi_plan_with_session(
908
944
  effort: request.effort,
909
945
  system_prompt: request.system_prompt,
910
946
  tool_categories: request.tool_categories,
911
- session_dir: &paths.sessions,
947
+ session_dir,
912
948
  session: PiSessionSelector::Resume { path: &exact_path },
913
949
  agent_id: request.agent_id,
914
950
  })?;
@@ -922,7 +958,7 @@ fn materialize_pi_plan_with_session(
922
958
  effort: request.effort,
923
959
  system_prompt: request.system_prompt,
924
960
  tool_categories: request.tool_categories,
925
- session_dir: &paths.sessions,
961
+ session_dir,
926
962
  session: PiSessionSelector::Fresh {
927
963
  session_id: session_id.as_str(),
928
964
  },
@@ -933,7 +969,7 @@ fn materialize_pi_plan_with_session(
933
969
  Ok(CommandPlan {
934
970
  argv,
935
971
  expected_session_id,
936
- provider_projects_root: Some(paths.sessions),
972
+ provider_projects_root: session_dir.map(Path::to_path_buf),
937
973
  managed_mcp_config: false,
938
974
  })
939
975
  }
@@ -189,6 +189,7 @@ pub(super) fn spawn_agents(
189
189
  tool_categories: &resolved_tool_refs,
190
190
  team_mcp_tools: &["send_message", "report_result"],
191
191
  mcp_config: &mcp_config,
192
+ session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
192
193
  },
193
194
  )
194
195
  .map_err(|e| LifecycleError::Provider(e.to_string()))?
@@ -402,6 +402,7 @@ pub(super) fn spawn_agent_window(
402
402
  tool_categories: &resolved_tool_refs,
403
403
  team_mcp_tools: &["send_message", "report_result"],
404
404
  mcp_config: &mcp_config,
405
+ session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
405
406
  };
406
407
  match resume_session_id {
407
408
  Some(session_id) => {
@@ -22,7 +22,7 @@ fn command_for_model(model: &str) -> Vec<String> {
22
22
  effort: Some(ProviderEffort::Medium),
23
23
  system_prompt: "worker contract",
24
24
  tool_categories: &["mcp_team"],
25
- session_dir: Path::new("/workspace/.team/runtime/pi/t1/w1/sessions"),
25
+ session_dir: Some(Path::new("/workspace/.team/runtime/pi/t1/w1/sessions")),
26
26
  session: PiSessionSelector::Fresh {
27
27
  session_id: "716ba0cb-c491-4471-b41a-43c8d5e1f05a",
28
28
  },
@@ -1,5 +1,6 @@
1
1
  use std::path::Path;
2
2
 
3
+ use crate::lifecycle::launch::pi_mcp::{pi_leader_session_scope, PiSessionScope};
3
4
  use crate::model::enums::ProviderEffort;
4
5
  use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
5
6
 
@@ -11,7 +12,9 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
11
12
  effort: Some(ProviderEffort::High),
12
13
  system_prompt: "frozen worker prompt",
13
14
  tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
14
- session_dir: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
15
+ session_dir: Some(Path::new(
16
+ "/workspace/.team/runtime/pi/team-a/worker-a/sessions",
17
+ )),
15
18
  session,
16
19
  agent_id: "worker-a",
17
20
  }
@@ -30,6 +33,73 @@ fn pi_command_omits_unset_model_and_thinking_flags() {
30
33
  assert!(!argv.iter().any(|arg| arg == "--thinking"), "argv={argv:?}");
31
34
  }
32
35
 
36
+ fn leader_request() -> PiCommandRequest<'static> {
37
+ PiCommandRequest {
38
+ executable: Path::new("/verified/pi"),
39
+ extension: Path::new("/workspace/.team/runtime/pi/current/leader/team-mcp.ts"),
40
+ model: None,
41
+ effort: None,
42
+ system_prompt: "frozen leader prompt",
43
+ tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
44
+ session_dir: None,
45
+ session: PiSessionSelector::Fresh {
46
+ session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
47
+ },
48
+ agent_id: "leader",
49
+ }
50
+ }
51
+
52
+ #[test]
53
+ fn pi_interactive_leader_argv_preserves_native_session_discovery() {
54
+ let argv = build_pi_command_argv(leader_request()).expect("native Pi leader command");
55
+
56
+ assert_eq!(
57
+ argv,
58
+ [
59
+ "/verified/pi",
60
+ "-e",
61
+ "/workspace/.team/runtime/pi/current/leader/team-mcp.ts",
62
+ "--append-system-prompt",
63
+ "frozen leader prompt",
64
+ "--session-id",
65
+ "da8c3622-2378-4d05-a26c-e826a6ef6d63",
66
+ "--name",
67
+ "leader",
68
+ ]
69
+ .map(str::to_string)
70
+ );
71
+ assert!(!argv.iter().any(|arg| arg == "--session-dir"));
72
+ for forbidden in ["--cwd", "--workspace", "--session"] {
73
+ assert!(!argv.iter().any(|arg| arg == forbidden), "argv={argv:?}");
74
+ }
75
+ }
76
+
77
+ #[test]
78
+ fn pi_leader_session_scope_keeps_explicit_and_external_routes_isolated() {
79
+ assert_eq!(
80
+ pi_leader_session_scope(&[], false, false),
81
+ PiSessionScope::NativeDefault
82
+ );
83
+ for (args, external_path, allow_nested_attach) in [
84
+ (
85
+ vec![
86
+ "--model".to_string(),
87
+ "openai-codex/gpt-5.6-sol".to_string(),
88
+ ],
89
+ false,
90
+ false,
91
+ ),
92
+ (Vec::new(), true, false),
93
+ (Vec::new(), false, true),
94
+ ] {
95
+ assert_eq!(
96
+ pi_leader_session_scope(&args, external_path, allow_nested_attach),
97
+ PiSessionScope::Isolated,
98
+ "only the zero-argument managed route may use Pi's native root"
99
+ );
100
+ }
101
+ }
102
+
33
103
  #[test]
34
104
  fn pi_command_preserves_direct_pi_defaults_with_only_team_runtime_additions() {
35
105
  let fresh = build_pi_command_argv(request(PiSessionSelector::Fresh {
@@ -59,7 +59,9 @@ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<St
59
59
  effort: Some(effort),
60
60
  system_prompt: "Pi worker contract.",
61
61
  tool_categories: tools,
62
- session_dir: Path::new("/workspace/.team/runtime/pi/t1/pi-worker/sessions"),
62
+ session_dir: Some(Path::new(
63
+ "/workspace/.team/runtime/pi/t1/pi-worker/sessions",
64
+ )),
63
65
  session: PiSessionSelector::Fresh {
64
66
  session_id: "c5ddf218-24a5-4e74-960c-ab6606ea7e8c",
65
67
  },
@@ -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, PiAdapterIdentity, PiExecutableChain,
9
- PiExecutableFileType, PiWrapperRequest,
11
+ write_pi_wrapper, write_pi_wrapper_with_publish,
10
12
  };
11
- use crate::provider::McpConfig;
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("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
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
  })
@@ -50,7 +50,8 @@ pub(crate) struct PiCommandRequest<'a> {
50
50
  pub effort: Option<ProviderEffort>,
51
51
  pub system_prompt: &'a str,
52
52
  pub tool_categories: &'a [&'a str],
53
- pub session_dir: &'a Path,
53
+ /// Team-owned session root for isolated seats; `None` preserves Pi's native default.
54
+ pub session_dir: Option<&'a Path>,
54
55
  pub session: PiSessionSelector<'a>,
55
56
  pub agent_id: &'a str,
56
57
  }
@@ -65,14 +66,15 @@ pub(crate) fn build_pi_command_argv(
65
66
  ) -> Result<Vec<String>, ProviderError> {
66
67
  if request.executable.as_os_str().is_empty()
67
68
  || request.extension.as_os_str().is_empty()
68
- || request.session_dir.as_os_str().is_empty()
69
+ || request
70
+ .session_dir
71
+ .is_some_and(|session_dir| session_dir.as_os_str().is_empty())
69
72
  || request.model.is_some_and(|model| model.trim().is_empty())
70
73
  || request.system_prompt.trim().is_empty()
71
74
  || request.agent_id.trim().is_empty()
72
75
  {
73
76
  return Err(ProviderError::Command(
74
- "Pi command requires executable, extension, prompt, session directory, and agent id"
75
- .to_string(),
77
+ "Pi command requires executable, extension, prompt, and agent id".to_string(),
76
78
  ));
77
79
  }
78
80
 
@@ -110,9 +112,13 @@ pub(crate) fn build_pi_command_argv(
110
112
  argv.extend([
111
113
  "--append-system-prompt".to_string(),
112
114
  request.system_prompt.to_string(),
113
- "--session-dir".to_string(),
114
- request.session_dir.to_string_lossy().into_owned(),
115
115
  ]);
116
+ if let Some(session_dir) = request.session_dir {
117
+ argv.extend([
118
+ "--session-dir".to_string(),
119
+ session_dir.to_string_lossy().into_owned(),
120
+ ]);
121
+ }
116
122
  // The shared lifecycle materializer always revalidates resume root/path/header.
117
123
  // Keep the pure argv builder usable before a fixture root is materialized, while
118
124
  // refusing a missing exact file once the recorded path's parent exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.72",
3
+ "version": "0.5.73",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.72",
24
- "@team-agent/cli-darwin-x64": "0.5.72",
25
- "@team-agent/cli-linux-x64": "0.5.72"
23
+ "@team-agent/cli-darwin-arm64": "0.5.73",
24
+ "@team-agent/cli-darwin-x64": "0.5.73",
25
+ "@team-agent/cli-linux-x64": "0.5.73"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -2,7 +2,7 @@
2
2
  name: team-agent
3
3
  description: Use only when the user explicitly asks to start, operate, inspect, shutdown, or restart a Team Agent team. Treat the team-agent CLI as a sealed appliance.
4
4
  requires_team_agent: ">=0.5.0"
5
- last_verified_against: "0.5.72"
5
+ last_verified_against: "0.5.73"
6
6
  ---
7
7
 
8
8
  # Team Agent