@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.
- package/package.json +1 -1
- package/src/team_agent/_legacy_pane_discovery.py +2 -5
- package/src/team_agent/approvals/status.py +5 -1
- package/src/team_agent/cli/commands.py +10 -0
- package/src/team_agent/cli/parser.py +19 -2
- package/src/team_agent/diagnose/comms.py +213 -0
- package/src/team_agent/display/adaptive.py +93 -7
- package/src/team_agent/display/close.py +3 -2
- package/src/team_agent/leader/__init__.py +20 -10
- package/src/team_agent/message_store/leader_notification_log.py +80 -39
- package/src/team_agent/message_store/schema.py +9 -6
- package/src/team_agent/message_store/schema_migration.py +7 -5
- package/src/team_agent/messaging/activity_detector.py +69 -5
- package/src/team_agent/messaging/leader.py +19 -7
- package/src/team_agent/messaging/leader_panes.py +6 -9
- package/src/team_agent/messaging/result_delivery.py +28 -4
- package/src/team_agent/messaging/scheduler.py +1 -1
- package/src/team_agent/messaging/send.py +5 -1
- package/src/team_agent/restart/orchestration.py +13 -2
- package/src/team_agent/runtime.py +27 -5
- package/src/team_agent/state.py +18 -10
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", ""),
|
package/src/team_agent/state.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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)
|