The prior flow was ambiguous: after hitting Start research, status
flipped to "processing" and a "Submit for review" button appeared
immediately with no indication that anything was actually running.
Users had to guess whether the pipeline was working or stalled.
Backend surfaces the truth as a signal:
- new topology_runs::active_runs_for_research_topic counts
queued+running runs whose research_topic_id matches
- TopicDetail includes runs_in_flight: i64 alongside the existing
status field, so the canvas can distinguish "pipeline still
working" from "runner stalled".
ResearchCanvas is now honest about state:
- while runs_in_flight > 0, the header status pill grows a cyan
"N runs in flight" badge with an inline SVG spinner
- the stage-explainer card turns cyan-bordered and shows a
"pipeline is running" hint, plus copy pointing the user at the
Agents tier where each teammate's activity streams live
- the "Submit for review (manual)" button is HIDDEN while any run
is in flight — it's an escape hatch for stalled runs only, not
the happy-path action. It reappears if runs_in_flight drops to
zero but the topic is still marked processing, so a stalled
runner can still be nudged along.
- the canvas polls getTopic every 4s while status is processing/
publishing or runs_in_flight > 0, so the spinner + outcome swap
in automatically when the pipeline completes.
ResearchList sidebar:
- each row's status dot becomes a spinner when the topic's status
is processing or publishing, matching the canvas at a glance
- the list also polls every 6s while ANY topic is active, so
transitions land in the sidebar without waiting on a parent bump.
The poll is gated on a derived boolean to avoid effect thrash.
Follow-up: same pattern belongs on LoopsList / LoopsCanvas for
loop iterations in flight — same signal (queued+running runs per
loop) but not wired here.
Three connected changes that turn "Start research" from a status
flip into a real pipeline that produces a reviewable artifact:
- Migration 0036: adds research_topics.topology_kind (default
'hub_spoke') and a new research_outcomes table
(id, topic_id, version DESC, body_md, produced_by_run_id, created_at)
so each run's final synthesis is versioned and persistent.
- Wizard now has a topology picker in the Outcome step —
hub_spoke / pipeline / hierarchical / star_moe — with copy that
steers users to the right shape (Pipeline for research → distill
→ analyze → implement rosters, hub_spoke for the coordinator-
and-specialists default).
- start_topic reads the chosen topology_kind, parses it into a
cm_topology::TopologyKind, and dispatches a per-shape coordinator
prompt via build_coordinator_task. Pipeline explicitly tells
stage 1 not to write the final artifact and propagates a
"final stage MUST emit a complete markdown document with
measurable acceptance criteria" instruction downstream. The
graph builder is called with the topology the user actually
picked instead of hard-coded HubSpoke.
- topology_worker::freeze_research_outcome fires after every
successful complete(). It looks up research_topic_id on the run;
if set and final_output is non-empty, it inserts a new
research_outcomes row (version auto-derived server-side via
coalesce(max(version), 0) + 1). Best-effort — a DB hiccup logs
but doesn't fail the run.
- TopicDetail now includes topology_kind and latest_outcome.
ResearchCanvas swaps in the outcome's body_md (rendered as
pre-wrap markdown, versioned header, produced-at timestamp)
whenever an outcome exists; the original prompt collapses into
an "Original prompt" <details> below so it's still one click
away. Pre-run topics still show the description as before.
Follow-ups still open: reject-with-revision loop feeding the
coordinator, publishing → published transition + real artifact
export (md / pdf), and an approvals inbox surface for reviewers.
The prior start_topic only flipped the status column — no work was
enqueued. Now clicking "Start research" actually launches the
assigned agents through the orchestrator.
- start_topic loads the topic + its research_topic_agents, picks a
coordinator (first slot with role_slot containing "coordinator";
else the first slot), and swaps it to index 0.
- Builds a hub_spoke topology graph via cm_topology::build with
roles = [coordinator, spoke1, spoke2, …]. hub_spoke wires edges
from the hub to every spoke and back, so the coordinator can
address any specialist per turn.
- Assembles a coordinator prompt from the topic's title,
description, outcome_kind, and a roster line for each teammate —
so the coordinator knows who's on the team and what each does.
- Enqueues via a new topology_runs helper
enqueue_run_for_research_topic that stores research_topic_id on
the run row. `topology_worker::maybe_transition_research_topic`
→ `notify_run_completed` already picks up on that back-ref and
flips the topic processing → reviewing when the last run
terminates — that path was dead code until now.
- Per-agent activity streams into each claw's card for free:
the orchestrator journals turn events into run_events; the
existing /api/world/live SSE normalizer emits
agent.reasoning.delta / agent.tool.call / agent.task.update
keyed by agent id, which ClawCommandCenter is already
subscribed to.
The `published` terminal state is still unreached (that's the
"publishing → published + artifact" step from the earlier
walkthrough — separate follow-up).
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.
Hooks the topology_worker's post-terminal path into a new
notify_run_completed repo helper that atomically transitions the topic
processing → reviewing when the completed run has research_topic_id set
AND no siblings for that topic are still queued or running. Guarded on
status='processing' so a retry, a re-fire, or a topic already past
processing are all no-ops. Best-effort at the worker; DB hiccups are
logged and never fail the run.
The manual /submit-review endpoint stays as an escape hatch for topics
that end up parked in processing with nothing to complete (updated the
doc comment).
Extend the topology-runs list route with an optional loop_id filter that
returns iterations for a single loop, newest-iteration-first. Adds the
iteration and finished_at columns to the summary (skip-null on the JSON
so compares stay compact). Backed by list_by_loop in the repo, which uses
the existing topology_runs_loop_idx partial index.
LoopsCanvas fetches the runs in parallel with the loop detail and renders
an iteration timeline card (iteration #, status pill, start time, duration,
run id prefix) between the graph section and the actions row.
#2 GLM judge (closes#114): registry NamedProvider gains a `format` field;
build_provider_registry builds an AnthropicProvider for format="anthropic".
GLM's coding/OpenAI endpoint is ToS-throttled for raw SDK, but its Anthropic
endpoint (api.z.ai/api/anthropic) accepts raw API calls (verified x-api-key
-> glm-4.7), so CLAWMATES_JUDGE_MODEL=glm:glm-4.7 routes the door governor /
topology judge through GLM with no runtime-routing. (Kimi-as-judge still needs
a Platform key — coding key is agent-only.)
#3 run-control: POST /api/topology-runs/{id}/cancel (workspace-scoped,
queued/running only); the worker honors it at the step boundary (checks
current_status in the checkpoint callback) and won't clobber a cancel with
`failed`. Frontend Run tab gains a Cancel button and clickable recent runs
that deep-link into a live/replayed stream (SSE replays from checkpoint).
cm-* tests (incl. new cancel test) + clippy + frontend lint/typecheck green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Evolve topology_runs into a durable-job table (migration 0009): status state
machine (queued/running/completed/failed/cancelled), kind, input graph,
per-step checkpoint, error, last_event_id, timestamps. comparison becomes
nullable (the result blob, absent until completion). Back-compat: existing
rows default to completed/compare.
cm-db repo gains the durable-job ops: enqueue_run, claim_next_queued (CAS via
FOR UPDATE SKIP LOCKED), checkpoint, complete, fail, requeue_stale (resume
sweep), and status(). Regenerated .sqlx cache. Also fix two pre-existing test
RuntimeConfig literals missing the providers field (from the registry work).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
saved run. e2e p8 green (39 suite); offline build + clippy clean.
Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.
Co-Authored-By: Claude Opus 4.8 <[email protected]>