@team-agent/installer 0.1.4 → 0.1.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 -2
- package/schemas/team.schema.json +1 -1
- package/src/team_agent/runtime.py +535 -30
- package/src/team_agent/spec.py +1 -1
- package/src/team_agent/state.py +15 -5
- package/tests/run_tests.py +0 -5474
|
@@ -35,7 +35,15 @@ from team_agent.providers import ResumeUnavailable, get_adapter, shell_command_f
|
|
|
35
35
|
from team_agent.routing import route_task
|
|
36
36
|
from team_agent.simple_yaml import dumps
|
|
37
37
|
from team_agent.spec import load_spec, validate_result_envelope, workspace_from_spec
|
|
38
|
-
from team_agent.state import
|
|
38
|
+
from team_agent.state import (
|
|
39
|
+
SESSION_CAPTURE_FIELDS,
|
|
40
|
+
SESSION_STATE_FIELDS,
|
|
41
|
+
load_runtime_state,
|
|
42
|
+
normalize_agent_session_state,
|
|
43
|
+
runtime_state_path,
|
|
44
|
+
save_runtime_state,
|
|
45
|
+
write_team_state,
|
|
46
|
+
)
|
|
39
47
|
from team_agent.task_graph import ready_tasks, update_task_status
|
|
40
48
|
from team_agent.task_graph import TASK_STATUSES
|
|
41
49
|
|
|
@@ -46,6 +54,7 @@ TMUX_PANE_FORMAT = (
|
|
|
46
54
|
"#{pane_current_path}\t#{session_attached}"
|
|
47
55
|
)
|
|
48
56
|
HEALTH_STATUSES = {"RUNNING", "IDLE", "AWAITING_APPROVAL", "BLOCKED", "ERROR", "DONE"}
|
|
57
|
+
GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
|
|
49
58
|
PEEK_MAX_LINES = 80
|
|
50
59
|
PEEK_SEARCH_SCAN_LINES = 300
|
|
51
60
|
PEEK_MAX_MATCHES = 5
|
|
@@ -448,10 +457,16 @@ def launch(
|
|
|
448
457
|
timeout_s=1.5,
|
|
449
458
|
exclude_session_ids=known_session_ids,
|
|
450
459
|
)
|
|
451
|
-
if state.get("display_backend") in
|
|
460
|
+
if state.get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
452
461
|
display_jobs.append((agent["id"], agent))
|
|
453
462
|
started.append({"agent_id": agent["id"], "provider": agent["provider"], "window": agent["id"]})
|
|
454
|
-
for agent_id, display in _open_worker_displays(
|
|
463
|
+
for agent_id, display in _open_worker_displays(
|
|
464
|
+
workspace,
|
|
465
|
+
session_name,
|
|
466
|
+
display_jobs,
|
|
467
|
+
event_log,
|
|
468
|
+
state.get("display_backend", "none"),
|
|
469
|
+
).items():
|
|
455
470
|
if agent_id in state["agents"]:
|
|
456
471
|
state["agents"][agent_id]["display"] = display
|
|
457
472
|
save_runtime_state(workspace, state)
|
|
@@ -520,10 +535,7 @@ def _load_snapshot_state(path: Path) -> dict[str, Any] | None:
|
|
|
520
535
|
state = json.loads(path.read_text(encoding="utf-8"))
|
|
521
536
|
except (OSError, json.JSONDecodeError):
|
|
522
537
|
return None
|
|
523
|
-
|
|
524
|
-
if isinstance(agent_state, dict):
|
|
525
|
-
for field in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
|
|
526
|
-
agent_state.setdefault(field, None)
|
|
538
|
+
normalize_agent_session_state(state)
|
|
527
539
|
return state
|
|
528
540
|
|
|
529
541
|
|
|
@@ -1570,8 +1582,7 @@ def _capture_agent_session(
|
|
|
1570
1582
|
result = adapter.capture_session_id(agent_id, spawn_context, timeout_s=timeout_s)
|
|
1571
1583
|
if not isinstance(result, dict) or not result.get("session_id"):
|
|
1572
1584
|
return None
|
|
1573
|
-
|
|
1574
|
-
agent_state[key] = result.get(key)
|
|
1585
|
+
_copy_session_metadata(agent_state, result)
|
|
1575
1586
|
agent_state.pop("_pending_session_id", None)
|
|
1576
1587
|
event_log.write(
|
|
1577
1588
|
"session.captured",
|
|
@@ -1585,6 +1596,16 @@ def _capture_agent_session(
|
|
|
1585
1596
|
return result
|
|
1586
1597
|
|
|
1587
1598
|
|
|
1599
|
+
def _copy_session_metadata(target: dict[str, Any], source: dict[str, Any]) -> None:
|
|
1600
|
+
for key in SESSION_STATE_FIELDS:
|
|
1601
|
+
target[key] = source.get(key)
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def _clear_session_capture_fields(target: dict[str, Any]) -> None:
|
|
1605
|
+
for key in SESSION_CAPTURE_FIELDS:
|
|
1606
|
+
target[key] = None
|
|
1607
|
+
|
|
1608
|
+
|
|
1588
1609
|
def _attach_profile_resume_root(workspace: Path, command_agent: dict[str, Any], previous: dict[str, Any]) -> dict[str, Any]:
|
|
1589
1610
|
profile_launch = command_agent.get("_provider_profile") or prepare_agent_profile_launch(workspace, command_agent)
|
|
1590
1611
|
if not profile_launch:
|
|
@@ -1631,8 +1652,7 @@ def _prepare_resume_state(
|
|
|
1631
1652
|
if not repaired:
|
|
1632
1653
|
repaired = adapter.recover_session_id(agent_id, prepared, workspace, exclude_session_ids or set())
|
|
1633
1654
|
if repaired:
|
|
1634
|
-
|
|
1635
|
-
prepared[key] = repaired.get(key)
|
|
1655
|
+
_copy_session_metadata(prepared, repaired)
|
|
1636
1656
|
event_log.write(
|
|
1637
1657
|
"resume.session_repaired",
|
|
1638
1658
|
agent_id=agent_id,
|
|
@@ -1657,8 +1677,7 @@ def _prepare_resume_state(
|
|
|
1657
1677
|
f"Cannot resume agent {agent_id}: stored session {session_id} is not available. "
|
|
1658
1678
|
"Use --allow-fresh only if losing that worker context is acceptable."
|
|
1659
1679
|
)
|
|
1660
|
-
|
|
1661
|
-
prepared[key] = None
|
|
1680
|
+
_clear_session_capture_fields(prepared)
|
|
1662
1681
|
event_log.write(
|
|
1663
1682
|
"resume.session_unavailable",
|
|
1664
1683
|
agent_id=agent_id,
|
|
@@ -1741,6 +1760,7 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1741
1760
|
if proc.returncode == 0:
|
|
1742
1761
|
log_path.write_text(proc.stdout, encoding="utf-8")
|
|
1743
1762
|
captured.append(str(log_path))
|
|
1763
|
+
_close_ghostty_workspace(state, event_log)
|
|
1744
1764
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1745
1765
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
1746
1766
|
closed_displays.add(agent_id)
|
|
@@ -1750,6 +1770,7 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1750
1770
|
event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
|
|
1751
1771
|
else:
|
|
1752
1772
|
event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
|
|
1773
|
+
_close_ghostty_workspace(state, event_log)
|
|
1753
1774
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1754
1775
|
if agent_id not in closed_displays:
|
|
1755
1776
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
@@ -1972,8 +1993,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1972
1993
|
if profile_launch.get("claude_projects_root"):
|
|
1973
1994
|
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
1974
1995
|
if restart_mode == "fresh":
|
|
1975
|
-
|
|
1976
|
-
agent_state[key] = None
|
|
1996
|
+
_clear_session_capture_fields(agent_state)
|
|
1977
1997
|
if command_agent.get("_session_id"):
|
|
1978
1998
|
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
1979
1999
|
_capture_agent_session(
|
|
@@ -1984,7 +2004,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1984
2004
|
timeout_s=1.5,
|
|
1985
2005
|
exclude_session_ids=known_session_ids,
|
|
1986
2006
|
)
|
|
1987
|
-
if display_backend in
|
|
2007
|
+
if display_backend in GHOSTTY_DISPLAY_BACKENDS:
|
|
1988
2008
|
display_jobs.append((agent["id"], agent))
|
|
1989
2009
|
new_agents[agent["id"]] = agent_state
|
|
1990
2010
|
restarted.append(
|
|
@@ -1995,7 +2015,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1995
2015
|
"display_target": None,
|
|
1996
2016
|
}
|
|
1997
2017
|
)
|
|
1998
|
-
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log)
|
|
2018
|
+
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log, display_backend)
|
|
1999
2019
|
for agent_id, display in display_results.items():
|
|
2000
2020
|
if agent_id in new_agents:
|
|
2001
2021
|
new_agents[agent_id]["display"] = display
|
|
@@ -2061,6 +2081,10 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2061
2081
|
display = agent_state.get("display") or {}
|
|
2062
2082
|
if display.get("status") != "opened":
|
|
2063
2083
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2084
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2085
|
+
display = agent_state.get("display") or {}
|
|
2086
|
+
if display.get("status") != "opened":
|
|
2087
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, agent_id, agent, display, event_log)
|
|
2064
2088
|
state["agents"][agent_id] = agent_state
|
|
2065
2089
|
save_runtime_state(workspace, state)
|
|
2066
2090
|
write_team_state(workspace, spec, state)
|
|
@@ -2231,13 +2255,20 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2231
2255
|
if profile_launch.get("claude_projects_root"):
|
|
2232
2256
|
agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
|
|
2233
2257
|
if start_mode == "fresh":
|
|
2234
|
-
|
|
2235
|
-
agent_state[key] = None
|
|
2258
|
+
_clear_session_capture_fields(agent_state)
|
|
2236
2259
|
if command_agent.get("_session_id"):
|
|
2237
2260
|
agent_state["_pending_session_id"] = command_agent["_session_id"]
|
|
2238
2261
|
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
|
2239
2262
|
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
2240
2263
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2264
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2265
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(
|
|
2266
|
+
session_name,
|
|
2267
|
+
agent_id,
|
|
2268
|
+
agent,
|
|
2269
|
+
previous.get("display") or {},
|
|
2270
|
+
event_log,
|
|
2271
|
+
)
|
|
2241
2272
|
state["agents"][agent_id] = agent_state
|
|
2242
2273
|
save_runtime_state(workspace, state)
|
|
2243
2274
|
store = MessageStore(workspace)
|
|
@@ -3020,7 +3051,7 @@ def preflight(team_dir: Path) -> dict[str, Any]:
|
|
|
3020
3051
|
ok = ok and bool(tmux_path)
|
|
3021
3052
|
ghostty = _ghostty_command()
|
|
3022
3053
|
ghostty_check = {"name": "ghostty", "ok": bool(ghostty), "path": ghostty, "required": False}
|
|
3023
|
-
if spec and spec.get("runtime", {}).get("display_backend") in
|
|
3054
|
+
if spec and spec.get("runtime", {}).get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
3024
3055
|
ghostty_check["required"] = True
|
|
3025
3056
|
ok = ok and bool(ghostty)
|
|
3026
3057
|
checks.append(ghostty_check)
|
|
@@ -4498,14 +4529,34 @@ def _ghostty_command() -> str | None:
|
|
|
4498
4529
|
)
|
|
4499
4530
|
|
|
4500
4531
|
|
|
4532
|
+
def _ghostty_app_exists() -> bool:
|
|
4533
|
+
return Path("/Applications/Ghostty.app").exists()
|
|
4534
|
+
|
|
4535
|
+
|
|
4536
|
+
def _ghostty_pids_by_title(title: str, wait_s: float = 0.0) -> list[int]:
|
|
4537
|
+
deadline = time.monotonic() + max(wait_s, 0.0)
|
|
4538
|
+
while True:
|
|
4539
|
+
pgrep = run_cmd(["pgrep", "-f", f"--title={title}"], timeout=5)
|
|
4540
|
+
if pgrep.returncode == 0:
|
|
4541
|
+
pids = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4542
|
+
if pids:
|
|
4543
|
+
return pids
|
|
4544
|
+
if time.monotonic() >= deadline:
|
|
4545
|
+
return []
|
|
4546
|
+
time.sleep(0.2)
|
|
4547
|
+
|
|
4548
|
+
|
|
4501
4549
|
def _open_worker_displays(
|
|
4502
4550
|
workspace: Path,
|
|
4503
4551
|
session_name: str,
|
|
4504
4552
|
jobs: list[tuple[str, dict[str, Any]]],
|
|
4505
4553
|
event_log: EventLog,
|
|
4554
|
+
display_backend: str = "ghostty_window",
|
|
4506
4555
|
) -> dict[str, dict[str, Any]]:
|
|
4507
4556
|
if not jobs:
|
|
4508
4557
|
return {}
|
|
4558
|
+
if display_backend == "ghostty_workspace":
|
|
4559
|
+
return _open_ghostty_workspace(workspace, session_name, jobs, event_log)
|
|
4509
4560
|
if len(jobs) == 1:
|
|
4510
4561
|
agent_id, agent = jobs[0]
|
|
4511
4562
|
return {agent_id: _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)}
|
|
@@ -4540,7 +4591,7 @@ def _open_ghostty_worker_window(
|
|
|
4540
4591
|
agent: dict[str, Any],
|
|
4541
4592
|
event_log: EventLog,
|
|
4542
4593
|
) -> dict[str, Any]:
|
|
4543
|
-
if not
|
|
4594
|
+
if not _ghostty_app_exists():
|
|
4544
4595
|
blocker = {
|
|
4545
4596
|
"backend": "ghostty_window",
|
|
4546
4597
|
"status": "blocked",
|
|
@@ -4582,15 +4633,129 @@ def _open_ghostty_worker_window(
|
|
|
4582
4633
|
if proc.returncode != 0:
|
|
4583
4634
|
display["reason"] = proc.stderr.strip() or proc.stdout.strip() or "open Ghostty.app failed"
|
|
4584
4635
|
else:
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
if pgrep.returncode == 0:
|
|
4588
|
-
display["pids"] = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4589
|
-
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4636
|
+
display["pids"] = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4637
|
+
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4590
4638
|
event_log.write("display.ghostty_window", agent_id=agent["id"], **display)
|
|
4591
4639
|
return display
|
|
4592
4640
|
|
|
4593
4641
|
|
|
4642
|
+
def _open_ghostty_workspace(
|
|
4643
|
+
workspace: Path,
|
|
4644
|
+
session_name: str,
|
|
4645
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4646
|
+
event_log: EventLog,
|
|
4647
|
+
) -> dict[str, dict[str, Any]]:
|
|
4648
|
+
if not _ghostty_app_exists():
|
|
4649
|
+
return _ghostty_workspace_blocked(jobs, event_log, "ghostty_app_missing")
|
|
4650
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
4651
|
+
linked_results = _prepare_ghostty_workspace_linked_sessions(session_name, jobs)
|
|
4652
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4653
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]] = []
|
|
4654
|
+
for agent_id, agent in jobs:
|
|
4655
|
+
linked = linked_results.get(agent_id, {})
|
|
4656
|
+
linked_session = linked.get("linked_session") or _ghostty_display_session_name(session_name, agent_id)
|
|
4657
|
+
if linked.get("ok"):
|
|
4658
|
+
linked_jobs.append((agent_id, agent, linked_session))
|
|
4659
|
+
continue
|
|
4660
|
+
displays.update(
|
|
4661
|
+
_ghostty_workspace_blocked(
|
|
4662
|
+
[(agent_id, agent)],
|
|
4663
|
+
event_log,
|
|
4664
|
+
linked.get("reason", "display_session_create_failed"),
|
|
4665
|
+
aggregator_session=aggregator_session,
|
|
4666
|
+
linked_sessions={agent_id: linked_session},
|
|
4667
|
+
error=linked.get("error"),
|
|
4668
|
+
target=f"{session_name}:{agent_id}",
|
|
4669
|
+
)
|
|
4670
|
+
)
|
|
4671
|
+
if not linked_jobs:
|
|
4672
|
+
return displays
|
|
4673
|
+
prepared = _prepare_ghostty_workspace_aggregator(aggregator_session, linked_jobs)
|
|
4674
|
+
if not prepared["ok"]:
|
|
4675
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4676
|
+
displays.update(
|
|
4677
|
+
_ghostty_workspace_blocked(
|
|
4678
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4679
|
+
event_log,
|
|
4680
|
+
prepared["reason"],
|
|
4681
|
+
aggregator_session=aggregator_session,
|
|
4682
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4683
|
+
error=prepared.get("error"),
|
|
4684
|
+
target=prepared.get("target"),
|
|
4685
|
+
)
|
|
4686
|
+
)
|
|
4687
|
+
return displays
|
|
4688
|
+
title = f"team-agent:{session_name}:workspace"
|
|
4689
|
+
launch_args = _ghostty_attach_args(aggregator_session, title)
|
|
4690
|
+
proc = run_cmd(launch_args, timeout=10)
|
|
4691
|
+
if proc.returncode != 0:
|
|
4692
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4693
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4694
|
+
displays.update(
|
|
4695
|
+
_ghostty_workspace_blocked(
|
|
4696
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4697
|
+
event_log,
|
|
4698
|
+
"open Ghostty.app failed",
|
|
4699
|
+
aggregator_session=aggregator_session,
|
|
4700
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4701
|
+
error=proc.stderr.strip() or proc.stdout.strip(),
|
|
4702
|
+
)
|
|
4703
|
+
)
|
|
4704
|
+
return displays
|
|
4705
|
+
pids = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4706
|
+
panes = {pane["agent_id"]: pane for pane in prepared["panes"]}
|
|
4707
|
+
for agent_id, agent, linked_session in linked_jobs:
|
|
4708
|
+
pane = panes.get(agent_id, {})
|
|
4709
|
+
display = {
|
|
4710
|
+
"backend": "ghostty_workspace",
|
|
4711
|
+
"status": "opened",
|
|
4712
|
+
"title": title,
|
|
4713
|
+
"pane_title": pane.get("title") or _ghostty_workspace_pane_title(agent),
|
|
4714
|
+
"target": f"{session_name}:{agent_id}",
|
|
4715
|
+
"linked_session": linked_session,
|
|
4716
|
+
"aggregator_session": aggregator_session,
|
|
4717
|
+
"display_session": aggregator_session,
|
|
4718
|
+
"pane_id": pane.get("pane_id"),
|
|
4719
|
+
"launch_args": launch_args,
|
|
4720
|
+
"pid": pids[0] if pids else None,
|
|
4721
|
+
"pids": pids,
|
|
4722
|
+
"tty": None,
|
|
4723
|
+
"fallback": "tmux_headless",
|
|
4724
|
+
"note": "Ghostty opens one aggregator tmux session; each pane attaches to a distinct linked session pinned to one base worker window, so runtime injection remains session:agent_id addressed.",
|
|
4725
|
+
}
|
|
4726
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
4727
|
+
displays[agent_id] = display
|
|
4728
|
+
return displays
|
|
4729
|
+
|
|
4730
|
+
|
|
4731
|
+
def _ghostty_workspace_blocked(
|
|
4732
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4733
|
+
event_log: EventLog,
|
|
4734
|
+
reason: str,
|
|
4735
|
+
aggregator_session: str | None = None,
|
|
4736
|
+
linked_sessions: dict[str, str] | None = None,
|
|
4737
|
+
error: str | None = None,
|
|
4738
|
+
target: str | None = None,
|
|
4739
|
+
) -> dict[str, dict[str, Any]]:
|
|
4740
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4741
|
+
for agent_id, _agent in jobs:
|
|
4742
|
+
linked_session = (linked_sessions or {}).get(agent_id)
|
|
4743
|
+
display = {
|
|
4744
|
+
"backend": "ghostty_workspace",
|
|
4745
|
+
"status": "blocked",
|
|
4746
|
+
"reason": reason,
|
|
4747
|
+
"error": error,
|
|
4748
|
+
"target": target or f"{agent_id}",
|
|
4749
|
+
"linked_session": linked_session,
|
|
4750
|
+
"aggregator_session": aggregator_session,
|
|
4751
|
+
"display_session": aggregator_session,
|
|
4752
|
+
"fallback": "tmux_headless",
|
|
4753
|
+
}
|
|
4754
|
+
event_log.write("display.ghostty_workspace_blocked", agent_id=agent_id, **display)
|
|
4755
|
+
displays[agent_id] = display
|
|
4756
|
+
return displays
|
|
4757
|
+
|
|
4758
|
+
|
|
4594
4759
|
def _ghostty_display_session_name(session_name: str, window_name: str) -> str:
|
|
4595
4760
|
raw = f"{session_name}:{window_name}"
|
|
4596
4761
|
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
@@ -4618,6 +4783,284 @@ def _prepare_ghostty_display_session(session_name: str, window_name: str, displa
|
|
|
4618
4783
|
return {"ok": True, "display_session": display_session}
|
|
4619
4784
|
|
|
4620
4785
|
|
|
4786
|
+
def _ghostty_workspace_aggregator_name(session_name: str) -> str:
|
|
4787
|
+
raw = f"{session_name}:workspace"
|
|
4788
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
4789
|
+
safe_session = re.sub(r"[^A-Za-z0-9_.-]", "_", session_name)[:80].strip("._-") or "team"
|
|
4790
|
+
return f"{safe_session}__display__workspace__{digest}"
|
|
4791
|
+
|
|
4792
|
+
|
|
4793
|
+
def _ghostty_workspace_pane_command(linked_session: str) -> str:
|
|
4794
|
+
return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
|
|
4795
|
+
|
|
4796
|
+
|
|
4797
|
+
def _ghostty_workspace_pane_title(agent: dict[str, Any]) -> str:
|
|
4798
|
+
return f"team-agent:{agent['id']}:{agent.get('role', '')}"
|
|
4799
|
+
|
|
4800
|
+
|
|
4801
|
+
def _prepare_ghostty_workspace_linked_sessions(
|
|
4802
|
+
session_name: str,
|
|
4803
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4804
|
+
) -> dict[str, dict[str, Any]]:
|
|
4805
|
+
def prepare(agent_id: str) -> dict[str, Any]:
|
|
4806
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
4807
|
+
result = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
4808
|
+
result["linked_session"] = linked_session
|
|
4809
|
+
return result
|
|
4810
|
+
|
|
4811
|
+
if len(jobs) == 1:
|
|
4812
|
+
agent_id, _agent = jobs[0]
|
|
4813
|
+
return {agent_id: prepare(agent_id)}
|
|
4814
|
+
results: dict[str, dict[str, Any]] = {}
|
|
4815
|
+
max_workers = min(4, len(jobs))
|
|
4816
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
4817
|
+
futures = {executor.submit(prepare, agent_id): agent_id for agent_id, _agent in jobs}
|
|
4818
|
+
for future in as_completed(futures):
|
|
4819
|
+
agent_id = futures[future]
|
|
4820
|
+
try:
|
|
4821
|
+
results[agent_id] = future.result()
|
|
4822
|
+
except Exception as exc:
|
|
4823
|
+
results[agent_id] = {
|
|
4824
|
+
"ok": False,
|
|
4825
|
+
"reason": "display_session_create_exception",
|
|
4826
|
+
"error": str(exc),
|
|
4827
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
4828
|
+
}
|
|
4829
|
+
return results
|
|
4830
|
+
|
|
4831
|
+
|
|
4832
|
+
def _prepare_ghostty_workspace_aggregator(
|
|
4833
|
+
aggregator_session: str,
|
|
4834
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]],
|
|
4835
|
+
) -> dict[str, Any]:
|
|
4836
|
+
window_name = "overview"
|
|
4837
|
+
if _tmux_session_exists(aggregator_session):
|
|
4838
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4839
|
+
if proc.returncode != 0:
|
|
4840
|
+
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
4841
|
+
|
|
4842
|
+
panes: list[dict[str, Any]] = []
|
|
4843
|
+
first_agent_id, first_agent, first_linked_session = linked_jobs[0]
|
|
4844
|
+
proc = run_cmd(
|
|
4845
|
+
[
|
|
4846
|
+
"tmux",
|
|
4847
|
+
"new-session",
|
|
4848
|
+
"-d",
|
|
4849
|
+
"-P",
|
|
4850
|
+
"-F",
|
|
4851
|
+
"#{pane_id}",
|
|
4852
|
+
"-s",
|
|
4853
|
+
aggregator_session,
|
|
4854
|
+
"-n",
|
|
4855
|
+
window_name,
|
|
4856
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
4857
|
+
],
|
|
4858
|
+
timeout=10,
|
|
4859
|
+
)
|
|
4860
|
+
if proc.returncode != 0:
|
|
4861
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
4862
|
+
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
4863
|
+
first_title = _ghostty_workspace_pane_title(first_agent)
|
|
4864
|
+
title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
4865
|
+
if not title_result["ok"]:
|
|
4866
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4867
|
+
return title_result
|
|
4868
|
+
panes.append({"agent_id": first_agent_id, "pane_id": first_pane_id, "title": first_title, "linked_session": first_linked_session})
|
|
4869
|
+
|
|
4870
|
+
proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
|
|
4871
|
+
if proc.returncode != 0:
|
|
4872
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4873
|
+
return {"ok": False, "reason": "display_session_remain_on_exit_failed", "error": proc.stderr.strip()}
|
|
4874
|
+
|
|
4875
|
+
for index, (agent_id, agent, linked_session) in enumerate(linked_jobs[1:], start=1):
|
|
4876
|
+
proc = run_cmd(
|
|
4877
|
+
[
|
|
4878
|
+
"tmux",
|
|
4879
|
+
"split-window",
|
|
4880
|
+
"-t",
|
|
4881
|
+
f"{aggregator_session}:{window_name}",
|
|
4882
|
+
"-h",
|
|
4883
|
+
"-P",
|
|
4884
|
+
"-F",
|
|
4885
|
+
"#{pane_id}",
|
|
4886
|
+
_ghostty_workspace_pane_command(linked_session),
|
|
4887
|
+
],
|
|
4888
|
+
timeout=10,
|
|
4889
|
+
)
|
|
4890
|
+
if proc.returncode != 0:
|
|
4891
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4892
|
+
return {"ok": False, "reason": "display_session_split_failed", "error": proc.stderr.strip(), "target": linked_session}
|
|
4893
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
|
|
4894
|
+
title = _ghostty_workspace_pane_title(agent)
|
|
4895
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, title)
|
|
4896
|
+
if not title_result["ok"]:
|
|
4897
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4898
|
+
return title_result
|
|
4899
|
+
panes.append({"agent_id": agent_id, "pane_id": pane_id, "title": title, "linked_session": linked_session})
|
|
4900
|
+
|
|
4901
|
+
proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
|
|
4902
|
+
if proc.returncode != 0:
|
|
4903
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4904
|
+
return {"ok": False, "reason": "display_session_layout_failed", "error": proc.stderr.strip()}
|
|
4905
|
+
return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
|
|
4906
|
+
|
|
4907
|
+
|
|
4908
|
+
def _set_ghostty_workspace_pane_title(pane_id: str, title: str) -> dict[str, Any]:
|
|
4909
|
+
proc = run_cmd(["tmux", "select-pane", "-t", pane_id, "-T", title], timeout=10)
|
|
4910
|
+
if proc.returncode != 0:
|
|
4911
|
+
return {"ok": False, "reason": "display_session_pane_title_failed", "error": proc.stderr.strip()}
|
|
4912
|
+
return {"ok": True}
|
|
4913
|
+
|
|
4914
|
+
|
|
4915
|
+
def _tmux_stdout_last_line(stdout: str) -> str | None:
|
|
4916
|
+
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
4917
|
+
return lines[-1] if lines else None
|
|
4918
|
+
|
|
4919
|
+
|
|
4920
|
+
def _open_ghostty_workspace_agent_display(
|
|
4921
|
+
session_name: str,
|
|
4922
|
+
agent_id: str,
|
|
4923
|
+
agent: dict[str, Any],
|
|
4924
|
+
previous_display: dict[str, Any],
|
|
4925
|
+
event_log: EventLog,
|
|
4926
|
+
) -> dict[str, Any]:
|
|
4927
|
+
if not _ghostty_app_exists():
|
|
4928
|
+
return _ghostty_workspace_blocked(
|
|
4929
|
+
[(agent_id, agent)],
|
|
4930
|
+
event_log,
|
|
4931
|
+
"ghostty_app_missing",
|
|
4932
|
+
aggregator_session=_ghostty_workspace_aggregator_name(session_name),
|
|
4933
|
+
linked_sessions={agent_id: _ghostty_display_session_name(session_name, agent_id)},
|
|
4934
|
+
target=f"{session_name}:{agent_id}",
|
|
4935
|
+
)[agent_id]
|
|
4936
|
+
aggregator_session = str(
|
|
4937
|
+
previous_display.get("aggregator_session")
|
|
4938
|
+
or previous_display.get("display_session")
|
|
4939
|
+
or _ghostty_workspace_aggregator_name(session_name)
|
|
4940
|
+
)
|
|
4941
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
4942
|
+
prepared = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
4943
|
+
if not prepared["ok"]:
|
|
4944
|
+
return _ghostty_workspace_blocked(
|
|
4945
|
+
[(agent_id, agent)],
|
|
4946
|
+
event_log,
|
|
4947
|
+
prepared["reason"],
|
|
4948
|
+
aggregator_session=aggregator_session,
|
|
4949
|
+
linked_sessions={agent_id: linked_session},
|
|
4950
|
+
error=prepared.get("error"),
|
|
4951
|
+
target=f"{session_name}:{agent_id}",
|
|
4952
|
+
)[agent_id]
|
|
4953
|
+
if not _tmux_session_exists(aggregator_session):
|
|
4954
|
+
return _ghostty_workspace_partial_update_display(
|
|
4955
|
+
session_name,
|
|
4956
|
+
agent_id,
|
|
4957
|
+
agent,
|
|
4958
|
+
event_log,
|
|
4959
|
+
reason="aggregator_session_missing",
|
|
4960
|
+
note="pane refresh requires full team restart",
|
|
4961
|
+
)
|
|
4962
|
+
|
|
4963
|
+
pane_title = _ghostty_workspace_pane_title(agent)
|
|
4964
|
+
command = _ghostty_workspace_pane_command(linked_session)
|
|
4965
|
+
pane_id = str(previous_display.get("pane_id") or "")
|
|
4966
|
+
refreshed = False
|
|
4967
|
+
if pane_id:
|
|
4968
|
+
proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
|
|
4969
|
+
refreshed = proc.returncode == 0
|
|
4970
|
+
if not refreshed:
|
|
4971
|
+
proc = run_cmd(
|
|
4972
|
+
[
|
|
4973
|
+
"tmux",
|
|
4974
|
+
"split-window",
|
|
4975
|
+
"-t",
|
|
4976
|
+
f"{aggregator_session}:overview",
|
|
4977
|
+
"-h",
|
|
4978
|
+
"-P",
|
|
4979
|
+
"-F",
|
|
4980
|
+
"#{pane_id}",
|
|
4981
|
+
command,
|
|
4982
|
+
],
|
|
4983
|
+
timeout=10,
|
|
4984
|
+
)
|
|
4985
|
+
if proc.returncode != 0:
|
|
4986
|
+
return _ghostty_workspace_partial_update_display(
|
|
4987
|
+
session_name,
|
|
4988
|
+
agent_id,
|
|
4989
|
+
agent,
|
|
4990
|
+
event_log,
|
|
4991
|
+
reason="aggregator_pane_refresh_failed",
|
|
4992
|
+
note=proc.stderr.strip() or "pane refresh requires full team restart",
|
|
4993
|
+
)
|
|
4994
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or pane_id
|
|
4995
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, pane_title)
|
|
4996
|
+
if not title_result["ok"]:
|
|
4997
|
+
return _ghostty_workspace_partial_update_display(
|
|
4998
|
+
session_name,
|
|
4999
|
+
agent_id,
|
|
5000
|
+
agent,
|
|
5001
|
+
event_log,
|
|
5002
|
+
reason=title_result["reason"],
|
|
5003
|
+
note=title_result.get("error") or "pane refresh requires full team restart",
|
|
5004
|
+
)
|
|
5005
|
+
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:overview", "even-horizontal"], timeout=10)
|
|
5006
|
+
title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
|
|
5007
|
+
pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
|
|
5008
|
+
display = {
|
|
5009
|
+
"backend": "ghostty_workspace",
|
|
5010
|
+
"status": "opened",
|
|
5011
|
+
"title": title,
|
|
5012
|
+
"pane_title": pane_title,
|
|
5013
|
+
"target": f"{session_name}:{agent_id}",
|
|
5014
|
+
"linked_session": linked_session,
|
|
5015
|
+
"aggregator_session": aggregator_session,
|
|
5016
|
+
"display_session": aggregator_session,
|
|
5017
|
+
"pane_id": pane_id,
|
|
5018
|
+
"pid": pids[0] if pids else None,
|
|
5019
|
+
"pids": pids,
|
|
5020
|
+
"tty": None,
|
|
5021
|
+
"fallback": "tmux_headless",
|
|
5022
|
+
"note": "Refreshed this worker's Ghostty workspace pane by respawning it against a distinct linked session.",
|
|
5023
|
+
}
|
|
5024
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
5025
|
+
return display
|
|
5026
|
+
|
|
5027
|
+
|
|
5028
|
+
def _ghostty_workspace_partial_update_display(
|
|
5029
|
+
session_name: str,
|
|
5030
|
+
agent_id: str,
|
|
5031
|
+
agent: dict[str, Any],
|
|
5032
|
+
event_log: EventLog,
|
|
5033
|
+
reason: str = "partial_update_requires_team_restart",
|
|
5034
|
+
note: str = "pane refresh requires full team restart",
|
|
5035
|
+
) -> dict[str, Any]:
|
|
5036
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
5037
|
+
display = {
|
|
5038
|
+
"backend": "ghostty_workspace",
|
|
5039
|
+
"status": "blocked",
|
|
5040
|
+
"reason": reason,
|
|
5041
|
+
"target": f"{session_name}:{agent_id}",
|
|
5042
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
5043
|
+
"aggregator_session": aggregator_session,
|
|
5044
|
+
"display_session": aggregator_session,
|
|
5045
|
+
"pane_title": _ghostty_workspace_pane_title(agent),
|
|
5046
|
+
"fallback": "tmux_headless",
|
|
5047
|
+
"note": note,
|
|
5048
|
+
"action": "restart the team to rebuild the Ghostty workspace layout",
|
|
5049
|
+
}
|
|
5050
|
+
event_log.write("display.ghostty_workspace_partial_update", agent_id=agent_id, **display)
|
|
5051
|
+
return display
|
|
5052
|
+
|
|
5053
|
+
|
|
5054
|
+
def _kill_ghostty_workspace_linked_sessions(linked_sessions: list[str]) -> list[str]:
|
|
5055
|
+
killed: list[str] = []
|
|
5056
|
+
for linked_session in dict.fromkeys(linked_sessions):
|
|
5057
|
+
if _tmux_session_exists(linked_session):
|
|
5058
|
+
proc = run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
|
|
5059
|
+
if proc.returncode == 0:
|
|
5060
|
+
killed.append(linked_session)
|
|
5061
|
+
return killed
|
|
5062
|
+
|
|
5063
|
+
|
|
4621
5064
|
def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
4622
5065
|
return [
|
|
4623
5066
|
"open",
|
|
@@ -4633,7 +5076,11 @@ def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
|
4633
5076
|
]
|
|
4634
5077
|
|
|
4635
5078
|
|
|
4636
|
-
def _close_ghostty_display(
|
|
5079
|
+
def _close_ghostty_display(
|
|
5080
|
+
agent_id: str,
|
|
5081
|
+
agent_state: dict[str, Any],
|
|
5082
|
+
event_log: EventLog,
|
|
5083
|
+
) -> None:
|
|
4637
5084
|
display = agent_state.get("display") or {}
|
|
4638
5085
|
if display.get("backend") != "ghostty_window":
|
|
4639
5086
|
return
|
|
@@ -4641,9 +5088,7 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4641
5088
|
pids = [str(pid) for pid in display.get("pids", []) if str(pid).isdigit()]
|
|
4642
5089
|
title = display.get("title")
|
|
4643
5090
|
if not pids and title:
|
|
4644
|
-
|
|
4645
|
-
if pgrep.returncode == 0:
|
|
4646
|
-
pids = [pid for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
5091
|
+
pids = [str(pid) for pid in _ghostty_pids_by_title(str(title))]
|
|
4647
5092
|
killed: list[str] = []
|
|
4648
5093
|
for pid in pids:
|
|
4649
5094
|
proc = run_cmd(["kill", pid], timeout=5)
|
|
@@ -4664,6 +5109,66 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4664
5109
|
)
|
|
4665
5110
|
|
|
4666
5111
|
|
|
5112
|
+
def _close_ghostty_workspace(state: dict[str, Any], event_log: EventLog) -> None:
|
|
5113
|
+
displays = [
|
|
5114
|
+
(agent_id, agent_state.get("display") or {})
|
|
5115
|
+
for agent_id, agent_state in state.get("agents", {}).items()
|
|
5116
|
+
if (agent_state.get("display") or {}).get("backend") == "ghostty_workspace"
|
|
5117
|
+
]
|
|
5118
|
+
if not displays:
|
|
5119
|
+
return
|
|
5120
|
+
aggregator_session = next(
|
|
5121
|
+
(
|
|
5122
|
+
str(display.get("aggregator_session") or display.get("display_session"))
|
|
5123
|
+
for _agent_id, display in displays
|
|
5124
|
+
if display.get("aggregator_session") or display.get("display_session")
|
|
5125
|
+
),
|
|
5126
|
+
None,
|
|
5127
|
+
)
|
|
5128
|
+
title = next((str(display.get("title")) for _agent_id, display in displays if display.get("title")), None)
|
|
5129
|
+
pids = {
|
|
5130
|
+
str(pid)
|
|
5131
|
+
for _agent_id, display in displays
|
|
5132
|
+
for pid in display.get("pids", [])
|
|
5133
|
+
if str(pid).isdigit()
|
|
5134
|
+
}
|
|
5135
|
+
if not pids and title:
|
|
5136
|
+
pids = {str(pid) for pid in _ghostty_pids_by_title(str(title))}
|
|
5137
|
+
|
|
5138
|
+
aggregator_closed = False
|
|
5139
|
+
if aggregator_session and _tmux_session_exists(aggregator_session):
|
|
5140
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
5141
|
+
if proc.returncode == 0:
|
|
5142
|
+
aggregator_closed = True
|
|
5143
|
+
else:
|
|
5144
|
+
event_log.write(
|
|
5145
|
+
"display.ghostty_workspace_close_failed",
|
|
5146
|
+
aggregator_session=aggregator_session,
|
|
5147
|
+
error=proc.stderr.strip(),
|
|
5148
|
+
)
|
|
5149
|
+
|
|
5150
|
+
linked_sessions = [
|
|
5151
|
+
str(display.get("linked_session"))
|
|
5152
|
+
for _agent_id, display in displays
|
|
5153
|
+
if display.get("linked_session")
|
|
5154
|
+
]
|
|
5155
|
+
linked_closed = _kill_ghostty_workspace_linked_sessions(linked_sessions)
|
|
5156
|
+
|
|
5157
|
+
killed: list[str] = []
|
|
5158
|
+
for pid in sorted(pids):
|
|
5159
|
+
proc = run_cmd(["kill", pid], timeout=5)
|
|
5160
|
+
if proc.returncode == 0:
|
|
5161
|
+
killed.append(pid)
|
|
5162
|
+
event_log.write(
|
|
5163
|
+
"display.ghostty_workspace_closed",
|
|
5164
|
+
pids=killed,
|
|
5165
|
+
title=title,
|
|
5166
|
+
aggregator_session=aggregator_session,
|
|
5167
|
+
linked_sessions=linked_closed,
|
|
5168
|
+
aggregator_closed=aggregator_closed,
|
|
5169
|
+
)
|
|
5170
|
+
|
|
5171
|
+
|
|
4667
5172
|
def get_adapter_or_raise(name: str) -> str:
|
|
4668
5173
|
if name == "tmux" and not shutil_which("tmux"):
|
|
4669
5174
|
raise RuntimeError("tmux is not installed; install tmux 3.3+ before launch")
|