loomcycle's Web UI ships a live agent terminal that lets the operator drive a long-running agent Claude-Code-style from the browser. Open /run, pick an agent, type a prompt — the agent streams back into a terminal. Type a new instruction while it's still working and it shows up as the next user turn before the model's next call. Answer its Yes/No questions inline. Close the page, come back two hours later, the run is still alive. Four headline mechanisms shipped at v0.26.0. (1) Mid-run steering: POST /v1/runs/{run_id}/input + a new internal/steer per-run buffered registry (depth 16, mirroring internal/cancel), with a drainSteer hook in the loop that pulls queued messages at the TOP of each iteration — never between a tool_use assistant turn and its tool_results user turn (that orphans the tool_use and 400s the provider; tested in TestRun_DrainSteer_OrderingWithToolResults which fails-before on the naive placement). A new EventSteer SSE event ("steer", not "user_input" — that name was taken by the persisted transcript event) renders the operator's input as a distinct row. (2) Persistent interactive runs that park at end_turn: the loop now emits EventAwaitingInput and parks waiting for operator input rather than terminating, with a heartbeat ticker pulsing OnHeartbeat so the staleness sweeper doesn't reap the idle run. Paired with per-agent unbounded_iterations that lifts the 16-iteration soft-cap for LLM agents (the same exemption code-js providers already get), keeping the 1<<20 hard ceiling as a runaway backstop; cancel becomes the stop. The flag round-trips through every AgentDef mirror (F14/F40 chain) so it survives the content-addressed substrate. (3) Inline interruption answers: when an agent raises a question via the Interruption tool, the terminal renders option buttons (for fixed-set asks) or a free-text answer box right in the live transcript — no need to bounce to a separate /interrupts inbox. The answer goes to the existing resolve endpoint; the parked run wakes on the same open stream. (4) The terminal itself: always-on prompt that routes by state (send routes to steer while running, continue between turns); inline interruption prompt takes precedence; awaiting_input renders "idle — waiting for operator input". v0.26.1 added cross-replica steering via a SteerCoordinator that mirrors the existing CancelCoordinator's shape (route by runs.replica_id to the owning replica, await ack, never re-broadcast — the cancel-storm lesson). v0.27.0 made interactive runs survive a view-switch — pre-fix the loop was bound to the HTTP request context and closing the SSE stream cancelled the request, which cascaded to the loop ctx and terminated the parked run. The fix needed two pieces: detach the loop into a background goroutine under context.WithoutCancel(r.Context()) so it keeps the request's auth principal + tenant values but is not cancelled on client disconnect; and have the handler stream by tailing the persisted event store via a new GetRunEventsSince(runID, afterSeq, limit) method (sqlite + postgres, indexed on events_by_run_seq) instead of writing live to a ResponseWriter that has returned. A new GET /v1/runs/{id}/stream endpoint replays from ?from_seq then live-tails, re-emitting each persisted event as the same SSE frame the live run produced. v0.27.0 also added Context op=self reporting the resolved provider + model (per-iteration so a mid-run provider fallback that swaps opts.Provider/opts.Model in place is reflected truthfully). v0.27.1 + v0.27.2 polished the rendering (auto-scroll, collapsed tool_call, multi-line input, tool_result fold). v0.29.0 (today) completed the polish layer with four pieces. User-message echo: the operator's typed prompt was invisible because the persisted user_input event was filtered from the live SSE tail. Fix is client-side: useRunStream pushes a synthetic user_echo transcript entry on its own sends, seeded in start() with the initial prompt, appended in send() (steer) and sendMessage() (continuation), rendered as "❯ {text}" distinct from a drained "» {text}" steer frame. Context-size gauge in the LiveRunPane header: "ctx 47.2k / 200k (24%)" with an amber > 70% / red > 90% color bar, computing the true prompt footprint as input + cache_read + cache_creation tokens (the prompt-caching honest accounting; input_tokens alone undercounts). When the provider doesn't report a context window (Ollama) the gauge shows just the absolute size without a bar. The plumbing: providers.Usage gained an additive optional MaxContextTokens field stamped on per-iteration EventUsage from opts.Provider.Capabilities().MaxContextTokens; @loomcycle/client 0.26.0 picks up the field. Agent editor sampling controls + advanced JSON/YAML overlay: the agent create/fork modal now has dedicated inputs for temperature/top_p/top_k/frequency_penalty/presence_penalty/seed/stop (blank = unset, "0" = explicit so temperature 0.0 stays distinct from default), and a scoped collapsible JSON/YAML overlay textarea for the long-tail overlay fields without dedicated controls (channels, interruption, *_def_scopes, etc.), shallow-merged over the structured overlay at submit. Deliberately differs from the v0.10.4 whole-overlay catch-all we removed in v0.11.6: system_prompt stays in its own textarea (no newlines-in-JSON), and the box is optional. Soft-reclaim of retired agent names: AgentDefSetRetired is now transactional and clears the agent_def_active pointer in the same transaction when retiring the currently-active def; lookup.resolveDynamic skips a retired active row as defense-in-depth (fixes a latent runtime bug where a retired-but-active def was still served to runs); LibraryEntry surfaces live_version_count + active_retired, and the create modal's name-collision check loosens so a name with no live active version is reclaimable for a fresh create that's allowed to widen the tool ceiling. Three engineering moments worth keeping: the orphaned-tool_use problem (provider message ordering is part of the contract — drain steers at iteration boundaries, never between tool_use → tool_results); the detach-from-request problem (interactive runs need context.WithoutCancel + persistent-store tailing to survive HTTP request lifecycle); the cross-replica problem (already solved for cancel; SteerCoordinator mirrors). What this unlocks: the substrate becomes a development surface, not just a production runtime. The interactive-terminal feature also codifies the "parked run, drained at iteration boundaries, woken by an external event" contract that self-evolving agents (exp6.5 v0.30.0 cross-instance mid-run resume) and agent ensembles (exp5 Channel.await consolidator) both build on. One residual deferred: cross-replica re-attach (a viewer on replica A re-attaching a run owned by replica B) — single-replica re-attach covers the common case.
Your AI agent is the most over-privileged account you own