fix(missions): re-assert a mission's crew when its container is recreated
Claws are provisioned exactly once, at on_launch. `ensure_container`
RECREATES a container that is not running, and recreation reseeds
.zeroclaw from the seed directory — which does not hold this mission's
claws. The agents still exist in Postgres and the crew query looks
perfect, so nothing reads as broken; the alias is simply gone from the
daemon and /ws/chat answers 400 Bad Request. A retried mission could
therefore never connect again.
Re-assert the crew after ensure_container. provision_claw is idempotent,
so this costs one call per claw on the happy path and is the difference
between a resumable mission and a dead one.
Two things this has to get right, both of which fail silently:
- Aim at the per-mission daemon via for_gateway(ec.endpoint), never
from_env() — that targets the shared global gateway and leaves this
container with no claws at all, exactly as for_gateway's own doc
comment warns.
- Provisioning creates the agent but cannot set workspace.path (the
config prop-schema has no way to express it), so follow with
pin_agent_workspaces or every claw runs in its own sandbox and
delivers nothing.
Verified on mission 01a00538: research and coding phases both completed
after four straight failures, with all five graph aliases present and
ten claws pinned to /mission/repo.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
85a6038c08
commit
fd5e71ccfe
@@ -678,6 +678,73 @@ async fn launch_phase(
|
|||||||
return Err(format!("copy checkout into {name}: {e}"));
|
return Err(format!("copy checkout into {name}: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Re-assert this mission's crew in the runtime config.
|
||||||
|
//
|
||||||
|
// Claws are provisioned once, at on_launch. `ensure_container`
|
||||||
|
// RECREATES a container that is not running, and recreation
|
||||||
|
// reseeds `.zeroclaw` from the seed directory — which does not
|
||||||
|
// contain this mission's claws. The agents still exist in
|
||||||
|
// Postgres, so nothing looks wrong, but the alias the turn
|
||||||
|
// dials is gone from the daemon and `/ws/chat` answers
|
||||||
|
// `400 Bad Request`. That is what a retry of mission 01a00538
|
||||||
|
// hit after its container was recreated.
|
||||||
|
//
|
||||||
|
// provision_claw is idempotent (created:false when present), so
|
||||||
|
// this costs one call per claw on the happy path and is the
|
||||||
|
// difference between a resumable mission and a dead one.
|
||||||
|
// Aim at THIS MISSION's daemon, never the global gateway. Each
|
||||||
|
// mission's turns run against its own container, which loads
|
||||||
|
// config at boot and never re-reads the file, so provisioning
|
||||||
|
// against `from_env()` writes the claws into the shared runtime
|
||||||
|
// and leaves this one with none — the failure mode
|
||||||
|
// `RuntimeProvisioner::for_gateway`'s doc comment describes, and
|
||||||
|
// the one this block exists to repair.
|
||||||
|
if let Some(rp) =
|
||||||
|
crate::runtime_provision::RuntimeProvisioner::for_gateway(ec.endpoint.clone())
|
||||||
|
{
|
||||||
|
let crew = sqlx::query(
|
||||||
|
"SELECT DISTINCT a.id, a.model_binding, t.risk_profile
|
||||||
|
FROM team_members tm
|
||||||
|
JOIN mission_teams mt ON mt.team_id = tm.team_id
|
||||||
|
JOIN teams t ON t.id = tm.team_id
|
||||||
|
JOIN agents a ON a.id = tm.claw_id
|
||||||
|
WHERE mt.mission_id = $1 AND a.deleted_at IS NULL",
|
||||||
|
)
|
||||||
|
.bind(mission_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut reasserted: Vec<cm_domain::AgentId> = Vec::new();
|
||||||
|
for row in &crew {
|
||||||
|
let aid: uuid::Uuid = row.get("id");
|
||||||
|
let model: Option<String> = row.get("model_binding");
|
||||||
|
let risk: Option<String> = row.get("risk_profile");
|
||||||
|
match rp
|
||||||
|
.provision_claw(
|
||||||
|
aid,
|
||||||
|
model.as_deref().unwrap_or("claude"),
|
||||||
|
risk.as_deref().unwrap_or("research_readonly"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => reasserted.push(cm_domain::AgentId::from(aid)),
|
||||||
|
Err(e) => eprintln!(
|
||||||
|
"phase_runner: re-provision claw {aid} for {mission_id} failed \
|
||||||
|
(continuing): {e}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Provisioning creates the agent; it does NOT set
|
||||||
|
// workspace.path (the prop-schema cannot). Without this the
|
||||||
|
// claws exist and every one of them runs in its own sandbox
|
||||||
|
// instead of the mission tree.
|
||||||
|
if let Err(e) = prov
|
||||||
|
.pin_agent_workspaces(mission_id, &reasserted, "/mission/repo")
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(format!("re-pin mission workspaces: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
|
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
|
||||||
pool,
|
pool,
|
||||||
mission_id,
|
mission_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user