teleo-codex/ops/pipeline-v2/lib/worktree_lock.py
m3taversal 05d74d5e32 sync: import all VPS pipeline + diagnostics code as baseline
Imports 67 files from VPS (/opt/teleo-eval/) into repo as the single source
of truth. Previously only 8 of 67 files existed in repo — the rest were
deployed directly to VPS via SCP, causing massive drift.

Includes:
- pipeline/lib/: 33 Python modules (daemon core, extraction, evaluation, merge, cascade, cross-domain, costs, attribution, etc.)
- pipeline/: main daemon (teleo-pipeline.py), reweave.py, batch-extract-50.sh
- diagnostics/: 19 files (4-page dashboard, alerting, daily digest, review queue, tier1 metrics)
- agent-state/: bootstrap, lib-state, cascade inbox processor, schema
- systemd/: service unit files for reference
- deploy.sh: rsync-based deploy with --dry-run, syntax checks, dirty-tree gate
- research-session.sh: updated with Step 8.5 digest + cascade inbox processing

No new code written — all files are exact copies from VPS as of 2026-04-06.
From this point forward: edit in repo, commit, then deploy.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:00:00 +01:00

85 lines
2.6 KiB
Python

"""File-based lock for ALL processes writing to the main worktree.
One lock, one mechanism (Ganymede: Option C). Used by:
- Pipeline daemon stages (entity_batch, source archiver, substantive_fixer) via async wrapper
- Telegram bot (sync context manager)
Protects: /opt/teleo-eval/workspaces/main/
flock auto-releases on process exit (even crash/kill). No stale lock cleanup needed.
"""
import asyncio
import fcntl
import logging
import time
from contextlib import asynccontextmanager, contextmanager
from pathlib import Path
logger = logging.getLogger("worktree-lock")
LOCKFILE = Path("/opt/teleo-eval/workspaces/.main-worktree.lock")
@contextmanager
def main_worktree_lock(timeout: float = 10.0):
"""Sync context manager — use in telegram bot and other external processes.
Usage:
with main_worktree_lock():
# write to inbox/queue/, git add/commit/push, etc.
"""
LOCKFILE.parent.mkdir(parents=True, exist_ok=True)
fp = open(LOCKFILE, "w")
start = time.monotonic()
while True:
try:
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
if time.monotonic() - start > timeout:
fp.close()
logger.warning("Main worktree lock timeout after %.0fs", timeout)
raise TimeoutError(f"Could not acquire main worktree lock in {timeout}s")
time.sleep(0.1)
try:
yield
finally:
fcntl.flock(fp, fcntl.LOCK_UN)
fp.close()
@asynccontextmanager
async def async_main_worktree_lock(timeout: float = 10.0):
"""Async context manager — use in pipeline daemon stages.
Acquires the same file lock via run_in_executor (Ganymede: <1ms overhead).
Usage:
async with async_main_worktree_lock():
await _git("fetch", "origin", "main", cwd=main_dir)
await _git("reset", "--hard", "origin/main", cwd=main_dir)
# ... write files, commit, push ...
"""
loop = asyncio.get_event_loop()
LOCKFILE.parent.mkdir(parents=True, exist_ok=True)
fp = open(LOCKFILE, "w")
def _acquire():
start = time.monotonic()
while True:
try:
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
return
except BlockingIOError:
if time.monotonic() - start > timeout:
fp.close()
raise TimeoutError(f"Could not acquire main worktree lock in {timeout}s")
time.sleep(0.1)
await loop.run_in_executor(None, _acquire)
try:
yield
finally:
fcntl.flock(fp, fcntl.LOCK_UN)
fp.close()