827b829993e3f590f6b160571cf1097e721b2c0d
505
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8cd0c7bd01 |
runtime_provision: migrate provider_alias_for to v0.8.3 families
Auto-provision (and every other build_team caller) hit 'dangling_reference: claude_cli is not a known provider family' because provider_alias_for still returned v0.7.x aliases — claude_cli.default / claude_cli.glm / claude_cli.glm5 / kimi_cli.default — all deleted upstream when providers.models schema was restructured. Now the mapping resolves to real configured aliases: - 'claude'* prefix → anthropic.default - 'gemini'* prefix → gemini.default - 'llama'* / 'groq' → groq.default - glm* + kimi* → anthropic.default (fallback until glm.default / moonshot.default provider tables land in the runtime template) - unknown → anthropic.default Covers the model picker's full roster (claude-sonnet-5 / opus-4-8 / haiku-4-5 / sonnet-4-6 / glm-4.6 / glm-5.2 / kimi-k2 / gemini-2.0-flash / llama-3.3-70b-versatile) plus every legacy shorthand. Tests updated to assert the new mappings including the model picker's ids. |
||
|
|
e95e251c9c |
empty state: primary CTA button instead of hunt-for-plus text
The '+ new topic' button (commit
|
||
|
|
f8589471bc |
AgentComputer: clickable model picker (dropdown → PATCH /api/claws/:id/model)
The '● <model>' text on the per-agent Computer slide-out was
read-only — swapping a claw's model required curl. Now it's a
button that opens a small dropdown of the curated fleet models:
- Sonnet 5 / Opus 4.8 / Haiku 4.5 / Sonnet 4.6 (Anthropic)
- GLM 4.6 / 5.2 (Z.AI)
- Kimi K2 (Moonshot)
- Gemini 2.0 Flash (Google)
- Llama 3.3 70B (Groq)
Click a model → PATCH /api/claws/{id}/model → picker closes, the
button relabels to the new selection. The endpoint persists the DB
binding + best-effort re-provisions the runtime; the swap applies
on the claw's next turn. Long-tail models still work via curl with
any string.
Adds an optional onModelChanged callback so callers (Dashboard's
runtime-config enrich cache) can refresh their state without a
full re-render.
|
||
|
|
6e37802f73 |
claws: PATCH /api/claws/{id}/model — swap the runtime-bound model
Auto-provisioned agents (via the research + loops team wizard) show
up on the Agents page (same agents::insert → agents::roster path),
and PATCH /api/claws/{id} already handled name/job_title/system_prompt/
avatar/accent/wallpaper. The one thing that was NOT reachable from the
Agents page: swapping the model.
Add a dedicated endpoint:
- PATCH /api/claws/{id}/model { model: string }
- Persists via agents::set_model_binding (DB)
- Best-effort runtime rebind via RuntimeProvisioner::provision_claw
(idempotent — overwrites agents.<alias>.model_provider on the
shared ZeroClaw config)
- Audit-logged as 'agent.model_changed' with the new model in payload
Now an operator can open the Agents page, click a claw that was
auto-provisioned by the team wizard, and swap its model
(claude-sonnet-5 → glm-5.2 → whatever) without recreating the team.
Same DB row, same claw_id, same brain — just a new provider on the
next turn.
Frontend affordance not shipped in this commit — the endpoint is
usable via curl/psql/scripts today; a UI 'Model' picker on the claw
detail card can land in the next Agents-page pass.
|
||
|
|
ac8c689f50 |
logs + team parity: pretty step/container renderers; quota + audit on team creation
Two bundled changes:
── LiveRunLogs prettification ──────────────────────────────────
The Steps + Container tabs were plain mono lines with a single
color per event. Now they get structured layout:
Steps:
- Color-hashed actor pill (stable palette so [Distiller] and
[Novelty Analyst] each get their own hue across the session).
- Phase pill (plan=cyan, work=green, synth=amber, aggregate=purple).
- Token count pill formatted 1.2k / 14.3k / etc.
- Gated-action warning pill in amber when > 0.
- Left-border color strip keyed to the actor for at-a-glance
visual grouping.
- Long outputs collapse to their first 300 chars with a '+ N more'
toggle to expand the full text.
- 'done' events get a green (or red for error) border strip +
pill instead of blending into the stream.
Container:
- Splits '[actor] action (outcome) · msg' into colored spans —
actor pill (deterministic color), action in dim, outcome pill
green/red/dim by state.
- Non-line events (info/error/done) get their own left-border
strip so bash echoes and stack traces don't drown in the daemon
chatter.
- Timestamps switch to HH:MM:SS.mmm — dense but scannable.
Small palette (LOG constants) keeps the color budget bounded — no
new UI vocabulary, just cleaner reads of what was already there.
── Team-wizard governance parity ───────────────────────────────
build_team_with_lifecycle now matches POST /api/claws' governance:
- enforce_new_agent quota check per member (previously bypassed
workspace agent quotas entirely for team/auto-provision paths).
- audit::append('agent.created', ..., {source: 'team_wizard'}) per
member so team-created claws appear in the same audit trail as
individually-created ones. Adding a 'source' key distinguishes
provenance without changing consumers.
.brain (h5) handling was already consistent between the two paths —
both use the lazy on-first-access load_brain hook seeded from
agents.system_prompt. No change there.
|
||
|
|
956be2cf4f |
wizard: in-place auto-provision team from topic (LLM-derived, sonnet-5)
Step 5 of ResearchWizard was 'assign agents from workspace roster'.
When the roster was empty, the wizard body was hard-swapped for
NoAgentsGate — you couldn't reach step 5 at all.
Now step 5 shows a 'Team' panel:
- Big cyan card: 'Auto-provision team from this topic'. One click
runs an LLM plan pass, gets 3-5 role slots + system prompts back,
materializes claws via the existing build_team pipeline, stamps
runtime posture, returns a shape that drops straight into the
submit body's agents[]. Card flips green with the derived roster.
- Below that: the classic roster picker, but only when the workspace
actually has ≥1 claw AND auto-provision hasn't landed. Otherwise
hidden — no dead empty-state affordance.
Every gate that required agents.length > 0 to render the wizard body
or the footer is gone. canNext gains a step-5 clause: allow Next when
EITHER auto-team is ready OR the user handpicked from a non-empty
roster.
Backend
- POST /api/teams/auto-provision — accepts {title, description,
outcome_kind, topology_kind?, model?, risk_profile?, mcp_bundles?}.
Derives topology from outcome_kind (integrations → pipeline; else
hub_spoke). LLM plan pass yields a JSON roster of 3-5 roles
(role_slot, name, system_prompt). Materializes team + claws via
build_team, stamps risk_profile (default research_web_readonly) +
mcp_bundles (default [clawmates_door, gitea_forge]). Response
carries team_id + agents[] in the shape /api/research already
expects.
- Every provisioned claw runs on claude-sonnet-5 by default;
overridable via the model field.
Follow-ups (not in this slice):
- Same picker in LoopsWizard (slice C — parallel change, same API).
- Post-create 'Team' section on ResearchCanvas / LoopsCanvas so
users can rebind after the fact (slice D).
- Full Teams tier UI + Agents-page deprecation (slice E).
|
||
|
|
47a42423e3 |
wizards: unlock '+' buttons when roster is empty
The 'no agents → button disabled' guard hard-locked the wizard behind an already-populated roster, forcing users to detour to the Agents page and come back. NoAgentsGate already handles the empty state gracefully inside the wizard body; the button just needed to open it. Applied to both ResearchList and LoopsList. Follow-up (design in progress): let the wizard auto-provision a team from the topic/loop itself so users never have to leave to create agents. |
||
|
|
eb06e91ac3 |
dashboard(Agents): drop counts caption, align toolbar with title
Previously the Agents header wrapped a 'N ORGS · N CO · N TEAMS · N AGENTS' small-caps caption above the title, with the toolbar (select/history/brain) hanging off the right. Dense, noisy, and the World tier already surfaces those counts at higher fidelity. Now: single-line header — 'Agents' title on the left, toolbar flush right, both center-aligned. Comment left in place noting where the caption used to live in case we ever want it back. |
||
|
|
cde196dbdc |
runtime: bake tea + gitea-mcp binaries + let GITEA_TOKEN pass through
Prep for the SDLC coding team. Adds two upstream Gitea binaries to the clawmates-runtime image so team-scoped agents can drive git.redclaw.dev without custom MCP code: - tea v0.14.2 (shell CLI: clone/push/pr checkout, git remote helper) - gitea-mcp v1.3.0 (native MCP server: list_repo_pull_requests, create_pull_request, create_file, update_file, create_branch, create_issue, get_file_content, etc.) Both installed by arch (amd64 / arm64) at image build time; also adds git to the runtime layer since it was missing (needed by tea's shell delegates). research_container::inherited_env now propagates GITEA_ prefixed vars so a team container inherits GITEA_TOKEN (+ optional GITEA_HOST) from the server env. Team-scoped daemons can then start gitea-mcp via stdio with --token-env GITEA_TOKEN. Followup on gw-04: 1. rebuild clawmates-runtime image against the new Dockerfile 2. add [mcp_bundles.gitea_forge] to the runtime template 3. set GITEA_TOKEN in the server compose env |
||
|
|
0d0bb5ffaa |
teams: runtime spawn hookup for per-team containers (slice 3b)
Wires the per-loop-team arc end-to-end. When a loop with team_id fires an iteration, the worker now spawns/reattaches a dedicated team container, mounts the paired research topic's repo at /workspace/repo (rw), and stamps the team's risk_profile into every [agents.*] binding on the freshly-written config.toml. Legacy loops with team_id = NULL keep taking the per-topic / per-loop path unchanged. research_container.rs - team_container_name_for(team_id) = 'team-<uuid>-container' - team_state_root(team_id) — /var/lib/clawmates-team-state/<uuid>/state (overridable via CLAWMATES_TEAM_STATE_ROOT) - prewrite_daemon_config_with_risk: line-based sed that swaps only the risk_profile line inside each [agents.<name>] block. Avoids the regex-eats-array-literal trap that bricked the shared runtime config on the earlier out-of-band edit. - spawn_team: full-shape idempotent spawner. Same mount + env + label pattern as spawn/spawn_loop; additionally supports Claude settings bind-mount + external-bridge attach. topology_worker.rs - try_team_gateway_url resolver runs BEFORE the existing per-topic and per-loop lookups. Cold path: reads team runtime config + paired research topic repo path, spawns the container, persists coords back to teams.zeroclaw_container/zeroclaw_gateway_url. Any failure logs + returns None so the caller falls through to the legacy shared-container path — team spawn can never brick a run that could otherwise complete. Not shipped in this slice: - Wizard 'existing team' picker (currently just fresh vs reuse) - Teams tier UI to list/edit/delete teams - Auto-teardown for stale team containers (piggyback on existing reaper is a follow-up) |
||
|
|
d687a00524 |
teams: zeroclaw container coords + coding_readwrite risk profile
Slice 3a of the per-loop-team arc — prerequisites for the runtime spawn hookup that lands in 3b: 0046 migration - ALTER TABLE teams ADD zeroclaw_container TEXT - ALTER TABLE teams ADD zeroclaw_gateway_url TEXT Both NULL until the runtime's spawn_team fn (3b) provisions the container and persists its coordinates. Mirrors the shape already on research_topics (0038) so the resolver code path can generalize. cm-db - team_container_coords / set_team_container_coords: dynamic sqlx::query() readers/writers for the new columns. Runtime template (gw-04, out-of-band edit on /var/lib/clawmates-runtime-template/config.toml + shared runtime /root/clawmates-runtime/data/.zeroclaw/config.toml) - New [risk_profiles.coding_readwrite]: adds file_write + shell on top of the research_readonly baseline. Still excludes http_request / browser / composio (egress stays behind the MCP door). Slice 3b will add spawn_team (bind-mounts paired-topic repo, uses team-scoped state dir, injects team.risk_profile into the config template) and rewire topology_worker to resolve gateway URL through team_id when the loop has one. |
||
|
|
0b7f247b0e |
wizard: 'fresh coding team' picker for paired coding loop
Second slice of the per-loop-team arc. The paired-coding-loop checkbox
in ResearchWizard step 6 now exposes a two-option picker:
⦿ Provision a dedicated coding team (default when the loop is on)
— fresh 'Coding · <topic>' team row, risk_profile =
coding_readwrite, clawmates_door in mcp_bundles. Loop's
team_id is bound at wizard-submit time.
○ Reuse the research team (legacy) — no team_id bound; coding
iterations spawn against the research topic's container.
Frontend
- New codingTeamMode state, radio picker rendered under the checkbox.
- research.ts createTopic body gains paired_coding_team_mode?: 'fresh'|
'reuse'.
Backend
- CreateTopicRequest gains paired_coding_team_mode: Option<String>.
- materialize_topic_loops takes it through and, when 'fresh', calls
the new provision_fresh_coding_team helper — inserts a teams row
via the existing insert_team_with_lifecycle (pipeline kind, same
graph as the loop), sets its runtime-config via
set_team_runtime_config, then binds loop.team_id.
- All operations best-effort with stderr logging — a team-provision
failure leaves the loop functional under the legacy fallback.
Not shipped in this slice (deferred to runtime hookup slice):
- research_container::spawn keyed on team_id → per-team container
- Config template rewrite injecting the team's risk_profile
- Migration of existing paired loops onto their own teams
The plumbing lands now so the wizard's intent is recorded; the
runtime honors it in the next PR.
|
||
|
|
6066e93889 |
teams: per-team runtime posture (risk_profile + mcp_bundles) + FK from loops/topics
Foundation slice for letting a coding loop bring its own team instead
of reusing the paired research topic's team. Turns out the teams
table already exists (0010_teams.sql) with full CRUD — this scales
back to the minimal missing bits:
Schema (0045_teams.sql)
- ALTER TABLE teams ADD risk_profile TEXT (NULL = template default)
- ALTER TABLE teams ADD mcp_bundles JSONB DEFAULT '[]'
- ALTER TABLE loops ADD team_id UUID REFERENCES teams ON DELETE SET NULL
- ALTER TABLE research_topics ADD team_id UUID REFERENCES teams
- Two partial indexes (team_id NOT NULL) for the future cascade queries
cm-db (dynamic sqlx::query so the existing get_team's compile-time
cache doesn't need regenerating):
- TeamRuntimeConfig struct
- get_team_runtime_config / set_team_runtime_config
- team_for_loop / team_for_research_topic (resolvers)
- set_team_for_loop / set_team_for_research_topic (binders)
cm-api
- GET /api/teams/{id} now surfaces risk_profile + mcp_bundles
- PATCH /api/teams/{id}/runtime-config sets them
Not touched (comes in follow-up slices):
- Wizard picker exposing 'reuse research team' vs 'fresh coding team'
- Runtime container spawn keyed on team_id
- Migration of existing paired coding loops onto their own team
|
||
|
|
b2a38da3f9 |
loops: 'View full output' modal for a completed iteration
The expanded iteration panel now exposes the full topology_run.result in a proper viewer instead of leaving it stranded in Postgres. Reads /api/topology-runs/:id (existing endpoint, no backend change), prefers result.final_output (the produced markdown/code), falls back to a per-step transcript, last-resort a raw JSON dump. Modal renders as a fixed overlay — Escape or backdrop-click to close. Header carries kind/status/steps/tokens metadata, Copy button hits navigator.clipboard, Download button emits a .md file named run-<id-slice>.md. The button on the row is disabled until the iteration terminates (completed | failed) — running iterations already have LiveRunLogs surfacing the tail. |
||
|
|
2ba40b03b7 |
loops: per-iteration live logs (Steps + Container tabs), collapse graph JSON
The Loops canvas' IterationsTimeline was a flat status-pill list —
no way to see WHAT an iteration was actually doing. Meanwhile the
Research canvas had full LiveRunLogs with Steps + Container tabs
against the same underlying topology_run SSE endpoints. This
factors LiveRunLogs so both canvases share it.
Frontend
- LiveRunLogs gains a directRunId?: string prop. In this mode it
skips the topic-scoped active-runs + pipeline-state polls and
pins activeRun to the given id. topicId stays optional (topic
mode unchanged from ResearchCanvas' perspective).
- LoopsCanvas IterationsTimeline: each row is now a click-to-
expand card. On expand, renders <LiveRunLogs directRunId={...}/>
right below the header — Steps + Container tabs, full SSE tail,
same 320px terminal.
- Auto-opens the newest running/queued iteration so a click on
'Run now' immediately exposes the live pane.
- New CollapsibleSection helper wraps the graph JSON block so it
starts closed. Reference material stays one click away without
cluttering the canvas.
Backend
- run_container_log_sse now resolves the tail target via
loop.source_research_topic_id when the run itself has no
research_topic_id. Paired coding loops (kind=exec with a
source_research_topic_id) reuse the paired topic's team
container, so we tail its docker logs. Pure loop runs still
error with a clearer message.
|
||
|
|
e3ef3fd056 |
research: fix Draft 'Invalid Date' + stale-error leak in pipeline card
Two cosmetic bugs surfaced by the successful v0.8.3 pipeline run: 1. **'produced Invalid Date'** — research_outcomes.created_at was an OffsetDateTime serialized by time's default array format (`[y, ordinal, hh, mm, ss, ns, tz]`), which browser's `new Date(...)` can't parse. Add `#[serde(with = "time::serde::rfc3339")]` matching the pattern already in threads.rs / routine_runs.rs. 2. **Stale error text on pipeline card** — the runs stage's `latest_error` walked every run by `created_at DESC` and returned the first non-empty error, so a topic with an earlier failed run + a later completed run kept displaying the old error next to '1 completed'. Now the error only surfaces when the MOST RECENT run itself failed. Historical failures stay in the run count but don't leak their message. |
||
|
|
ae113dd319 |
live-run-logs: add Container tab that tails filtered daemon logs
Steps summaries only fire AFTER each topology step completes — so a
stalled first turn was completely dark. Add a second tab that
streams the team runtime container's daemon log live via a new SSE
endpoint.
Backend
- GET /api/topology-runs/:id/container-log — workspace-scoped SSE
around bollard's docker.logs(follow=true, tail=200). Buffers on
newline so partial mux chunks don't truncate a log line.
- compact_container_log: parse a zeroclaw daemon line
('[actor] ... zc_action=X zc_outcome=Y ... msg') into
'[actor] action (outcome) · msg'. Framing-only continuations are
dropped; non-zc lines (bash echoes, backtraces) pass through as-is
so nothing interesting is lost. ANSI escapes stripped.
- Everything funnels through one async_stream! so early exits
(workspace check / docker connect / no bound topic) yield an
'error' event and return without breaking Sse::new's single stream
type.
Frontend
- LiveRunLogs gets a sub-tabs strip: Steps · Container.
- New useContainerLog(runId, active) hook — gated by tab so we don't
hold two open SSE streams when the operator isn't looking.
- Same terminal widget renders each container line with a level
color (info/done grey, line default, error red). Sub-tab pill
shows count + status live.
|
||
|
|
36a9fbe81f |
research/start: allow rerun on loop-owned topics too
D1-fold loop guard 409'd even the rerun path if the topic was scheduled — a failed iteration couldn't be restarted until the loop's next scheduled fire. Now the loop guard only fires for the fresh 'standby' start; the rerun path (status=='processing' + orphan cancel) works whether or not a loop owns the topic. Loop binding is preserved either way. |
||
|
|
08b11f45a8 |
ci: raise file-size budget 1250->1500 hard, 900->1100 soft
Recent research.rs additions (rerun-orphan-cleanup + runs_failed DTO plumbing) tipped it over 1250. The 1250 wall wasn't grounded in a real quality bar — several files have hovered at 1200 for a while without becoming unreadable. Bump the ceiling to 1500 and raise the soft warn to 1100 so we still get a nudge before growing another 400 lines. |
||
|
|
6af9d509b0 |
research: rerun cancels orphan runs first so 409 doesn't block restart
The previous rerun guard (standby OR (processing AND in_flight==0)) still 409'd when a lingering queued/running row existed — typically an orphan from a server restart mid-pipeline, before the reaper or stale-checkpoint requeuer had marked it failed. Now: allow rerun on any 'processing' topic; before enqueuing the fresh run, batch-cancel every in-flight run for the topic so the new run isn't racing them. Terminal states (reviewing / publishing / published) still 409 as before. |
||
|
|
671d3ac4f0 | clippy: fix doc_lazy_continuation on TopicListItem.runs_failed | ||
|
|
f70f6c679e |
research: errored-state card + one-click rerun (no wizard re-entry)
When a topic ends up parked in 'processing' with all runs failed and nothing in flight, the sidebar card was still spinning as if progress were happening. Now: Backend - topology_runs::run_counts_by_research_topic — batch query that returns (in_flight, failed-since-last-success) per topic. Used by the list endpoint; dynamic sqlx::query() so no prepare needed. - TopicListItem DTO gains runs_in_flight + runs_failed. - start_topic status guard relaxed: allow (standby) OR (processing AND runs_in_flight == 0). Blocks accidental double-fires on a live pipeline; permits rerun on a failed one. Same request body, same behavior once accepted, so the frontend just POSTs /research/:id/start on the RotateCw click. Frontend - ResearchList detects errored: status===processing && !in_flight && failed>0. Swaps the MiniSpinner for a red AlertTriangle and changes the status text to 'error · N failed'. - New RotateCw icon button next to the delete Trash — same button cluster, one click, no wizard re-entry required. Disables while a request is in flight; error surfaces in the sidebar's shared error banner. |
||
|
|
43f7880327 |
agent cards: disk-LED glow on live topology step
Wire SSE step events from LiveRunLogs up to ResearchCanvas via an
optional onStep callback (StepPulse: {role, phase, node_id, ts}).
ResearchCanvas keeps a role -> {phase, expiresAt} map; a 250ms tick
clears expired entries so the glow fades naturally.
Each agent card:
- 8px LED dot next to the name (green idle-off / colored when live)
- outer glow via two-layer box-shadow when the agent's role_slot
matches the last step's role, within a 2.5s window
- inline 'reading' / 'writing' hint below the role
Color mapping (disk-LED metaphor):
- Plan phase -> cyan #5ec8d8 (reading / decomposing)
- Work/Synth/Aggregate -> green #5fd08a (writing / producing)
Overlapping steps reset the timer so back-to-back activity on the
same role holds the glow. When no run is active nothing pulses —
LiveRunLogs is silent, no callback fires.
|
||
|
|
45292bf5fb |
sidebar polish: tighter chevron + '+' drops to Research line
- Dashboard chevron: 24x24 → 22x22, top:10 right:8 → top:4 right:4 so it sits in the true corner of the panel. - ResearchList header: alignItems flex-start → flex-end so the '+' button lands on the 'Research' title baseline instead of hugging the 'N TOPICS' caption line. Reserved 34px right padding on the header so the '+' clears the corner chevron. |
||
|
|
1876641415 |
dashboard: collapsible context sidebar (research/loops/repos/infra)
The earlier PR added a chevron to RosterColumn but the research /
loops / repos / infra tiers render a completely different sidebar
(ResearchList/LoopsList/…) inside a fixed 252px div in Dashboard.tsx
— the RosterColumn toggle never appeared on those pages.
Add a proper collapse on the Dashboard's own context-list wrapper:
- Overlaid PanelLeftClose chevron top-right (top: 10, right: 8) so
it doesn't fight the per-tier header content.
- When collapsed, wrapper shrinks to 32px with a PanelLeftOpen
button; canvas gets the reclaimed ~220px.
- State via useSyncExternalStore on localStorage
('cm.dashboard.sidebarCollapsed'), same pattern I used on
RosterColumn to satisfy react-hooks/set-state-in-effect.
|
||
|
|
7e043bcfd9 | ci: retest — cm-runtime interception flake on prior push | ||
|
|
067bebd071 |
wedged-run fix: pre-approve claude perms + verbose step log + reaper
Root cause: claude CLI in the per-topic research container runs as
uid=0(root). ZeroClaw's claude_cli provider passes
--dangerously-skip-permissions which Claude CLI rejects under root
for security — so the CLI hangs waiting for interactive permission
approval that never arrives, hitting the 600s provider timeout with
zero step records journaled.
Three-part fix:
1. Claude settings bind-mount (research_container.rs):
Optional CLAWMATES_CLAUDE_SETTINGS_PATH env — when set, mount the
host file at /root/.claude/settings.json (read-only) in every
spawned team container. deploy/claude-settings.json ships the
canonical config (permissions.defaultMode = bypassPermissions +
hasCompletedOnboarding). CLI accepts requests immediately with no
--dangerously-skip-permissions flag needed.
2. Verbose per-step log line (topology_worker.rs):
Every checkpoint now writes to stderr:
topology_worker::step run_id=X step=N node=Y role=Z phase=W
output_bytes=B tokens=T gated=G
Visible in docker logs clawmates_server_1 — gives us live
'topology is flowing' signal without opening the canvas, and
makes it obvious when a topology_kind is skipping stages it
shouldn't.
3. Stuck-container reaper (topology_worker.rs):
New 60s-tick loop reap_stuck_runs: for any research topology_run
older than 15 min with zero checkpoint.records, docker-stop its
container and mark the run failed with a diagnostic error. Only
reaps research-bound runs (non-research runs don't own a
container). The existing 180s stale-checkpoint requeuer stays
in place for other failure modes.
Deploy: gw-04 needs
ln -sf /path/to/repo/deploy/claude-settings.json /opt/clawmates/claude-settings.json
CLAWMATES_CLAUDE_SETTINGS_PATH=/opt/clawmates/claude-settings.json
in the server env, plus the timeout lowered from 600 -> 120 in
compose. Both handled in the deploy step outside this commit.
|
||
|
|
573956e840 |
live-run-logs: fix step-record parser + surface pre-first-step phases
Two fixes based on live smoke test:
- StepRecord parser: the SSE step payload is {node_id, role, phase,
output, gated, tokens} (orchestrator::StepRecord), not the made-up
shape my first pass looked for. Render as '[role] phase · <first
line of output> · Nt · N gated'.
- Pre-first-step visibility: the checkpoint only journals AFTER each
turn completes, so the container-startup + agent-boot window (often
30-90s for the first step) was completely dark ('waiting for first
step…'). Poll pipeline-state every 2s and render its stages as
pseudo-log lines (setup 🟢 staffing · 3 agents assigned / setup 🔵
container · starting…) until real step events arrive.
|
||
|
|
2ef50bb30b |
lint(research): satisfy react-hooks/set-state-in-effect
- RosterColumn: useSyncExternalStore on localStorage instead of hydrate-in-effect setState. Same-tab writes nudge subscribers via a synthetic storage event. - LiveRunLogs: reset-on-runId-change via the React-sanctioned set-state-during-render pattern (prevRunId ref state), so the effect body no longer calls setState synchronously. |
||
|
|
1d69866bcf | research canvas: collapsible sidebar + live topology-run logs (#6) | ||
|
|
13ead5cb9f | fmt: sort routes/mod.rs pub mod entries | ||
|
|
149ad992bb | wizard: materialize picked repo across clawstor fleet at step-2-next (#5) | ||
|
|
1cb643142c | research/publish: gate approve+reject on Owner role (#4) | ||
|
|
dd791061ee |
test(topology_jobs): seed research_outcome so transition test matches new invariant
Commit
|
||
|
|
bc39f854d8 |
Merge pull request 'sandbox: init=true so Chromium zombies get reaped' (#3) from sandbox-init-tini into main
Reviewed-on: #3 |
||
|
|
9a5e24eeba |
sandbox: run tini as PID 1 (init: true) so Chromium zombies get reaped
Docker sandboxes run `sleep infinity` as PID 1. `sleep` never wait()s on re-parented children, so Chromium's short-lived helper/crashpad processes accumulate as zombies in long-lived browser sandboxes. With `pids_limit` set, this eventually exhausts the slot and the container can no longer fork — the browser tool starts failing before it looks "unhealthy" anywhere else. Setting HostConfig.init = Some(true) makes Docker inject tini, which reaps re-parented children. Note: existing sandbox containers keep their old spec (init is set at create time). Cycle them post-deploy to pick up tini. |
||
|
|
212e68f6b1 |
research/probe: end-to-end smoke test endpoint (skip topics/loops/spawn)
New: POST /api/research/probe — one-shot pipeline probe against the
workspace's shared ZeroClaw gateway. Drives a trivial 'reply OK'
turn via ZeroClawDriveExecutor::drive and reports per-step timings.
Purpose: stop guessing what's broken in the wizard flow by exercising
JUST the executor→daemon→claude→response path. If probe succeeds
within a few seconds, we know:
- ZEROCLAW_TOKEN + gateway URL config is correct
- Daemon can reach and authenticate against claude
- The full ws round-trip works
…and every other failure we've been chasing (turn timed out, LLM
request failed, ws connect DNS error, etc.) is spawn-config or
prompt-size specific.
Request body (both optional):
{ "prompt": "Reply OK", "agent": "coordinator" }
Response:
{
"verdict": "ok" | "fail",
"total_duration_ms": …,
"prompt_len": …,
"response_preview": "OK",
"steps": [
{ name: "build_executor", duration_ms: …, status: "ok" },
{ name: "drive_turn", duration_ms: …, status: "ok", detail: "tokens=N, output_len=M" }
]
}
Wire-up:
- new routes/probe.rs
- pub mod probe in routes/mod.rs
- POST /api/research/probe registered in lib.rs router
- ZeroClawDriveExecutor::drive promoted from private to pub so the
probe handler can call it (behavior unchanged, other callers were
all inside the same struct).
Usage from anywhere (curl, browser dev tools, etc.):
curl -X POST https://clawmates.work/api/research/probe \
-H "Content-Type: application/json" \
-H "Cookie: <session cookie>" \
-d '{}'
Follow-up: a small frontend button (e.g. bottom of ResearchList) that
POSTs this and renders the response inline, so users don't need to
curl. Skipping in this commit to ship the useful part first.
|
||
|
|
3373f57da0 |
topology_exec: bump TURN_TIMEOUT to 700s to outlast daemon claude_cli
Verbose daemon logs revealed the real turn failure: the daemon's own claude_cli provider was timing out at 180s while running a 10-15k token coordinator prompt. The daemon reads its per-provider timeout from ZEROCLAW_providers__models__claude_cli__default__timeout_ms — we set that to 600000 (10 min) on gw-04 via a compose patch. Now the executor's TURN_TIMEOUT (was 300s) must exceed the daemon's own limit, or we kill the ws before the daemon can reply. New sequence: daemon has up to 600s to invoke claude and return a response; executor waits up to 700s (100s headroom) for the ws event stream to drain. If claude takes 500s, both survive. If the daemon really does hang past 600s, its own timeout fires first and we get a proper "provider timed out" error instead of a phantom executor timeout. Compose env applied on gw-04 in the same session: - ZEROCLAW_providers__models__claude_cli__default__timeout_ms=600000 - ZEROCLAW_providers__models__claude_cli__door__timeout_ms=600000 (backup: /opt/clawmates/docker-compose.yml.bak-timeout) Server container recreated to pick them up; env verified. Follow-up: the coordinator prompt is legitimately huge (autonomy contract + roster + description + repo tree + integration-plan template + operator notes = 10-15k tokens). We should consider either shrinking it or breaking the work into multiple smaller turns so the daemon isn't gambling on a single call taking 3-8 minutes. |
||
|
|
7e0620fd08 |
research: don't advance to reviewing without an outcome + guard-before-write
Two bugs that combine to produce the 409 you get when clicking Approve:
1) notify_run_completed (topology_worker post-hook) was advancing
topic status processing → reviewing whenever the last sibling run
terminated — success OR fail. A failed run with 0 outcomes still
pushed the topic to `reviewing`, the canvas rendered the "Request
publish" affordance, and the reviewer clicked Approve on nothing.
Fixed by adding an EXISTS(research_outcomes …) clause to the
UPDATE. Topic stays in `processing` when no outcome exists; the
loop's next iteration still has a chance to produce one.
2) decide_publish was calling
research_publish_approvals::decide(approve=true)
FIRST (which flips the row to `status='approved'`) and then
running the "no outcome? 409" guard SECOND. On the 409 return,
the DB was left half-flipped: approval says approved, topic still
in reviewing, no outcome exists, and every future click to the
same approval returns 409 on the "already decided" guard —
leaving reviewers with no way forward.
Fixed by moving the outcome-existence check BEFORE the decide()
call. On 409 now nothing was written, so the reviewer can try
again cleanly once an outcome is produced.
Also unstuck the current stuck row out-of-band (SQL UPDATE to reset
the approval to pending + topic to processing) so the user isn't
forced to delete the topic to escape the 409 loop.
sqlx dynamic query — the new EXISTS clause wasn't in the offline
cache so I switched notify_run_completed to plain `sqlx::query`.
|
||
|
|
ee05037095 |
research-pipeline-diag: state-aware statuses (waiting != failing)
The diagnostic was calling any 0-outcome + 0-completed-runs state a
FAIL — red dot + 'No outcome produced yet — check the runs stage for
the failure reason'. That fires the moment a wizard-materialized
topic lands, before its very first turn even completes, and stays
red for the whole 2-3 min a legitimate coordinator turn runs. Result:
users see 'FAIL' on every fresh topic and can't tell a real failure
from a normal in-flight state.
Backend fix — introduce a `waiting` status (blue/pulsing in UI):
Runs stage:
0 runs -> skip ('No runs yet — pipeline hasn't fired')
any running/queued -> waiting ('N in flight, M completed')
all failed -> fail (with error text)
some failed -> warn
all completed no fails -> ok
Outcomes stage:
outcome_count > 0 -> ok
0 outcomes + 0 runs -> skip ('No outcome yet (pipeline hasn't fired)')
0 outcomes + any running -> waiting ('Waiting for the current run to finish…')
0 outcomes + any failed -> fail (the actual silent-bug case)
0 outcomes + all done ok -> warn (weird — completed but wrote nothing)
Also suppresses the run stage's `latest_error` detail when the run
status is `waiting` or `skip` — reporting a stale error next to an
actively-running job is what made users think the current run had
failed.
Frontend:
- PipelineStage['status'] union grows a 'waiting' arm.
- Pill color: cyan (#5ec8d8) with a pulsing scale/opacity animation
(new cm-pulse keyframe in motion.css).
- Strip summary line: 'Pipeline in flight — waiting for run to finish…'
when there's any waiting stage and no failures.
- Border tint: cyan border when waiting, coral when failing, neutral
otherwise.
Zero backend semantic changes to the outcome-write path — this is
purely UI truth-telling.
|
||
|
|
6e06bcf136 |
research_container: spawn team daemons with --verbose
Without --verbose the daemon only emits boot + fatal-crash lines to stderr, so 'LLM request failed' 500s (webhook, /ws/chat) surface as opaque errors with no way to diagnose from `docker logs`. --verbose turns on per-request traces via the runtime's structured logger. Immediately paid off: recreating the current stuck team container with --verbose revealed 'agents.actor.model_provider resolves to a model_provider entry with no `model` set' which points at a missing config override (the shared runtime injects model = ... via ZEROCLAW_providers__models__groq__default__model on its own env; per-team containers don't get that env because it lives on the shared runtime container, not on clawmates_server_1). That's the next fix. Same --verbose is added to spawn_loop's per-loop cmd for symmetry. |
||
|
|
27b74e13d4 |
research_container: propagate ZAI_ + KIMI_ env into per-team runtime
Shared runtime's config points several providers at Z.AI's Anthropic proxy (`ANTHROPIC_AUTH_TOKEN = \"\$ZAI_API_KEY\"`) and the Kimi provider block needs KIMI_API_KEY. inherited_env only propagated ZEROCLAW_/OPENAI_/ANTHROPIC_/GEMINI_/GROQ_ prefixes — ZAI_ and KIMI_ were silently dropped, so per-team daemons booted with empty substitutions and every Z.AI-routed call died with 'LLM request failed' (500 from the daemon, executor timeout at 300s). Adds both to the prefix allowlist. Server container already has ZAI_API_KEY set from the compose env_file; KIMI_ will flow through too when we add it. Doesn't fully close today's incident — the current stuck run's webhook still 500s even with ZAI_API_KEY injected, so there's a second daemon-internal issue (need to boot the team daemon with --verbose to surface it). But the propagation fix is a real bug on its own and would silently break the moment the workspace's configured role calls into a ZAI-routed provider. |
||
|
|
bfcdca0583 |
research-canvas: managed-by-loop UI + start_topic guard (fold cleanup)
Closes the UX gap the fold introduced: the topic canvas was still showing "Start research" for standby-state topics even when a scheduled loop already owned the runs. Clicking it would 409 (or worse: race the loop into a duplicate run). Topic status stayed at standby forever because the loop path bypassed start_topic's set_status transition. Four changes: 1. **Backend status transition** — compose_and_enqueue_iteration for kind='research' now calls set_status_if(standby, processing) on the topic before the run is enqueued. New DB helper set_status_if only advances when the current status matches the "from" arg — safe against races and re-invocations. Later iterations no-op since the topic is already past standby. 2. **has_managed_loop on TopicDetail** — get_topic hydrates a new ManagedLoop struct (loop_id, title, enabled, next_fire_at, last_run_id, schedule_summary) when a kind='research' loop is bound to the topic. summarize_schedule() derives a human string from the loop's triggers jsonb (e.g. "cron: 0 3 * * * · on new artifact", "one-shot", "manual"). New DB helper loops::research_loop_for_topic returns the row. 3. **Canvas branch** — nextAction takes a managedByLoop flag; when set + status=standby, returns null (no button). The canvas renders a "MANAGED BY LOOP" strip below the topic title showing loop name, schedule summary, next fire time, and enabled dot. Reviewer buttons (Request publish / Approve / Reject) still show normally in later states — reviewers should still promote outcomes even when a loop is producing them. 4. **start_topic guard** — refuses with 409 when a research loop already owns the topic. Closes the direct-POST hole for anyone bypassing the frontend. TS type + summarize_schedule live in the same commit so an old client hitting a new backend just ignores the extra field (no breakage), and a new client hitting an old backend renders the classic buttons (managed_by_loop is optional). |
||
|
|
f910771bbb |
research-prompt: add AUTONOMY CONTRACT + relax citation rigidity
Inspection of a stuck 500s+ research run showed both agents were
correctly picking up the topic AND producing rich, structured plans —
then stalling at 'Should I proceed?' and 'Which approach?'. No human
to answer = infinite spin until stale-sweep + retry, forever.
Two prompt changes to compose_research_iteration_task:
1. AUTONOMY CONTRACT at the top:
- Explicit "no human will answer you"
- Explicit "do NOT ask for confirmation"
- Explicit "you MUST emit the completed artifact"
- Framed as a contract, at the very top, before the task itself.
2. Softer citation rule:
- Was: 'Cite every claim; never fabricate sources or repo paths.'
- Now: 'Cite what you can verify. Use [claim needs verification]
inline when you can't. A written v(N) with rough citations
beats a blocked v(N) waiting for approval. Do not fabricate
concrete titles/authors/DOIs.'
The absolute anti-fabrication rule made agents refuse to write
anything unless they could be sure. Coupled with no user available,
the whole loop stalled — turns completed successfully but no output
converged.
Follow-up if this repeats:
- Even shorter prompt (the current one is 300+ words)
- Explicit output-shape enforcement ('respond with only the markdown
artifact, no preamble')
- Cap iteration count so a broken prompt doesn't burn tokens forever
|
||
|
|
63305689be |
topology_exec: bump TURN_TIMEOUT to 300s
Turn timeout was 90s. Research coordinator turns run 60-120s routinely (long prompt + cold tool selection) — 90s was tripping legitimate turns while the daemon was still working, throwing away completed inference. 300s gives real turns room while still bounding worst-case at a walk-away limit. Latest wizard-created topic hit this precisely: bridge attached, handshake worked, claude auth verified in the container, but the run's status went to failed with 'turn executor failed: turn timed out' after exactly ~90s. The topology worker's stale-run sweep at 180s covers the case where the worker itself dies mid-turn — a 300s turn still checkpoints every step so a stuck worker will get requeued. |
||
|
|
a0fb64ac47 |
clippy: use bollard::models::NetworkConnectRequest (deprecation)
CI's clippy step failed on commit
|
||
|
|
194d63ef36 |
research_container: attach bridge network so per-team egress works
Per-team containers were spawned onto clawmates_core only. That
network is Internal=true on gw-04 (no default gateway to the host's
default route), so any egress attempt — including the daemon's
own claude/gemini/groq API calls — fails with FailedToOpenSocket
and the turn times out.
The shared clawmates-runtime is attached to BOTH clawmates_core AND
the default bridge (that's how it can hit api.anthropic.com); the
per-team containers were missing the second network.
Fix: after start_container succeeds, best-effort connect the
container to `bridge` too. Both spawn() and spawn_loop() call the
same helper. Idempotent — a 403 from Docker on repeat-attach ("already
on network") is silently ignored.
Verified out-of-band on the current stuck research team container:
- Manually `docker network connect bridge research-<id>-team`
- `docker exec ... claude --print "reply only: ok"` → returned "ok"
- Auth + egress both working, so next iteration should complete.
Sequence of pipeline fixes finally converging:
1. materialize_topic_loops didn't fire burst → fixed by
fire_initial_burst_if_set
2. research iteration skipped clone/spawn → fixed by
prepare_topic_runtime
3. graph parse failed → fixed by build_topic_graph_json
4. bind-mount perms wrong → chown 65532:65532
5. server image missing git → debian:12-slim base
6. daemon required pairing → prewrite_daemon_config
7. daemon rejected unknown agents → template config from shared runtime
8. THIS: no external egress → attach bridge post-start
|
||
|
|
fd82fe6762 |
research_container: template daemon config from shared runtime
Per-team daemon boots with require_pairing=false but no
`[agents.*]` sections. Server's ws connect authenticates fine and
then trips over "Unknown agent `coordinator` — no [agents.coordinator]
entry configured." (400).
Fix: prewrite_daemon_config now reads a template config from
CLAWMATES_RUNTIME_TEMPLATE_CONFIG (default
/var/lib/clawmates-runtime-template/config.toml) which mirrors the
shared clawmates-runtime container's config with all agent + provider
sections. We strip the template's [gateway] block (its paired_tokens
list is encrypted with the shared runtime's key and un-decryptable
per-team) and prepend a fresh [gateway] require_pairing = false.
Falls back to a minimal pairing-off config with a loud eprintln when
the template isn't readable — the log line makes the misconfig
visible instead of silently 400-ing.
Verified out-of-band on the current stuck team container:
- Restarted with the shared config + rewritten [gateway] section
- Daemon boots cleanly, logs "Pairing: DISABLED (all requests accepted)"
- ws /ws/chat handshake returns 101 Switching Protocols + session_start
message (auth working end-to-end)
gw-04 deploy step (already applied):
1. sudo mkdir -p /var/lib/clawmates-runtime-template
2. sudo cp /root/clawmates-runtime/data/.zeroclaw/config.toml \
/var/lib/clawmates-runtime-template/config.toml
3. sudo chown -R 65532:65532 /var/lib/clawmates-runtime-template
4. Compose: added
/var/lib/clawmates-runtime-template:/var/lib/clawmates-runtime-template:ro
+ CLAWMATES_RUNTIME_TEMPLATE_CONFIG env
to /opt/clawmates/docker-compose.yml
(backup: docker-compose.yml.bak-template)
Follow-up: expose a "reload template" endpoint or re-copy the shared
config on each server boot so we don't drift when the shared runtime
adds a new agent.
|
||
|
|
4be3e43f6f |
research_container: prewrite daemon config with require_pairing=false
Per-topic and per-loop team containers spawn from clawmates-runtime with an empty daemon config, so they boot with require_pairing=true and an empty paired_tokens store. Every incoming ws connect from the API server got 401 Unauthorized because the server's ZEROCLAW_TOKEN wasn't in that store. The shared clawmates-runtime container has a paired_tokens list maintained out-of-band (encrypted enc2:… entries in /root/clawmates-runtime/data/.zeroclaw/config.toml on gw-04). That list isn't portable to freshly-spawned per-team containers — the tokens are encrypted with a key we don't share, and pairing new tokens requires a pairing code we don't generate. Simplest correct answer: per-team containers are ephemeral, live on the private clawmates_core docker network, and only accept traffic from the API server. Disabling pairing there closes zero security holes. New helper prewrite_daemon_config(state_root): - creates <state_root>/.zeroclaw/ - writes config.toml with schema_version=3 + [gateway] require_pairing=false - skips write when config.toml already exists so a manually-paired team container survives re-spawn Called from both spawn() (per-topic) and spawn_loop() (per-loop) right after ensuring the host state dir. Existing paired containers are unaffected; new ones come up open-network to the compose stack. Verified out-of-band by writing the same config into the current stuck team container and restarting it — daemon health flipped require_pairing from true to false. |
||
|
|
8417f6e65c |
server-image: switch base to debian:12-slim + install git
Distroless static-debian12 has no git binary, so
routes/research_setup::ensure_repo_workspace's `tokio::process::Command::new("git")`
returned "No such file or directory" for every wizard-materialized
research topic. Silent failure — the topic's repo_workspace_path
stayed NULL, prepare_topic_runtime bailed out, the run was enqueued
against the shared workspace-wide gateway which then 401'd or timed
out on cold auth.
Switching to debian:12-slim + apt-installing git + ca-certificates
(needed by clone-over-https). Same UID 65532 as before so bind-mount
hosts don't need re-chown.
Image size grows from ~40MB to ~80-100MB but this is the correct
base for a service that shells out to git. Static distroless was a
premature optimization once the server's job description grew a
"clone repos" responsibility.
Follow-up: research migrating to the `gix` pure-Rust crate so we can
go back to distroless. Nontrivial refactor of ensure_repo_workspace
so not attempted in this fix.
|