118 lines
3.6 KiB
Python
118 lines
3.6 KiB
Python
import secrets
|
|
import shutil
|
|
from pathlib import Path
|
|
import threading
|
|
|
|
# Claude Code conversation directory
|
|
CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
|
|
|
|
# Codex conversation directory
|
|
CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions"
|
|
|
|
# Plugin path for zellij-send-keys
|
|
ZELLIJ_PLUGIN = Path.home() / ".config" / "zellij" / "plugins" / "zellij-send-keys.wasm"
|
|
|
|
|
|
def _resolve_zellij_bin():
|
|
"""Resolve zellij binary even when PATH is minimal (eg launchctl)."""
|
|
from_path = shutil.which("zellij")
|
|
if from_path:
|
|
return from_path
|
|
|
|
common_paths = (
|
|
"/opt/homebrew/bin/zellij", # Apple Silicon Homebrew
|
|
"/usr/local/bin/zellij", # Intel Homebrew
|
|
"/usr/bin/zellij",
|
|
)
|
|
for candidate in common_paths:
|
|
p = Path(candidate)
|
|
if p.exists() and p.is_file():
|
|
return str(p)
|
|
return "zellij" # Fallback for explicit error reporting by subprocess
|
|
|
|
|
|
ZELLIJ_BIN = _resolve_zellij_bin()
|
|
|
|
# Runtime data lives in XDG data dir
|
|
DATA_DIR = Path.home() / ".local" / "share" / "amc"
|
|
SESSIONS_DIR = DATA_DIR / "sessions"
|
|
EVENTS_DIR = DATA_DIR / "events"
|
|
|
|
# Source files live in project directory (relative to this module)
|
|
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
|
DASHBOARD_DIR = PROJECT_DIR / "dashboard"
|
|
|
|
PORT = 7400
|
|
STALE_EVENT_AGE = 86400 # 24 hours in seconds
|
|
STALE_STARTING_AGE = 3600 # 1 hour - sessions stuck in "starting" are orphans
|
|
CODEX_ACTIVE_WINDOW = 600 # 10 minutes - only discover recently-active Codex sessions
|
|
|
|
# Cache for Zellij session list (avoid calling zellij on every request)
|
|
_zellij_cache = {"sessions": None, "expires": 0}
|
|
|
|
# Cache for Codex pane info (avoid running pgrep/ps/lsof on every request)
|
|
_codex_pane_cache = {"pid_info": {}, "cwd_map": {}, "expires": 0}
|
|
|
|
# Cache for parsed context usage by transcript file path + mtime/size
|
|
# Limited to prevent unbounded memory growth
|
|
_context_usage_cache = {}
|
|
_CONTEXT_CACHE_MAX = 100
|
|
|
|
# Cache mapping Codex session IDs to transcript paths (or None when missing)
|
|
_codex_transcript_cache = {}
|
|
_CODEX_CACHE_MAX = 200
|
|
|
|
# Codex sessions dismissed during this server lifetime (prevents re-discovery)
|
|
# Uses dict (not set) for O(1) lookup + FIFO eviction via insertion order (Python 3.7+)
|
|
_dismissed_codex_ids = {}
|
|
_DISMISSED_MAX = 500
|
|
|
|
# Serialize state collection because it mutates session files/caches.
|
|
_state_lock = threading.Lock()
|
|
|
|
# Projects directory for spawning agents
|
|
PROJECTS_DIR = Path.home() / 'projects'
|
|
|
|
# Default Zellij session for spawning
|
|
ZELLIJ_SESSION = 'infra'
|
|
|
|
# Lock for serializing spawn operations (prevents Zellij race conditions)
|
|
_spawn_lock = threading.Lock()
|
|
|
|
# Rate limiting: track last spawn time per project (prevents spam)
|
|
_spawn_timestamps: dict[str, float] = {}
|
|
SPAWN_COOLDOWN_SEC = 10.0
|
|
|
|
# Auth token for spawn endpoint
|
|
_auth_token: str = ''
|
|
|
|
|
|
def generate_auth_token():
|
|
"""Generate a one-time auth token for this server instance."""
|
|
global _auth_token
|
|
_auth_token = secrets.token_urlsafe(32)
|
|
return _auth_token
|
|
|
|
|
|
def validate_auth_token(request_token: str) -> bool:
|
|
"""Validate the Authorization header token."""
|
|
return request_token == f'Bearer {_auth_token}'
|
|
|
|
|
|
def start_projects_watcher():
|
|
"""Start background thread to refresh projects cache every 5 minutes."""
|
|
import logging
|
|
from amc_server.mixins.spawn import load_projects_cache
|
|
|
|
def _watch_loop():
|
|
import time
|
|
while True:
|
|
try:
|
|
time.sleep(300)
|
|
load_projects_cache()
|
|
except Exception:
|
|
logging.exception('Projects cache refresh failed')
|
|
|
|
thread = threading.Thread(target=_watch_loop, daemon=True)
|
|
thread.start()
|