e1ace38414
Three separate defects, found by asking one question -- what temperature does @coder run at. 1. THE WRONG CODER. There were TWO bullpen-coder processes: one in grind, which holds the `coder` nick, and one in workers, which holds dispatcher, callboy, researcher, skeptic and triage -- and no coder nick at all. My own roster commit this morning put bullpen-coder.service on workers, with the comment "der Programmierer, keine Leiche"; grind had declared it correctly all along. The comment produced the corpse. Whichever answered first won the ask, so I spent an hour repairing the copy in workers while the one in grind kept replying 404. workers is now disabled and the roster declares it on grind only. 2. THE NAME FOLLOWED THE EXPERIMENT. BULLPEN_MODEL was qwen3.6-coding, and the proxy advertises a local backend under the ALIAS OF THE LOADED MODEL, not the backend name. boltzmann :8085 has carried a running experiment since 08:58, so the name vanished from the catalogue and the proxy answered 404 model_not_found -- correctly; it refuses rather than substituting silently. The tagged form then reached the gate and got 503 backend busy: that backend has slots:1 and the experiment holds it. @coder has no business queueing behind an experiment, so it now asks bosch/DS4 instead. The tag is required: for an untagged name _classify() probes the backend and swallows every exception, landing on "unknown" -> 404. 3. THE ERROR SAID NOTHING. `codegen error: HTTP Error 404: Not Found` names neither URL nor model nor the response body -- and the proxy does send one, listing the models it knows. That is most of why the hour went the way it did. HTTPError now reports status, URL, model and body. Also: the hardcoded "temperature": 0.2 is gone. It was the only sampling value @coder sent and it beat every server default, so the model card had no say at all. The backend unit carries the card row for whatever it serves; one decision, one place -- same reason the cost filter came out of models-json. Measured, not assumed: job 1209 -> 1212, clean Lua, ran OK, output 3/1/2, "served by deepseek-v4-flash". Exactly one coder process remains, in grind.
139 lines
6.9 KiB
Python
Executable File
139 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""@coder — bullpen worker: turn a request into a Lua file, write it, run it, report.
|
|
Reactive. Engine = dspark via the gated proxy. It ONLY ever writes into its sandbox and
|
|
runs `lua5.4` there as an unprivileged user with a timeout, and — when the ticket names a
|
|
SPEC — `bullpen-attest` under the SAME bound. Nothing else (mechanical bound). It holds no
|
|
room token, so an anchored spec comes back red here BY DESIGN; see attest().
|
|
bullpen-coder # room loop (systemd)
|
|
bullpen-coder --once "a lua function that reverses a string, with a self-test"
|
|
"""
|
|
import json, os, re, shutil, subprocess, sys, urllib.request, urllib.error
|
|
sys.path[:0] = [p for p in (os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "lib"), "/usr/local/lib/bullpen") if os.path.isdir(p)]
|
|
import bullpen_worker as bw
|
|
import bullpen_config as cfg
|
|
|
|
SANDBOX = cfg.CODER_DIR
|
|
# Gemessen 2026-08-09 gegen `[local] qwen3.6-coding`: ein realistischer
|
|
# Auftrag (1205 Prompt-Token -> 1128 Antwort-Token) braucht 307 s bei
|
|
# 3,67 Tok/s. Die alte Grenze von 200 s war damit fuer eine echte
|
|
# Codegenerierung nie erreichbar -- der Fehlschlag sah nach Ausfall aus
|
|
# und war Arithmetik. Der Motor selbst antwortet auf einen Einzeiler in 6 s.
|
|
# 1800 wie Lurker und oc-run: dieselbe Schranke fuer jeden Modellaufruf.
|
|
# @coder faehrt als einziger ein LOKALES CPU-Modell (gemessen 3,67 Tok/s),
|
|
# also traf die knappste Frist die langsamste Maschine. 900 s sind bei
|
|
# dieser Rate rund 3300 Token Ausgabe — weniger, als eine vollstaendige
|
|
# Datei braucht.
|
|
CODEGEN_TIMEOUT = int(os.environ.get("BULLPEN_CODEGEN_TIMEOUT", "1800"))
|
|
ENGINE = cfg.PROXY # gated proxy (gate+failover)
|
|
MODEL = cfg.MODEL
|
|
MAXBODY = 1400
|
|
|
|
SYS = ("You are a Lua coder. Given a task and any reference material, output ONLY valid Lua "
|
|
"source that solves it AND includes a small self-test with sample data printed to stdout. "
|
|
"No markdown, no prose, no code fences — just the Lua.")
|
|
|
|
def gen_code(request):
|
|
"""Returns (code, served_model). The proxy may fail over, so the model actually SERVING
|
|
the request isn't necessarily MODEL — the response body's own `model` field is ground
|
|
truth; report that, not the ask."""
|
|
payload = json.dumps({"model": MODEL,
|
|
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": request}],
|
|
# No temperature here. The backend unit carries the model card's row for
|
|
# whatever it serves; a request parameter would override it, and then the
|
|
# card has no say. max_tokens stays -- that is this caller's budget, not a
|
|
# property of the model.
|
|
"max_tokens": 1400}).encode()
|
|
req = urllib.request.Request(ENGINE, payload, {"Content-Type": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=CODEGEN_TIMEOUT) as r:
|
|
d = json.load(r)
|
|
txt = d["choices"][0]["message"]["content"]
|
|
m = re.search(r"```(?:lua)?\s*(.*?)```", txt, re.S) # strip fences if the model adds them
|
|
return (m.group(1) if m else txt).strip(), d.get("model")
|
|
|
|
def run_lua(path):
|
|
os.chmod(path, 0o644)
|
|
r = subprocess.run(
|
|
["timeout", "10", "su", "-s", "/bin/sh", "nobody", "-c", f"cd {SANDBOX} && lua5.4 {path}"],
|
|
capture_output=True, text=True)
|
|
out = r.stdout.strip()
|
|
if r.stderr.strip():
|
|
out += ("\n[stderr] " + r.stderr.strip())
|
|
return out.strip(), r.returncode
|
|
|
|
SPEC_RE = re.compile(r"^\s*SPEC:\s*(\S+)\s*$", re.M)
|
|
|
|
|
|
def attest(impl, spec):
|
|
"""Run bullpen-attest under the same bound as the generated code itself.
|
|
|
|
`su nobody` with a timeout — one named command with fixed arguments, not a
|
|
shell. The test is executed by attest, so it runs caged too; that matters,
|
|
because a spec is a file from a repo and this worker's whole point is that
|
|
nothing it touches gets more than lua5.4 and a clock.
|
|
|
|
Returns (certificate_text, rc) or (None, None) when the tool is absent.
|
|
"""
|
|
tool = shutil.which("bullpen-attest") or "/opt/bullpen-src/bin/bullpen-attest"
|
|
if not os.path.exists(tool):
|
|
return None, None
|
|
r = subprocess.run(
|
|
["timeout", "120", "su", "-s", "/bin/sh", "nobody", "-c",
|
|
f"cd {SANDBOX} && {tool} --impl {impl} --test {spec}"],
|
|
capture_output=True, text=True)
|
|
return ((r.stdout or "") + (r.stderr or "")).strip(), r.returncode
|
|
|
|
|
|
def dispatch(msg):
|
|
req, rid = msg.get("body", ""), msg.get("id")
|
|
os.makedirs(SANDBOX, exist_ok=True)
|
|
try:
|
|
code, served = gen_code(req)
|
|
except urllib.error.HTTPError as e:
|
|
# Der Server schickt bei 404/503 einen JSON-Rumpf mit dem Grund und oft
|
|
# der Liste der bekannten Modelle. Ohne ihn ist die Meldung nicht
|
|
# diagnostizierbar -- genau das kostete am 2026-08-11 eine Stunde.
|
|
try:
|
|
detail = e.read()[:300].decode("utf-8", "replace")
|
|
except Exception:
|
|
detail = "<Rumpf nicht lesbar>"
|
|
return (f"codegen error: HTTP {e.code} von {e.url} "
|
|
f"(Modell {MODEL!r}) — {detail}")
|
|
except Exception as e:
|
|
return f"codegen error: {type(e).__name__}: {e} (Modell {MODEL!r}, Ziel {ENGINE})"
|
|
path = f"{SANDBOX}/job{rid}.lua"
|
|
open(path, "w").write(code + "\n")
|
|
out, rc = run_lua(path)
|
|
status = "ran OK" if rc == 0 else f"exit {rc}"
|
|
model_tag = f"asked {MODEL}, served by {served}" if served and served != MODEL else f"model {MODEL}"
|
|
body = (f"wrote {path} ({len(code)} chars, {model_tag}) — {status}. "
|
|
f"output:\n{out[:400]}\n--- code ---\n{code}")
|
|
|
|
# A ticket that names a spec gets a certificate, not a claim. Without one
|
|
# the reply is unchanged — the old behaviour is the default, and a caller
|
|
# opts in by naming the test.
|
|
m = SPEC_RE.search(req)
|
|
if m:
|
|
spec = m.group(1)
|
|
urkunde, arc = attest(path, spec)
|
|
if urkunde is None:
|
|
body += ("\n--- ABNAHME ---\nbullpen-attest ist in diesem Container nicht "
|
|
"vorhanden; ich kann nicht attestieren. Das ist zu melden, nicht zu "
|
|
"umgehen.")
|
|
else:
|
|
body += f"\n--- ABNAHME (rc {arc}) ---\n{urkunde[:1200]}"
|
|
if "LMCP_PROBE_URL" in urkunde:
|
|
body += ("\n\nHINWEIS ZUR GRENZE: diese Spezifikation verankert sich an einem "
|
|
"laufenden Server. Ich halte kein Raum-Token — ich fuehre frisch "
|
|
"erzeugten Code aus, und ein Token in meiner Umgebung waere fuer "
|
|
"diesen Code lesbar. Ein verankerter Abnahmetest gehoert deshalb zu "
|
|
"einer Rolle, die Zugangsdaten halten darf. Das ist die Eingrenzung, "
|
|
"kein Fehlschlag meinerseits.")
|
|
return body
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 2 and sys.argv[1] == "--once":
|
|
print(dispatch({"body": sys.argv[2], "id": 0}))
|
|
else:
|
|
bw.run("coder", dispatch, online="coder online — writes+runs Lua (sandboxed); attests when the ticket names SPEC:. @coder <task>",
|
|
ack="…coding")
|