M4.3: a broken build VM blocks its projects until it's ready
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m38s
CI / e2e (pull_request) Successful in 4m20s

The driver can request_reimage(reason) when its build VM is broken past the
usual fixes (before the go-ahead too: it asks, it doesn't act). The harness
parks the work in progress (pushed to Gitea), then blocks the VM in the state
store (schema 5, vm_blocks): no item on any project on it is taken up, across
restarts. The operator hears once at top level, with the command to clear it,
and each blocked item's thread gets a line. `vm ready <project>` lifts the
block and trusts the VM's new host key; a reimaged VM is set up again by the
toolchain step, and the item that found it goes first.

loop.qnt gains the block (blocked_vm_runs_nothing) and a scenario; it found
that a reimage lost work in progress, hence parking first. The walk requests
reimages and recovers them, reimaged or not. The unreachable case reuses the
block in #137. Architecture P5 is softer, and D3 describes the block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-08 19:27:28 +00:00
1 parent 7c3c09ab98
commit c794a1e52b
10 files changed
+486 -16

No files matched your search

+38 -7
View File
@@ -107,6 +107,9 @@ class PolicyModel:
toolchain=[{"name": "python", "version_command": command}], # type: ignore[arg-type]
)
]
if "request_reimage" in offered and self.rng.random() < 0.03:
# M4.3: the build VM seems broken past the usual fixes.
return [call("request_reimage", reason=f"broken {self.n}")]
roll = self.rng.random()
if roll < 0.08:
return [call("ask_human", question=f"question {self.n}?")]
@@ -196,7 +199,8 @@ class PolicyModel:
class Watched:
"""The driver's model, watched: a run on a project item (not its
preflight) must find the project's marker on the VM."""
preflight) must find the project's marker on the VM, and no model runs
for an item whose build VM is blocked."""
def __init__(self, walk: "Walk", inner: Any) -> None:
self.walk = walk
@@ -211,6 +215,10 @@ class Watched:
)
if item is not None and item.scope != "general" and working and not marked:
self.walk.unready.append(item.id)
if item is not None and item.scope in self.walk.worlds:
host = self.walk.worlds[item.scope]["host"].describe()
if host in self.walk.world["store"].vm_blocks():
self.walk.on_blocked.append(item.id)
return self.inner.complete(events, tools, model, system)
@@ -291,6 +299,7 @@ class Walk:
self.markers = Path(self.world["repo"].root).parent / "markers"
self.unready: list[str] = []
self.wiped_at: dict[str, int] = {} # project -> last event before a wipe
self.on_blocked: list[str] = [] # items run while their VM was blocked
if self.merge == "approved":
for w in worlds.values():
self._watch_merges(w["forge"])
@@ -395,6 +404,8 @@ class Walk:
"issue": 1,
"chat": 1,
}
if self.world["store"].vm_blocks():
weights.update({"vm-ready": 3})
item = self.rng.choice(open_items) if open_items else None
loop = loop_of(item) if item else None
# The forge an item's project lives on; a new issue picks one.
@@ -423,12 +434,20 @@ class Walk:
if action == "intake":
self.agent.intake()
elif action == "wipe":
# The build VM is rebuilt (D3): the clone and its marker gone.
w = self.worlds[project]
shutil.rmtree(w["repo"].root, ignore_errors=True)
events = self.world["store"].all_events()
self.wiped_at[project] = events[-1].id if events else 0
(self.markers / f"{project}.ready").unlink(missing_ok=True)
self._wipe(project)
elif action == "vm-ready":
# M4.3: the operator recovers a blocked VM, reimaged or not; a
# reimage wipes every project on it.
blocks = self.world["store"].vm_blocks()
on_it = [
name
for name, w in self.worlds.items()
if w["host"].describe() in blocks
]
if self.rng.random() < 0.5:
for name in on_it:
self._wipe(name)
self.transport.receive(DON, f"vm ready {self.rng.choice(on_it)}")
elif action == "done" and loop:
# The operator says a new project is ready; no entry was added.
self.transport.receive(DON, "done", thread=loop.thread)
@@ -557,6 +576,14 @@ class Walk:
self.agent.watch()
self.agent._schedule()
def _wipe(self, project: str) -> None:
"""The build VM is rebuilt (D3): the clone and its marker gone."""
w = self.worlds[project]
shutil.rmtree(w["repo"].root, ignore_errors=True)
events = self.world["store"].all_events()
self.wiped_at[project] = events[-1].id if events else 0
(self.markers / f"{project}.ready").unlink(missing_ok=True)
# -- the properties (specs/loop.qnt) -------------------------------------
def _dangerous_only_when_allowed(self, where: str) -> None:
@@ -592,6 +619,10 @@ class Walk:
where = f"seed {self.seed}, steps {self.steps}"
# M4.1: the driver never works on a project whose VM has no marker.
assert not self.unready, f"ready_before_work: {self.unready}: {where}"
# M4.3: nothing runs for a project whose build VM is blocked.
assert not self.on_blocked, (
f"blocked_vm_runs_nothing: {self.on_blocked}: {where}"
)
# D5: the agent asks Gitea to merge only what an operator approved
# as it is (Gitea would refuse the rest; the harness mustn't ask).
unapproved = [pr for pr, approved in self.agent_merges if not approved]