Add conversation_mtime_ns field to session state that tracks the actual
modification time of conversation files. This enables more responsive
dashboard updates by detecting changes that occur between hook events
(e.g., during streaming tool execution).
Changes:
- state.py: Add _get_conversation_mtime() to stat conversation files
and include mtime_ns in session payloads when available
- conversation.py: Add stable message IDs (claude-{session}-{n} format)
for React key stability and message deduplication
- control.py: Fix FIFO eviction for dismissed_codex_ids - set.pop()
removes arbitrary element, now uses dict with insertion-order iteration
- context.py: Update dismissed_codex_ids type from set to dict
The mtime approach complements existing last_event_at tracking:
- last_event_at: Changes on hook events (session boundaries)
- conversation_mtime_ns: Changes on every file write (real-time)
Dashboard can now detect mid-session conversation updates without
waiting for the next hook event.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
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()
|