# Message Read Path Performance Boundaries

> Canonical path: `specs/message/read-path-performance-boundaries.md`
> Status: target design
> Related specs:
> - [`specs/message/scope-model.md`](./scope-model.md)
> - [`specs/message/scope-target-access-contract.md`](./scope-target-access-contract.md)
> - [`specs/message/scope-visible-summary.md`](./scope-visible-summary.md)
> - [`specs/message/workspace-global-seq-cursor-design.md`](./workspace-global-seq-cursor-design.md)
> - [`specs/message/unread-runtime-memory.md`](./unread-runtime-memory.md)
> - [`specs/message/delivery-ledger-optimization.md`](./delivery-ledger-optimization.md)
> - [`specs/observability/observability.md`](../observability/observability.md)

This spec defines the durable cost contract for message-domain read paths that
run during first paint, conversation navigation, unread/subscription summary,
realtime recovery, message sync, mention resolution, and agent roster loading.

The central rule is:

> A read path must scale with the explicit user intent, page size, dirty-scope
> page, requested target list, or product-bounded diagnostic scope. It must not
> scale with all visible scopes, all workspace conversations, all historical
> cursors, all agents, or all accumulated messages unless the product operation
> is explicitly a bounded workspace-wide job.

## 1. Scope

In scope:

- browser first-paint bootstrap and workspace shell refresh;
- conversation list, conversation summary, unread counts, subscribed threads,
  and thread previews;
- message sync and realtime gap recovery;
- message history and explicit scope reads;
- mention and target resolution on send, autocomplete, and command surfaces;
- agent list and agent runtime summary reads when they are used by the message
  surface;
- authorization, workspace/member/session facts loaded as part of these paths.

Out of scope:

- write-path fanout cost, which is governed by
  [`delivery-ledger-optimization.md`](./delivery-ledger-optimization.md);
- observability collector cost, which is governed by
  [`observability.md`](../observability/observability.md);
- one-off admin diagnostics that run only after explicit operator intent and
  expose their own limit, continuation, or job scope.

## 2. Cost Model

Supported runtime read cost shapes:

```text
O(requested scope ids)
O(page size + bounded overfetch)
O(dirty scope page + dirty candidate scan budget + scope batch size)
O(requested mention tokens)
O(visible result page hydration)
O(explicit diagnostic target set)
```

Unsupported runtime read cost shapes:

```text
O(all workspace scopes)
O(all visible scopes for a member)
O(all workspace conversations before pagination)
O(all historical cursor generations)
O(all workspace agents for an optional badge)
O(all workspace members or agents for one requested target)
O(all messages or activity rows accumulated in the workspace)
```

Broad workspace reads are allowed only when the route, worker, or diagnostic is
itself explicitly workspace-wide and has an operator-visible limit, continuation,
or batch/job boundary. A helper that enumerates workspace visibility is not safe
to reuse from a narrow route merely because the result is filtered later.

## 3. Conversation Visibility Reads

Conversation and thread visibility is an authorization predicate, not a product
page source. Hot reads must choose the candidate set first, then authorize and
hydrate those candidates.

### 3.1 Explicit target reads

When a request names a channel, DM, thread, task discussion, or list of scope
IDs, the SQL shape must start from those requested IDs. It may join memberships,
parent scopes, root messages, private grants, agents, or machines to prove
visibility, but it must not enumerate every visible thread in the workspace and
then filter to the requested target.

This applies to history, scoped unread, thread summary, message page hydration,
source preview, attachment access, and reply-thread adjunct reads.

### 3.2 Conversation list

Conversation list reads must start from a bounded product candidate source such
as:

- current page of recent active scopes;
- current page of subscribed scopes;
- current page of task-inbox conversation state;
- current page of explicit search/filter results;
- a materialized per-member conversation projection.

The query may overfetch by a bounded factor to compensate for authorization
filtering. It must not build the full set of visible conversations before
ordering and paginating. If the candidate source cannot produce enough
authorized rows within its scan budget, the response must include a continuation
or partial-state signal rather than falling back to a workspace-wide visibility
scan.

Filtered Inbox views are separate candidate intents. When a request selects
specific conversation kinds, for example `kind=dm`, `kind=replyThread`,
`kind=task`, or `kind=serviceAccount`, the read path must build only the candidate
branches for those normalized kinds before hydration. The unfiltered Inbox may
still merge channel, DM, thread, task, and service-account candidates for the
global page, but filtered views must not pay candidate scan cost for excluded
kinds and then discard them after paging.

### 3.3 Conversation summary and unread surfaces

First-paint summary surfaces must not compute exact unread counts by scanning all
visible scopes. The workspace-level product contract is `hasUnread`, as defined
in [`workspace-global-seq-cursor-design.md`](./workspace-global-seq-cursor-design.md).

The target unread serving model is defined in
[`unread-runtime-memory.md`](./unread-runtime-memory.md). Warm workspace red-dot,
sidebar badge, active scope badge, refresh, and heartbeat reads must use the
workspace owner unread runtime, not request-time database aggregation. In split
profiles, central workspace list routes do not own workspace-local runtime; V1
central lists suppress unread instead of owner-hopping or querying TiDB.

Compatibility or recovery-only summary shapes:

- policy-filtered `EXISTS` queries;
- existing owning-domain sparse facts such as sync dirty frontier; removed
  taskattention state must not be used as a recovery input;
- bounded aggregation over the current visible page;
- lazy exact counts after the user opens a scope, thread panel, or diagnostic.

These shapes may be used during migration, diagnostics, cold lazy recovery,
operator repair, or explicit lazy reads, but they are not the target warm
first-paint, workspace switcher, or heartbeat implementation.

Explicit Inbox exact totals are a compatibility exception until the Inbox flow
uses runtime-backed or otherwise intent-bounded state. Replacing that endpoint
with workspace-level existence semantics would change visible Inbox badge and
bulk mark-read behavior. Therefore exact Inbox summary reads may preserve exact
count semantics for explicit Inbox intent, but they must not be required by
workspace shell first paint, workspace switcher badges, document-title
attention, or route-independent bootstrap.

Legacy per-scope unread detail for sidebar row badges has the same runtime-state
gap, and the old exact `/api/channels/unread` style compatibility responses are
retired from the normal Web serving path. Sidebar row badges should use the
active workspace unread store seeded by snapshot, updated by UnreadRuntime
deltas, and cleared by local optimistic read actions; new unread work must not
add request-time aggregation over all visible scopes.

The enforced compatibility boundary is:

- legacy exact unread database aggregation helpers are store-local
  compatibility/diagnostic code or removed cleanup debt; they must not be
  referenced by API, realtime, route bootstrap, or web warm-refresh code;
- explicit sidebar row-badge compatibility reads must be owner-runtime backed
  and must not fall back to `UnreadCountsForUser` or equivalent request-time
  database aggregation;
- route-independent workspace bootstrap and snapshot hydration may seed unread
  hints only from owner runtime state, trusted runtime snapshot hints, or the
  already requested bounded conversation page;
- workspace switcher/list warm refreshes request workspace rows without unread
  recomputation and preserve cached/runtime unread hints;
- socket heartbeat and reconnect are liveness and transport recovery surfaces,
  not unread count repair surfaces;
- explicit Inbox and detail surfaces may issue their own lazy, user-intent
  reads, but exact unread totals from those surfaces must not drive workspace
  shell first paint, title attention, workspace switcher badges, or route-level
  bootstrap state.

`guardrails/message/unread-compat-builders.sh` enforces this boundary by
keeping legacy exact unread aggregation helpers out of production warm-path
callers, keeping removed legacy conversation unread-summary helpers removed,
and by checking the first-paint, workspace switcher, heartbeat,
route-independent bootstrap, and warm-refresh frontend files for exact unread
endpoint/query-option use.

Forbidden summary shapes:

- enumerating all visible threads to compute optional badges;
- joining all scope messages newer than read state for first paint;
- using full unread counts as a required workspace-shell render dependency;
- broad fallback paths that replace a failed runtime/snapshot/recovery read
  with request-time aggregation over all visible scopes.

### 3.4 Subscribed threads and previews

Subscribed-thread reads must start from subscription state for the current
member or from an explicit thread candidate list. They must not start from all
visible threads and later filter to subscribed rows.

Preview hydration for subscribed threads follows the same page-adjunct rule as
reply summaries: hydrate only the bounded thread IDs selected for the current
page.

## 4. First Paint And Client Refresh

Workspace shell first paint should render from the minimum facts required to
show the active workspace and current route. Optional state is lazy unless the
route itself requires it.

Required constraints:

- bootstrap may load identity, current workspace access, route-critical channel
  or conversation facts, and a bounded freshness cursor;
- bootstrap must not eagerly load all unread details, all subscribed thread
  previews, all agents, all runtime failure summaries, or all mention candidates;
- realtime gap recovery may trigger message sync, but sync page count and page
  size are product limits and must not hide an unbounded visibility scan;
- cache invalidation after sync must refresh only the affected route, visible
  page, dirty scopes, or summary fact, not every broad workspace surface.

Frontend tests that protect first-paint load policy should assert that optional
broad endpoints are not called before user intent or route demand.

## 5. Message Sync Cursor Reads

Message sync follows the target model in
[`workspace-global-seq-cursor-design.md`](./workspace-global-seq-cursor-design.md):
opaque cursor, immutable retry semantics, and dirty-scope discovery.

Additional performance decisions:

- Each sync page must be bounded by dirty-scope page size, an explicit dirty
  candidate scan budget, scope batch size, and per-scope message page size.
- Dirty discovery must apply ordered candidate windows before visibility,
  notification, and cursor-frontier joins. The scan budget may skip stale or
  currently ineligible candidates within the same request, but the hot query
  must not construct all dirty scopes for the workspace before applying the
  response limit.
- Sync must not scan all visible scopes to discover changes.
- Sync must not scan all historical messages in a workspace.
- Sync must not recursively read all historical cursor ancestors on each page.
- Cursor compaction and garbage collection are maintenance concerns; hot sync
  reads must remain bounded even before a cleanup job runs.
- Target state: initial frontier creation for a large workspace must be paged
  or deferred. It must not require one first-paint request to materialize every
  visible scope.
- Compatibility exception: if a newly created opaque cursor must mean "after
  every message visible at cursor creation time" and message writes no longer
  maintain a workspace-wide commit-order scalar, the exact implementation must
  capture per-scope frontiers inside the cursor creation transaction. A lazy
  timestamp baseline is not equivalent because message timestamps do not encode
  commit visibility; `workspace_global_seq` is also not equivalent once it is
  only a per-scope compatibility alias. This exception should be replaced by a
  durable sync epoch/fence or a paged initialization protocol that preserves the
  same snapshot semantics.

Acceptable implementations include a materialized current frontier, a
per-member dirty-scope index, compact cursor snapshots, or another equivalent
structure. The correctness requirement is retry-safe frontier advancement; the
performance requirement is that the query shape is independent of historical
cursor generation count.

## 6. Auth, Workspace, And Member Facts

Authorization facts loaded for every HTTP request are hot even when each query is
small. Message-domain handlers must distinguish:

- minimal session/user validation;
- minimal workspace/member access proof;
- full workspace summary DTOs;
- product payload hydration.

Middleware and route adapters may carry authenticated user, workspace, and
member facts in request context after they are loaded. A handler must not call a
full workspace summary query merely to prove access when a narrower access fact
is already available or sufficient.

Repeated membership lookups inside the same request should be replaced by
request-scoped reuse or a bounded batch query. The batch must be keyed by
requested workspace/member IDs, not by all memberships in the workspace.

## 7. Agent Roster And Runtime Summaries

The message surface uses agent information for DMs, mentions, wake/routing
decisions, and diagnostics. These are not the same product read.

Required split:

- lightweight agent/member roster reads for route-critical UI and target
  resolution;
- paged administrative agent lists for workspace management;
- lazy runtime health and failure-summary reads for diagnostics or visibly
  relevant agents.

First paint and ordinary conversation navigation must not require loading every
agent plus runtime failure summaries. Failure summaries should be fetched only
for the selected agent set, visible page, active runtime panel, or explicit
diagnostic request.

## 8. Mention And Target Resolution

Mention and target resolution must be bounded by the requested tokens, explicit
target refs, current scope, or autocomplete page.

Rules:

- send-time mention resolution starts from the parsed mention tokens or explicit
  target refs;
- scope membership checks start from the current target scope and requested
  members/agents;
- autocomplete may search a workspace roster only behind a prefix, page size,
  and continuation;
- a single requested token must not cause a scan of all workspace members,
  agents, apps, and scopes before filtering;
- resolver helpers must return typed target/access roles as defined by
  [`scope-target-access-contract.md`](./scope-target-access-contract.md), so
  callers do not re-resolve broad authority later.

## 9. Observability And Verification

Implementations that change these paths should add tests or diagnostics that
verify query shape, not only response correctness.

Recommended checks:

- backend query-count tests comparing one requested scope with many requested
  scopes and proving no N+1 growth;
- SQL-shape tests that narrow target reads start from request IDs rather than a
  broad visible-thread CTE;
- sync tests proving dirty-scope discovery is bounded by dirty page / candidate
  scan budget / scope batch size and independent of cursor ancestor count;
- frontend first-paint tests proving optional broad endpoints are lazy;
- statement-summary review for new high-frequency digests after rollout.

Operational statement summaries and slow-query samples are evidence for
prioritization, not canonical product facts. Do not copy one-time row counts or
latency totals into this spec as requirements. If a future change adds a durable
guardrail script, place it under `guardrails/message/` and link it here.

## 10. Acceptance Criteria

- New narrow message reads start from requested IDs, current page candidates,
  dirty scope pages, or explicit diagnostic targets.
- First-paint bootstrap avoids optional broad unread, subscribed-thread, agent,
  runtime-failure, and mention-candidate reads.
- Conversation list and summary surfaces do not enumerate all visible threads as
  the normal page source.
- Message sync hot reads do not recurse through historical cursor chains or scan
  all visible scopes.
- Workspace-level unread remains `hasUnread` / existence semantics, not exact
  first-paint count aggregation.
- Auth and workspace/member facts loaded by middleware are reused when handlers
  need the same proof.
- Agent roster, runtime failure summaries, and diagnostics are separate lazy or
  paged reads.
- Mention resolution is bounded by requested tokens, current scope, explicit
  target refs, or autocomplete page size.
