ca2b47eb5f
- runbook: how executor binaries are resolved (bins or PATH) and what that assumes on oregon (no codex installed) and win10; cancellation and max_progress_rounds in the item lifecycle; secrets scrubbed from children. - design: blocked-by ordering is a TODO (not read by the scheduler); failure classification, success-when-tagged, one-time immediate rebuild, retries outside the thread-round budget, ID-based new-comment filter. - executor contract: how core reads ExecResult, kill used for cancel, Windows quoting assumptions, codex exec resume without -C/-s.
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""JSON state store under state/: one file for all projects, an exclusive tick lock, heartbeat and log.
|
|
|
|
state/agentd-state.json:
|
|
{"projects": {PROJECT: {"issues": {issue_uuid: rec}}}, "paused_groups": {group: {...}}}
|
|
rec: seq, runs, thread_id, thread_on [terminal, executor], thread_rounds, tags [...], last_message,
|
|
seen_comments [...], last_comment_at, retries, retry {at, reason}, rebuild, progress_streak, inflight {...}"""
|
|
import contextlib
|
|
import fcntl
|
|
import json
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
class Busy(Exception):
|
|
pass
|
|
|
|
|
|
class Store:
|
|
def __init__(self, state_dir, echo=True):
|
|
self.dir = Path(state_dir)
|
|
self.echo = echo
|
|
self.path = self.dir / "agentd-state.json"
|
|
|
|
def load(self):
|
|
if self.path.exists():
|
|
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
else:
|
|
data = {}
|
|
data.setdefault("projects", {})
|
|
data.setdefault("paused_groups", {})
|
|
return data
|
|
|
|
def _atomic_write(self, path, obj):
|
|
"""Write temp + fsync + rename, so a reader (or a crash) never sees half a file."""
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_suffix(".tmp")
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
fh.write(json.dumps(obj, ensure_ascii=False, indent=1))
|
|
fh.flush()
|
|
os.fsync(fh.fileno())
|
|
os.replace(tmp, path)
|
|
|
|
def save(self, data):
|
|
self._atomic_write(self.path, data)
|
|
|
|
@contextlib.contextmanager
|
|
def lock(self):
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
|
with open(self.dir / "agentd.lock", "w") as fh:
|
|
try:
|
|
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except BlockingIOError:
|
|
raise Busy("another tick holds %s" % (self.dir / "agentd.lock")) from None
|
|
yield
|
|
|
|
def heartbeat(self, info):
|
|
info = dict(info, ts=time.time(), at=time.strftime("%Y-%m-%dT%H:%M:%S"))
|
|
self._atomic_write(self.dir / "heartbeat.json", info)
|
|
|
|
def log(self, msg):
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
|
line = time.strftime("%Y-%m-%dT%H:%M:%S") + " " + msg
|
|
if self.echo:
|
|
print(line, flush=True)
|
|
with open(self.dir / "agentd.log", "a", encoding="utf-8") as fh:
|
|
fh.write(line + "\n")
|