Skip to content
6 changes: 6 additions & 0 deletions src/cli_agent_orchestrator/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,12 +378,18 @@ def _build_pty_env() -> Dict[str, str]:
async def health_check():
import shutil

from cli_agent_orchestrator.backends.herdr_backend import HerdrBackend

def _probe(binary: str) -> str:
return "ok" if shutil.which(binary) else "unavailable"

backend = get_backend()
backend_name = "herdr" if isinstance(backend, HerdrBackend) else "tmux"

return {
"status": "ok",
"service": "cli-agent-orchestrator",
"terminal_backend": backend_name,
"components": {
"cao": "ok",
"herdr": _probe("herdr"),
Expand Down
46 changes: 1 addition & 45 deletions src/cli_agent_orchestrator/backends/herdr_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _sanitize_herdr_args(args: List[str]) -> List[str]:


# Cache TTL for pane_id resolution (seconds).
# Used by _resolve_pane_id() (inbox service path, herdr-native terminal_ids) and
# Used by get_pane_id() (fast-path, reads the cache populated at create time) and
# _resolve_workspace_id(). _resolve_pane_id_from_window() never caches pane_ids —
# herdr renumbers panes on deletion, so it resolves the pane fresh every call.
_PANE_CACHE_TTL = 5.0
Expand Down Expand Up @@ -202,42 +202,6 @@ def _parse_herdr_json(self, stdout: str) -> dict:
return cast(dict, data["result"])
return cast(dict, data)

def _resolve_pane_id(self, terminal_id: str) -> str:
"""Resolve terminal_id to current compact pane_id.

Uses a cache with 5s TTL to reduce redundant herdr pane list calls.

Args:
terminal_id: Stable terminal identifier

Returns:
Current compact pane_id

Raises:
TerminalNotFoundError: If terminal_id not found in pane list
"""
# Check cache
if terminal_id in self._pane_cache:
pane_id, cached_at = self._pane_cache[terminal_id]
if time.time() - cached_at < _PANE_CACHE_TTL:
return pane_id

# Resolve via herdr pane list
result = self._run_herdr(["pane", "list"])
try:
data = self._parse_herdr_json(result.stdout)
panes = data.get("panes", []) if isinstance(data, dict) else data
except json.JSONDecodeError as e:
raise TerminalBackendError(f"Failed to parse herdr pane list output: {e}") from e

for pane in panes:
if pane.get("terminal_id") == terminal_id:
pane_id = str(pane["pane_id"])
self._pane_cache[terminal_id] = (pane_id, time.time())
return pane_id

raise TerminalNotFoundError(terminal_id)

def _resolve_workspace_id(self, session_name: str) -> str:
"""Resolve session_name (workspace label) to workspace ID.

Expand Down Expand Up @@ -887,11 +851,3 @@ def _resolve_pane_id_from_window(self, session_name: str, window_name: str) -> s
return str(pane["pane_id"])

raise TerminalNotFoundError(f"{session_name}:{window_name}")

def invalidate_cache(self) -> None:
"""Invalidate all cached pane_id mappings.

Called after herdr reconnection when pane_ids may have compacted.
"""
self._pane_cache.clear()
self._workspace_cache.clear()
29 changes: 17 additions & 12 deletions src/cli_agent_orchestrator/cli/commands/info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Info command for CLI Agent Orchestrator CLI."""

import os
import subprocess

import click
Expand All @@ -20,18 +21,22 @@ def info():
# Display database path
click.echo(f"Database path: {DATABASE_FILE}")

# Try to get current session name from tmux
session_name = None
try:
result = subprocess.run(
["tmux", "display-message", "-p", "#S"],
capture_output=True,
text=True,
check=True,
)
session_name = result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# Try to get current session name:
# 1. Check CAO_SESSION_NAME env var (set by herdr backend)
# 2. Fall back to tmux display-message (works for tmux backend)
session_name = os.environ.get("CAO_SESSION_NAME")

if not session_name:
try:
result = subprocess.run(
["tmux", "display-message", "-p", "#S"],
capture_output=True,
text=True,
check=True,
)
session_name = result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass

if session_name and session_name.startswith(SESSION_PREFIX):
try:
Expand Down
10 changes: 9 additions & 1 deletion src/cli_agent_orchestrator/cli/commands/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
SERVER_PORT,
)
from cli_agent_orchestrator.models.terminal import TerminalStatus
from cli_agent_orchestrator.utils.terminal import poll_until_done, wait_until_terminal_status
from cli_agent_orchestrator.utils.terminal import (
poll_until_done,
sync_backend_from_server,
wait_until_terminal_status,
)

# Providers that require workspace folder access
PROVIDERS_REQUIRING_WORKSPACE_ACCESS = {
Expand Down Expand Up @@ -302,6 +306,10 @@ def launch(
# if it times out we still attach so the user can inspect the
# half-initialized session rather than orphan it in tmux.
if not headless:
# Align the CLI's backend singleton with the running server.
# Without this, ``cao-server --terminal herdr`` + no config.json
# entry causes the CLI to default to tmux. See issue #308.
sync_backend_from_server()
ready = wait_until_terminal_status(
terminal["id"],
{TerminalStatus.IDLE, TerminalStatus.COMPLETED},
Expand Down
2 changes: 2 additions & 0 deletions src/cli_agent_orchestrator/cli/commands/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from cli_agent_orchestrator.backends.registry import get_backend
from cli_agent_orchestrator.constants import API_BASE_URL, TERMINAL_LOG_DIR
from cli_agent_orchestrator.utils.terminal import sync_backend_from_server


@click.group()
Expand Down Expand Up @@ -62,6 +63,7 @@ def restore(terminal_id: str):
window_shell = f"exec {login_shell} -l"

try:
sync_backend_from_server()
get_backend().create_window(
session_name,
window_name,
Expand Down
Loading
Loading