Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/studio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,30 @@ jobs:
echo "Proprietary SPDX headers are not allowed in studio/" >&2
exit 1
fi

# Browser smoke over the real proxy tier against the fixture daemon.
# Separate job so a browser-infra flake never masks the checks above.
e2e:
name: Playwright (fixture daemon)
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: studio/.nvmrc
cache: npm
cache-dependency-path: studio/package-lock.json

- name: Install
working-directory: studio
run: |
npm ci --ignore-scripts
npx playwright install --with-deps chromium

- name: Build and test
working-directory: studio
run: npm run test:e2e
96 changes: 96 additions & 0 deletions docs/adr/0289-studio-server-backed-chats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# ADR 0289 — Studio's chat list is the daemon's session store

- Status: Accepted
- Date: 2026-08-18
- Scope: `studio/` — where a chat's existence, title, and transcript live, and which
client actions are server calls

> History: re-lands the unmerged draft from PR #615 (numbered 0227 there before
> that id was taken on main), adapted to the Atrium workspace UI.

## Context

The prototype Studio descended from kept its chat list in browser state — first
a `localStorage` key, then mock fixtures mirrored in module scope. `mecated`
persisted the same sessions the whole time: the daemon already exposed
`GET /v1/sessions`, `GET /v1/sessions/{id}/transcript`,
`POST /v1/sessions/{id}/rename`, and `POST /v1/sessions/{id}/delete` — and the
client called none of them.

Every consequence followed from that one gap:

- A second browser, or a second machine, saw nothing. The daemon held the work;
the client that opened it held the only record of what the work was called.
- Renaming a chat renamed a local label that no other client would ever see.
- Deleting a chat dropped the client's copy and left the session in the store
forever — "delete" quietly meant "hide".
- A reload during a run orphaned it. `mecated` keeps running after the page
that started it goes away, but the client had no way back to the result: the
run streams off the `POST /prompt` response body, and that body is gone.
- The client also minted its own session ids and mapped them lazily onto daemon
sessions, so the daemon's record and the sidebar disagreed about what even
existed.

The persistence was never missing. It was unused.

## Decision

The daemon's session store is the record of which chats exist, what each is
called, and what was said in each. Studio reads and writes that record, and
**the sidebar id IS the daemon session id** — there is no client-side session
mapping.

- **The chat list is the session inventory.** Studio walks `GET /v1/sessions`
when the daemon becomes reachable and on a slow poll, and merges the result
into its list. A row is removed only when a COMPLETE walk proves it gone —
a partial walk updates what it saw and never deletes. Rows whose one
not-a-chat reason is `inspect_only_kind` (subagents, team members, scheduled
fires) are filtered by the decoder, not by id-prefix guessing.
- **Rename is `POST …/rename`.** The local update is optimistic and rolls back
if the daemon refuses. Studio adopts the title the daemon actually stored,
which is clamped, rather than the one it asked for.
- **Delete is `POST …/delete`.** A refusal keeps the row: the chat still
exists, and hiding it locally is the exact failure this ADR is about. Only a
404 — the daemon saying it has no such session — removes a row without a
witnessed walk.
- **Opening a chat reads `GET …/transcript`.** The authoritative message-level
snapshot, which also covers scheduler-tick fires whose conversation never
reached the durable event log, and works identically in external mode. A
transcript the daemon cannot prove whole says so in the UI.
- **Eligibility is the daemon's, not Studio's.** Each inventory row carries
`capabilities` plus a closed machine-readable reason per disabled action.
Studio renders the reason; it does not re-derive who may rename or delete
what, so tightening the rule server-side needs no client change.
- **A new chat is a draft.** No daemon session is created until the first
prompt is sent; the mint happens then, and the route adopts the daemon's id.
Empty sessions never accumulate in the store from idle "new chat" clicks.

## Consequences

A chat renamed or deleted on one machine is renamed or deleted on every
machine, and survives clearing the browser. Sessions can finally be removed
from the store through the UI. A reload during a run no longer loses it: the
inventory row shows the session still running on the daemon, and the
transcript is read back when it ends.

The costs, stated plainly:

- **The list reorders on rename.** A rename advances the daemon's stored
mtime, and that mtime is the only ordering every client can agree on.
- **A rehydrated transcript is not identical to the live one.** The transcript
endpoint returns the model's conversation, which includes the harness's own
synthetic continuations as user-role messages with no provenance to
distinguish them. Fixing this needs a provenance field on the daemon's
`ConversationMessage`, not string-sniffing in the client.
- **Reattaching to a LIVE run rides the durable session watch.** A tab that
did not start the run follows it via `GET /v1/sessions/{id}/watch`
([ADR 0250](./0250-durable-cursors-and-watch.md)), landing later in this
series. The honest residual: the driving tab's own `POST …/prompt` stream
still cancels the run on disconnect.
- **Polling.** The list reconciles on an interval rather than a push, so a
change made elsewhere appears within seconds, not instantly.

## See also

- [ADR 0287](./0288-studio-atrium-module.md) — the Studio module and its
daemon-only posture
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE
- [0088 — Explicit daemon.yaml (listener topology config)](./0088-daemon-config-file.md)
- [0222 — mecatui: ctrl+t routes by ask type; full-screen ask-args view](./0222-mecatui-ask-args-view.md)
- [0288 — Studio: the Atrium workspace as mecatl's daemon-only web client](./0288-studio-atrium-module.md)
- [0289 — Studio's chat list is the daemon's session store](./0289-studio-server-backed-chats.md)
- [0247 — mecatui generated status lines](./0247-mecatui-status-line.md)
- [0280 — Automatic light theme selection in mecatui](./0280-mecatui-light-theme-autodetect.md)

Expand Down
2 changes: 1 addition & 1 deletion docs/design/PRODUCTION-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ record; current behaviour is in the linked [architecture](../architecture.md) do
| mecak8s (storage-free k8s-native agent) | ✅ shipped (MVP) · ✅ OPT-IN `/metrics` loopback scrape + OTLP push (ADR 0098) · ✅ verified external Redis TLS/ACL with transactional projected-file reload + last-valid generations (ADR 0240) · ✅ Helm 0.3.0 secure real-provider in-pod TLS+OIDC or edge-terminated TLS+OIDC (ClusterIP h2c), nullable spend ceilings, and pod scheduling controls · ⛔ CRD/Operator · ⛔ HPA (custom-metrics on active-runs) · ⛔ managed Redis provisioning (ElastiCache/MemoryStore — endpoint only) · ⛔ fix `mecated`'s unbounded `GracefulStop` (pre-existing, follow-up) | [mecak8s.md](../adr/0048-mecak8s.md) · [0098](../adr/0098-headless-telemetry.md) · [0240](../adr/0240-mecak8s-credential-reload-and-chart-security.md) · [0278](../adr/0278-mecak8s-edge-terminated-tls.md) · [MECAK8S-PLAN.md](./MECAK8S-PLAN.md) | [overview](../architecture.md) |
| ACP adapter (editor stdio surface) | ✅ Phase 1+2 + bounded Phase 3 + multimodal shipped · ⛔ Phase 3 long-tail (rule persistence, grep-over-buffers, fs/* on resume) | [0001-acp-adapter.md](../adr/0001-acp-adapter.md) | [api surface](../architecture/api-surface.md) |
| Conversation fork (peer session from a history snapshot) | ✅ shipped · ✅ effort override (mid-conversation effort switch, keeps the transcript — [0068](../adr/0068-effort-change-via-fork.md)) · ⛔ cross-provider/model fork (v2: replay-blob stripping) · ⛔ workspace-branching fork · ⛔ fork-from-event-log-at-arbitrary-point · ⛔ fork lineage (`forked_from` label) | [0065-conversation-fork.md](../adr/0065-conversation-fork.md) | [overview](../architecture.md) |
| Studio (web client) | 🚧 landing as a stacked PR series: ✅ module foundation (vendored Atrium UI kit, root chrome, npm/Biome/knip/vitest toolchain, CI checks incl. the license-header gate) · server tier (proxy + managed-mode controller) · protocol seam + harness · ⛔ the five surfaces (Chats · Scheduled · Skills · Memory · Settings) · ⛔ browser e2e | [0288](../adr/0288-studio-atrium-module.md) | [overview](../architecture.md) |
| Studio (web client) | 🚧 landing as a stacked PR series: ✅ module foundation (vendored Atrium UI kit, toolchain, CI gates) · server tier (trusted proxy + managed-mode controller core, hermetic suite) · protocol seam + harness transport · ✅ workspace shell + runtime status · ✅ Chats core + hermetic browser e2e (fixture daemon) · ⛔ Scheduled · Skills · Memory · Settings surfaces · ⛔ advanced chat tiers (attachments, steer/queue, threads, re-attach, modes, mobile) | [0288](../adr/0288-studio-atrium-module.md) · [0289](../adr/0289-studio-server-backed-chats.md) | [overview](../architecture.md) |
| _Historical / retired_ | — | [ARCHITECTURE.md](../adr/0004-v1-architecture.md) · [STEP-CHAIN.md](../adr/0006-v1-step-chain.md) · [TWELVE-PATTERNS-AUDIT.md](../adr/0007-twelve-patterns-audit.md) · [REPOMAP-TREE-SITTER.md](../adr/0029-repomap-tree-sitter.md) | — |

## Security
Expand Down
Loading
Loading