Files
amc/amc_server/context.py
2026-02-26 16:58:02 -05:00

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()