missions: wire per-mission runtime container into launch + retry (C3 slice 2)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 32s
ci / e2e (push) Skipped
ci / publish (push) Skipped

- mission_orchestrator::on_launch now calls ensure_container after
  the repo checkout, persists the container_name + endpoint on the
  missions row. Non-fatal — logs and continues on docker errors so
  dev-mode + tests keep working.
- phase_runner::launch_phase does the same as a fallback for any
  mission whose runtime_endpoint is null (pre-C3 or torn down).

Nothing reads the endpoint yet; slice 3 swaps topology_worker over.
This commit is contained in:
Omar Sobh
2026-07-21 22:30:22 -07:00
parent f648bcd26e
commit 7649b213ad
2 changed files with 75 additions and 0 deletions
+37
View File
@@ -65,6 +65,43 @@ pub async fn on_launch(
), ),
} }
// Provision the per-mission ZeroClaw runtime container (C3).
// Idempotent: returns the endpoint if the container is already
// running. Falls back silently when docker is unreachable so
// dev-mode + tests still work — the topology_worker will use the
// shared runtime endpoint in that case.
if let Some(prov) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
match prov.ensure_container(mission_id).await {
Ok(endpoint) => {
let container_name = crate::mission_runtime::container_name(mission_id);
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
pool,
mission_id,
workspace_id.as_uuid(),
Some(&container_name),
Some(&endpoint),
)
.await
{
eprintln!(
"mission_orchestrator: bind runtime container for {mission_id} failed: {e}"
);
} else {
eprintln!(
"mission_orchestrator: runtime container {container_name} → {endpoint} for mission {mission_id}"
);
}
}
Err(e) => eprintln!(
"mission_orchestrator: provision runtime container for {mission_id} failed (continuing with shared runtime): {e}"
),
}
} else {
eprintln!(
"mission_orchestrator: docker unreachable, mission {mission_id} will use shared runtime"
);
}
// Skip team materialization if already bound. // Skip team materialization if already bound.
if mission.team_id.is_some() { if mission.team_id.is_some() {
eprintln!( eprintln!(
+38
View File
@@ -142,6 +142,44 @@ async fn launch_phase(
return Ok(()); return Ok(());
} }
// Provision the per-mission runtime container if it isn't already
// bound. Idempotent — on_launch sets this on initial launch, but
// pre-C3 missions or retries against a torn-down container land
// here. Non-fatal: if docker is unreachable the run falls back to
// the shared runtime.
let mission = cm_db::repo::missions::get(
pool,
mission_id,
workspace_id,
)
.await
.map_err(|e| format!("load mission for runtime binding: {e}"))?;
if let Some(m) = mission {
if m.runtime_endpoint.is_none() {
if let Some(prov) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
match prov.ensure_container(mission_id).await {
Ok(endpoint) => {
let name = crate::mission_runtime::container_name(mission_id);
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
pool,
mission_id,
workspace_id,
Some(&name),
Some(&endpoint),
)
.await
{
eprintln!("phase_runner: bind runtime container for {mission_id} failed: {e}");
} else {
eprintln!("phase_runner: runtime container {name} → {endpoint} for mission {mission_id}");
}
}
Err(e) => eprintln!("phase_runner: provision runtime container for {mission_id} failed (continuing): {e}"),
}
}
}
}
// Ensure the mission's repo is checked out before firing any run. // Ensure the mission's repo is checked out before firing any run.
// Idempotent: fetch+reset on existing clones, clone on missing. // Idempotent: fetch+reset on existing clones, clone on missing.
// Runs on EVERY phase launch — including retries — so a retry // Runs on EVERY phase launch — including retries — so a retry