teams: ephemeral lifecycle for Scheduled + Triggered planner modes
Migration 0033: adds teams.lifecycle ('permanent' | 'ephemeral') and a
topology_runs.team_id back-ref with a partial index for the sibling-in-
flight check.
cm-db repo:
- teams::insert_team_with_lifecycle (insert_team keeps the permanent default)
- topology_runs::enqueue_run_for_team (populates team_id)
- topology_runs::check_ephemeral_teardown — atomic SELECT that only
returns Some when the team is ephemeral AND no siblings are still
queued/running; carries the workspace + bound claw ids for cleanup.
cm-api:
- topology_worker post-terminal hook maybe_teardown_ephemeral_team
runs deprovision_claw on each bound claw (best-effort; failures log
but don't block Postgres deletion), then hard_purge each agent row,
then delete_team.
- routes::teams::build_team_with_lifecycle (build_team keeps default);
run_team enqueues with team_id.
- planner ScaffoldRequest gains mode; lifecycle_for(mode) sets the team
to ephemeral for scheduled + triggered, permanent otherwise.
Frontend MasterPlannerModal passes mode in the scaffold payload so the
backend can derive lifecycle without duplicating the mode taxonomy.
Tests: 3 new (returns claws when no siblings, holds when siblings queued,
ignores permanent teams). 10/10 topology_jobs green; workspace clippy
--tests clean.
This commit is contained in:
@@ -154,6 +154,51 @@ async fn maybe_transition_research_topic(pool: &PgPool, id: Uuid) {
|
||||
Ok(false) => {}
|
||||
Err(e) => eprintln!("topology_worker: notify_run_completed({id}) failed: {e}"),
|
||||
}
|
||||
maybe_teardown_ephemeral_team(pool, id).await;
|
||||
}
|
||||
|
||||
/// Post-terminal hook: if this run's team is `ephemeral` and no siblings are
|
||||
/// still in flight, deprovision every bound claw on the ZeroClaw daemon,
|
||||
/// delete the claw rows, and delete the team row. Best-effort — a failure to
|
||||
/// tear down leaves the team intact and logs; a future sweep can retry.
|
||||
async fn maybe_teardown_ephemeral_team(pool: &PgPool, id: Uuid) {
|
||||
let teardown = match cm_db::repo::topology_runs::check_ephemeral_teardown(pool, id).await {
|
||||
Ok(Some(t)) => t,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
eprintln!("topology_worker: check_ephemeral_teardown({id}) failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Deprovision each claw on the daemon before deleting rows — if the daemon
|
||||
// side fails we still delete our rows (the daemon can be swept for orphans
|
||||
// by the fleet-reconcile timer). This is the trade cm-api owns everywhere:
|
||||
// Postgres is authoritative, the daemon config is a cache.
|
||||
if let Some(prov) = crate::runtime_provision::RuntimeProvisioner::from_env() {
|
||||
for cid in &teardown.claw_ids {
|
||||
if let Err(e) = prov.deprovision_claw(*cid).await {
|
||||
eprintln!("topology_worker: deprovision_claw({cid}) failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
for cid in &teardown.claw_ids {
|
||||
if let Err(e) = cm_db::repo::agents::hard_purge(pool, cm_domain::AgentId::from(*cid)).await
|
||||
{
|
||||
eprintln!("topology_worker: agents::hard_purge({cid}) failed: {e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = cm_db::repo::teams::delete_team(
|
||||
pool,
|
||||
teardown.team_id,
|
||||
cm_domain::WorkspaceId::from(teardown.workspace_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!(
|
||||
"topology_worker: teams::delete_team({}) failed: {e}",
|
||||
teardown.team_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive a graph to completion with the durable per-step checkpoint +
|
||||
|
||||
Reference in New Issue
Block a user