# Message Search

> Canonical path: `specs/message/search.md`
> Related specs: [scope 消息模型](scope-model.md), [scope 消息模型实现设计](scope-implementation-design.md)

Date: 2026-04-22
Status: current

This document defines the supported message-search contract for AgentTeam's scope message model.

## Backend

Message search is TiDB-only.

- Search is backed by `scope_message_search_docs` and `scope_message_search_state`.
- The TiDB FULLTEXT artifact must use the `MULTILINGUAL` parser so contiguous Chinese, Japanese, and Korean text can be searched.
- Search is available only after the TiDB projection is backfilled, finalized with the FULLTEXT artifact, and marked read-ready.
- Upgrade migration copies eligible hot search documents into `scope_message_search_docs`, but initializes scope search readiness as not read-ready. Existing pre-scope search readiness never authorizes scope search.
- If the projection is not ready, the product behaves as if search is unavailable. It must not fall back to `LIKE`, client-side filtering, or a storage-engine-specific alternate path.

## Search scope

Search covers the live `scope_messages` transcript only.

- Archived message objects are out of scope.
- Search documents are projections; they are not read-access facts.
- Every result must pass the same SQL readable predicate / visible-row mapper contract used by history, read-check, root/source context, attachments, and diagnostics preview. Server `/receive` is not a search/read authority surface.
- Results include enough metadata to navigate back to the message's scope and message position.

## User surfaces

Human API:

- `GET /api/messages/search?q=<query>&scope_id=<optional>&author_kind=<optional>&author_id=<optional>&limit=<optional>&cursor=<optional>`
- Responses include `messages`, `hasMore`, and opaque string `nextCursor`
  tokens when more candidate/result pages are available. Legacy clients may
  still send `offset`, but `cursor` and `offset` are mutually exclusive; cursor
  pagination is the production hot-path shape.

The optional author filter is available to the Human API and AgentTeam Web Messages
tab. `author_kind` and `author_id` must either both be omitted or both be
present. Supported kinds are:

- `member`: `author_id` is a human `members.id`; only messages written by that
  human match;
- `agent`: `author_id` is a product `agents.id`; only messages written by that
  exact Agent match;
- `member_agents`: `author_id` is a human `members.id`; messages written by the
  product Agents owned by that human match, but messages written by the human
  do not.

Malformed, partial, or unsupported author filters return `400`. A well-formed
ID that does not identify the requested author kind in the current workspace
matches no messages. Author eligibility is applied in SQL before ranking,
candidate budgeting, and `LIMIT`; it is combined with `scope_id` and the shared
readable predicate using `AND`. It never widens the caller's readable set.

Agent ownership follows the workspace's canonical Human-to-machine-to-Agent
relationship. Inactive and archived Agents remain eligible while their Agent
row exists. Hard-deleted Agents have no remaining ownership row and therefore
cannot be recovered by `member_agents`.

AgentTeam Web's exact-Agent picker lists Agents discoverable through the current
workspace Agent directory. Archived Agents and Agents on inactive machines are
not newly discoverable in that picker. This discovery boundary does not change
matching: a retained exact-Agent selection still matches while its Agent row
exists, and `member_agents` includes those retained Agent rows.

In AgentTeam Web, selecting a channel, DM, or reply-thread message result opens its
source scope and positions the target row as close to the center of the
transcript as scroll bounds permit. The target row receives a brief, clearly
perceptible highlight, and visible occurrences of the active query terms within
the rendered message are emphasized more strongly than the surrounding text.
Both effects are transient presentation state and are not stored as message
state.

Agent surface:

- MCP tool `search_messages`

Both surfaces rely on the same TiDB-backed store logic and the same scope visibility contract. The MCP tool does not expose the Human API author filter.

## Readiness gate

The runtime must not expose search until the TiDB projection is ready.

Readiness depends on `scope_message_search_state` reporting:

- migration complete;
- backfill complete;
- finalize complete;
- `read_ready = true`.

Readiness is evaluated against the workspace-global TiDB projection for the relevant workspace. Scope-filtered queries do not expose a separate scope-level admin/readiness contract in v1; they rely on the same workspace-global readiness row plus normal visibility checks.

The API readiness gate must use the same full contract. A state row with `read_ready = true` is not enough if migration, backfill, or finalize is not complete.

## Visibility, pagination, and cost

Search queries fetch bounded candidate documents from TiDB FULLTEXT and join the shared SQL readable predicate before returning rows. The Go visible-row layer only scans, deduplicates, paginates, maps `visibility_reason` / `grant_id`, and fails closed if the SQL row omits required authority metadata.

- Candidate scanning is bounded per request.
- If invisible candidates consume the request budget, the response returns explicit continuation state: `has_more`, `candidate_budget_exhausted`, and a continuation cursor.
- An empty result page with continuation state is not an end-of-results signal.
- Pagination must not let invisible results hide older visible matches.
- Workspace-wide cursor pagination must continue by the full search order tuple
  `(global_seq DESC, scope_id ASC, seq DESC, id ASC)` so duplicate global
  sequence values in different scopes cannot be skipped.
- Clients must use a cursor only with the same normalized query, scope, and
  author filter that produced it. Changing any of them starts pagination from
  the first page.
- Query descriptors must name the target FULLTEXT/index path and the candidate-budget amplification bound.

## Access control

Human users can search messages they can already read through normal scope visibility rules:

- channel results require membership in the channel scope;
- DM results require participation in the DM scope;
- reply-thread results require visibility of the thread scope and the relevant root/source context;
- registered product-scope results require the product permission basis declared by that scope kind.

Agents can search messages in scopes and source contexts that are visible through the same SQL readable predicate used by `read_history`, read-check, and source/context checks. Daemon delivery payload authorization is a separate fanout-outbox projection and does not turn search into an offline inbox.

Private-agent search does not derive access from member-list display. It uses membership intervals, private grants, author/source intervals, non-message activities (for diagnostics only), and task-native ingress facts.

## Administration

The supported low-level operational flow for one workspace-global projection is:

1. `agentteam-tidb-admin workspace-message-search-backfill`
2. `agentteam-tidb-admin workspace-message-search-finalize`
3. `agentteam-tidb-admin workspace-message-search-promote-ready`
4. `agentteam-tidb-admin workspace-message-search-status`

These low-level repair/debug commands target exactly one workspace-global readiness row, selected by `AGENTTEAM_WORKSPACE_MESSAGE_SEARCH_WORKSPACE_ID`.

The supported high-level orchestration command is:

5. `agentteam-tidb-admin workspace-message-search-ensure-ready`

`workspace-message-search-ensure-ready` enumerates every workspace in deterministic `created_at ASC, id ASC` order, fast-returns already-ready rows, runs backfill/finalize/promote on the rest, fails fast on the first workspace error, writes phase logs to stderr, and prints the final machine-readable summary to stdout JSON.

The message applier writes fresh committed scope messages into `scope_message_search_docs` in the same transaction as the message fact. The admin backfill command fills or repairs historical gaps. Backfill/finalize/promote are idempotent operations: re-running them after a projection is read-ready must not downgrade `read_ready` or make search unavailable.

Finalize creates or verifies the `MULTILINGUAL` TiDB FULLTEXT artifact for `scope_message_search_docs`; it requires `tidb_enable_fulltext_index=ON` and TiFlash/TiFlash Compute registration. Promote-ready must verify the projection is finalized with that artifact and has no missing rows before setting `read_ready=true`.

The store treats search as unavailable until these steps complete successfully for the workspace-global projection, and continuing live writes keep the projection up to date after promotion.

## Non-goals

- archived-history search;
- storage-engine-specific fallback behavior;
- separate product behavior by storage engine;
- exposing search documents as an access-control or delivery authority.
