A schedule is a time trigger, and only the first step of a pipeline is really
waiting on time — every later step waits on the previous step's result. Modeling
"watch a source -> write a spec -> implement it" as three schedules made each fire
blind: on a quiet day the coding agent still spawned, paid a full model run to find
there was nothing to do, and left an empty run in Activity.
So an agent can start the next step itself. `dispatch_agent` (a tool on the bundled
MCP server, and on the pi bridge through the same --call seam) enqueues an ordinary
spawn command in the agent's own project, tagged requested_by=agent:<id> — so a
handoff is visible in Activity with no new surface to build.
- Project-scoped by construction: project_id is read from the spawn environment and
never from the tool arguments.
- Bounded rather than gated: MAX_DISPATCH_PER_RUN counts the command rows the agent
already wrote; MAX_DISPATCH_DEPTH rides in the spawn payload and is recovered by
spawn._dispatch_depth, so a chain keeps its place across a resume and a cycle
terminates instead of fanning out.
- New scout and planner roles, with built-in skills (handler-scout, handler-planner,
handler-dispatch) carrying the judgment code can't: dedupe against a memory-note
watermark, treat "nothing new" as a complete run, and write a task the receiving
cold-start agent can act on.
- A scout ending on a clean tree skips the test gate and records the new
tests_status='skipped' (migration 0017, additive CHECK widening). The gate promises
`done` means tests passed for the work that shipped; nothing shipped.
Rejected a `condition` field on schedules: "is this paper new and does it matter
here?" is a semantic judgment, so it belongs to a model, not a scheduler column. The
scout is the condition; dispatch is how it reports true — one mechanism that covers
future pipelines too.
426 tests (14 new for dispatch, 3 for the gate exemption).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcbDevyMcJWE6qPA56C7mZ
Operators can register Anthropic-API-compatible endpoints (a local Qwen/Llama
behind LiteLLM or claude-code-router, an LLM gateway) on the dashboard's
Claude -> Models tab and pick one from a Model dropdown when spawning an agent.
The agent still launches as the same claude binary with the same hooks, skills,
connectors, plugins, and gates — only its ANTHROPIC_BASE_URL / ANTHROPIC_MODEL /
ANTHROPIC_AUTH_TOKEN env differs — and it stays pinned to its backend across
resumes. No selection keeps the worker's Claude subscription untouched.
- claude_models table (+ agents.model_id pin), migration 0012
- control.models resolves a row into the launch env (API keys Fernet-encrypted
at rest, decrypted only in the control container; placeholder key when none is
stored so the subscription OAuth token never reaches a local endpoint)
- /claude/models CRUD (admin-gated writes, key never returned), spawn route +
worker + CLI (--model) pass the selection through, fail-fast on missing or
disabled backends
- dashboard: Models tab, spawn-form dropdown, model badge in the agents table
- docs/local-models.md: why bare OpenAI-compatible servers break tool calling
with Qwen-Coder, and working vLLM/LiteLLM/llama.cpp stacks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzDofD7gP63WpeLG8vEdZu
- worker: heartbeat every loop pass (workers registry); reaper pass
every ~15s marks a silent worker's running runs crashed and flips
agents stuck in 'working' to crashed (paused/blocked keep their
still-accurate status). Idempotent via finish_run's running-guard;
any surviving worker can reap; no auto-requeue (half-done runs may
have pushed). Dead workers' registry rows are dropped once settled.
- api: GET /projects/{p}/agents/{name}/events - the persisted
stream-json event log, oldest-first, cursor-paged by row id;
AgentOut exposes session_id/worker_id
- frontend: Run events panel in the run detail (assistant text, tool
chips, result footer with cost/turns, runner notices, raw lines),
cursor-appended on the existing 5s poll; 'Crashed' filter + danger
badge; crashed agents show their frozen last frame ('last output
before crash'); static export regenerated
Suite 290 -> 296 green; next build clean.
The mise-init agent wedged on launch and the UI reported it green. Three
distinct problems, fixed together:
1. Onboarding wedge (the proximate bug). A freshly-installed claude opens
interactive setup — theme picker, then a folder-trust prompt — before
the REPL. A detached tmux agent has no one to answer it, so it sat on
the theme picker forever while agents.status said 'working'. New
control.claude_config.ensure_onboarded() marks onboarding complete and
trusts the working dir in ~/.claude.json (merge-only, so the login
flow's oauthAccount survives); spawn() calls it before launching.
2. Config-name gate. control/mise.py only recognized `.mise.toml`, so a
repo shipping `mise.toml` (no dot) — or config under `.config/mise/` —
failed the [tasks.test] gate even when healthy. It now accepts the
filenames mise itself reads and scans them all for the test task.
3. "Done" != done (the design gap). A spawned agent's real state lives in
its tmux pane, but the socket is control-container-only, so the API
couldn't see it. The worker now snapshots each working agent's pane
tail (last ~40 lines) into two new agents columns (last_output,
output_at, migration 0007) on its existing poll loop; the API
serializes them and AgentsSection renders a live-output <pre> under
each running agent. A wedged agent now shows the theme picker instead
of a misleading green badge.
Tests: home-dir writes are isolated to tmp in conftest; added coverage for
claude_config seeding/merge, the mise filename set, the worker capture
(including dead-session skip), and the API serialization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
Rebuild the bundled web UI as a Next.js (React + TypeScript) static export
implementing the Claude Activity Dashboard design: a left-nav "Control Center"
hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and
Shared, styled with the Leeworks design-system tokens (flat, dark, border-led).
The dashboard is a pure client of the existing API (same contract as curl):
the browser prompts for the token once, stores it in localStorage, attaches it
to every call, and renders all API values as React text so agent-authored
strings stay inert. Control actions enqueue a command and poll it to a terminal
state, matching the worker model.
The build output is committed to src/handler/api/static/ so the wheel ships it
and FastAPI serves it same-origin. app.py now mounts the export at "/" after the
API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite).
UI-serving tests updated for the export; frontend source lives in frontend/.
Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD
Co-authored-by: Claude <noreply@anthropic.com>