# Scope Visible Summary

> Canonical path: `specs/message/scope-visible-summary.md`
> Status: draft
> Scope: durable human-visible message summary for scope topology and reply-thread hydration.

## Background

Message-page hydration needs reply thread summaries for the bounded parent messages on the current page. A separate reply-summary fact source is no longer a reliable runtime dependency. Current scope topology represents reply thread scopes as child `scopes`, with messages in `scope_messages` and participants in `scope_memberships`.

AgentTeam maintains durable visible-message summary directly on `scopes` and reads reply summaries from child scopes. General cache platform design is separate from this summary contract.

## Goals

- Maintain `visible_message_count` and `visible_last_message_at` for every scope.
- Keep ordinary message insert maintenance O(1) on the affected scope row when a new human-visible message is inserted.
- Read message-page reply summaries by bounded parent message IDs from child thread scopes.
- Use `scopes.visible_*` and avoid `scope_messages` aggregation in hot reply-summary reads.
- Keep repair paths scoped to explicit affected scope IDs.

## Non-goals

- No storecache/read-through cache framework.
- No Redis/backend/namespace cache platform design.
- No unread-count or workspace-navigation cache.
- No request-time fallback that aggregates `scope_messages` for reply summaries.

## Schema

`scopes` has durable summary columns:

```sql
visible_message_count BIGINT NOT NULL DEFAULT 0
visible_last_message_at TIMESTAMP NULL DEFAULT NULL
```

Reply-summary hot reads require an index that can point lookup a child thread scope by parent scope and root message:

```sql
KEY scopes_parent_root_thread_idx (
  parent_scope_id,
  root_message_id,
  type,
  status,
  archived_at
)
```

Do not route this lookup through `identity_key`; that would bind a business query to string construction rules instead of the actual topology columns.

No separate reply-summary fact source is part of the final shipped schema. Runtime reply-summary reads, bootstrap schema, cleanup migrations, and fixtures must converge on `scopes.visible_*`; do not keep an expand-contract fallback or alternate read path.

## Human-visible predicate

`visible_*` uses the same semantics as `HumanVisibleMessageTypeSQL`.

Hidden messages:

- `task_dispatch`

Retired non-task hidden envelopes and system-card rows are removed by upgrade
migrations rather than kept as visible-summary predicate branches.

Visible examples:

- ordinary/default messages
- ordinary `system` messages
- stickers

The Go helper used by the write path must be tested against the SQL predicate cases.

## Write path

After inserting into `scope_messages`, ordinary message application updates `scopes.visible_*` in the same transaction only when:

1. `ScopeMessageInsertResult.Inserted == true`; and
2. the message is human-visible.

For a human-visible insert:

```sql
visible_message_count = visible_message_count + 1
visible_last_message_at = GREATEST(COALESCE(visible_last_message_at, message.created_at), message.created_at)
```

For non-human-visible messages or idempotent replay, `visible_*` must not change.

The write granularity is the affected scope row. This summary path must not add workspace-level, large channel-level, or global generation hot rows.

## Backfill and repair

Because `visible_*` is a generic scope summary and the write path maintains all scope types, offline backfill must populate all scopes, not only thread scopes.

Repair paths that can change the visible message set should call a scoped recompute helper:

```go
RecomputeScopeVisibleMessageStats(ctx, tx, scopeID, updatedAt)
```

The helper must:

- accept an explicit `scopeID`;
- compute `COUNT(*)` and `MAX(created_at)` from `scope_messages` with the human-visible predicate;
- write the result back to that one scope in the same transaction.

Do not add workspace-wide or channel-wide request-time recompute fallback to hot reads.

## Reply summary read path

`ListReplyThreadSummariesForParentMessageIDs` remains a bounded page adjunct:

1. build a CTE for the current page's parent message IDs;
2. resolve the parent scope;
3. join child thread scopes by `parent_scope_id`, `root_message_id`, `type = 'thread'`, active status, and non-archived state;
4. select `root_message_id`, child thread scope ID, `visible_message_count`, and `visible_last_message_at`;
5. load participants with a bounded `scope_memberships` query for selected child thread IDs.

Hot read guardrails:

- no separate reply-summary fact-source runtime read;
- no `scope_messages` `COUNT` / `MAX` aggregation;
- no broad fallback scanning all child thread scopes in a large parent channel;
- no O(n+1) participant lookup.

## Guardrail tests

Required coverage:

- bootstrap and cleanup migration include `visible_message_count`, `visible_last_message_at`, and `scopes_parent_root_thread_idx`, and remove the separate reply-summary fact source;
- migration/backfill updates all scope types with the human-visible predicate;
- human-visible Go helper matches SQL semantic cases;
- inserts update `visible_*` only for `Inserted == true` and human-visible messages;
- client message idempotent replay does not double count;
- reply summary hot read source reads child `scopes.visible_*` and does not aggregate `scope_messages`;
- scoped recompute repairs `visible_message_count` and `visible_last_message_at` for affected scopes.
