Symmetric with the research pipeline: every enabled loop can now have
its own per-loop team container so scheduled runs don't share state
with other loops or with research. Same daemon image, same clawmates
network, deterministic name loop-<id>-team.
Backend surface:
- Migration 0040 adds nullable `zeroclaw_container` +
`zeroclaw_gateway_url` columns to loops (parallel to
research_topics).
- research_container.rs grows loop_container_name_for(), spawn_loop()
(state-only mount, no repo), and teardown_loop(). Kept in the same
module to share the docker connect() + inherited_env() plumbing;
each pattern gets its own labels (clawmates.role=loop-team) so ps
filters can tell them apart.
- cm_db::repo::loops gains set_zeroclaw_container() +
zeroclaw_gateway_url() (dynamic sqlx queries — no offline cache
regen needed).
- cm_db::repo::topology_runs gets loop_id_for_run(): mirror of
research_topic_id, used by the worker.
Wiring:
- routes/loops::run_now + webhook_receive call ensure_loop_container()
before enqueuing an iteration. Idempotent: an already-running
container is just reattached. Failures are logged and do NOT block
the enqueue — topology_worker falls back to the workspace gateway
when the URL isn't set on the loop.
- routes/loops::disable_loop + delete_loop both fire teardown_loop()
so paused / deleted loops don't hold a docker slot.
- topology_worker's per-run URL resolution: existing research fast
path unchanged; when it doesn't hit, the worker now looks up
loop_id and reads the loop's gateway URL.
Deploy step (required on gw-04 for state to persist across container
restarts): add a `/var/lib/clawmates-loops:/var/lib/clawmates-loops`
bind mount + `CLAWMATES_LOOPS_STATE_ROOT=/var/lib/clawmates-loops`
env var to clawmates_server_1 in the compose. Without it, loops still
run — the state dir lives inside the API container's filesystem so
persistence is limited to that container's lifetime.
Follow-up:
- Scheduler-tick fires (cron-driven, not run_now) — they call
enqueue_iteration in cm-scheduler and don't yet go through
ensure_loop_container. Add a symmetric spawn there so cron fires
also land on the isolated daemon.
- Compose file reconciliation — deploy/compose/docker-compose.yml in
the repo has drifted from prod; when we sync it, add the loops mount
at the same time.