hooks: any agent's hook restarts the project's web server when it is down ([server] port and start; at most once a minute, detached), so the board and chat no longer depend on the coordinator's session

This commit is contained in:
KeelrLeek committed 2026-10-07 08:43:49 -05:00
1 parent 940cacc2d2
commit 9b6ea3bc57
2 files changed
+40 -1

No files matched your search

+37
View File
@@ -200,6 +200,42 @@ def _context(host, event, text):
print(text)
def ensure_server(project, every=60):
"""Restart the project's web server when it is down: [server] port and start (a command). Any agent's hook
does it, at most once a minute, so the board and chat come back without the coordinator's session. Detached,
so the server outlives the hook (and the app) that started it."""
port, start = project.server.get("port"), project.server.get("start")
if not port or not start:
return
stamp = project.db.parent / "server-check.stamp"
try:
import time
if stamp.exists() and time.time() - stamp.stat().st_mtime < every:
return
stamp.touch()
import socket
with socket.socket() as connection:
connection.settimeout(0.3)
if connection.connect_ex(("127.0.0.1", int(port))) == 0:
return
import subprocess
flags = 0
if os.name == "nt":
flags = (subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.CREATE_BREAKAWAY_FROM_JOB | subprocess.CREATE_NO_WINDOW)
command = start if isinstance(start, list) else [start]
try:
subprocess.Popen(command, creationflags=flags, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, close_fds=True)
except OSError: # the job may refuse breakaway: start it without
subprocess.Popen(command, creationflags=flags & ~getattr(subprocess, "CREATE_BREAKAWAY_FROM_JOB", 0),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
close_fds=True)
print(f"harbor: the web server on port {port} was down; restarting it", file=sys.stderr)
except Exception as error: # never break the host because of this
print(f"harbor: server check failed: {error}", file=sys.stderr)
def run(event, project_spec, agent, host="generic"):
payload = _payload()
try:
@@ -216,6 +252,7 @@ def run(event, project_spec, agent, host="generic"):
agent = project.session_identity(agent or os.environ.get("HARBOR_AGENT"), cwd) # the session's own folder
if not agent or agent not in project.ai_names:
return 0
ensure_server(project)
harbor = Harbor(project, busy_ms=1500, write_attempts=2)
try:
return _run(event, harbor, agent, host, payload)
+3 -1
View File
@@ -96,6 +96,7 @@ class Project:
shared_files: tuple = () # glob patterns (repo-relative) every agent edits and the integrator merges: not collisions
logbook: dict = field(default_factory=dict) # [logbook]: dir, auto, decomp, stats_repo (harbor.logbook_auto)
reverse: dict = field(default_factory=dict) # [reverse]: image, base, facts, imports, annotations, src (harbor.reverse)
server: dict = field(default_factory=dict) # [server]: port, start (a command that starts the web server)
@property
def names(self):
@@ -223,7 +224,8 @@ def project_from_dict(data, source=None, home=None):
autopilot=bool(data.get("autopilot", False)),
shared_files=tuple(str(p).replace("\\", "/") for p in data.get("shared_files", [])),
logbook=dict(data.get("logbook", {})),
reverse=dict(data.get("reverse", {})))
reverse=dict(data.get("reverse", {})),
server=dict(data.get("server", {})))
def load_file(path, home=None):