From 71755978b88dbe7f7fd87671b4dc83d8be996013 Mon Sep 17 00:00:00 2001 From: ChakraFusion <112588066+ChakraFusion@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:47:03 +0200 Subject: [PATCH] Start llama-server outside the starter's job; README: start Hawk on its own 2026-10-05 20:57: Hawk had been started from a terminal inside another app; that app's update ended its Windows job and with it Hawk and the llama-server Hawk had started, so the night's run stopped with nothing left to restart llama. spawn_independent() starts llama-server with CREATE_BREAKAWAY_FROM_JOB (normal start when the job forbids it; a new session on Linux). Verified from inside a job: the child is outside it. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++++ opencode_hawk/coordinator.py | 16 ++++++++++++++++ opencode_hawk/llama_restart.py | 3 +-- opencode_hawk/probes.py | 6 +----- tests/selftest_coordinator.py | 24 ++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 762f9d9..e5a79b6 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,12 @@ nothing. Set Linux support is new. Bug reports from real Linux setups are welcome. +**Windows: start Hawk on its own.** Start it from Explorer, a terminal of its own, or +Task Scheduler. Started from inside another app (an IDE or AI-assistant terminal), +Hawk belongs to that app's process group (job), and an update or restart of that +app ends Hawk with it. llama-server, which Hawk starts, always leaves that group, +so it keeps running either way. + ## Agent markers Hawk's injected prompts tell the agent which markers to use, so normally no diff --git a/opencode_hawk/coordinator.py b/opencode_hawk/coordinator.py index cba2c7c..2093384 100644 --- a/opencode_hawk/coordinator.py +++ b/opencode_hawk/coordinator.py @@ -771,6 +771,22 @@ def creation_flags() -> int: return 0 +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 + + +def spawn_independent(argv, **kw): + """Start a long-lived process (llama-server) that must outlive whoever started Hawk. On Windows a process + inherits its parent's job object: 2026-10-05 Hawk had been started from inside another app, that app's update + ended its job, and Hawk and llama-server died with it. So leave the job when the job allows it, else start + normally.""" + if os.name != "nt": + return subprocess.Popen(argv, start_new_session=True, **kw) + try: + return subprocess.Popen(argv, creationflags=creation_flags() | CREATE_BREAKAWAY_FROM_JOB, **kw) + except OSError: + return subprocess.Popen(argv, creationflags=creation_flags(), **kw) + + def git(project_dir, *args): res = subprocess.run( ["git", "-C", project_dir, *args], diff --git a/opencode_hawk/llama_restart.py b/opencode_hawk/llama_restart.py index 6051003..a02ffba 100644 --- a/opencode_hawk/llama_restart.py +++ b/opencode_hawk/llama_restart.py @@ -398,12 +398,11 @@ def _respawn(spec) -> int | None: except Exception: fh = None try: - proc = subprocess.Popen( + proc = coordinator.spawn_independent( spec["argv"], cwd=spec.get("cwd") or None, env=spec.get("env") or None, stdout=fh, stderr=subprocess.STDOUT, - creationflags=coordinator.creation_flags(), ) return proc.pid except Exception as e: diff --git a/opencode_hawk/probes.py b/opencode_hawk/probes.py index 385e44d..fea8a43 100644 --- a/opencode_hawk/probes.py +++ b/opencode_hawk/probes.py @@ -261,11 +261,7 @@ def llama_respawn(cfg, timeout_s=None) -> bool: argv = ["cmd", "/c", bat] else: argv = [bat] if os.access(bat, os.X_OK) else ["sh", bat] - proc = subprocess.Popen( - argv, - stdout=fh, stderr=subprocess.STDOUT, - creationflags=core.creation_flags(), - ) + proc = core.spawn_independent(argv, stdout=fh, stderr=subprocess.STDOUT) except Exception as e: core.log("LLAMA-DOWN: respawn launch failed: %s" % e) return False diff --git a/tests/selftest_coordinator.py b/tests/selftest_coordinator.py index d4a286c..d260b3b 100644 --- a/tests/selftest_coordinator.py +++ b/tests/selftest_coordinator.py @@ -614,6 +614,30 @@ def mkllama_idle_test(cpu_frac, slots_processing, gate_slots, "OK" if ok_m else "MISMATCH", "escalation note written to session DB as synthetic user msg + text part")) + # N00: llama-server is started outside the starter's job (Windows), falling back when breakaway is refused + import subprocess as _sp + _calls = [] + class _FakePopen: + def __init__(self, argv, **kw): + _calls.append(kw) + if os.name == "nt" and kw.get("creationflags", 0) & 0x01000000 and len(_calls) == 1: + raise PermissionError("breakaway not allowed") + _orig_popen = _sp.Popen + _sp.Popen = _FakePopen + try: + hawk.spawn_independent(["llama-server"]) + finally: + _sp.Popen = _orig_popen + if os.name == "nt": + ok_brk = (len(_calls) == 2 and _calls[0]["creationflags"] & 0x01000000 + and not _calls[1]["creationflags"] & 0x01000000) + else: + ok_brk = len(_calls) == 1 and _calls[0].get("start_new_session") is True + results.append(("spawn_independent", "pass", "pass" if ok_brk else "fail", ok_brk, + "llama-server leaves the starter's job; falls back when not allowed", [])) + print("%-22s expect=%-9s got=%-9s %s" % ("spawn_independent", "pass", "pass" if ok_brk else "fail", + "OK" if ok_brk else "MISMATCH")) + # N0: `hawk init` on a fresh install creates the (missing) home folder and a private topic import tempfile as _tf from opencode_hawk import cli as _cli