night 2026-09-24: chaos rounds 1-8 evidence; 07/08/09/capability map for decisions 26-28 + digests; R-681
gates / gates (push) Successful in 24s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-24 14:21:14 +02:00
parent 3208cb2083
commit 4502af6bb1
54 changed files with 9647 additions and 9 deletions
@@ -103,7 +103,7 @@ def readback(app, s):
"files": nf.verify_files(w, SUB[app], A, {names[0]: S["files"][names[0]], names[2]: S["files"][names[2]]}, w.say)}
if not A:
return {"A": None, "note": "no seed recorded"}
w.wait_app(SUB[app], "/", tries=90, delay=4)
w.wait_app(SUB[app], "/", want=("200", "302", "303", "307", "401", "403"), tries=90, delay=4)
return {"A": bool(FX[app].verify(w, SUB[app], A, w.say))}
@@ -185,6 +185,11 @@ class Arm:
self.note["accident"] = f"POST /api/backup/run -> {code} {str(d)[:160]} at {time.strftime('%H:%M:%S')}"
w.say(" >>> " + self.note["accident"])
return
# R-320: the box's log off the machine BEFORE the accident — round 3's power cut replaced the controller
# container and took the pre-cut lines with it. Only from the main thread: walk.guest shares one temp file.
if threading.current_thread() is threading.main_thread():
open(os.path.join(EVD, f"round-{self.n:02d}-controller-pre.log"), "w").write(
w.guest(f"docker logs --since {self.note['started']} felhom-controller 2>&1 | grep -vE 'auth: valid session|router.go:81|Status refresh' | tail -3000"))
json.dump({"round": self.n, "kind": self.kind, "at": time.time(), "why": why},
open(os.path.join(EVD, f"arm-{self.n:02d}.json"), "w"))
@@ -216,8 +221,14 @@ def wait_controller(cap=900):
# ------------------------------------------------------------------------------------------ actions
def a_unhealthy_install(app, note, arm, t0):
"""crash_loop / oom_app: install the drill app through the product; the box must stop it by itself."""
for _ in range(12): # the drill template must be on the box first (attempt 0 of round 1: "not found")
w.sync_rescan()
if w.ctl("GET", f"/api/stacks/{app}/deploy-fields")[0] == "200":
break
time.sleep(5)
vals = w.deploy_values(app, app)
code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": vals})
w.say(f" deploy -> {code} {str(d)[:120]}")
note["act"] = f"deploy -> {code}"
threading.Timer(20, arm, args=("20 s after the install",)).start()
return steady(app, t0, cap=1500, until=lambda st: st.get("hold_kind") == "unhealthy_stop")
@@ -262,7 +273,7 @@ def a_install(app, note, arm, t0, s):
return secs, st
def a_update(app, note, arm, t0, on_verify=None):
def a_update(app, note, arm, t0, on_verify=None, arm_at_starting=False):
code, d = w.ctl("POST", f"/api/stacks/{app}/update")
note["act"] = f"Update -> {code} {str(d)[:140]}"
w.say(" " + note["act"])
@@ -281,7 +292,7 @@ def a_update(app, note, arm, t0, on_verify=None):
if on_verify:
on_verify()
arm("at verifying")
if ph == "starting":
if ph == "starting" and arm_at_starting:
arm("at starting")
if st and not st.get("updating") and ph in ("done", "failed", "undone") and time.time() - t0 > 3:
break
@@ -352,7 +363,7 @@ def a_ladder_step(app, note, arm, t0, s):
st = w.stack(app)
note["steps_left_before"] = st.get("ladder_steps_left")
note["pinned_before"] = (st.get("app_config") or {}).get("pinned_images")
r = a_update(app, note, arm, t0)
r = a_update(app, note, arm, t0, arm_at_starting=True)
w.sync_rescan()
st = w.stack(app)
note["steps_left_after"] = st.get("ladder_steps_left")
@@ -443,8 +454,29 @@ def run_round(n):
w.say(f"==== ROUND {n} END: steady in {note['time_to_steady_s']}s; readback={note['readback']}; events={note['events']}")
def collect(n, t_iso, extra):
"""Finish a round whose runner was stopped: the same five observations, no action."""
s = load()
sched = {r["round"]: r for r in schedule()}[n]
app = TARGET[n]
w.login()
note = {"round": n, **sched, "app": app, "started": t_iso, "collected_by_hand": True, **extra}
note["after"] = household(app)
if not w.stack(app).get("deployed"):
note["after"]["leftovers"] = w.guest(f"docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}} {{{{.Status}}}}'; docker volume ls -q | grep '^{app}_' || true; ls /opt/docker/stacks/{app}/ 2>/dev/null || true").strip()
note["readback"] = readback(app, s)
note["events"] = events_since(t_iso)
note["box_log"] = box_log(t_iso, app)
open(os.path.join(EVD, f"round-{n:02d}-controller.log"), "w").write(
w.guest(f"docker logs --since {t_iso} felhom-controller 2>&1 | grep -vE 'auth: valid session|router.go:81|Status refresh' | tail -3000"))
json.dump(note, open(os.path.join(EVD, f"round-{n:02d}.json"), "w"), indent=2, ensure_ascii=False, default=str)
w.say(f"==== ROUND {n} COLLECTED: after={json.dumps(note['after'], ensure_ascii=False)[:400]} events={note['events']}")
if __name__ == "__main__":
if sys.argv[1] == "setup":
if sys.argv[1] == "collect":
collect(int(sys.argv[2]), sys.argv[3], json.loads(sys.argv[4]) if len(sys.argv) > 4 else {})
elif sys.argv[1] == "setup":
setup()
elif sys.argv[1] == "round":
run_round(int(sys.argv[2]))