refactor(server): extract context.py into focused modules
Split the monolithic context.py (117 lines) into five purpose-specific modules following single-responsibility principle: - config.py: Server-level constants (DATA_DIR, SESSIONS_DIR, PORT, STALE_EVENT_AGE, _state_lock) - agents.py: Agent-specific paths and caches (CLAUDE_PROJECTS_DIR, CODEX_SESSIONS_DIR, discovery caches) - auth.py: Authentication token generation/validation for spawn endpoint - spawn_config.py: Spawn feature configuration (PENDING_SPAWNS_DIR, rate limiting, projects watcher thread) - zellij.py: Zellij binary resolution and session management constants This refactoring improves: - Code navigation: Find relevant constants by domain, not alphabetically - Testing: Each module can be tested in isolation - Import clarity: Mixins import only what they need - Future maintenance: Changes to one domain don't risk breaking others All mixins updated to import from new module locations. Tests updated to use new import paths. Includes PROPOSED_CODE_FILE_REORGANIZATION_PLAN.md documenting the rationale and mapping from old to new locations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,18 +5,99 @@ import subprocess
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from amc_server.context import (
|
||||
from amc_server.agents import (
|
||||
CODEX_ACTIVE_WINDOW,
|
||||
CODEX_SESSIONS_DIR,
|
||||
SESSIONS_DIR,
|
||||
_CODEX_CACHE_MAX,
|
||||
_codex_pane_cache,
|
||||
_codex_transcript_cache,
|
||||
_dismissed_codex_ids,
|
||||
)
|
||||
from amc_server.config import SESSIONS_DIR
|
||||
from amc_server.spawn_config import PENDING_SPAWNS_DIR
|
||||
from amc_server.logging_utils import LOGGER
|
||||
|
||||
|
||||
def _parse_session_timestamp(session_ts):
|
||||
"""Parse Codex session timestamp to Unix time. Returns None on failure."""
|
||||
if not session_ts:
|
||||
return None
|
||||
try:
|
||||
# Codex uses ISO format, possibly with Z suffix or +00:00
|
||||
ts_str = session_ts.replace('Z', '+00:00')
|
||||
dt = datetime.fromisoformat(ts_str)
|
||||
return dt.timestamp()
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def _match_pending_spawn(session_cwd, session_start_ts):
|
||||
"""Match a Codex session to a pending spawn by CWD and timestamp.
|
||||
|
||||
Args:
|
||||
session_cwd: The CWD of the Codex session
|
||||
session_start_ts: The session's START timestamp (ISO string from Codex metadata)
|
||||
IMPORTANT: Must be session start time, not file mtime, to avoid false
|
||||
matches with pre-existing sessions that were recently active.
|
||||
|
||||
Returns:
|
||||
spawn_id if matched (and deletes the pending file), None otherwise
|
||||
"""
|
||||
if not PENDING_SPAWNS_DIR.exists():
|
||||
return None
|
||||
|
||||
normalized_cwd = os.path.normpath(session_cwd) if session_cwd else ""
|
||||
if not normalized_cwd:
|
||||
return None
|
||||
|
||||
# Parse session start time - if we can't parse it, we can't safely match
|
||||
session_start_unix = _parse_session_timestamp(session_start_ts)
|
||||
if session_start_unix is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
for pending_file in PENDING_SPAWNS_DIR.glob('*.json'):
|
||||
try:
|
||||
data = json.loads(pending_file.read_text())
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
# Check agent type (only match codex to codex)
|
||||
if data.get('agent_type') != 'codex':
|
||||
continue
|
||||
|
||||
# Check CWD match
|
||||
pending_path = os.path.normpath(data.get('project_path', ''))
|
||||
if normalized_cwd != pending_path:
|
||||
continue
|
||||
|
||||
# Check timing: session must have STARTED after spawn was initiated
|
||||
# Using session start time (not mtime) prevents false matches with
|
||||
# pre-existing sessions that happen to be recently active
|
||||
spawn_ts = data.get('timestamp', 0)
|
||||
if session_start_unix < spawn_ts:
|
||||
continue
|
||||
|
||||
# Match found - claim the spawn_id and delete the pending file
|
||||
spawn_id = data.get('spawn_id')
|
||||
try:
|
||||
pending_file.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
LOGGER.info(
|
||||
'Matched Codex session (cwd=%s) to pending spawn_id=%s',
|
||||
session_cwd, spawn_id,
|
||||
)
|
||||
return spawn_id
|
||||
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class SessionDiscoveryMixin:
|
||||
def _discover_active_codex_sessions(self):
|
||||
"""Find active Codex sessions and create/update session files with Zellij pane info."""
|
||||
@@ -131,6 +212,13 @@ class SessionDiscoveryMixin:
|
||||
session_ts = payload.get("timestamp", "")
|
||||
last_event_at = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
|
||||
|
||||
# Check for spawn_id: preserve existing, or match to pending spawn
|
||||
# Use session_ts (start time) not mtime to avoid false matches
|
||||
# with pre-existing sessions that were recently active
|
||||
spawn_id = existing.get("spawn_id")
|
||||
if not spawn_id:
|
||||
spawn_id = _match_pending_spawn(cwd, session_ts)
|
||||
|
||||
session_data = {
|
||||
"session_id": session_id,
|
||||
"agent": "codex",
|
||||
@@ -145,6 +233,8 @@ class SessionDiscoveryMixin:
|
||||
"zellij_pane": zellij_pane or existing.get("zellij_pane", ""),
|
||||
"transcript_path": str(jsonl_file),
|
||||
}
|
||||
if spawn_id:
|
||||
session_data["spawn_id"] = spawn_id
|
||||
if context_usage:
|
||||
session_data["context_usage"] = context_usage
|
||||
elif existing.get("context_usage"):
|
||||
|
||||
Reference in New Issue
Block a user