Studio data flow
This page maps one Studio request end to end — HTTP preload → AsyncLocalStorage → Next.js server rendering → client refresh and SSE — and marks the query, projection, serialization and cache boundaries.
The chain
Studio has two hosts that share the same application layer:
- Next.js host (
omk studio, DSH plugin/omk observe, CLI evaluation preview):next-server.tshandles pages in two ways./observeand conversation/inbox/health pages,/measure*,/knowledgeand/knowledge/skills/*, and/agentspreload models throughhttp/pages/, settle 400/404/503 before Next streams, and expose request snapshots through AsyncLocalStorage bridged bySymbol.for./agentsrenders missing or unreadable reports as page states. A missing store is an assembly error (studio_context_missing→ 500), distinct from an unavailable source. Preload failures render a bilingual Next recovery page at the original address with the determined HTTP status andno-store; a host-owned header rewrite supplies only a whitelisted fault projection, never a source exception. Read-again reloads the original request; list recovery preserves an explicit workspace and respects mounted page groups. Missing comparison parameters (400) return to report selection. The private rendering destination is not a public route. Trailing-slash redirects are disabled so a missing identity segment remains the host's HTML 404 at the same URL. Repeated slashes and raw backslashes are malformed request separators: the host rejects them before reading a source with the existing plain-text 404 identity,no-store, and noLocation; this is the only separator-specific exception to the HTML failure view. BREAKING-HTTP: recognized page failures now return HTML instead of a bare error string; stable fault identities remain visible in diagnostic details. JSON API/SSE error contracts and the existing GET-only page policy (HEAD/other methods: 405) are unchanged. - Client-loaded knowledge pages: Next also handles
/knowledge/candidates,/knowledge/entities, and/knowledge/library, without preloading knowledge models. Clients query candidates, entities, the library, and carriers through/api/*and show loading/failure states. These pages do not have one RSC knowledge snapshot per request; a page's 200 status does not prove its API reads succeeded. - Host switches and unmatched addresses:
observationInbox: falsedisables inbox pages and APIs;studioPages: falsedisables observe/knowledge page groups. CLI evaluation preview serves only Measure pages and hides primary navigation. Next still serves/_next/*assets. Unmatched addresses return to the HTTP handler for a plain-text 404; Next's client-sidenot-founddoes not replace the host's preload error contract. - Standalone report-server host:
request-handler.tsresolves the directory selection once per request and dispatches through the declarative route table (routes/router.ts) to JSON APIs, SSE,/healthand plain-text status documents. It renders no page HTML; page routes return 404 even when the Next host disables its page group. The Next host wraps this handler, so every non-intercepted path — including/api/reports(the evaluation JSON projection) andGET /(302 →/observeon hosts that mount the observe group, 404 otherwise) — is served here.studioPages: falsealso suppresses the observations directory side effect, which is why a measure-only mount never writes into the user's project.
Boundaries
- Request authority: the listener validates a single well-formed
Hostbefore Next or API dispatch. It accepts loopback hosts and an explicitly configured listen hostname; wildcard network bindings also accept literal IP addresses for LAN access. Other DNS names receive403 studio_host_not_trusted, even when Origin and Host agree, to block DNS rebinding. Explicit network exposure still requires a trusted network; this check does not add authentication. - Query (
application/): Among Skill-report queries,buildSkillIndexuses caching (bounded keyed LRU, capacity 8; directory and presentation language in the key; fingerprint recomputed per request from file metadata).listAnalyses/loadAnalysis/querySkillTrendare deliberately uncached single-pass scans. The conversation catalog keeps a per-thread rollout index file cache validated by size + mtime. - Observation evidence: problem-pattern counts retain all distinct evidence and physical session identities through invocation, session and skill aggregation; diagnostic previews show at most five references and five recent sessions. Skill-definition lookup uses an LRU of eight search roots, revalidated against searched directories, so adding or removing a skill is visible without restarting. Background conversation indexing has a two-minute process deadline (
indexProcessTimeoutMsfor embedders), terminates a stalled worker and releases the pending entry for retry. Failed or empty soft-standard extraction is not saved or cached; historical derived reports remain unchanged and must be rebuilt from their original traces to apply corrected conclusions and counts. - Projection (
view-models/+ page loaders): a preloaded page model is projected from its request snapshot — the same in-memory object feeds both the HTML props and the activity revision, so a page can never render a revision that disagrees with its content. - Serialization: preloaded models cross the RSC boundary; client-loaded knowledge pages consume APIs separately; APIs answer JSON with
Cache-Control: no-store. - Cache: see above; nothing caches rendered HTML.
Refresh model
| Surface | Mechanism | Gate |
|---|---|---|
| Conversation list / detail | 5 s polling on /api/conversations/*/activity | sha256 revision over lifecycle fields only: thread ids, title, archived flag, turn count, open-task id (list); title, per-task id/status/start/end (detail) |
| Task trajectory | SSE /api/conversations/:thread/tasks/:turn/live | revision = sourceSize:mtimeMs:status; hub polls the source file every 750 ms with a stat fast path |
| Measure pages | none | static per load; refresh by navigation |
Revision-gating means router.refresh() fires only when user-visible lifecycle state changes. Growth signals are deliberately excluded — tool-call counts, event counts and recent-activity timestamps advance continuously while a task runs, and including them would re-render the whole page on every poll. The trajectory page owns live following; list and detail pages refresh on lifecycle transitions (new task, status change, rename, archive). This tradeoff is locked by conversation-activity-server.test.ts ("without tracking event growth").
Current invariants
- One snapshot per request. Next preloaded pages load one model and derive revisions from that object; the standalone host loads no HTML page model; concurrent requests are isolated by ALS (covered by
next-context.test.ts). - No duplicate refresh mechanisms. Conversation lifecycle polling and task-trajectory SSE each have a specific purpose. Each page uses the mechanisms required by its data lifecycle.
- Revision coverage. Trajectory revisions are file-stat based and cover every content change. List/detail revisions cover all lifecycle-visible changes including renames; growth signals are excluded by design, see above.
- Race, cancel and cleanup paths. The client poll aborts on unmount, skips while
document.hidden, and serializes overlapping fetches; the SSE client debounces refresh (250 ms), closes onliveObservable: falseandtrajectory-error, and exposes retry. Server side,PollingSubscriptionHubshares one sequential loop per task key, disposes on terminal state, error, or last unsubscribe, unrefs its timers, andclose()runs on server shutdown. - Failure semantics. Refresh failure keeps the rendered data and offers an explicit retry (
ActivityNotice); assembly errors and source unavailability produce distinct 500/503 codes at the host boundary.
Recorded tradeoffs
- Conversation list transfers the full list and filters/paginates client-side (20/page); current Next-page and remote-host capacity still require verification on the corresponding machine and data scale.
- Measure pages have no live refresh today; running evaluations are re-examined by navigation. Recorded as a decision, not an oversight.
- Title rename bumps the revision; growth signals do not. If a future surface needs live counters on list/detail pages, add them to the snapshot state deliberately — not implicitly.