Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd5e71ccfe | ||
|
|
85a6038c08 |
@@ -272,6 +272,29 @@ pub async fn sync_in(container: &str, mission_id: uuid::Uuid) -> Result<(), Stri
|
||||
.map_err(|e| format!("create empty workspace {}: {e}", repo.display()))?;
|
||||
}
|
||||
let docker = crate::container_exec::connect()?;
|
||||
// `upload_to_container` requires the DESTINATION to exist: uploading into
|
||||
// `/mission` when the container has no `/mission` fails with
|
||||
// "404 Could not find the file /mission in container", which reads like a
|
||||
// missing source file rather than a missing target directory. Nothing else
|
||||
// creates it — not the image, not the container spec (in copy mode there is
|
||||
// no `/mission` bind) — so create it here, immediately before the copy that
|
||||
// depends on it.
|
||||
let mkdir = [
|
||||
"mkdir".to_string(),
|
||||
"-p".to_string(),
|
||||
CONTAINER_MISSION_DIR.to_string(),
|
||||
];
|
||||
if let Err(e) = crate::container_exec::exec_as_root(
|
||||
&docker,
|
||||
container,
|
||||
None,
|
||||
&mkdir,
|
||||
std::time::Duration::from_secs(20),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(format!("create {CONTAINER_MISSION_DIR} in {container}: {e}"));
|
||||
}
|
||||
copy_in(&docker, container, &repo, "repo").await
|
||||
}
|
||||
|
||||
|
||||
@@ -678,6 +678,73 @@ async fn launch_phase(
|
||||
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(
|
||||
pool,
|
||||
mission_id,
|
||||
|
||||
Reference in New Issue
Block a user