M4.3: a broken build VM blocks its projects until it's ready
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:
1 parent
7c3c09ab98
commit
c794a1e52b
10 files changed
+486
-16
No files matched your search
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user