Repository navigation
Efficiency, robustness and Studio: fewer model calls, retries instead of dead ends, a live and linked Studio - #38
Merged
piotrlaczkowski merged 27 commits intoSep 10, 2026
Conversation
A blocked card rendered only task.error, the last line of a longer story.
The expanded card now shows the attempts timeline (attempt_log), the
review verdict, the structured criteria checklist, and a next-step row:
Send back to Ready, Retry (POST /api/tasks/{id}/retry, falling back to
the Ready move on a server without the endpoint), Reassign team (opens
the edit form on the team control) and Steer (prefills LiveFeedback with
@task:ID, parking the request across navigation when the composer is not
mounted).
LiveTaskPanel's Blocked/Failed counters are now filters over the list,
every attention row carries Retry, and a focusTaskId prop expands,
scrolls to and flashes one task.
Also: additive Task/QuerySession/PendingChange fields, a shared
window-event contract (ui/events.ts), an ErrorState primitive, a roving
tabindex helper, a confetti layer, and Modal's focus trap extracted into
useFocusTrap so other dialogs can reuse it.
… everything ResultPanel grows a What-next row: Resume interrupted run, Open blocked on Board (/board?column=blocked), Review N pending, Run again with this prompt. All data can be passed as props by the Live view; without props the panel fetches the resumable list and newest prompt itself and runs the prompt through RUN_PROMPT_EVENT, starting it directly when no listener claims the event. Layout reacts to run_end: a success or error toast, a flashing tab title while the tab is hidden, a browser Notification when permission was already granted and the tab is hidden, and a two-second confetti layer on a green run (skipped under prefers-reduced-motion). A run_end replayed from the server snapshot on page load is ignored. Keyboard: g t goes to Teams; Cmd/Ctrl+Enter and Cmd/Ctrl+. dispatch RUN_PROMPT_EVENT / STOP_RUN_EVENT while the run prompt is focused; Cmd/Ctrl+K opens a self-contained command palette over pages, tasks, agents, teams, recent runs and the theme. ShortcutSheet picks the new rows up from the shortcut table.
SettingsPanel and MarkdownEditor swallowed save errors — a 409 during a run left Save looking like a no-op. Both now report through the shared toast, naming the active run as the reason and saying the edits are kept. AgentManager, SkillManager and PipelineEditor render an inline ErrorState with Retry when their list fails to load instead of an empty state over a dead backend, and their secondary loads and reset report through the toast. FileInspector drops its private toast for the shared one and accepts /files?path= to open a file directly. ReviewView refreshes on the review_pending SSE event and on a 15 s poll while a run is writing, not only when it stops; each change shows its provenance (task, agent, run) with links, plus Open in Files. Empty states on Review, Agents and Skills say what the page is for and offer the first action.
… workers The review path was a speculative race: at the default max_parallel it fanned out to a local "acceptance" probe, the reviewer and reviewer-strict at once. The probe re-tested a subset of fastPath's conditions with none of its guards (open criteria, scope, blocking gates, tester role), so a task fastPath had just declined could be approved "acceptance race won" with no reviewer; and reviewer-strict was dispatched on every review while its answer was used only when the primary's was empty. The race is gone: decideReview is fastPath, then a harness fast-reject (a fired hard gate, or a worker that finalized blocked with no evidence — the reviewer could not change either outcome), then ONE reviewer request; reviewer-strict runs sequentially and only when the primary produced no readable verdict. Every dispatch goes through execOne, so the runway clamp and the call budget apply to reviews as they do everywhere else; the speculate primitive keeps its tests but no longer sits on the review path. A reviewer transport error no longer blocks the task and cascades to its dependents: the gathered gate signals are judged first, the reviewer is re-asked once when budget and runway allow, and otherwise the task goes to the human backlog as "reviewer unavailable". Worker failures: a timed-out worker is re-queued to resume from its ReAct checkpoint while under the attempt ceiling and parked in to_scope only at the ceiling; transient and rate-limited errors get one bounded retry that honors Retry-After; the checkpoint is saved before any retry so the retry resumes instead of cold-starting. The timeout message reports the clamped per-call budget the request was actually dispatched with.
… pack first formatCorrectPrompt clipped the previous output head-first at 2.5 KB while runGates appends the failing command's section and observation to the END of Task.Output, so once the prose passed the budget the corrector was told to fix a smoke failure it was never shown. It now uses the same body / evidence split the reviewer prompt uses (clipForReview): the prose is budgeted and the evidence kept whole. It also carries the HARD SCOPE focus list (via the new shared agents.FocusFilesSection, which BuildWorkerPrompt uses too), the acceptance and the project language hint, so the corrector is shown the same scope the gate grades it against. The self-critique dispatch is clamped to the runway like every other call. BuildWorkerPrompt opened with "ID / Title / Column / Role" BEFORE the scoped context pack, so two tasks sharing one pack shared a prefix of a few dozen bytes and the whole pack was re-prefilled per task. When the description starts with a pack, the pack (and the project-wide language line) now come first and the task header sits immediately before "## Task instructions"; the Column line, which changed between attempts at the same task, is dropped. Descriptions without a pack keep their shape.
…as unset reviewer-strict pins Temperature 0.0, and buildBody emitted the key only when the value was > 0 — so the one temperature that was set on purpose was dropped and the server sampled at its default. RoleSpec gains TemperatureSet, set for reviewer-strict and carried on the provider Directives; the structured request always emits `temperature` for a role that set one, while roles that never set it keep the unset behavior. The delegated (non-structured) path is built inside GoLangGraph and still applies its default to a zero; docs/decoding.md says so.
…ocus, tabs that roam RunHistory list rows show the archive's numbers when the server sends them — duration, tasks done/total, failed count, tokens, cost and team chips — and stay as they were for older archives. Every run gets a Replay on the floor button (navigates to /?run=ID&replay=1; harmless while Live ignores the parameters), /runs?run=ID preselects a run, and the empty state distinguishes no runs from no filter match with a CTA. HITLPopup reuses Modal's focus trap: Tab stays inside the gate and Esc returns focus to the first action with a note on why the gate stays, since a gate cannot be dismissed. The Artifacts tabs on Runs and the kind tabs on Blocks use a roving tabindex with aria-controls. Blocks' empty state explains what a block is and offers the first one. TopBar's model search waits 200 ms and drops superseded answers via an AbortController instead of firing per keystroke. docs/studio.md gains the stuck-task story, the run-end actions, the keyboard table and the cross-component event/URL contract.
Live ReAct compaction, deterministic half only. A ReAct iteration is one provider completion and every role is bound to its own registration, so a provider wrapper is the one place that sees iteration N of a 16-iteration worker with its full transcript — the same route live token streaming took. BindRole now stacks liveElideProvider outermost (above the structured wrapper, so the delegate and the direct constrained-decoding call send the same transcript) for any role whose Directives carry a LiveElide policy. Once the estimated transcript passes AtPercent of the model's context window the content of every tool RESULT but the last five is replaced with a placeholder. Every tool call, every tool_call_id pair, the leading system message and every user/assistant turn stay; nothing is summarized — on a live request the head is the role's tool contract. The rewrite is per request and never mutates the agent's conversation, so it is repeatable and the elided prefix is byte-identical between iterations until the keep window slides. The factory sizes the policy per tool-using role from the model profile's context_limit; the orchestrator sets it from react_compact and react_compact_at_percent. Tool-less roles, an unset policy and a model with no known window install nothing. The config docstring, schema description, readiness comment and docs/context.md describe both paths.
…compaction is wired A checkpoint is saved when tool calls are still pending, so the restored transcript ends on an assistant tool_calls message and the finalize steer could not be appended as a user turn there. It was left to req.Input — and the agent library adds Input as a user turn on a cold start only, keeping it out of a seeded conversation. So the one resume that most needed the turn-budget warning never saw it. The steer is now injected as a system message right after the leading system message on that path, the technique CompactChatMessagesWithDigest uses for its digest. LiveReactCompactionWired flips to true: pkg/backends now elides old tool results on live requests for tool-using roles (deterministic elision only; summarization stays at checkpoint and resume). ReactCompactionStatus says so, and the readiness and config consumers read it as before.
…y review entries by kind, per-ask shell approvals
Workspace:
- checkSymlinkEscape returned nil when EvalSymlinks failed, so a symlink whose
target did not exist yet passed the jail and os.WriteFile followed it,
creating the target anywhere on the host. Dangling links are now resolved
with Readlink and judged lexically against the real root (re-checked for a
chained link); anything that cannot be judged is refused.
- ws_edit/ws_write/ws_patch accept the argument spellings small models mix in
(old_string/search/old, new_string/replace/new, contents/text/body,
diff/hunk); the canonical key always wins.
- ws_write refuses an empty body on a new file (it used to write 0 bytes and
report success) and names the accepted keys; emptying an existing file needs
allow_shrink.
- A ws_read gutter on EVERY line of old_str/patch is stripped and the edit
proceeds, with a note appended so evolve still sees the drift; a partial
gutter is still refused by name.
- replace_all is honored on the tolerant ladder too: all spans of the first
matching strategy are replaced instead of reporting ambiguity.
- ws_patch: pure-insertion hunks (-N,0) insert AFTER line N as unified diff
semantics require; the marker-less SEARCH/REPLACE form is reachable; trailing
prose and Markdown fences no longer become unmatched context lines.
- The syntax guard memoizes the before-verdict per path by content hash, so
consecutive edits spawn one checker each after the first instead of two.
Review queue:
- Entries are stamped with from (ws_mv source), task_id, agent and query_id
at record time; a shell ask is no longer mirrored into the queue.
- Both appliers (Studio POST /api/review/apply and slmcode apply) switch on
kind: delete removes, mv moves the source, shell is hidden and refused.
Applying every kind as a write truncated deleted files, left moved sources
behind and wrote shell.sh at the project root.
- review_pending {pending: N} is emitted after apply/reject and whenever a
running agent records a proposal.
HITL:
- Shell asks live in per-ask files (shell/asks/<id>.json, answers/<id>.json).
The single ask.json slot let a second parallel worker overwrite the first
ask and delete the winner's answer. GET /api/shell/pending returns asks[]
alongside the first ask's fields for compatibility.
- plan.AddBoardHook / workspace.AddPendingHook are the change feeds Studio
turns into task_update / review_pending events.
Column labels, ticket-state labels and seat/role glyphs move to
components/shared/labels.ts; the flat and 3D floors and the dossier
read them from there instead of each carrying a copy. API field names
are untouched — only what a person reads is unified (Team, Manager,
Task; the floor keeps "ticket" for what lies on a table).
EntityLink renders a task, agent, team, run or file as one consistent
chip that goes somewhere: task/agent -> Live ?task=/?agent= (the
dossier), team -> Teams ?team=, run -> Runs ?run=, file -> Files ?file=.
Outside a router it degrades to a plain anchor to the same path.
Types: QuerySession gains the per-run totals the server is adding
(duration_ms, tokens, cost_usd, tasks_total, tasks_done, failed_tasks,
teams); RunEvent.data may carry a task (task_update) or a pending
count (review_pending). Client: retryTask(id) for POST /api/tasks/{id}/retry.
… walked by keyboard State: the selection is now controlled by the page (LiveView keeps it in the URL) with the wrapper's own state as the fallback; the pulse feed, the last floor it was diffed against, the camera, follow and spin live in a module-level store mirrored to sessionStorage, so leaving for the Board and coming back finds the scene where it was left and what happened in between still pulses. "reset view" restores OrbitControls' saved state instead of remounting the Canvas. Rendering: frameloop is 'always' only while something moves (a run, live tickets, fresh pulses, follow, spin), 'demand' when the floor is still — camera glides and hover eases ask for their own frames — and 'never' while the tab is hidden. Per-frame allocations are gone: scratch vectors for the lerps and camera maths, precomputed emissive colours, curve.getPoint(u, target), and one wall-clock read per frame shared by everything that ages a pulse; per-table pulse filtering is memoised. Robustness: the body cursor is reset when the pointer leaves the canvas; webglcontextlost drops the page to the flat map with a toast, and picking 3D again retries. Mobile: below sm the 3D/map toggle sits in a strip under the floor and the feed opens from it as a bottom sheet; a coarse pointer with no stored preference defaults to the map. Keyboard: every person and ticket is also a visually hidden button that opens the same dossier; the dossier moves focus to its close button on open and restores it on close, and its Esc handler ignores typing targets.
…e-pipeline-composition-d6v8vx
…ation, and URL-backed selection Board store (hooks/useBoardStore): seeded from GET /api/tasks and GET /api/squads, updated by the stream's task_update events (data.task replaced by id), re-read every 30 s as a safety net, on both edges of a run, on reconnect, and — until the server pushes its first task_update — shortly after a structural log line, so an older server still feels live. task_update and review_pending are board events: folded into the store and the review badge, never rows in the log. The Board, the Live floor, the ticker and the Teams page read it; their own pollers (3 s, 5 s, 5 s) are gone, as is the Sidebar's second health poll — the review count comes from the stream's health poll and the review_pending event. useBoardStore() is exported so the task rail can switch to it. Run derivation (hooks/runDerived): phases seen, active phase and agent, task ids, files written, token/cost totals and the newest composition are folded once per event in the stream hook and published per flush; LiveView, NowBar and ActivityRail consume it instead of scanning the whole log. EventLog caches describeEvent/level/signature by event identity, folds its insight summary incrementally, windows its rows to the viewport plus a margin while keeping stick-to-bottom, and its agent, task and file chips are EntityLinks. URL state: the floor's selection lives in ?task= / ?agent= (&team=), so a dossier survives navigation and reload and can be linked to; the Board keeps ?team= and ?column= (a new per-column filter). focusTicket now passes the id through as focusTaskId to the Tasks rail (LiveTaskPanel accepts the prop; acting on it is left to its owner). NowBar derives who is working from the floor model's working set (readActivity/nowFor, now exported), so the clock stops after agent_end, and its agent, task and team are links.
…e-pipeline-composition-d6v8vx # Conflicts: # web/src/components/Live/LiveTaskPanel.tsx # web/src/types/index.ts
An unassigned task whose files fall in two or more squads' territory used to get no brief at all — BriefFor returned "" for every task without a squad — so the one worker building both sides of the seam was the one building without the interface spec. BriefFor now resolves the task's files to their owners and, when two or more squads own them, returns a bounded seam brief: the squads spanned and what each owns, then every interface of the frozen contract with its provider, consumers and a clipped spec. Interfaces only, no charters; capped at twelve interfaces and 320 chars of spec each. An unassigned task inside one territory, or outside every territory, still gets nothing.
…quests Runner gains two optional hooks: RoleTimeout(ctx, role) returns the measured budget for one request of a role, and OnRoleLatency(role, d, evidence) reports how long each request took. execOne and dispatchWave dispatch on min(callTimeout, RoleTimeout(base role)) — the measured value can only tighten the flat Timeout — and report every request's duration under its base role (an escalation rung shares its base role's series) with the orchestrator's evidence rule: a success or a timeout counts, any other failure does not. reviewSlots sizes the reviewer slot from the same budget. Workers, reviewers and correctors own most of a run's wall clock and used to run on the flat task_timeout while contributing no latency samples at all; with the hooks unset nothing changes.
… team state, early team gate, fewer idle model calls One concurrency limit. max_parallel was enforced only in the execute wave; context ran beside explore, architect beside clarify, speculative digs and review races added slots. Every model request now takes a slot of one run-wide weighted semaphore sized max_parallel, acquired in the executor adapter handed to the loop, in runRoleTracked and around the multipass cycle; the phase pairs run sequentially at max_parallel=1. llm_requests counts every assistant turn of a result's transcript instead of one per result, and the multipass passes are counted too. Measured role timeouts in the loop. buildRunner wires Runner.RoleTimeout to roleTimeoutWithin and Runner.OnRoleLatency to recordRoleLatency, so loop roles are dispatched on their measured budget and finally feed latency memory. Smoke memo. runSmokeIn remembers each command's result per (command, tree fingerprint) for the run and reuses it until something is written; the fingerprint now carries a write sequence so a rewrite of an already-changed file counts, and the formatter and a dependency install advance it. The pre-test result is handed to runQAGate for round 1; team acceptance and integration check gateRoundAffordable before running. The QA bootstrap is applied once per tree state. Stale per-run state. Team gates, the squad plan, the single team and the team selection memo are reset at the top of both runSLM and Resume; the squad plan write is locked; restoreSquadPlan restores a one-squad squads.json as the single staffing team. Correction attempts for team tickets are now counted under the key the ticket is stamped with (they were looked up under a key nothing matched, so every ticket was attempt 1). Model calls with nothing to do. The memory distillation runs single-shot and only when the run left a notable lesson, a changed file or a failed task; the per-wave distillation and after-wave coordinator run only on a wave that failed, escalated or produced a notable lesson; the splitter is skipped for a one-step plan over one or two known files. Every skip is announced as an info event with its reason. Early team gate. AfterWave proves a team's half as soon as its lane is complete and has changed since it was last proved, so a red half's ticket rides the next wave; a defect is ticketed at most twice per run, then handed to a human. Team selection is memoized per run so the composer and the charter phase do not both rescan the workspace and reload the block library.
…e-pipeline-composition-d6v8vx # Conflicts: # pkg/loop/runner.go
The test slept 100ms on every request against a 150ms probe budget, so a busy test run could make the very first request miss the window and fail the guard for the wrong reason. The first request now answers at once and only the later ones sleep past the deadline, which is the shape the test describes.
…ot, buffered event log; Studio task/review/history API
Server lifecycle:
- POST /api/runs/stop no longer flips running to false while the run goroutine
is still unwinding. It sets stopping, cancels the run's own context and lets
finishRun clear running when the goroutine exits, so a following POST
/api/runs is a 409 until then. finishRun only touches shared state when its
run generation is still current, so a stale goroutine can never clear the
live run's flag, restore its options or publish over its result.
- hitl.ClearAll runs after the running check: a second click on Run (a 409)
no longer deletes the in-flight run's open shell ask.
- The SSE ring is not cleared per run (subscribers were mid-stream on it);
/api/runs/latest and the current-run activity view scope by runStartSeq.
- handleLatestRun copies under s.mu and encodes after release: the engine
emits synchronously into Server.emit, so a stalled browser fetch used to
block a worker mid tool-call.
- /api/runs/latest and /api/status report stopping.
Studio API:
- task_update events: every board task change (plan.AddBoardHook) is
published with phase = current run phase or "board", task_id, message
"<id> -> <column>" and data {"task": <task as GET /api/tasks renders it>}.
- POST /api/tasks/{id}/retry: ready_to_dev, retries reset, error cleared,
"retried from Studio" appended to attempt_log, persisted, task_update
emitted; 409 without a board, 404 for an unknown id.
- GET /api/queries items carry duration_ms, tokens, cost_usd, tasks_total,
tasks_done, failed_tasks and teams; token/cost totals are cached per
(id, size, mtime) of the event log.
- GET /api/teams/activity?query= caches the derived timeline per
(query, size, mtime) instead of re-parsing the log on every visit.
Session event log:
- One buffered appender per run instead of open/write/close per event under a
global mutex. Token deltas are not persisted; structural kinds flush on
write; chatty kinds flush on a 500ms timer; run_end/run_stop and the turn's
end (WriteTurnSummary) flush, fsync and close. ReadEvents flushes first so a
live run's tail is always visible.
…lta, restore the gate terminal on cancel, keep evolve informed of in-tool repairs; docs - retryProvider.CompleteStream / CompleteStreamWithMode now go through retryDo. The ReAct loop takes the streaming path whenever a token sink is attached (TUI, Studio), so a 429 or a connection refused by a model server that was still loading got zero attempts exactly when someone was watching. A callback wrapper tracks whether a delta was delivered: a failure before any delta is retried like a failed Complete (fresh deadline per attempt, the caller's callback only sees the successful attempt's deltas); once a delta reached the callback the error is surfaced as-is, since a partial stream cannot be replayed. - TerminalGateHost.AskGate restores the terminal on the ctx.Done branch. The read goroutine owned raw mode and stayed blocked in ReadKey, so a gate abandoned by a stop or shutdown left the shell without echo. The goroutine's own release skips the restore when a newer gate has since taken the terminal. - A ws_edit/ws_patch call whose ws_read gutter was stripped in-tool is still presented to the tool observer as the refusal it would have been, followed by a repaired retry that landed, without a second edit — so the failure is fingerprinted, the memory rule credited and the metrics row moves. - docs: tools.md, troubleshooting.md (aliases, empty-content refusal, gutter stripping, ws_patch insertion/bare-form/prose semantics), studio.md (review kinds and stamps, task_update / review_pending events, retry endpoint, queries fields, shell pending list, stopping), permissions.md (kind-aware apply, per-ask shell files).
…e-pipeline-composition-d6v8vx # Conflicts: # docs/studio.md
…d store also refreshes on agent_end The engine emits no task_start/task_done kinds of its own, so on a server without task_update events the store's structural cue is the end of the role that worked the task. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011RfCkcsbJZgHoQLzwkqFUL
…id stats for the newer gosec CI's golangci-lint 2.13.1 carries gosec's taint analysis (G703). The review applier now checks every pending path against the project root before touching it, which is a real guarantee rather than a comment; the two os.Stat calls in the team-activity cache are annotated, since session.TurnDir already reduces the id to [A-Za-z0-9_-]. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011RfCkcsbJZgHoQLzwkqFUL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A review of where a run's wall clock and tokens actually go, and of what the Studio leaves the user unable to act on, followed by the fixes. Four reviews (loop and decoding, orchestration, tools and server, Studio UX) produced ranked findings; this PR implements the top items of each. Full detail in
docs/changelog.mdunder Unreleased.Efficiency — fewer, shorter, better-aimed model calls
react_compactis now what the docs said it was.max_parallelon every model call (prepare phases, speculation, reviews, critique), not only the execute wave; paired phases run sequentially at 1.Robustness
applyis kind-aware (delete deletes, move moves, shell asks are not files); parallel shell asks get their own files;ws_edit/ws_writeaccept common argument aliases, strip aws_readgutter instead of refusing, refuse empty new files, and honorreplace_allon drifted matches;ws_patchpure-insertion offset, bare SEARCH/REPLACE form, trailing prose.Studio
task_update/review_pendingstream events replace the pollers; one store feeds the Board, the floor, Teams and the Review badge.?task=,?agent=,?column=,?team=), selection survives navigation and reload.POST /api/tasks/{id}/retry), Send back, Reassign, Steer; a run's end offers next steps with toast, title flash, notification and a short celebration on green.g t, ⌘/Ctrl+Enter / ⌘/Ctrl+. / ⌘/Ctrl+K palette.API added
POST /api/tasks/{id}/retry; SSE kindstask_update,review_pending;GET /api/queriesitems gain duration/tokens/cost/tasks/teams;GET /api/review/pendingitems gainfrom/task_id/agent/query_id;GET /api/shell/pendinglists all asks;stoppingon run status.Verification
go test ./...green (also per stream before merging),go vet, gofmt, golangci-lint 0 issues besides the pre-existing autoresearch gosec finding; race and coverage floor run locally and in CI.?task=/?agent=, Board?column=filter, Runs/Review/Teams/Agents empty states, command palette; zero console errors.TestSlowEndpointDoesNotPoisonCapabilitiesForAWeek) made independent of machine load without changing what it asserts.🤖 Generated with Claude Code
https://claude.ai/code/session_011RfCkcsbJZgHoQLzwkqFUL
Generated by Claude Code