Scheduling core, state store, CLI and unit tests
One tick polls in-flight runs, posts results, classifies the first line and moves state/assignee; failures get a structured note with transient retry and backoff, timeouts are killed and handed over, resume failures and long threads rebuild with a handoff summary. Then the lowest Todo items are dispatched within terminal concurrency and serialize groups, and a heartbeat is written. CLI: tick, new, show, comment, state, assign, smoke, check-config.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""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, 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 save(self, data):
|
||||
self.dir.mkdir(parents=True, exist_ok=True)
|
||||
tmp = self.path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
os.replace(tmp, self.path)
|
||||
|
||||
@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):
|
||||
self.dir.mkdir(parents=True, exist_ok=True)
|
||||
info = dict(info, ts=time.time(), at=time.strftime("%Y-%m-%dT%H:%M:%S"))
|
||||
(self.dir / "heartbeat.json").write_text(json.dumps(info, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
|
||||
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")
|
||||
Reference in New Issue
Block a user