Repository navigation
feat(studio): chat — sessions, streamed runs, controls, and the chat workspace - #1756
Conversation
Review finding addressed: model and effort selection was permanently unavailableConfirmed and fixed. This layer hard-coded The inventory comes from
|
Follow-up addressed: the mobile sheet and the side-thread panelYou were right that the first fix was partial. Two more call sites rendered the control with an empty inventory:
Both now follow the same rule as the desktop menu and the session controls: the control is rendered only when a deployment model inventory exists. The mobile sheet omits the Model and Effort rows together, since effort without a selectable model is the same false affordance in a different shape. There are now four call sites and no remaining |
…workspace Implementation layer of docs/acceptance/studio-chat.md, built against plan commit 310fc4b. BFF: the session inventory and lifecycle routes under /api/v1/sessions (paged list with inspect-only filtering and relayed capabilities, create with mode / model / effort / toolAccess, detail with cumulative usage, rename, delete, mode, compaction, fork, clear, transcript); runs, activity replay, and retries as Server-Sent Events carrying Studio's run.started / run.event / run.error union, with unknown SDK kinds forwarded flagged and raw; cancel, steer, and permission verdicts addressed to the exact durable run (409 stale_run_control otherwise). Every mutation, the two POST streams included, sits behind the bootstrap's CSRF and session gates. The bootstrap's stub stream schema is replaced by the chat union. Activity replay is BOUNDED and RESUMABLE, answering a blocking review finding against an unconditional replay of a session's whole durable history: every run.event frame carries its durable cursor in the SSE id field, a request resumes exactly after Last-Event-ID (or the resumeFrom query parameter, the header winning), a no-cursor replay stops after STUDIO_ACTIVITY_REPLAY_MAX durable replay events with a run.truncated frame pointing the client at the authoritative transcript, a durable gap ends the stream instead of crossing the hole, an unusable cursor is 400 invalid_cursor decided before any SSE header is committed, and STUDIO_ACTIVITY_MAX_STREAMS bounds concurrent streams per session with 429. Live events stay unbounded so a long turn streams in full. Web: the chat workspace ported from the prototype (sidebar, transcript reducer, composer with images, approvals, steering, model and effort menus, folders, queue, diffs, code highlighting, voice input) plus the shortcuts provider it depends on. The model and reasoning-effort control renders only when a deployment model inventory exists, at every one of its four call sites (the desktop menu, the session controls, the mobile chat-options sheet, and the side-thread panel), so this layer ships no dead affordance: the inventory arrives with the settings surface, which owns GET /settings/runtime. browser persistence moved to the studio.* key namespace; Chats is the nav's first item and / redirects to it. 55 server and 98 web Vitest proofs; smoke-tested end to end against a spawned mecated --mock. The experimental image label and README banner are removed now that Studio has a usable feature. Tracking: #1736 Signed-off-by: Samuele Verzi <samu@stacklok.com>
Summary
What lands
409 stale_run_controlotherwise). Mutations, the twoPOSTstreams included, sit behind the CSRF and session gates. The bootstrap's stub stream schema is replaced.studio.*keys; Chats is the nav's first item and/redirects to it.org.stacklok.mecatl.studio.stability=early-access. A first usable feature is not a stability promise.Bounded, resumable activity replay
Implements the amended contract (see the plan layer below).
apps/server/src/mecatl/chat.tstakes anActivityRequest(cursor,replayMax,signal) and yields each frame beside the durable cursor it projects; it counts onlyphase: "replay"envelopes against the bound, so a long live turn is never truncated.apps/server/src/routes/chat.tsreadsLast-Event-IDorresumeFrom, pulls the first delivery before any SSE header is committed so an unusable cursor is an ordinary400instead of an error frame inside a200, writes each cursor as the SSEid, and gates concurrent streams per session with429plusRetry-After. The browser handles the newrun.truncatedframe by loading the authoritative transcript; the reducer treats it as a stream boundary, not transcript content.The resume query parameter is named
resumeFromrather thancursoron purpose: the client generator readscursoras a pagination marker and emits an infinite-query wrapper that cannot type a Server-Sent Events response.Six new proofs cover it: bounded replay with the truncation cursor, exact resume with no duplication, the durable-gap stop, live events staying unbounded, header-versus-query precedence with
idstamping, and the invalid-cursor plus stream-cap behaviour.Amended after review
A review finding: this layer hard-coded an empty model list, and both the draft composer and the session controls only disabled their model and effort menu when that list was empty, so the control was visible and permanently dead while the plan advertised model selection. The inventory comes from
GET /api/v1/settings/runtime, which the settings plan owns, so this layer has no source for one.Both call sites now render the control only when an inventory exists, so this layer ships no false affordance; the settings layer supplies the inventory and the control appears. The plan's narrative no longer claims model selection for this layer, records the deferral under Human decisions with its rationale, lists it under Out of scope, and adds AC4.7 for the guard.
Verification
55 server and 98 web Vitest proofs (24 ACs, 0 missing under
task ac-trace);task studio:check,task docs,task lint:actionspass; smoke against a spawnedmecated --mock: create, six-frame SSE run, transcript, rename, stale control.Further amendments
Implements the amended chat contract (see the plan layer below):
Activity route. It commits its headers at the replay boundary through a delivery-less attach marker and sends a keep-alive comment every 15 seconds. It follows an already-aborted request signal and refuses the SDK's
start/nowsentinels and oversized cursors.Adapter. It maps a daemon
activity_gaperror to a gap truncation.run.errormessages go throughsanitizeUpstreamDetail, and the base64 regex is replaced by a linear check.Web. Each stream carries the session it belongs to. Switching chats detaches it, and controls, approvals, and the queue address the owning session. A
boundtruncation reattaches from its cursor with a truthful notice. The stored UI scale is applied at startup.Docs.
docs/architecture.mdlistsrun.truncatedin the event union. The README no longer lists features this layer does not ship.Server. The resume, bound, and boundary rules follow the amended contract. The replay-bound test now counts envelope pulls, and a new test covers a request that was already disconnected. A body-limit middleware rejects oversized requests before JSON validation.
Web. Side threads reattach on truncation through the shared
run-stream.tshelpers. The client treatsrun.startedas idempotent, and a durable event from another run moves the controls to that run. URL-only images render as links.groupModelshas one implementation, and each panel stores its own width.Plan status. This layer's diff moves its acceptance plan to
landed(header and index), per the plan lifecycle; the transition is authoritative only when this PR merges, andtask ac-trace-strictnow gates the plan.Re-verified on the rebased stack:
task studio:checkpasses (80 server + 121 web Vitest), andtask ac-trace-strictpasses with every Studio plan gated aslanded(141 ACs, 0 failures).Development stage
Contract linkage
/api/v1/sessions…and the run SSE union) inside the boundary ADR 0351 fixed; no new durable architecture decisiongh stack) delivery for the whole Studio series: nothing merges until every layer is reviewed, so the plan's approval-by-merge event is deferred to the end (recorded in each plan's Delivery line), and each implementation layer records the exact plan commit it built against.docs/acceptance/studio-chat.md310fc4b681f679033a801a7162ad342d2d71f4d0(the plan layer below; not yet merged — stacking exception above)Interface conformance
Issue relationship
Relates to #1747 (parent #1736; stack #1766). Non-closing: the step completes when both of its layers merge.
Type of change
Test plan
Baseline checks
task lint)task test)go run ./cmd/mecademo)task docs)Studio gates (this workspace is not covered by
task lint/task test, which are Go-only):task studio:check(Biome, typecheck, Vitest, generated-drift),task ac-trace,task lint:actions— all pass; see Verification above.User-facing change
Studio gains chat: sessions, streamed runs with images, approvals, steering, model and effort selection.
Special notes for reviewers
Part of a fourteen-layer stack; review bottom-up. Nothing merges until the whole series is reviewed.