fix(missions): re-assert a mission's crew when its container is recreated
deploy / test (push) Successful in 4m10s
deploy / build (push) Successful in 5m15s

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:
Omar Sobh
2026-08-16 07:25:58 -07:00
co-authored by Claude Opus 5
parent 85a6038c08
commit fd5e71ccfe
+67
View File
@@ -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,