feat: deliver installable local alpha sessions
Some checks failed
tamq-ci / test (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-24 16:10:59 +02:00
parent d56bd3c79f
commit 5774fbd548
18 changed files with 890 additions and 94 deletions

View file

@ -2,9 +2,12 @@ from __future__ import annotations
import os
import shlex
import shutil
from uuid import uuid4
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from .registry import repository_paths, validate_targets
@ -19,6 +22,8 @@ class Endpoint:
pid: int
repos: list[str]
instance_id: str = ""
created_session: bool = False
created_windows: tuple[str, ...] = ()
@property
def endpoint_id(self) -> str:
@ -29,48 +34,154 @@ class Endpoint:
return self.instance_id or self.endpoint_id
@dataclass(frozen=True)
class LaunchPlan:
repos: tuple[str, ...]
paths: dict[str, str]
command: tuple[str, ...]
class TmuxManager:
def __init__(self, session: str = "tamq"):
def __init__(
self,
session: str = "tamq",
*,
tmux_command: Sequence[str] | None = None,
tamq_command: Sequence[str] | None = None,
):
self.session = session
socket_name = os.environ.get("TAMQ_TMUX_SOCKET")
self.tmux_command = tuple(tmux_command or (["tmux", "-L", socket_name] if socket_name else ["tmux"]))
self.tamq_command = tuple(tamq_command or ["tamq"])
def _run(self, *args: str, check: bool = True) -> str:
result = subprocess.run(["tmux", *args], text=True, capture_output=True, check=False)
result = subprocess.run([*self.tmux_command, *args], text=True, capture_output=True, check=False)
if check and result.returncode:
raise TmuxError(result.stderr.strip() or "tmux command failed")
return result.stdout.strip()
def ensure(self, repos: list[str], command: str = "codex") -> Endpoint:
if not repos:
raise TmuxError("at least one repository is required")
try:
validate_targets(repos)
except Exception as exc:
raise TmuxError(str(exc)) from exc
result = subprocess.run(["tmux", "has-session", "-t", self.session], capture_output=True)
def _existing_session_identity(self) -> tuple[int, str] | None:
result = subprocess.run(
[*self.tmux_command, "has-session", "-t", self.session],
capture_output=True,
)
if result.returncode != 0:
self._run("new-session", "-d", "-s", self.session, "-n", "__tamq_boot", "-c", os.getcwd(), "sleep", "infinity")
return None
pid_text = self._run("display-message", "-p", "-t", self.session, "#{pid}")
try:
pid = int(pid_text)
except ValueError as exc:
raise TmuxError(f"unable to determine tmux PID: {pid_text!r}") from exc
endpoint = Endpoint(self.session, pid, list(dict.fromkeys(repos)), f"tmux-amq-{pid}-{uuid4().hex[:12]}")
instance_id = self._run(
"show-options", "-v", "-t", self.session, "@tamq_instance_id", check=False,
)
managed = self._run(
"show-options", "-v", "-t", self.session, "@tamq_managed", check=False,
)
if managed != "1" and not instance_id.startswith(f"tmux-amq-{pid}-"):
raise TmuxError(
f"tmux session already exists and is not managed by tamq: {self.session}"
)
return pid, instance_id
def preflight(self, repos: list[str], command: str = "codex") -> LaunchPlan:
if not repos:
raise TmuxError("at least one repository is required")
if not self.tmux_command or shutil.which(self.tmux_command[0]) is None:
raise TmuxError("tmux is required; install it and ensure 'tmux' is on PATH")
if shutil.which("gita") is None:
raise TmuxError("gita is required; install it and ensure 'gita' is on PATH")
try:
command_parts = tuple(shlex.split(command))
except ValueError as exc:
raise TmuxError(f"invalid agent command: {exc}") from exc
if not command_parts:
raise TmuxError("agent command must not be empty")
if shutil.which(command_parts[0]) is None:
raise TmuxError(f"agent command not found on PATH: {command_parts[0]}")
unique_repos = tuple(dict.fromkeys(repos))
try:
validate_targets(list(unique_repos))
except Exception as exc:
raise TmuxError(str(exc)) from exc
paths = repository_paths()
windows = self._run("list-windows", "-t", self.session, "-F", "#{window_name}").splitlines()
for index, repo in enumerate(endpoint.repos):
if repo not in windows:
selected_paths: dict[str, str] = {}
for repo in unique_repos:
try:
repo_path = paths[repo]
except KeyError as exc:
raise TmuxError(f"gita path missing for registered repository: {repo}") from exc
if not Path(repo_path).is_dir():
raise TmuxError(f"gita path is not a directory for registered repository {repo}: {repo_path}")
selected_paths[repo] = repo_path
self._existing_session_identity()
return LaunchPlan(unique_repos, selected_paths, command_parts)
def ensure(self, repos: list[str], command: str = "codex") -> Endpoint:
return self.ensure_plan(self.preflight(repos, command))
def ensure_plan(self, plan: LaunchPlan) -> Endpoint:
created_session = False
created_windows: list[str] = []
try:
identity = self._existing_session_identity()
if identity is None:
first_repo = plan.repos[0]
self._run(
"new-session", "-d", "-s", self.session, "-n", "__tamq_boot",
"-c", plan.paths[first_repo],
)
created_session = True
pid_text = self._run("display-message", "-p", "-t", self.session, "#{pid}")
try:
pid = int(pid_text)
except ValueError as exc:
raise TmuxError(f"unable to determine tmux PID: {pid_text!r}") from exc
instance_id = ""
else:
pid, instance_id = identity
endpoint_id = f"tmux-amq-{pid}"
if not instance_id.startswith(f"{endpoint_id}-"):
instance_id = f"{endpoint_id}-{uuid4().hex[:12]}"
self._run("set-option", "-t", self.session, "@tamq_instance_id", instance_id)
self._run("set-option", "-t", self.session, "@tamq_managed", "1")
windows = self._run("list-windows", "-t", self.session, "-F", "#{window_name}").splitlines()
for index, repo in enumerate(plan.repos):
if repo in windows:
continue
if index == 0 and "__tamq_boot" in windows:
if not created_session:
self._run("respawn-pane", "-k", "-t", f"{self.session}:__tamq_boot", "-c", plan.paths[repo])
self._run("rename-window", "-t", f"{self.session}:__tamq_boot", repo)
windows[windows.index("__tamq_boot")] = repo
else:
try:
repo_path = paths[repo]
except KeyError as exc:
raise TmuxError(f"gita path missing for registered repository: {repo}") from exc
self._run("new-window", "-d", "-t", self.session, "-n", repo, "-c", repo_path, "sleep", "infinity")
tap = ["tamq", "tap", "--repo", repo, "--endpoint", endpoint.endpoint_id, "--", command]
self._run("new-window", "-d", "-t", self.session, "-n", repo, "-c", plan.paths[repo])
windows.append(repo)
created_windows.append(repo)
tap = [
*self.tamq_command,
"tap", "--repo", repo, "--endpoint", endpoint_id, "--", *plan.command,
]
self._run("send-keys", "-t", f"{self.session}:{repo}", shell_join(tap), "C-m")
return endpoint
self._run("select-window", "-t", f"{self.session}:{plan.repos[0]}")
return Endpoint(
self.session,
pid,
list(plan.repos),
instance_id,
created_session,
tuple(created_windows),
)
except Exception:
self.rollback(Endpoint(self.session, 0, list(plan.repos), created_session=created_session, created_windows=tuple(created_windows)))
raise
def rollback(self, endpoint: Endpoint) -> None:
if endpoint.created_session:
self._run("kill-session", "-t", endpoint.session, check=False)
return
for window in reversed(endpoint.created_windows):
self._run("kill-window", "-t", f"{endpoint.session}:{window}", check=False)
def shell_join(parts: list[str]) -> str: