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:
@@ -11,7 +11,7 @@ use serde_json::{json, Value};
|
||||
use std::convert::Infallible;
|
||||
|
||||
use crate::routes::claws::{apply_reference_to_claw, enhance_and_publish, extract_json};
|
||||
use crate::routes::teams::{build_team, TeamMemberInput};
|
||||
use crate::routes::teams::TeamMemberInput;
|
||||
use crate::{AppState, Authed};
|
||||
|
||||
fn sse(v: Value) -> Result<Event, Infallible> {
|
||||
@@ -173,11 +173,23 @@ pub struct ScaffoldRequest {
|
||||
#[serde(default)]
|
||||
pub schedule: Option<ScaffoldSchedule>,
|
||||
pub members: Vec<ScaffoldMember>,
|
||||
/// Original planner mode — decides the team's lifecycle. `scheduled` and
|
||||
/// `triggered` produce `ephemeral` teams (torn down by the topology_worker
|
||||
/// after the last run terminates). Anything else is `permanent`.
|
||||
#[serde(default)]
|
||||
pub mode: String,
|
||||
}
|
||||
fn default_kind() -> String {
|
||||
"hub_spoke".to_string()
|
||||
}
|
||||
|
||||
fn lifecycle_for(mode: &str) -> &'static str {
|
||||
match mode {
|
||||
"scheduled" | "triggered" => "ephemeral",
|
||||
_ => "permanent",
|
||||
}
|
||||
}
|
||||
|
||||
/// `POST /api/planner/scaffold` — build the approved team (SSE progress): create
|
||||
/// agents + topology, refine+attach a brain per agent, set up the nightly loop.
|
||||
pub async fn planner_scaffold(
|
||||
@@ -200,7 +212,8 @@ pub async fn planner_scaffold(
|
||||
system_prompt: m.system_prompt.clone(),
|
||||
accent: String::new(),
|
||||
}).collect();
|
||||
let (team_id, claw_ids) = match build_team(&state, user.workspace_id, user.user_id, &body.team_name, &body.topology_kind, &members).await {
|
||||
let lifecycle = lifecycle_for(&body.mode);
|
||||
let (team_id, claw_ids) = match crate::routes::teams::build_team_with_lifecycle(&state, user.workspace_id, user.user_id, &body.team_name, &body.topology_kind, &members, lifecycle).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => { yield sse(json!({"stage":"error","pct":100,"label":"Team creation failed"})); return; }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user