@team-agent/installer 0.2.3 → 0.2.5

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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/team_agent/abnormal_track.py +253 -0
  3. package/src/team_agent/cli/commands.py +17 -1
  4. package/src/team_agent/cli/parser.py +2 -2
  5. package/src/team_agent/compiler.py +1 -1
  6. package/src/team_agent/coordinator/lifecycle.py +20 -2
  7. package/src/team_agent/display/__init__.py +31 -0
  8. package/src/team_agent/display/adaptive.py +425 -0
  9. package/src/team_agent/display/backend.py +46 -0
  10. package/src/team_agent/display/close.py +6 -0
  11. package/src/team_agent/display/rebuild.py +102 -0
  12. package/src/team_agent/display/tiling.py +156 -0
  13. package/src/team_agent/display/worker_window.py +4 -0
  14. package/src/team_agent/display/workspace.py +36 -127
  15. package/src/team_agent/idle_predicate.py +200 -0
  16. package/src/team_agent/idle_takeover.py +59 -0
  17. package/src/team_agent/idle_takeover_wiring.py +111 -0
  18. package/src/team_agent/launch/core.py +13 -4
  19. package/src/team_agent/leader/__init__.py +444 -61
  20. package/src/team_agent/message_store/agent_health.py +6 -2
  21. package/src/team_agent/message_store/core.py +51 -18
  22. package/src/team_agent/message_store/leader_notification_log.py +63 -38
  23. package/src/team_agent/message_store/result_watchers.py +17 -11
  24. package/src/team_agent/message_store/schema.py +19 -2
  25. package/src/team_agent/message_store/schema_migration.py +386 -0
  26. package/src/team_agent/messaging/delivery.py +45 -2
  27. package/src/team_agent/messaging/leader_panes.py +115 -21
  28. package/src/team_agent/messaging/send.py +33 -0
  29. package/src/team_agent/messaging/tmux_io.py +49 -10
  30. package/src/team_agent/messaging/trust_auto_answer.py +11 -3
  31. package/src/team_agent/provider_state/README.md +78 -0
  32. package/src/team_agent/provider_state/__init__.py +86 -0
  33. package/src/team_agent/provider_state/claude.py +86 -0
  34. package/src/team_agent/provider_state/codex.py +84 -0
  35. package/src/team_agent/provider_state/common.py +207 -0
  36. package/src/team_agent/provider_state/registry.py +118 -0
  37. package/src/team_agent/restart/orchestration.py +9 -9
  38. package/src/team_agent/runtime.py +62 -12
  39. package/src/team_agent/spec.py +4 -3
  40. package/src/team_agent/wake.py +58 -0
@@ -0,0 +1,425 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform as platform_module
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from team_agent.display.ghostty import ghostty_display_session_name
9
+ from team_agent.display.tiling import (
10
+ display_pane_title,
11
+ prepare_tmux_attached_panes,
12
+ set_tmux_display_pane_title,
13
+ team_scoped_display_window_name,
14
+ )
15
+ from team_agent.display.workspace import (
16
+ kill_ghostty_workspace_linked_sessions,
17
+ )
18
+ from team_agent.events import EventLog
19
+
20
+
21
+ ADAPTIVE_BLOCK_REASONS = {
22
+ "leader_not_in_tmux",
23
+ "split_failed",
24
+ "window_create_failed",
25
+ "worker_session_missing",
26
+ "not_implemented_this_platform",
27
+ "aggregator_rebuild_failed",
28
+ }
29
+
30
+
31
+ def probe_display_capabilities(
32
+ env: dict[str, str] | None = None,
33
+ platform: str | None = None,
34
+ tmux: Any | None = None,
35
+ ) -> dict[str, Any]:
36
+ env_map = dict({} if env is None else env)
37
+ platform_name = _display_platform(platform, env_map)
38
+ unsupported = platform_name.startswith("win") or platform_name in {"windows", "wsl"}
39
+ tmux_info = _current_tmux_info(tmux, env_map) if tmux is not None else {}
40
+ leader_session = tmux_info.get("leader_session") or env_map.get("TEAM_AGENT_LEADER_SESSION_NAME")
41
+ leader_pane = tmux_info.get("leader_pane") or env_map.get("TMUX_PANE") or env_map.get("TEAM_AGENT_LEADER_PANE_ID")
42
+ in_tmux = bool(env_map.get("TMUX") or env_map.get("TMUX_PANE") or tmux_info.get("ok")) and not unsupported
43
+ caps = {
44
+ "tmux_append_windows": bool(in_tmux and not unsupported),
45
+ "adaptive_display": bool(in_tmux and not unsupported),
46
+ }
47
+ return {
48
+ "in_tmux": in_tmux,
49
+ "platform": platform_name,
50
+ "leader_session": leader_session,
51
+ "leader_pane": leader_pane,
52
+ "caps": caps,
53
+ "adaptive_status": "not_implemented_this_platform" if unsupported else ("available" if in_tmux else "leader_not_in_tmux"),
54
+ "reason": "not_implemented_this_platform" if unsupported else (None if in_tmux else "leader_not_in_tmux"),
55
+ }
56
+
57
+
58
+ def open_adaptive_display(
59
+ workspace: Path,
60
+ session_name: str,
61
+ jobs: list[tuple[str, dict[str, Any]]],
62
+ event_log: EventLog,
63
+ capability_probe: dict[str, Any] | None = None,
64
+ ) -> dict[str, dict[str, Any]]:
65
+ from team_agent.runtime import run_cmd
66
+ _ = workspace
67
+ probe = capability_probe or probe_display_capabilities(env=dict(os.environ), tmux=run_cmd)
68
+ if probe.get("reason") == "not_implemented_this_platform":
69
+ return adaptive_blocked(jobs, event_log, "not_implemented_this_platform", platform=probe.get("platform"))
70
+ leader_session = str(probe.get("leader_session") or _state_leader_session(workspace) or "")
71
+ if not probe.get("in_tmux") or not leader_session:
72
+ return adaptive_blocked(jobs, event_log, "leader_not_in_tmux", platform=probe.get("platform"))
73
+
74
+ linked_results = prepare_adaptive_linked_sessions(session_name, jobs)
75
+ displays: dict[str, dict[str, Any]] = {}
76
+ linked_jobs: list[tuple[str, dict[str, Any], str]] = []
77
+ for agent_id, agent in jobs:
78
+ linked = linked_results.get(agent_id, {})
79
+ linked_session = linked.get("linked_session") or ghostty_display_session_name(session_name, agent_id)
80
+ if linked.get("ok"):
81
+ linked_jobs.append((agent_id, agent, linked_session))
82
+ continue
83
+ displays.update(
84
+ adaptive_blocked(
85
+ [(agent_id, agent)],
86
+ event_log,
87
+ "worker_session_missing",
88
+ leader_session=leader_session,
89
+ linked_sessions={agent_id: linked_session},
90
+ error=linked.get("error") or linked.get("reason"),
91
+ target=f"{session_name}:{agent_id}",
92
+ )
93
+ )
94
+ if displays:
95
+ kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
96
+ return adaptive_blocked(
97
+ jobs,
98
+ event_log,
99
+ "worker_session_missing",
100
+ leader_session=leader_session,
101
+ linked_sessions={agent_id: linked.get("linked_session") for agent_id, linked in linked_results.items()},
102
+ error=next((display.get("error") for display in displays.values() if display.get("error")), None),
103
+ )
104
+ if not linked_jobs:
105
+ return displays
106
+
107
+ close_adaptive_windows(leader_session, session_name, event_log)
108
+ prepared = prepare_adaptive_windows(leader_session, session_name, linked_jobs)
109
+ if not prepared["ok"]:
110
+ close_adaptive_windows(leader_session, session_name, event_log)
111
+ kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
112
+ displays.update(
113
+ adaptive_blocked(
114
+ [(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
115
+ event_log,
116
+ prepared["reason"],
117
+ leader_session=leader_session,
118
+ linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
119
+ error=prepared.get("error"),
120
+ target=prepared.get("target"),
121
+ )
122
+ )
123
+ return displays
124
+
125
+ panes = {pane["agent_id"]: pane for pane in prepared["panes"]}
126
+ for agent_id, agent, linked_session in linked_jobs:
127
+ pane = panes.get(agent_id, {})
128
+ display = {
129
+ "backend": "adaptive",
130
+ "status": "opened",
131
+ "window": pane.get("window_name"),
132
+ "workspace_window": pane.get("window_name"),
133
+ "pane_id": pane.get("pane_id"),
134
+ "pane_title": pane.get("title") or display_pane_title(agent),
135
+ "target": f"{session_name}:{agent_id}",
136
+ "target_worker_session": f"{session_name}:{agent_id}",
137
+ "linked_session": linked_session,
138
+ "leader_session": leader_session,
139
+ "display_session": leader_session,
140
+ "fallback": "tmux_headless",
141
+ "note": "Adaptive display appends tagged tmux windows to the leader session; each pane attaches to a linked worker session.",
142
+ }
143
+ event_log.write("display.adaptive_opened", agent_id=agent_id, worker_id=agent_id, **display)
144
+ displays[agent_id] = display
145
+ return displays
146
+
147
+
148
+ def prepare_adaptive_windows(
149
+ leader_session: str,
150
+ session_name: str,
151
+ linked_jobs: list[tuple[str, dict[str, Any], str]],
152
+ ) -> dict[str, Any]:
153
+ prepared = prepare_tmux_attached_panes(
154
+ leader_session,
155
+ linked_jobs,
156
+ window_name_for_index=lambda index: team_scoped_display_window_name(session_name, index),
157
+ create_first_as_session=False,
158
+ reason_map={
159
+ "create_window": "window_create_failed",
160
+ "title": "aggregator_rebuild_failed",
161
+ "remain": "aggregator_rebuild_failed",
162
+ "split": "split_failed",
163
+ "layout": "aggregator_rebuild_failed",
164
+ },
165
+ stderr_reason_allowlist=ADAPTIVE_BLOCK_REASONS,
166
+ )
167
+ if prepared.get("ok"):
168
+ prepared["leader_session"] = leader_session
169
+ return prepared
170
+
171
+
172
+ def prepare_adaptive_linked_sessions(
173
+ session_name: str,
174
+ jobs: list[tuple[str, dict[str, Any]]],
175
+ ) -> dict[str, dict[str, Any]]:
176
+ from team_agent.runtime import _tmux_session_exists, run_cmd
177
+ results: dict[str, dict[str, Any]] = {}
178
+ for agent_id, _agent in jobs:
179
+ linked_session = ghostty_display_session_name(session_name, agent_id)
180
+ if linked_session == session_name:
181
+ results[agent_id] = {"ok": False, "reason": "worker_session_missing", "linked_session": linked_session}
182
+ continue
183
+ if _tmux_session_exists(linked_session):
184
+ cleanup = run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
185
+ if cleanup.returncode != 0:
186
+ results[agent_id] = {
187
+ "ok": False,
188
+ "reason": "worker_session_missing",
189
+ "error": cleanup.stderr.strip(),
190
+ "linked_session": linked_session,
191
+ }
192
+ continue
193
+ created = run_cmd(["tmux", "new-session", "-d", "-t", session_name, "-s", linked_session], timeout=10)
194
+ if created.returncode != 0:
195
+ results[agent_id] = {
196
+ "ok": False,
197
+ "reason": "worker_session_missing",
198
+ "error": created.stderr.strip() or created.stdout.strip(),
199
+ "linked_session": linked_session,
200
+ }
201
+ continue
202
+ selected = run_cmd(["tmux", "select-window", "-t", f"{linked_session}:{agent_id}"], timeout=10)
203
+ if selected.returncode != 0:
204
+ run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
205
+ results[agent_id] = {
206
+ "ok": False,
207
+ "reason": "worker_session_missing",
208
+ "error": selected.stderr.strip() or selected.stdout.strip(),
209
+ "linked_session": linked_session,
210
+ }
211
+ continue
212
+ results[agent_id] = {"ok": True, "linked_session": linked_session}
213
+ return results
214
+
215
+
216
+ def adaptive_blocked(
217
+ jobs: list[tuple[str, dict[str, Any]]],
218
+ event_log: EventLog,
219
+ reason: str,
220
+ leader_session: str | None = None,
221
+ linked_sessions: dict[str, str] | None = None,
222
+ error: str | None = None,
223
+ target: str | None = None,
224
+ platform: str | None = None,
225
+ ) -> dict[str, dict[str, Any]]:
226
+ reason = reason if reason in ADAPTIVE_BLOCK_REASONS else "aggregator_rebuild_failed"
227
+ displays: dict[str, dict[str, Any]] = {}
228
+ for agent_id, _agent in jobs:
229
+ display = {
230
+ "backend": "adaptive",
231
+ "status": "blocked",
232
+ "reason": reason,
233
+ "error": error,
234
+ "target": target or f"{agent_id}",
235
+ "target_worker_session": target or f"{agent_id}",
236
+ "leader_session": leader_session,
237
+ "linked_session": (linked_sessions or {}).get(agent_id),
238
+ "display_session": leader_session,
239
+ "fallback": "tmux_headless",
240
+ "hint": "Start the leader inside tmux to enable adaptive team display." if reason == "leader_not_in_tmux" else None,
241
+ "platform": platform,
242
+ }
243
+ event_log.write("display.adaptive_blocked", agent_id=agent_id, worker_id=agent_id, **display)
244
+ displays[agent_id] = display
245
+ return displays
246
+
247
+
248
+ def close_adaptive_display(state: dict[str, Any], event_log: EventLog) -> None:
249
+ displays = [
250
+ (agent_id, agent_state.get("display") or {})
251
+ for agent_id, agent_state in state.get("agents", {}).items()
252
+ if (agent_state.get("display") or {}).get("backend") == "adaptive"
253
+ ]
254
+ if not displays:
255
+ return
256
+ killed_windows: list[str] = []
257
+ linked_sessions: list[str] = []
258
+ for _agent_id, display in displays:
259
+ linked = display.get("linked_session")
260
+ if linked:
261
+ linked_sessions.append(str(linked))
262
+ seen_targets: set[str] = set()
263
+ for _agent_id, display in displays:
264
+ leader_session = str(display.get("leader_session") or "")
265
+ window_name = str(display.get("workspace_window") or display.get("window") or "")
266
+ if not leader_session or not window_name:
267
+ continue
268
+ target = f"{leader_session}:{window_name}"
269
+ if target in seen_targets:
270
+ continue
271
+ seen_targets.add(target)
272
+ if kill_adaptive_window(target):
273
+ killed_windows.append(target)
274
+ linked_closed = kill_ghostty_workspace_linked_sessions(linked_sessions)
275
+ event_log.write("display.adaptive_closed", windows=killed_windows, linked_sessions=linked_closed)
276
+
277
+
278
+ def close_adaptive_windows(leader_session: str, session_name: str, event_log: EventLog | None = None) -> list[str]:
279
+ from team_agent.runtime import run_cmd
280
+ prefix = f"team-agent:{session_name}:overview"
281
+ proc = run_cmd(["tmux", "list-windows", "-t", leader_session, "-F", "#{window_name}"], timeout=10)
282
+ if proc.returncode != 0:
283
+ return []
284
+ killed: list[str] = []
285
+ for window_name in proc.stdout.splitlines():
286
+ if window_name != prefix and not window_name.startswith(f"{prefix}-"):
287
+ continue
288
+ target = f"{leader_session}:{window_name}"
289
+ if kill_adaptive_window(target):
290
+ killed.append(target)
291
+ if event_log is not None and killed:
292
+ event_log.write("display.adaptive_stale_windows_closed", leader_session=leader_session, windows=killed)
293
+ return killed
294
+
295
+
296
+ def kill_adaptive_window(target: str) -> bool:
297
+ from team_agent.runtime import run_cmd
298
+ proc = run_cmd(["tmux", "kill-window", "-t", target], timeout=10)
299
+ return proc.returncode == 0
300
+
301
+
302
+ def set_adaptive_pane_title(pane_id: str, title: str) -> dict[str, Any]:
303
+ return set_tmux_display_pane_title(pane_id, title, "aggregator_rebuild_failed")
304
+
305
+
306
+ def _display_platform(value: str | None, env: dict[str, str]) -> str:
307
+ if value:
308
+ return value.lower()
309
+ if env.get("WSL_DISTRO_NAME") or env.get("WSL_INTEROP"):
310
+ return "wsl"
311
+ return platform_module.system().lower()
312
+
313
+
314
+ def _current_tmux_info(tmux: Any, env: dict[str, str]) -> dict[str, Any]:
315
+ pane = env.get("TMUX_PANE") or ""
316
+ commands: list[list[str]] = []
317
+ if pane:
318
+ commands.insert(0, ["tmux", "display-message", "-p", "-t", pane, "-F", "#{session_name}\t#{pane_id}"])
319
+ commands.insert(1, ["tmux", "display-message", "-p", "-t", pane, "-F", "#{session_name}"])
320
+ commands.insert(2, ["tmux", "display-message", "-p", "-t", pane, "#{session_name}"])
321
+ if env.get("TMUX"):
322
+ commands.extend(
323
+ [
324
+ ["tmux", "display-message", "-p", "-F", "#{session_name}\t#{pane_id}"],
325
+ ["tmux", "display-message", "-p", "-F", "#{session_name}"],
326
+ ["tmux", "display-message", "-p", "#{session_name}\t#{pane_id}"],
327
+ ["tmux", "display-message", "-p", "#{session_name}"],
328
+ ]
329
+ )
330
+ for command in commands:
331
+ proc = _call_tmux(tmux, command)
332
+ parsed = _parse_tmux_session_pane(proc)
333
+ if parsed:
334
+ return parsed
335
+ if pane:
336
+ listed = _leader_from_tmux_panes(tmux, pane)
337
+ if listed:
338
+ return listed
339
+ session = _first_tmux_session(tmux)
340
+ if session:
341
+ return {"ok": True, "leader_session": session, "leader_pane": pane}
342
+ return {"ok": False}
343
+
344
+
345
+ def _call_tmux(tmux: Any, args: list[str]) -> Any | None:
346
+ try:
347
+ if callable(tmux):
348
+ try:
349
+ return tmux(args, timeout=5)
350
+ except TypeError:
351
+ return tmux(args)
352
+ if hasattr(tmux, "run_cmd"):
353
+ return tmux.run_cmd(args)
354
+ except Exception:
355
+ return None
356
+ return None
357
+
358
+
359
+ def _parse_tmux_session_pane(proc: Any | None) -> dict[str, Any] | None:
360
+ if not proc or getattr(proc, "returncode", 1) != 0:
361
+ return None
362
+ parts = str(getattr(proc, "stdout", "")).strip().split("\t")
363
+ if len(parts) >= 2 and parts[0].startswith("%") and parts[1]:
364
+ return {"ok": True, "leader_session": parts[1], "leader_pane": parts[0]}
365
+ if len(parts) >= 2 and parts[0]:
366
+ return {"ok": True, "leader_session": parts[0], "leader_pane": parts[1]}
367
+ if len(parts) == 1 and parts[0] and not parts[0].startswith("%"):
368
+ return {"ok": True, "leader_session": parts[0], "leader_pane": None}
369
+ return None
370
+
371
+
372
+ def _leader_from_tmux_panes(tmux: Any, pane: str) -> dict[str, Any] | None:
373
+ proc = _call_tmux(
374
+ tmux,
375
+ [
376
+ "tmux",
377
+ "list-panes",
378
+ "-a",
379
+ "-F",
380
+ "#{pane_id}\t#{session_name}\t#{pane_current_command}\t#{pane_active}",
381
+ ],
382
+ )
383
+ if not proc or getattr(proc, "returncode", 1) != 0:
384
+ return None
385
+ rows = [line.split("\t") for line in str(getattr(proc, "stdout", "")).splitlines() if line.strip()]
386
+ if pane:
387
+ for row in rows:
388
+ if len(row) >= 2 and row[0] == pane:
389
+ return {"ok": True, "leader_session": row[1], "leader_pane": row[0]}
390
+ for row in rows:
391
+ if len(row) >= 3 and _leader_shaped_command(row[2]):
392
+ return {"ok": True, "leader_session": row[1], "leader_pane": row[0]}
393
+ if rows and len(rows[0]) >= 2:
394
+ return {"ok": True, "leader_session": rows[0][1], "leader_pane": rows[0][0]}
395
+ return None
396
+
397
+
398
+ def _leader_shaped_command(command: str) -> bool:
399
+ lowered = command.lower()
400
+ return any(token in lowered for token in ("claude", "codex", "fake"))
401
+
402
+
403
+ def _first_tmux_session(tmux: Any) -> str | None:
404
+ for command in (
405
+ ["tmux", "list-clients", "-F", "#{client_session}"],
406
+ ["tmux", "list-sessions", "-F", "#{session_name}"],
407
+ ):
408
+ proc = _call_tmux(tmux, command)
409
+ if not proc or getattr(proc, "returncode", 1) != 0:
410
+ continue
411
+ for line in str(getattr(proc, "stdout", "")).splitlines():
412
+ if line.strip():
413
+ return line.strip()
414
+ return None
415
+
416
+
417
+ def _state_leader_session(workspace: Path) -> str | None:
418
+ try:
419
+ from team_agent.state import load_runtime_state
420
+ state = load_runtime_state(workspace)
421
+ except Exception:
422
+ return None
423
+ receiver = state.get("leader_receiver") if isinstance(state.get("leader_receiver"), dict) else {}
424
+ session_name = receiver.get("session_name")
425
+ return str(session_name) if session_name else None
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ ADAPTIVE_DISPLAY_BACKEND = "adaptive"
7
+ GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
8
+ DISPLAY_BACKENDS_WITH_WORKER_VIEWS = GHOSTTY_DISPLAY_BACKENDS | {ADAPTIVE_DISPLAY_BACKEND}
9
+ VALID_DISPLAY_BACKENDS = {"none", "tmux_attach", "iterm"} | DISPLAY_BACKENDS_WITH_WORKER_VIEWS
10
+
11
+
12
+ def resolve_display_backend(
13
+ requested: str | None,
14
+ *,
15
+ recorded: str | None = None,
16
+ event_log: Any | None = None,
17
+ source: str,
18
+ ) -> str:
19
+ resolved = requested or recorded or ADAPTIVE_DISPLAY_BACKEND
20
+ reason = "explicit" if requested else ("recorded" if recorded else "default")
21
+ if event_log is not None and reason == "default":
22
+ event_log.write(
23
+ "display.backend_resolved",
24
+ requested=None,
25
+ resolved=resolved,
26
+ reason=reason,
27
+ source=source,
28
+ )
29
+ return resolved
30
+
31
+
32
+ def resolve_restart_display_backend(spec: dict[str, Any], state: dict[str, Any], event_log: Any) -> str:
33
+ return resolve_display_backend(
34
+ spec.get("runtime", {}).get("display_backend"),
35
+ recorded=state.get("display_backend"),
36
+ event_log=event_log,
37
+ source="restart",
38
+ )
39
+
40
+
41
+ def display_backend_has_worker_views(display_backend: str) -> bool:
42
+ return display_backend in DISPLAY_BACKENDS_WITH_WORKER_VIEWS
43
+
44
+
45
+ def display_backend_opens_before_leader_rebind(display_backend: str) -> bool:
46
+ return display_backend_has_worker_views(display_backend) and display_backend != ADAPTIVE_DISPLAY_BACKEND
@@ -3,10 +3,16 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from team_agent.events import EventLog
6
+ from team_agent.display.adaptive import close_adaptive_display
6
7
  from team_agent.display.ghostty import ghostty_pids_by_title
7
8
  from team_agent.display.workspace import kill_ghostty_workspace_linked_sessions
8
9
 
9
10
 
11
+ def close_team_display_backends(state: dict[str, Any], event_log: EventLog) -> None:
12
+ close_adaptive_display(state, event_log)
13
+ close_ghostty_workspace(state, event_log)
14
+
15
+
10
16
  def close_ghostty_display(
11
17
  agent_id: str,
12
18
  agent_state: dict[str, Any],
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.events import EventLog
7
+
8
+
9
+ def rebuild_restart_display_after_rebind(
10
+ display_backend: str,
11
+ workspace: Path,
12
+ session_name: str,
13
+ spec: dict[str, Any],
14
+ event_log: EventLog,
15
+ restarted: list[dict[str, Any]],
16
+ receiver: dict[str, Any] | None = None,
17
+ ) -> dict[str, Any]:
18
+ if display_backend != "adaptive":
19
+ return {}
20
+ from team_agent.restart.snapshot import save_team_runtime_snapshot
21
+ from team_agent.state import load_runtime_state, save_runtime_state, write_team_state
22
+ state = load_runtime_state(workspace)
23
+ state, display_results = rebuild_adaptive_display_after_rebind(
24
+ workspace,
25
+ session_name,
26
+ spec,
27
+ state,
28
+ event_log,
29
+ save_runtime_state,
30
+ save_team_runtime_snapshot,
31
+ write_team_state,
32
+ receiver=receiver,
33
+ )
34
+ for item in restarted:
35
+ display = display_results.get(item["agent_id"])
36
+ if display:
37
+ item["display_target"] = display
38
+ return state
39
+
40
+
41
+ def rebuild_adaptive_display_after_rebind(
42
+ workspace: Path,
43
+ session_name: str,
44
+ spec: dict[str, Any],
45
+ state: dict[str, Any],
46
+ event_log: EventLog,
47
+ save_state: Any,
48
+ save_snapshot: Any,
49
+ write_team_state: Any,
50
+ receiver: dict[str, Any] | None = None,
51
+ ) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
52
+ if receiver:
53
+ state["leader_receiver"] = receiver
54
+ receiver = receiver or (state.get("leader_receiver") if isinstance(state.get("leader_receiver"), dict) else {})
55
+ rebind_session = latest_rebind_session(event_log)
56
+ if rebind_session:
57
+ receiver = {**receiver, "session_name": rebind_session}
58
+ jobs = [
59
+ (agent["id"], agent)
60
+ for agent in spec.get("agents", [])
61
+ if agent["id"] in state.get("agents", {}) and state["agents"][agent["id"]].get("status") == "running"
62
+ ]
63
+ from team_agent.runtime import _open_worker_displays
64
+ display_results = _open_worker_displays(
65
+ workspace,
66
+ session_name,
67
+ jobs,
68
+ event_log,
69
+ "adaptive",
70
+ capability_probe={
71
+ "in_tmux": bool(receiver.get("session_name")),
72
+ "leader_session": receiver.get("session_name"),
73
+ "leader_pane": receiver.get("pane_id"),
74
+ "platform": None,
75
+ "caps": {"adaptive_display": bool(receiver.get("session_name"))},
76
+ "reason": None if receiver.get("session_name") else "leader_not_in_tmux",
77
+ },
78
+ )
79
+ for agent_id, display in display_results.items():
80
+ if agent_id in state.get("agents", {}):
81
+ state["agents"][agent_id]["display"] = display
82
+ event_log.write(
83
+ "display.adaptive_rebuilt",
84
+ session=session_name,
85
+ workers=sorted(display_results),
86
+ leader_session=next((display.get("leader_session") for display in display_results.values()), None),
87
+ stale_windows_recreated=True,
88
+ )
89
+ save_state(workspace, state)
90
+ save_snapshot(workspace, state)
91
+ write_team_state(workspace, spec, state)
92
+ return state, display_results
93
+
94
+
95
+ def latest_rebind_session(event_log: EventLog) -> str | None:
96
+ for event in reversed(event_log.tail(50)):
97
+ if event.get("event") != "leader_receiver.rebind_applied":
98
+ continue
99
+ session = event.get("new_session_name") or event.get("session_name")
100
+ if session:
101
+ return str(session)
102
+ return None