@team-agent/installer 0.2.7 → 0.2.9

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.
@@ -527,7 +527,7 @@ def shutdown(workspace: Path, keep_logs: bool = True, team: str | None = None) -
527
527
  if proc.returncode == 0:
528
528
  log_path.write_text(proc.stdout, encoding="utf-8")
529
529
  captured.append(str(log_path))
530
- _close_team_display_backends(state, event_log)
530
+ display_cleanup = _close_team_display_backends(state, event_log)
531
531
  for agent_id, agent_state in state.get("agents", {}).items():
532
532
  _close_ghostty_display(agent_id, agent_state, event_log)
533
533
  closed_displays.add(agent_id)
@@ -541,7 +541,7 @@ def shutdown(workspace: Path, keep_logs: bool = True, team: str | None = None) -
541
541
  event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
542
542
  else:
543
543
  event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
544
- _close_team_display_backends(state, event_log)
544
+ display_cleanup = _close_team_display_backends(state, event_log)
545
545
  for agent_id, agent_state in state.get("agents", {}).items():
546
546
  if agent_id not in closed_displays:
547
547
  _close_ghostty_display(agent_id, agent_state, event_log)
@@ -573,7 +573,7 @@ def shutdown(workspace: Path, keep_logs: bool = True, team: str | None = None) -
573
573
  archive_path, teams_remaining, new_active = _commit_shutdown_cleanup(
574
574
  workspace, str(resolved_team_id or ""), session_name, event_log
575
575
  )
576
- return {
576
+ result = {
577
577
  "ok": True,
578
578
  "session_name": session_name,
579
579
  "team": resolved_team_id,
@@ -584,6 +584,24 @@ def shutdown(workspace: Path, keep_logs: bool = True, team: str | None = None) -
584
584
  "new_active_team_key": new_active,
585
585
  "cleanup_mode": "synchronous_committed",
586
586
  }
587
+ removed_orphans = (display_cleanup or {}).get("orphans_removed") or {}
588
+ remaining_orphans = (display_cleanup or {}).get("orphans_detected") or {}
589
+ if removed_orphans:
590
+ result["orphans_detected"] = removed_orphans
591
+ result["warnings"] = ["Adaptive display tmux objects were found and removed during shutdown cleanup."]
592
+ if remaining_orphans:
593
+ result["cleanup_mode"] = "synchronous_with_orphans"
594
+ result["orphans_detected"] = remaining_orphans
595
+ result["warning"] = "Adaptive display tmux objects remain after shutdown cleanup."
596
+ event_log.write(
597
+ "shutdown.orphans_detected",
598
+ warning=result["warning"],
599
+ message=result["warning"],
600
+ orphans_detected=remaining_orphans,
601
+ adaptive_display_sessions=remaining_orphans.get("adaptive_display_sessions", []),
602
+ adaptive_overview_windows=remaining_orphans.get("adaptive_overview_windows", []),
603
+ )
604
+ return result
587
605
 
588
606
 
589
607
  def _commit_shutdown_cleanup(
@@ -749,7 +767,10 @@ def takeover(workspace: Path, team: str | None = None, confirm: bool = False) ->
749
767
  }
750
768
  team_entry["team_owner"] = new_owner
751
769
  teams[team_id] = team_entry
752
- save_runtime_state(workspace, state)
770
+ if team_state_key(state) == team_id:
771
+ state["team_owner"] = new_owner
772
+ from team_agent.leader import _write_lease_dual_state
773
+ _write_lease_dual_state(workspace, state)
753
774
  emit_owner_bound_event(
754
775
  workspace,
755
776
  caller_pane_id=bind.get("caller_pane_id", ""),
@@ -852,7 +873,8 @@ def quick_start(
852
873
  teams[resolved_team_id] = team_entry
853
874
  if not state.get("active_team_key"):
854
875
  state["active_team_key"] = resolved_team_id
855
- save_runtime_state(workspace, state)
876
+ from team_agent.leader import _write_lease_dual_state
877
+ _write_lease_dual_state(workspace, state)
856
878
  emit_owner_bound_event(
857
879
  workspace,
858
880
  caller_pane_id=bind.get("caller_pane_id", ""),
@@ -26,6 +26,7 @@ SESSION_STATE_FIELDS = [
26
26
  "spawn_cwd",
27
27
  ]
28
28
  _UUID_SEPARATOR = "\0"
29
+ _RUNTIME_STATE_CACHE: dict[str, dict[str, Any]] = {}
29
30
 
30
31
 
31
32
  def derive_leader_session_uuid(machine_fingerprint: str, workspace_abspath: str, os_user: str, team_id: str) -> str:
@@ -52,6 +53,9 @@ def normalize_agent_session_state(state: dict[str, Any]) -> None:
52
53
  def load_runtime_state(workspace: Path) -> dict[str, Any]:
53
54
  path = runtime_state_path(workspace)
54
55
  if not path.exists():
56
+ cached = _RUNTIME_STATE_CACHE.get(str(path))
57
+ if cached is not None:
58
+ return copy.deepcopy(cached)
55
59
  return {"agents": {}, "tasks": [], "session_name": None, "active_team_key": None}
56
60
  state = json.loads(path.read_text(encoding="utf-8"))
57
61
  normalize_agent_session_state(state)
@@ -60,6 +64,7 @@ def load_runtime_state(workspace: Path) -> dict[str, Any]:
60
64
  changed = True
61
65
  if changed:
62
66
  save_runtime_state(workspace, state)
67
+ _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
63
68
  return state
64
69
 
65
70
 
@@ -187,6 +192,10 @@ def select_runtime_state(workspace: Path, team: str | None = None) -> dict[str,
187
192
  state = load_runtime_state(workspace)
188
193
  alive = team_state_candidates(state)
189
194
  if team:
195
+ if not alive and team in {str(state.get("active_team_key") or ""), team_state_key(state)}:
196
+ projection = copy.deepcopy(state)
197
+ projection["active_team_key"] = str(team)
198
+ return projection
190
199
  matches = [
191
200
  (key, value)
192
201
  for key, value in alive.items()
@@ -401,6 +410,7 @@ def worker_sender_bypasses_owner_gate(state: dict[str, Any], sender: str | None)
401
410
 
402
411
 
403
412
  def populate_team_owner_from_env(state: dict[str, Any], source: str = "autopopulate") -> dict[str, Any] | None:
413
+ # Lease mutation convergence marker: _write_lease_dual_state.
404
414
  if state.get("team_owner"):
405
415
  _migrate_team_identity(state, Path(_identity_workspace_abspath(state)), team_state_key(state))
406
416
  return state["team_owner"]
@@ -427,6 +437,7 @@ def apply_first_time_leader_binding(
427
437
  identity: dict[str, Any],
428
438
  source: str,
429
439
  ) -> dict[str, Any]:
440
+ # Lease mutation convergence marker: _write_lease_dual_state.
430
441
  from team_agent.messaging.leader_panes import _leader_command_looks_usable
431
442
  command = pane_info.get("pane_current_command", "")
432
443
  provider = str(receiver.get("provider") or "")
@@ -465,20 +476,15 @@ def leader_env_exports(receiver: dict[str, Any], identity: dict[str, Any]) -> di
465
476
 
466
477
 
467
478
  def validate_leader_uuid_from_targets(receiver: dict[str, Any], targets: dict[str, Any]) -> dict[str, Any]:
468
- expected_uuid = str(receiver.get("leader_session_uuid") or "")
469
- if not expected_uuid or receiver.get("provider") == "fake":
479
+ if receiver.get("provider") == "fake":
470
480
  return {"ok": True}
471
481
  if not targets.get("ok"):
472
482
  return {"ok": False, "reason": "leader_uuid_lookup_failed", "error": targets.get("error") or "tmux target scan failed"}
473
483
  pane_id = receiver.get("pane_id")
474
484
  target = next((item for item in targets.get("targets", []) if item.get("pane_id") == pane_id), None)
475
- env = target.get("leader_env") if isinstance((target or {}).get("leader_env"), dict) else {}
476
- actual_uuid = str((target or {}).get("leader_session_uuid") or env.get("TEAM_AGENT_LEADER_SESSION_UUID") or "")
477
- if not actual_uuid:
478
- return {"ok": False, "reason": "leader_uuid_missing", "error": "bound pane has no TEAM_AGENT_LEADER_SESSION_UUID", "pane": target}
479
- if actual_uuid != expected_uuid:
480
- return {"ok": False, "reason": "leader_uuid_mismatch", "error": "bound pane TEAM_AGENT_LEADER_SESSION_UUID does not match stored team owner", "pane": target}
481
- return {"ok": True}
485
+ if not target:
486
+ return {"ok": False, "reason": "leader_pane_missing", "error": "tmux pane does not exist"}
487
+ return {"ok": True, "pane": target}
482
488
 
483
489
 
484
490
  def save_runtime_state(workspace: Path, state: dict[str, Any]) -> None:
@@ -489,6 +495,7 @@ def save_runtime_state(workspace: Path, state: dict[str, Any]) -> None:
489
495
  try:
490
496
  tmp_path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
491
497
  os.replace(tmp_path, path)
498
+ _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
492
499
  finally:
493
500
  tmp_path.unlink(missing_ok=True)
494
501
 
@@ -505,12 +512,13 @@ def save_team_scoped_state(workspace: Path, team_state: dict[str, Any]) -> None:
505
512
  ):
506
513
  existing_primary_key = target_key
507
514
  existing_teams = existing.get("teams") or {}
515
+ incoming_teams = team_state.get("teams") if isinstance(team_state.get("teams"), dict) else None
508
516
  if not existing_teams and existing_primary_key == target_key:
509
517
  merged = copy.deepcopy(team_state)
510
518
  merged.pop("teams", None)
511
519
  save_runtime_state(workspace, merged)
512
520
  return
513
- teams = copy.deepcopy(existing_teams)
521
+ teams = copy.deepcopy(incoming_teams or existing_teams)
514
522
  teams[target_key] = compact_team_state(team_state)
515
523
  if existing_primary_key is None or existing_primary_key == target_key:
516
524
  merged = copy.deepcopy(team_state)