@team-agent/installer 0.5.94 → 0.5.98

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.
@@ -299,7 +299,7 @@ fn dispatch_routes_sessions_subcommand() {
299
299
  // + NO session_name + NO agents -> issues=[] -> EXIT 0.
300
300
  #[test]
301
301
  #[serial(env)]
302
- fn dispatch_routes_diagnose_healthy_leader() {
302
+ fn dispatch_routes_diagnose_checks_runtime_beyond_healthy_leader() {
303
303
  let _env = EnvUnsetGuard::unset(&[
304
304
  "TMUX",
305
305
  "TMUX_PANE",
@@ -332,17 +332,26 @@ fn dispatch_routes_diagnose_healthy_leader() {
332
332
  pane_pid: None,
333
333
  leader_env: Default::default(),
334
334
  }]);
335
- let code = crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
336
- run(
337
- &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
338
- &ws,
339
- )
335
+ crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
336
+ for command in ["doctor", "diagnose"] {
337
+ let code = run(
338
+ &cli_argv(&[command, "--workspace", &ws.to_string_lossy(), "--json"]),
339
+ &ws,
340
+ );
341
+ assert_eq!(code, ExitCode::Error, "healthy binding alone cannot hide missing coordinator");
342
+ }
343
+ let result = cmd_diagnose(&DiagnoseArgs {
344
+ workspace: ws.clone(), json: true, team: None,
345
+ }).expect("diagnostic report");
346
+ let CmdOutput::Json(report) = result.output else {
347
+ panic!("expected JSON diagnostic report");
348
+ };
349
+ assert_eq!(report["coordinator"]["ok"], false, "{report}");
350
+ let issues = report["issues"].to_string();
351
+ for binding_failure in ["leader_not_attached", "leader_receiver_unbound", "leader_workspace_mismatch"] {
352
+ assert!(!issues.contains(binding_failure), "healthy binding regressed: {report}");
353
+ }
340
354
  });
341
- assert_eq!(
342
- code,
343
- ExitCode::Ok,
344
- "`diagnose` must be Ok when canonical owner/receiver, production registry, and live align; got {code:?}"
345
- );
346
355
  let _ = std::fs::remove_dir_all(&ws);
347
356
  }
348
357
 
@@ -123,7 +123,6 @@ fn outcome_text(r: Result<CmdResult, CliError>) -> String {
123
123
  }
124
124
 
125
125
  mod base;
126
- mod compile;
127
126
  mod divergence;
128
127
  mod lane_c;
129
128
  mod leader_watch;
@@ -23,6 +23,49 @@ fn anchors(raw: &[&str]) -> BTreeSet<String> {
23
23
  raw.iter().map(|s| s.to_string()).collect()
24
24
  }
25
25
 
26
+ #[test]
27
+ fn leader_anchors_are_endpoint_scoped_and_unknown_anchors_stay_protected() {
28
+ use crate::cli::lifecycle_port::collect_state_leader_anchor_pane_ids as collect;
29
+ let state = json!({
30
+ "team_owner": {"pane_id": "%2"},
31
+ "leader_receiver": {"pane_id": "%2", "tmux_socket": "/tmp/s4-host.sock"},
32
+ "teams": {
33
+ "local": {
34
+ "team_owner": {"pane_id": "%3", "tmux_socket": "/tmp/s4-worker.sock"},
35
+ "leader_receiver": {"pane_id": "%3"}
36
+ },
37
+ "legacy": {"team_owner": {"pane_id": "%4"}}
38
+ }
39
+ });
40
+ assert_eq!(collect(&state, Some("/tmp/s4-worker.sock")), anchors(&["%3", "%4"]));
41
+ assert_eq!(collect(&state, Some("/tmp/s4-host.sock")), anchors(&["%2", "%4"]));
42
+ assert_eq!(collect(&state, None), anchors(&["%2", "%3", "%4"]));
43
+
44
+ // An unrelated receiver's socket is not evidence about the owner's pane.
45
+ let different_panes = json!({
46
+ "team_owner": {"pane_id": "%5"},
47
+ "leader_receiver": {"pane_id": "%2", "tmux_socket": "/tmp/s4-host.sock"}
48
+ });
49
+ assert_eq!(collect(&different_panes, Some("/tmp/s4-worker.sock")), anchors(&["%5"]));
50
+ }
51
+
52
+ #[test]
53
+ #[cfg(unix)]
54
+ fn leader_anchor_endpoint_aliases_preserve_same_server_protection() {
55
+ use crate::cli::lifecycle_port::collect_state_leader_anchor_pane_ids as collect;
56
+ let ws = tmp_shutdown_workspace("anchor-endpoint-alias");
57
+ let socket = ws.join("server.sock");
58
+ let alias = ws.join("alias.sock");
59
+ std::fs::write(&socket, b"fixture").unwrap();
60
+ std::os::unix::fs::symlink(&socket, &alias).unwrap();
61
+ let state = json!({"leader_receiver": {"pane_id": "%2", "tmux_socket": alias}});
62
+ assert_eq!(collect(&state, socket.to_str()), anchors(&["%2"]));
63
+ let short_name = format!("s4-anchor-{}", std::process::id());
64
+ let full_path = crate::tmux_backend::socket_path_for_name(&short_name).unwrap();
65
+ let state = json!({"leader_receiver": {"pane_id": "%2", "tmux_socket": short_name}});
66
+ assert_eq!(collect(&state, full_path.to_str()), anchors(&["%2"]));
67
+ }
68
+
26
69
  // RC-4: even an apparently exclusive socket uses per-session cleanup.
27
70
  #[test]
28
71
  fn rc4_exclusive_socket_is_fail_closed() {
@@ -35,43 +35,27 @@ pub(crate) fn render(command: &str, report: &Value) -> String {
35
35
  // generic emitter redacts again for defense in depth.
36
36
  let report = crate::redaction::redact_external_value(report);
37
37
  let ok = report.get("ok").and_then(Value::as_bool).unwrap_or(false);
38
- let mut lines = vec![bounded(format!(
38
+ let runtime = report
39
+ .get("runtime")
40
+ .and_then(|runtime| runtime.get("status"))
41
+ .and_then(Value::as_str);
42
+ let issues = array_items(report.get("issues"));
43
+ let repairs = array_items(report.get("suggested_repairs"));
44
+ let mut summary = format!(
39
45
  "{command}: {}",
40
46
  if ok { "ok" } else { "needs attention" }
41
- ))];
42
-
43
- if command == "doctor" {
44
- for finding in array_items(
45
- report
46
- .get("secret_scan")
47
- .and_then(|scan| scan.get("findings")),
48
- ) {
49
- let Some(object) = finding.as_object() else {
50
- continue;
51
- };
52
- let (Some(rule), Some(path), Some(line)) = (
53
- object.get("rule").and_then(Value::as_str),
54
- object.get("path").and_then(Value::as_str),
55
- object.get("line").and_then(Value::as_u64),
56
- ) else {
57
- continue;
58
- };
59
- lines.push(bounded(format!("warn: {rule} in {path}:{line}")));
60
- }
47
+ );
48
+ if let Some(runtime) = runtime {
49
+ summary.push_str(&format!("; runtime={runtime}"));
61
50
  }
62
-
63
- for issue in array_items(report.get("issues")) {
51
+ summary.push_str(&format!("; issues={}; repairs={}", issues.len(), repairs.len()));
52
+ let mut lines = vec![bounded(summary)];
53
+ for issue in issues {
64
54
  lines.push(bounded(format!("issue: {}", summarize(issue, false))));
65
55
  }
66
- for repair in array_items(report.get("suggested_repairs")) {
56
+ for repair in repairs {
67
57
  lines.push(bounded(format!("repair: {}", summarize(repair, true))));
68
58
  }
69
- if let Some(error) = report.get("error").filter(|value| !value.is_null()) {
70
- lines.push(bounded(format!("detail: {}", summarize(error, false))));
71
- }
72
- if lines.len() == 1 {
73
- lines.push(bounded("detail: no issues detected".to_string()));
74
- }
75
59
  lines.join("\n")
76
60
  }
77
61
 
@@ -95,6 +79,15 @@ fn summarize(value: &Value, repair: bool) -> String {
95
79
  .as_slice()
96
80
  };
97
81
  let mut parts = Vec::new();
82
+ if let Some(path) = object.get("path").and_then(Value::as_str) {
83
+ let location = match object.get("line").and_then(Value::as_u64) {
84
+ Some(line) => format!("{path}:{line}"),
85
+ None => path.to_string(),
86
+ };
87
+ // Keep the target ahead of prose so bounded human lines retain
88
+ // the distinguishing location rather than only the issue id.
89
+ parts.push(format!("at={location}"));
90
+ }
98
91
  for key in keys {
99
92
  if let Some(value) = object.get(*key).and_then(Value::as_str) {
100
93
  parts.push(format!("{key}={value}"));
@@ -166,6 +159,13 @@ mod tests {
166
159
  fn renderer_projects_secret_findings_without_values() {
167
160
  let report = json!({
168
161
  "ok": false,
162
+ "issues": [{
163
+ "id": "secret_scan_finding",
164
+ "rule": "api_key_assignment",
165
+ "path": "leaky-role.md",
166
+ "line": 7,
167
+ }],
168
+ "suggested_repairs": [{"issue": "secret_scan_finding", "action": "remove secret finding at leaky-role.md:7"}],
169
169
  "secret_scan": {
170
170
  "findings": [{
171
171
  "rule": "api_key_assignment",
@@ -176,7 +176,7 @@ mod tests {
176
176
  }
177
177
  });
178
178
  let output = render("doctor", &report);
179
- assert!(output.contains("warn: api_key_assignment in leaky-role.md:7"));
179
+ assert!(output.contains("issue: at=leaky-role.md:7; id=secret_scan_finding"));
180
180
  assert!(!output.contains("match_excerpt"));
181
181
  assert!(!output.contains("OPENAI_API_KEY=secret"));
182
182
  }
@@ -321,14 +321,6 @@ pub struct InitArgs {
321
321
  pub json: bool,
322
322
  }
323
323
 
324
- /// `compile`(`parser.py:125`)。
325
- #[derive(Debug, Clone, PartialEq, Eq)]
326
- pub struct CompileArgs {
327
- pub team: PathBuf,
328
- pub out: PathBuf,
329
- pub json: bool,
330
- }
331
-
332
324
  /// `send`(`parser.py:262`)。`target` xor `--to`(fanout);`message` 多 token join 空格。
333
325
  #[derive(Debug, Clone, PartialEq)]
334
326
  pub struct SendArgs {
@@ -697,7 +689,9 @@ pub struct WaitArgs {
697
689
  pub json: bool,
698
690
  }
699
691
 
700
- /// `diagnose`(`parser.py:298`) runtime health report, distinct from `doctor`.
692
+ /// `diagnose` compatibility spelling for the canonical `doctor` health report.
693
+ /// The CLI normalizes it to `DoctorArgs`; this narrow type remains for direct
694
+ /// Rust callers that used the pre-unification API.
701
695
  #[derive(Debug, Clone, PartialEq, Eq)]
702
696
  pub struct DiagnoseArgs {
703
697
  pub workspace: PathBuf,
@@ -1041,6 +1041,32 @@ fn restart_with_selected_team_and_transport(
1041
1041
  attach_commands,
1042
1042
  });
1043
1043
  }
1044
+ // Respawn changes the generation used by shutdown's ownership check, even
1045
+ // when build-before-destroy keeps the tmux session itself alive. Stamp only
1046
+ // the selected session, after its replacement state is durable.
1047
+ if let Some(generation) = state
1048
+ .get("generation")
1049
+ .and_then(serde_json::Value::as_str)
1050
+ .or_else(|| {
1051
+ state
1052
+ .get("agents")
1053
+ .and_then(serde_json::Value::as_object)
1054
+ .and_then(|agents| {
1055
+ agents.values().find_map(|agent| {
1056
+ agent.get("spawned_at").and_then(serde_json::Value::as_str)
1057
+ })
1058
+ })
1059
+ })
1060
+ {
1061
+ transport
1062
+ .set_session_owner_with_generation(
1063
+ &session_name,
1064
+ &selected.run_workspace,
1065
+ &selected.team_key,
1066
+ generation,
1067
+ )
1068
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?;
1069
+ }
1044
1070
  // RM-039-SESS-001 step 2 (architect verdict 2026-06-22): post-respawn
1045
1071
  // resume backing validation.
1046
1072
  //
@@ -168,8 +168,10 @@ fn launch_restart_start_add_and_fork_all_delegate_to_the_profile_launch_resolver
168
168
  }
169
169
 
170
170
  #[test]
171
+ #[serial_test::serial(env)]
171
172
  fn diagnose_missing_profile_is_rc1_ok_false_and_zero_token() {
172
- let ws = tmp_dir("diagnose-missing-profile");
173
+ let env = hermetic_guard::HermeticTestEnv::enter("diagnose-missing-profile");
174
+ let ws = env.workspace("missing-profile");
173
175
  let team = write_claude_profile_team(&ws, "diagteam", "clauder");
174
176
  std::fs::remove_file(
175
177
  ws.join(".team")
@@ -180,7 +182,7 @@ fn diagnose_missing_profile_is_rc1_ok_false_and_zero_token() {
180
182
  .unwrap();
181
183
 
182
184
  let output = Command::new(crate::lifecycle::tests::test_binary_path())
183
- .args(["diagnose", "--workspace", ws.to_str().unwrap(), "--json"])
185
+ .args(["diagnose", team.to_str().unwrap(), "--workspace", ws.to_str().unwrap(), "--json"])
184
186
  .current_dir(&team)
185
187
  .output()
186
188
  .expect("diagnose subprocess should run");
@@ -197,10 +199,14 @@ fn diagnose_missing_profile_is_rc1_ok_false_and_zero_token() {
197
199
  json!(false),
198
200
  "diagnose missing profile must emit ok:false JSON; stdout={stdout} stderr={stderr}"
199
201
  );
202
+ assert_eq!(json["profile_smoke"]["ok"], false, "{json}");
200
203
  assert!(
201
- stdout.contains("profile") || stderr.contains("profile"),
202
- "diagnose missing profile must name the profile problem without invoking Claude; stdout={stdout} stderr={stderr}"
204
+ json["profile_smoke"].to_string().contains("profile"),
205
+ "diagnose missing profile must identify the scoped configuration failure: {json}"
203
206
  );
207
+ assert!(json["providers"].as_object().unwrap().values().all(|provider| {
208
+ provider["probe_status"] == "not_run"
209
+ }));
204
210
  assert!(
205
211
  !stdout.contains("tokens") && !stderr.contains("tokens"),
206
212
  "diagnose dry-run must not consume provider tokens or report token usage; stdout={stdout} stderr={stderr}"
@@ -30,7 +30,7 @@ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
30
30
  use team_agent::transport::{
31
31
  AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
32
32
  InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
33
- SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
33
+ SessionOwner, SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
34
34
  TurnVerification, WindowName,
35
35
  };
36
36
 
@@ -130,6 +130,95 @@ fn server_exited_during_replacement_spawn_does_not_cascade_session_disappeared_a
130
130
  );
131
131
  }
132
132
 
133
+ #[test]
134
+ #[serial(env)]
135
+ fn scoped_shutdown_after_restart_refreshes_owner_and_preserves_sibling() {
136
+ let case = RestartCase::new("restart-shutdown-owner", worker_ids(2), ProviderShape::Fake);
137
+ let transport = BuildBeforeDestroyTransport::recording(worker_ids(2));
138
+ for pane in transport.state.lock().unwrap().panes.values_mut() {
139
+ pane.current_path = Some(case.workspace.clone());
140
+ pane.pane_pid = None;
141
+ }
142
+ let session = SessionName::new(TEAM_SESSION);
143
+ let sibling_session = SessionName::new("team-sibling");
144
+ let mut state = case.read_state();
145
+ // Host and worker tmux servers may allocate the same pane ID. The host
146
+ // receiver must not protect the replacement worker on this transport.
147
+ state["leader_receiver"] = json!({
148
+ "pane_id": "%new-1", "tmux_socket": "/tmp/s4-host-only.sock"
149
+ });
150
+ state["team_owner"] = json!({"pane_id": "%new-1"});
151
+ state["teams"][TEAM]["leader_receiver"] = state["leader_receiver"].clone();
152
+ state["teams"][TEAM]["team_owner"] = state["team_owner"].clone();
153
+ state["teams"]["sibling"] = json!({
154
+ "team_key": "sibling",
155
+ "session_name": sibling_session.as_str(),
156
+ "tmux_socket": TMUX_ENDPOINT,
157
+ "agents": {
158
+ "sibling-worker": {
159
+ "provider": "fake", "status": "running", "pane_id": "%sibling",
160
+ "spawned_at": "sibling-generation"
161
+ }
162
+ }
163
+ });
164
+ save_runtime_state(&case.workspace, &state).unwrap();
165
+ let sibling_before = case.read_state()["teams"]["sibling"].clone();
166
+ let mut sibling_pane = pane_info(
167
+ sibling_session.clone(), WindowName::new("sibling-worker"), PaneId::new("%sibling"),
168
+ );
169
+ sibling_pane.current_path = Some(case.workspace.clone());
170
+ sibling_pane.pane_pid = None;
171
+ transport.state.lock().unwrap().panes.insert("%sibling".to_string(), sibling_pane.clone());
172
+ transport.set_session_owner_with_generation(
173
+ &session, &case.workspace, TEAM, "2026-07-14T00:00:00+00:00",
174
+ ).unwrap();
175
+ transport.set_session_owner_with_generation(
176
+ &sibling_session, &case.workspace, "sibling", "sibling-generation",
177
+ ).unwrap();
178
+ let sibling_owner = transport.session_owner(&sibling_session).unwrap();
179
+
180
+ let report = restart_with_transport(&case.workspace, true, Some(TEAM), &transport).unwrap();
181
+ assert!(matches!(report, RestartReport::Restarted { .. }), "{report:?}");
182
+ let state = case.read_state();
183
+ let expected_generation = state["teams"][TEAM]["agents"]["w1"]["spawned_at"].as_str().unwrap();
184
+ assert_ne!(expected_generation, "2026-07-14T00:00:00+00:00");
185
+ let owner = transport.session_owner(&session).unwrap().unwrap();
186
+ assert_eq!(owner.workspace, case.workspace.canonicalize().unwrap().to_string_lossy());
187
+ assert_eq!(owner.team, TEAM);
188
+ assert_eq!(owner.generation, expected_generation);
189
+ assert_no_original_session_kill(&transport.ops(), "restart retains the session");
190
+
191
+ let out = team_agent::cli::lifecycle_port::shutdown_with_transport(
192
+ &case.workspace, true, Some(TEAM), &transport,
193
+ ).unwrap();
194
+ assert_eq!(out["ok"], true, "{out}");
195
+ assert_eq!(out["session_killed"], true, "{out}");
196
+ assert_eq!(out["spared_sessions"], json!([]), "{out}");
197
+ assert_eq!(out["coordinator_stop_reason"], "scoped_live_sibling_present", "{out}");
198
+ assert!(!transport.has_session(&session).unwrap());
199
+ assert!(transport.has_session(&sibling_session).unwrap());
200
+ assert_eq!(transport.session_owner(&sibling_session).unwrap(), sibling_owner);
201
+ assert_eq!(case.read_state()["teams"]["sibling"], sibling_before);
202
+ assert_eq!(transport.state.lock().unwrap().panes.get("%sibling"), Some(&sibling_pane));
203
+ }
204
+
205
+ #[test]
206
+ #[serial(env)]
207
+ fn failed_replacement_preserves_original_session_owner() {
208
+ let case = RestartCase::new("failed-restart-owner", worker_ids(1), ProviderShape::Fake);
209
+ let transport = BuildBeforeDestroyTransport::recording(worker_ids(1))
210
+ .with_spawn_failure("w1", "injected replacement spawn failure");
211
+ let session = SessionName::new(TEAM_SESSION);
212
+ transport.set_session_owner_with_generation(
213
+ &session, &case.workspace, TEAM, "2026-07-14T00:00:00+00:00",
214
+ ).unwrap();
215
+ let owner_before = transport.session_owner(&session).unwrap();
216
+ let report = restart_with_transport(&case.workspace, true, Some(TEAM), &transport).unwrap();
217
+ assert_restart_did_not_succeed(&report);
218
+ assert_eq!(transport.session_owner(&session).unwrap(), owner_before);
219
+ assert_eq!(transport.ops().iter().filter(|op| op.starts_with("set_session_owner:")).count(), 1);
220
+ }
221
+
133
222
  struct RestartCase {
134
223
  env: hermetic_guard::HermeticTestEnv,
135
224
  workspace: PathBuf,
@@ -354,6 +443,7 @@ struct TransportState {
354
443
  session_present: bool,
355
444
  ops: Vec<String>,
356
445
  panes: BTreeMap<String, PaneInfo>,
446
+ owners: BTreeMap<String, SessionOwner>,
357
447
  next_pane: usize,
358
448
  spawns: usize,
359
449
  }
@@ -416,6 +506,7 @@ impl BuildBeforeDestroyTransport {
416
506
  kind: &'static str,
417
507
  session: &SessionName,
418
508
  window: &WindowName,
509
+ cwd: &Path,
419
510
  ) -> Result<SpawnResult, TransportError> {
420
511
  let mut state = self.state.lock().unwrap();
421
512
  state
@@ -434,10 +525,10 @@ impl BuildBeforeDestroyTransport {
434
525
  let pane_id = PaneId::new(format!("%new-{}", state.next_pane));
435
526
  state.next_pane += 1;
436
527
  state.session_present = true;
437
- state.panes.insert(
438
- pane_id.as_str().to_string(),
439
- pane_info(session.clone(), window.clone(), pane_id.clone()),
440
- );
528
+ let mut pane = pane_info(session.clone(), window.clone(), pane_id.clone());
529
+ pane.current_path = Some(cwd.to_path_buf());
530
+ pane.pane_pid = None;
531
+ state.panes.insert(pane_id.as_str().to_string(), pane);
441
532
  if self.server_exit_after_first_spawn && state.spawns == 1 {
442
533
  state.session_present = false;
443
534
  }
@@ -451,7 +542,12 @@ impl BuildBeforeDestroyTransport {
451
542
 
452
543
  fn real_tmux_has_session(&self, session: &SessionName) -> Result<bool, TransportError> {
453
544
  let Some(socket) = self.real_socket.as_ref() else {
454
- return Ok(self.state.lock().unwrap().session_present);
545
+ let state = self.state.lock().unwrap();
546
+ return Ok(if session.as_str() == TEAM_SESSION {
547
+ state.session_present
548
+ } else {
549
+ state.panes.values().any(|pane| pane.session == *session)
550
+ });
455
551
  };
456
552
  Ok(Command::new("tmux")
457
553
  .args([
@@ -535,10 +631,10 @@ impl Transport for BuildBeforeDestroyTransport {
535
631
  session: &SessionName,
536
632
  window: &WindowName,
537
633
  _argv: &[String],
538
- _cwd: &Path,
634
+ cwd: &Path,
539
635
  _env: &BTreeMap<String, String>,
540
636
  ) -> Result<SpawnResult, TransportError> {
541
- self.spawn("spawn_first", session, window)
637
+ self.spawn("spawn_first", session, window, cwd)
542
638
  }
543
639
 
544
640
  fn spawn_into(
@@ -546,10 +642,10 @@ impl Transport for BuildBeforeDestroyTransport {
546
642
  session: &SessionName,
547
643
  window: &WindowName,
548
644
  _argv: &[String],
549
- _cwd: &Path,
645
+ cwd: &Path,
550
646
  _env: &BTreeMap<String, String>,
551
647
  ) -> Result<SpawnResult, TransportError> {
552
- self.spawn("spawn_into", session, window)
648
+ self.spawn("spawn_into", session, window, cwd)
553
649
  }
554
650
 
555
651
  fn inject(
@@ -648,6 +744,27 @@ impl Transport for BuildBeforeDestroyTransport {
648
744
  Ok(windows.into_iter().collect())
649
745
  }
650
746
 
747
+ fn set_session_owner_with_generation(
748
+ &self,
749
+ session: &SessionName,
750
+ workspace: &Path,
751
+ team: &str,
752
+ generation: &str,
753
+ ) -> Result<(), TransportError> {
754
+ let mut state = self.state.lock().unwrap();
755
+ state.ops.push(format!("set_session_owner:{}", session.as_str()));
756
+ state.owners.insert(session.as_str().to_string(), SessionOwner {
757
+ workspace: workspace.canonicalize().unwrap().to_string_lossy().into_owned(),
758
+ team: team.to_string(),
759
+ generation: generation.to_string(),
760
+ });
761
+ Ok(())
762
+ }
763
+
764
+ fn session_owner(&self, session: &SessionName) -> Result<Option<SessionOwner>, TransportError> {
765
+ Ok(self.state.lock().unwrap().owners.get(session.as_str()).cloned())
766
+ }
767
+
651
768
  fn set_session_env(
652
769
  &self,
653
770
  _session: &SessionName,
@@ -665,7 +782,7 @@ impl Transport for BuildBeforeDestroyTransport {
665
782
  .push(format!("kill_session:{}", session.as_str()));
666
783
  if session.as_str() == TEAM_SESSION {
667
784
  self.state.lock().unwrap().session_present = false;
668
- self.state.lock().unwrap().panes.clear();
785
+ self.state.lock().unwrap().panes.retain(|_, pane| pane.session != *session);
669
786
  if let Some(socket) = self.real_socket.as_ref() {
670
787
  let status = Command::new("tmux")
671
788
  .args([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.94",
3
+ "version": "0.5.98",
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.94",
24
- "@team-agent/cli-darwin-x64": "0.5.94",
25
- "@team-agent/cli-linux-x64": "0.5.94"
23
+ "@team-agent/cli-darwin-arm64": "0.5.98",
24
+ "@team-agent/cli-darwin-x64": "0.5.98",
25
+ "@team-agent/cli-linux-x64": "0.5.98"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",