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:
@@ -56,6 +56,30 @@ pub(crate) async fn build_team(
|
||||
name: &str,
|
||||
kind_str: &str,
|
||||
members: &[TeamMemberInput],
|
||||
) -> Result<(Uuid, Vec<Uuid>), ApiError> {
|
||||
build_team_with_lifecycle(
|
||||
state,
|
||||
workspace_id,
|
||||
user_id,
|
||||
name,
|
||||
kind_str,
|
||||
members,
|
||||
"permanent",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Same as `build_team` but with an explicit `lifecycle` (`permanent` |
|
||||
/// `ephemeral`). Ephemeral teams are torn down by the topology_worker after
|
||||
/// their last run terminates — used by the Scheduled + Triggered planner modes.
|
||||
pub(crate) async fn build_team_with_lifecycle(
|
||||
state: &AppState,
|
||||
workspace_id: cm_domain::WorkspaceId,
|
||||
user_id: cm_domain::UserId,
|
||||
name: &str,
|
||||
kind_str: &str,
|
||||
members: &[TeamMemberInput],
|
||||
lifecycle: &str,
|
||||
) -> Result<(Uuid, Vec<Uuid>), ApiError> {
|
||||
if members.is_empty() {
|
||||
return Err(ApiError::BadRequest);
|
||||
@@ -101,13 +125,14 @@ pub(crate) async fn build_team(
|
||||
|
||||
let team_id = Uuid::now_v7();
|
||||
let graph_json = serde_json::to_value(&graph).map_err(|_| ApiError::Internal)?;
|
||||
cm_db::repo::teams::insert_team(
|
||||
cm_db::repo::teams::insert_team_with_lifecycle(
|
||||
&state.pool,
|
||||
team_id,
|
||||
workspace_id,
|
||||
name,
|
||||
kind.as_str(),
|
||||
&graph_json,
|
||||
lifecycle,
|
||||
)
|
||||
.await?;
|
||||
for (i, node) in graph.nodes.iter().enumerate() {
|
||||
@@ -373,12 +398,13 @@ pub async fn run_team(
|
||||
let team = cm_db::repo::teams::get_team(&state.pool, id, user.workspace_id).await?;
|
||||
crate::quota::enforce_new_run(&state, user.workspace_id).await?;
|
||||
let run_id = Uuid::now_v7();
|
||||
cm_db::repo::topology_runs::enqueue_run(
|
||||
cm_db::repo::topology_runs::enqueue_run_for_team(
|
||||
&state.pool,
|
||||
run_id,
|
||||
user.workspace_id,
|
||||
&body.task,
|
||||
&team.graph,
|
||||
id,
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
|
||||
Reference in New Issue
Block a user