slice 9 cleanup: drop legacy research/loops backend + tables
Retires the legacy research/loops backend after the missions arc
(slices 1-9) fully replaced it. Frontend cutover was 4663348; this
commit finishes the job on the backend + database.
Migration:
- 0053_drop_legacy_research_loops.sql — drops the 8 legacy tables
(research_topics, research_topic_agents, research_outcomes,
research_publish_approvals, loops, loop_agents, loop_orgs,
loop_teams) and the 3 topology_runs FK columns
(research_topic_id, loop_id, iteration). parent_run_id stays;
recursive_exec still uses it.
Files deleted (11):
- crates/cm-api/src/routes/{research,loops,research_setup,
research_pipeline,wizard_repo,probe}.rs
- crates/cm-api/src/research_container.rs
- crates/cm-db/src/repo/{research_topics,research_outcomes,
research_publish_approvals,loops}.rs
- crates/cm-runtime/src/loops.rs
- crates/cm-api/tests/research_publish_role.rs
Files edited:
- crates/cm-api/src/lib.rs — dropped 20 legacy route registrations
(all /api/research/* + /api/loops/* + /webhooks/loops + probe)
and module decls
- crates/cm-api/src/topology_worker.rs — deleted legacy dispatch
(freeze_research_outcome, advance_loop_after_completion,
continue_initial_burst, maybe_transition_research_topic,
parse_reorder_rationale, per-topic/loop gateway resolver).
reap_stuck_runs now keys on mission_id (not topic_id).
Executor path unconditionally uses ZeroClawDriveExecutor::from_env
— mission_orchestrator provisions each claw as an agent inside
the shared runtime via RuntimeProvisioner, so per-team gateway
resolution is no longer applicable.
- crates/cm-api/src/routes/topology.rs — deleted container-log SSE
endpoint (research/loop-specific), dropped loop_id filter and
iteration field from ListRunsQuery/RunSummary
- crates/cm-api/src/routes/world.rs — removed
active_research_topics/active_loops/preseed_repo_paths;
World SSE no longer emits repo:{topic}/loop:{id} landmark orbs
(follow-up task #21 tracks adding mission:{id} equivalents)
- crates/cm-api/src/runtime_provision.rs — removed now-unused
mint_workspace_service_token
- crates/cm-db/src/repo/topology_runs.rs — removed 9 legacy
helpers (research_topic_id lookup, loop_id_for_run,
iteration_for_run, active_runs_for_research_topic, etc.)
- crates/cm-db/src/repo/teams.rs — removed 4 dead helpers
(team_for_loop, team_for_research_topic + setters)
- crates/cm-api/tests/topology_jobs.rs — removed loop/topic
tests, dropped enqueue_run_with_topic helper
- crates/bins/clawmates-server/src/main.rs — removed
spawn_loop_scheduler call
- crates/cm-api/src/routes/mod.rs, crates/cm-db/src/repo/mod.rs,
crates/cm-runtime/src/lib.rs — module decls stripped
sqlx cache: regenerated against post-migration schema
(71 files changed, ~+70 / -8896 net)
Test/build: SQLX_OFFLINE=true cargo check --workspace clean;
cargo test --workspace --no-run clean.
Follow-up (task #21): World view lost the in-flight-work landmarks
when repo:{topic} / loop:{id} orbs disappeared. Add mission:{id}
orbs as the missions-era replacement.
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
//! (CAS) → checkpoint → complete, plus the stale-run resume sweep. This is the
|
||||
//! foundation that lets long-horizon topology runs survive worker restarts.
|
||||
|
||||
use cm_db::repo::{
|
||||
loops, research_outcomes, research_topics, teams, topology_runs, users, workspaces,
|
||||
};
|
||||
use cm_db::repo::{teams, topology_runs, users, workspaces};
|
||||
use cm_domain::{
|
||||
AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -24,33 +22,6 @@ async fn seed_user(pool: &sqlx::PgPool, ws: WorkspaceId, email: &str) -> UserId
|
||||
user.id
|
||||
}
|
||||
|
||||
/// Enqueue a durable topology run with `research_topic_id` set. Kept in the
|
||||
/// test file to avoid a production repo helper for the topic-scoped enqueue
|
||||
/// path (nothing else in the codebase writes this column yet).
|
||||
async fn enqueue_run_with_topic(
|
||||
pool: &sqlx::PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
task: &str,
|
||||
topic_id: Uuid,
|
||||
) -> Uuid {
|
||||
let id = Uuid::now_v7();
|
||||
let graph = json!({"kind": "pipeline", "nodes": [{"id":"n","role":"r"}], "edges": []});
|
||||
sqlx::query!(
|
||||
"INSERT INTO topology_runs
|
||||
(id, workspace_id, task, kind, status, graph, tier, research_topic_id)
|
||||
VALUES ($1, $2, $3, 'run', 'queued', $4, 'team', $5)",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
task,
|
||||
graph,
|
||||
topic_id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
async fn seed_workspace(pool: &sqlx::PgPool) -> WorkspaceId {
|
||||
let ws = Workspace {
|
||||
id: WorkspaceId::new(),
|
||||
@@ -176,222 +147,6 @@ async fn cancel_transitions_only_active_runs() {
|
||||
assert!(!topology_runs::cancel(&pool, id2, other).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_by_loop_returns_only_that_loops_iterations_newest_first() {
|
||||
let pool = cm_testkit::test_pool().await;
|
||||
let ws = seed_workspace(&pool).await;
|
||||
|
||||
// A loop needs a valid `created_by` user in the same workspace.
|
||||
let user_id = seed_user(&pool, ws, "[email protected]").await;
|
||||
|
||||
let graph = json!({"kind": "pipeline", "nodes": [{"id":"n1","role":"drafter"}], "edges": []});
|
||||
let triggers = json!({});
|
||||
let repeat = json!({"kind": "infinite"});
|
||||
let loop_id = loops::create(
|
||||
&pool,
|
||||
loops::NewLoop {
|
||||
workspace_id: ws.as_uuid(),
|
||||
title: "L",
|
||||
description: "d",
|
||||
graph: &graph,
|
||||
task_template: "task",
|
||||
triggers: &triggers,
|
||||
repeat_policy: &repeat,
|
||||
enabled: true,
|
||||
next_fire_at: None,
|
||||
webhook_token: None,
|
||||
webhook_signing_key: None,
|
||||
created_by: user_id.as_uuid(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Three iterations of the target loop, plus a naked run + another loop's
|
||||
// iteration that should be filtered out.
|
||||
let it1 = loops::enqueue_iteration(&pool, loop_id, ws.as_uuid(), "t1", &graph, 1, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let it2 = loops::enqueue_iteration(&pool, loop_id, ws.as_uuid(), "t2", &graph, 2, Some(it1))
|
||||
.await
|
||||
.unwrap();
|
||||
let it3 = loops::enqueue_iteration(&pool, loop_id, ws.as_uuid(), "t3", &graph, 3, Some(it2))
|
||||
.await
|
||||
.unwrap();
|
||||
topology_runs::enqueue_run(&pool, Uuid::now_v7(), ws, "naked", &graph)
|
||||
.await
|
||||
.unwrap();
|
||||
let other_loop = loops::create(
|
||||
&pool,
|
||||
loops::NewLoop {
|
||||
workspace_id: ws.as_uuid(),
|
||||
title: "L2",
|
||||
description: "d2",
|
||||
graph: &graph,
|
||||
task_template: "task2",
|
||||
triggers: &triggers,
|
||||
repeat_policy: &repeat,
|
||||
enabled: true,
|
||||
next_fire_at: None,
|
||||
webhook_token: None,
|
||||
webhook_signing_key: None,
|
||||
created_by: user_id.as_uuid(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let _other_it =
|
||||
loops::enqueue_iteration(&pool, other_loop, ws.as_uuid(), "other", &graph, 1, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rows = topology_runs::list_by_loop(&pool, ws, loop_id, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rows.len(), 3, "only the target loop's iterations");
|
||||
// Newest iteration first.
|
||||
assert_eq!(rows[0].iteration, Some(3));
|
||||
assert_eq!(rows[0].id, it3);
|
||||
assert_eq!(rows[1].iteration, Some(2));
|
||||
assert_eq!(rows[1].id, it2);
|
||||
assert_eq!(rows[2].iteration, Some(1));
|
||||
assert_eq!(rows[2].id, it1);
|
||||
// Each iteration is still a durable run — finished_at is None until completion.
|
||||
assert!(rows.iter().all(|r| r.finished_at.is_none()));
|
||||
assert!(rows.iter().all(|r| r.kind == "run"));
|
||||
|
||||
// Wrong workspace: nothing.
|
||||
let other_ws = seed_workspace(&pool).await;
|
||||
let cross = topology_runs::list_by_loop(&pool, other_ws, loop_id, 20)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(cross.is_empty(), "loops are scoped by workspace");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_run_completed_transitions_topic_when_no_siblings_in_flight() {
|
||||
let pool = cm_testkit::test_pool().await;
|
||||
let ws = seed_workspace(&pool).await;
|
||||
let user_id = seed_user(&pool, ws, "[email protected]").await;
|
||||
|
||||
let topic = research_topics::create(
|
||||
&pool,
|
||||
research_topics::NewTopic {
|
||||
workspace_id: ws.as_uuid(),
|
||||
title: "Topic",
|
||||
description: "desc",
|
||||
outcome_kind: "spec",
|
||||
topology_kind: "hub_spoke",
|
||||
repo_id: None,
|
||||
created_by: user_id.as_uuid(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// The auto-transition only fires while the topic is `processing`.
|
||||
research_topics::set_status(&pool, topic, ws.as_uuid(), "processing")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let run_id = enqueue_run_with_topic(&pool, ws, "task", topic).await;
|
||||
// Flip to a terminal state before calling — mirrors the worker order.
|
||||
let result = json!({"final_output": "done"});
|
||||
topology_runs::complete(&pool, run_id, &result)
|
||||
.await
|
||||
.unwrap();
|
||||
// A completed run is not enough on its own: `notify_run_completed` only
|
||||
// advances the topic to `reviewing` once at least one outcome exists,
|
||||
// so mirror the worker order and persist the artifact first.
|
||||
research_outcomes::insert(&pool, topic, "# body", Some(run_id))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transitioned = topology_runs::notify_run_completed(&pool, run_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(transitioned, "no siblings in flight → topic transitions");
|
||||
let t = research_topics::get(&pool, topic, ws.as_uuid())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(t.status, "reviewing");
|
||||
|
||||
// Idempotent: a second call after the topic has already left `processing`
|
||||
// is a no-op.
|
||||
let again = topology_runs::notify_run_completed(&pool, run_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!again,
|
||||
"second call is a no-op — topic is no longer processing"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_run_completed_leaves_topic_processing_when_siblings_in_flight() {
|
||||
let pool = cm_testkit::test_pool().await;
|
||||
let ws = seed_workspace(&pool).await;
|
||||
let user_id = seed_user(&pool, ws, "[email protected]").await;
|
||||
|
||||
let topic = research_topics::create(
|
||||
&pool,
|
||||
research_topics::NewTopic {
|
||||
workspace_id: ws.as_uuid(),
|
||||
title: "Topic",
|
||||
description: "desc",
|
||||
outcome_kind: "spec",
|
||||
topology_kind: "hub_spoke",
|
||||
repo_id: None,
|
||||
created_by: user_id.as_uuid(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
research_topics::set_status(&pool, topic, ws.as_uuid(), "processing")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let done = enqueue_run_with_topic(&pool, ws, "first", topic).await;
|
||||
let _still_queued = enqueue_run_with_topic(&pool, ws, "second", topic).await;
|
||||
topology_runs::complete(&pool, done, &json!({"final_output": "x"}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transitioned = topology_runs::notify_run_completed(&pool, done)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!transitioned, "sibling still queued → hold");
|
||||
let t = research_topics::get(&pool, topic, ws.as_uuid())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(t.status, "processing");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_run_completed_ignores_runs_with_no_research_topic() {
|
||||
let pool = cm_testkit::test_pool().await;
|
||||
let ws = seed_workspace(&pool).await;
|
||||
|
||||
let id = Uuid::now_v7();
|
||||
let graph = json!({"kind": "pipeline", "nodes": [{"id":"n","role":"r"}], "edges": []});
|
||||
topology_runs::enqueue_run(&pool, id, ws, "task", &graph)
|
||||
.await
|
||||
.unwrap();
|
||||
topology_runs::complete(&pool, id, &json!({}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transitioned = topology_runs::notify_run_completed(&pool, id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!transitioned,
|
||||
"no research_topic_id → nothing to transition"
|
||||
);
|
||||
}
|
||||
|
||||
async fn seed_team(
|
||||
pool: &sqlx::PgPool,
|
||||
ws: WorkspaceId,
|
||||
|
||||
Reference in New Issue
Block a user