# Workspace Global Seq 与 Scope Frontier Cursor 设计

> Canonical path: `specs/message/workspace-global-seq-cursor-design.md`
> Status: target design
> Related: `docs/tech_design.md`, `specs/message/delivery-ledger-optimization.md`, `specs/message/read-path-performance-boundaries.md`, `specs/message/unread-runtime-memory.md`, `specs/message/search.md`, `specs/service-account/service-account.md`

本文定义消息同步 cursor 的目标方向：AgentTeam 不再把 workspace 级消息序列作为长期运行时依赖，而是迁移到 scope-local frontier + opaque cursor。这个决策依赖一个产品取舍：workspace 层只展示是否有未读的红点，不展示精确未读条数。Unread 热路径的运行时内存模型由 `specs/message/unread-runtime-memory.md` 定义。

结论先行：

1. `workspace_global_seq` 是当前实现里的 workspace-local scalar visible cursor。它让 `/api/messages/sync?since_seq=N` 可以用单个整数跨 channel、DM、reply-thread 拉取消息，但也要求同一个 workspace 的所有消息写入共享一个线性化点。
2. `workspaces.latest_message_seq` 是当前 workspace 级写热点。只要保留精确的 workspace scalar cursor，就需要继续维护这个 allocator，或引入等价的 sequencer / commit log。
3. 产品上不再要求 workspace 级精确未读条数。workspace switcher / workspace list 只需要 `hasUnread`，也就是红点语义。
4. 在没有公开旧客户端兼容承诺的前提下，目标方案是直接迁移到 scope-local frontier / opaque cursor，而不是先实现 workspace seq block allocator。
5. Server 升级到新协议后不兼容旧 `/sync?since_seq` 或旧 server-side daemon receive cursor。旧 daemon 必须收到明确的 upgrade-required 响应，并完整升级后才能继续使用；门禁不得要求每个 message API 请求携带 `X-*` 自定义协议 header。
6. Opaque cursor protocol 必须是 retry-safe 的：sync 响应生成 next cursor，但不能原地推进旧 cursor；旧 cursor 在 GC 前可重复请求，客户端成功应用响应并持久化 next cursor 后才进入新 frontier。
7. 迁移完成后，新消息和 non-message activity 写入只需要 scope-local sequence；sync、read-state、workspace unread、search、diagnostics 都应收敛到 `(scope_id, seq)`、scope-local activity cursor、`message_id`、`created_at` 和 server-side cursor state，不再依赖 `workspace_global_seq`。

## 1. 当前模型

当前消息主模型已经以 scope 为中心：

| 层级 | 字段 / 表 | 当前作用 |
| --- | --- | --- |
| workspace allocator | `workspaces.latest_message_seq` | 每个 workspace 一行的 workspace-local global seq high watermark。当前每条新消息都会更新这一行并得到 `workspace_global_seq`。 |
| scope | `scopes.latest_seq`、`scopes.latest_global_seq`、`scopes.latest_message_at` | scope 级 tail metadata。`latest_seq` 是 scope-local sequence；`latest_global_seq` 是迁移到 scope-frontier cursor 前的 scalar mirror。 |
| message | `scope_messages.seq`、`scope_messages.workspace_global_seq` | canonical live transcript row。`seq` 在 `scope_id` 内唯一；`workspace_global_seq` 在 `workspace_id` 内唯一。 |
| read state | `scope_memberships.last_read_seq`、`scope_memberships.last_read_global_seq`、`scope_memberships.last_activity_global_seq` | scope-local read-state 已经存在，但 web/app 迁移前路径仍使用 global read cursor。 |
| notification policy | `scope_notification_settings` | active / quiet / mute / auto-wake / no-wake current-state row；用于当前 unread / wake preference，不保存历史 receive grant 区间。 |
| non-message activity | `scope_activity_events.activity_global_seq` | reaction 等没有 message row 的 activity cursor。 |
| search projection | `scope_message_search_docs.global_seq`、`scope_message_search_state` | 当前 search projection 仍保留 global seq，便于迁移到 scope-frontier cursor 前定位 scalar ordering；最终查询定位收敛到 `(scope_id, seq)` / `message_id`。 |

`workspace_global_seq` 里的 "global" 只表示同一个 workspace 内跨 scope 的顺序。它不是全站唯一 ID。

当前 human sync API 的核心语义是：

```sql
FROM scope_messages m
JOIN visible_scope_refs ON visible_scope_refs.scope_id = m.scope_id
WHERE m.workspace_id = $1
  AND m.workspace_global_seq > $2
ORDER BY m.workspace_global_seq ASC
LIMIT $3
```

客户端只保存一个 `since_seq`。这个模型要求后端保证：客户端已经推进到 `since_seq=N` 后，不会再出现一条对它可见且 `workspace_global_seq <= N` 的新提交消息。为了保证这个性质，所有同 workspace 的消息写入必须经过同一个 visible-order fence。

## 2. 为什么不继续优化 Workspace Scalar Cursor

### 2.1 Block allocator 不是目标架构

Workspace seq block allocator 可以把 `workspaces.latest_message_seq` 的 DB 更新从每条消息一次降到每 N 条一次，但它仍然保留 workspace-wide order：

```text
workspace owner/queue
  -> 取 workspace_global_seq
  -> 写 scope_messages 事务
  -> commit/rollback
```

这个方案适合已经上线且必须兼容 `/sync?since_seq` 的系统，因为它不改客户端协议。但它没有消除 workspace 级线性化点，只是把瓶颈从 DB row lock 移到应用层 owner/queue。若产品和客户端协议还可以改，block allocator 会把系统继续绑在旧约束上。

### 2.2 AUTO_INCREMENT / TSO 也不直接解决问题

TiDB cached `AUTO_INCREMENT`、sequence 或 commit timestamp 只有在能保证 visible commit order 时才能替代 scalar cursor。否则会出现低 seq 后提交、高 seq 先被客户端确认的漏消息问题：

```text
T1: 分配 seq = 100，写消息 A，但事务还没提交
T2: 分配 seq = 101，写消息 B，并先提交
客户端 sync since_seq=99，看到 B，cursor 推进到 101
T1 随后提交 A(seq=100)
客户端下次 sync since_seq=101，因此永远不会拉到 A
```

只要继续要求单个 scalar cursor 精确表达跨 scope 可见消息顺序，就仍然需要 workspace 级 commit fence、sequencer 或 commit log。

## 3. 产品语义

### 3.1 Workspace 只展示 Has Unread

Workspace 级未读语义改为：

> 当前成员在该 workspace 下是否存在至少一个可见、按通知策略应计入未读的 scope，有消息或 activity 尚未读。

UI 展示为红点，不展示精确数字。API 应返回 `hasUnread: boolean`；如果过渡期仍保留 `unreadCount` 字段，它不得作为产品承诺，也不得驱动后端精确 count 查询。

`hasUnread` 的目标服务路径是 workspace owner 的 UnreadRuntime 内存状态，而不是每次 workspace list、first paint 或 socket heartbeat 都执行数据库 `EXISTS` 查询。数据库查询只能作为迁移、冷启动懒恢复、诊断或显式有界后台任务使用，不能为在线精确 unread 扫描全量 workspace。Split profile 下 central `/api/v1/workspaces` 不拥有 workspace owner runtime；V1 central workspace list 不显示 unread，除非未来引入 external runtime substrate 或 explicit owner-hop API。

仅用于保持 shell roster 最新的 warm workspace metadata refresh 可以请求不计算 unread 的 workspace rows（例如 `includeUnread=false`），并必须保留客户端或 runtime 已从 snapshot / realtime 事件持有的 unread hint，不能重新扫描 message unread truth。

这条规则只收窄 workspace 层。Channel、DM、reply-thread 或 task 层是否继续展示精确未读数，由各自产品 spec 决定；这些局部 unread 应优先使用 scope-local state。

### 3.2 不承诺 Workspace 内全局消息顺序

新 sync 协议不再承诺把一个 workspace 的所有新消息合成为单一严格全序流。它只承诺：

- 同一个 scope 内，消息按 `scope_messages.seq` 单调递增。
- 不同 scope 之间，服务端可以用 `created_at`、`latest_message_at` 或响应分组顺序提供展示 hints，但这些 hints 不是 cursor 正确性的基础。
- 客户端不能把跨 scope 排序当成去重、分页或已同步判断依据。

## 4. 目标模型

### 4.1 写入路径

目标写入路径只分配 scope-local sequence：

1. 开启事务。
2. `UPDATE scopes SET latest_seq = latest_seq + 1 WHERE id = ?`，得到 scope-local `seq`。
3. 插入 `scope_messages(id, workspace_id, scope_id, seq, author_member_id, ...)`。
4. 写入 message-owned facts：sources、fanout outbox、search docs、必要的 membership / notification mutations。
5. 更新 `scopes.latest_seq`、`scopes.latest_message_at` 等 scope tail metadata。
6. 提交事务后广播 realtime / daemon delivery side effects。

目标状态下，新消息写入不再更新 `workspaces.latest_message_seq`，也不再需要为 runtime sync 分配 `workspace_global_seq`。过渡期可以保留字段以便回滚或数据迁移，但新代码不应依赖它们的顺序语义。

### 4.2 Sync Cursor

新 human sync API 使用 opaque cursor：

```http
GET /api/v1/messages/sync?cursor=<opaque>&limit=...
```

响应按 scope 分组返回增量：

```json
{
  "cursor": "opaque-next-cursor",
  "scopes": [
    {
      "scopeId": "scp_...",
      "messages": [],
      "nextSeq": 42,
      "hasMore": false
    }
  ]
}
```

Opaque cursor 是客户端不可解释的 token。推荐实现为服务端存储 frontier，token 只携带 cursor id、workspace id、version、expiry / rotation data 和签名。不要把所有 scope frontier 直接塞进客户端 token，否则 workspace scope 数量增长会让 token 变大并泄露内部结构。

Cursor 记录必须是 immutable / append-only 语义。客户端携带 cursor `C0` 请求 sync，服务端读取 `C0` 对应的 frontier，生成响应和新的 cursor `C1`，但不修改 `C0`。如果响应丢失、客户端崩溃或网络断开，客户端可以继续用 `C0` 重试并得到同一 frontier 之后的增量。客户端只有在成功应用响应并持久化 `C1` 后，才会在下一次请求携带 `C1`；这个请求可以视为对 `C1` 的隐式确认。旧 cursor 只能在 TTL、容量上限或显式 ack 后延迟 GC。

服务端 cursor snapshot 概念上是：

```json
{
  "workspaceId": "wsp_...",
  "memberId": "mem_...",
  "clientId": "browser-or-device-id",
  "frontier": {
    "scp_channel": 182,
    "scp_dm": 45,
    "scp_thread": 7
  },
  "membershipVersion": "optional-version"
}
```

实现可以把 cursor snapshot 拆成 cursor 表和 cursor item 表，也可以使用 compact JSON 存储；关键约束是查询必须 bounded，不能因为 workspace 历史消息增长而放大。若使用 mutable "current cursor" 表，也必须用 cursor generation / sequence 保证旧 generation 可重试，不能在响应发送前覆盖客户端仍可能重试的 frontier。

如果新建 opaque cursor 的语义必须等同于“cursor 创建时已经可见的所有消息之后”，而写路径已经不再维护 workspace-wide commit-order scalar，那么精确初始化必须在 cursor 创建事务内捕获对应 scope frontier。用 `created_at` 懒推断 snapshot 不等价，因为 message timestamp 不表达 commit visibility；用 `workspace_global_seq` 也不等价，因为目标迁移后它只是 per-scope 兼容别名。这个兼容例外只能由 durable sync epoch / fence，或能保持相同 snapshot 语义的分页初始化协议替换。

### 4.3 Changed Scope Discovery

Cursor sync 不能每次扫描当前成员可见的所有 scopes。目标实现应维护 per-member dirty scope index / outbox，或者等价的 bounded changed-scope 索引。概念行可以是：

```text
member_scope_sync_dirty(
  workspace_id,
  member_id,
  scope_id,
  latest_message_seq,
  latest_activity_seq,
  reason,
  updated_at
)
```

普通消息、activity、membership/visibility/notification 变化写入时，把受影响 member/scope 标为 dirty；cursor sync 按 workspace/member 分页读取 dirty scopes，再对每个 dirty scope 比较 cursor frontier 和 scope tail。Dirty marker 应是 idempotent tail state，而不是生成一次响应就删除的 queue item；只有当客户端已确认的 cursor frontier 覆盖该 marker 的 `latest_message_seq` / `latest_activity_seq` 后，才可以清理或降级该 marker。这样旧 cursor 重试仍能重新发现同一 changed scope。

允许实现用物化 tail 表、outbox 或投递 ledger 派生这个索引，但读取成本必须受 dirty scope page size、显式 dirty candidate scan budget、scope batch size 约束，而不是受 workspace 可见 scope 总数或历史消息总数约束。维护 dirty scope 索引时也不能把每条公开消息简单 fan out 到 workspace 全量成员；写放大必须受明确投递对象、notification policy、已有 outbox / ledger 或 sparse tail 设计约束。

Dirty discovery 应先按 dirty index 顺序读取小的 candidate window，并在同一次 sync 请求内用显式 scan budget 跳过已被当前 cursor frontier 覆盖、当前不可见或 notification policy 不 eligible 的候选。只允许在该 bounded candidate set 内做 visibility、notification 和 cursor frontier join；不能先构造 workspace 内全部 `scope_sync_dirty` / `member_scope_sync_dirty` / grouped dirty scopes 后再应用响应 `LIMIT`。

首屏、conversation visibility、unread、subscribed-thread、mention 和 agent roster
的通用读路径成本规则见
[`read-path-performance-boundaries.md`](./read-path-performance-boundaries.md)。

Cursor sync 的读取流程：

1. 按 workspace/member 从 dirty scope index 分页读取候选 scopes，并验证当前 visibility / notification policy。
2. 对每个候选 scope 比较 scope message/activity tail 与 cursor frontier 中该 scope 的 delivered cursor。
3. 只对有变化的 scope 拉取 `scope_messages.seq > frontier_seq` 或 activity cursor 之后的 bounded page。
4. 构建响应和 next cursor snapshot；不要原地推进旧 cursor。
5. 客户端应用响应后持久化 next cursor；服务端可在后续请求或显式 ack 后延迟 GC 旧 cursor。

必须避免以下放大形态：

- 每次 sync 扫描 workspace 全量历史消息。
- 对每个 scope 做一条独立 SQL，导致 scope 数增长时 N+1。
- 为了 workspace 红点计算精确 count。
- 在 first paint 中 eager 拉取所有 scope 的完整 unread 明细。

候选 scope 查询应受 dirty scope page、workspace membership、visible scope page、最新活动窗口或产品限制约束。需要全量 frontier 初始化时，应分页进行，并让客户端接受多轮 sync。

### 4.4 Membership 和 Visibility 变化

Scope frontier 必须定义 membership / visibility 变化时的初始位置，否则容易漏消息或回放过量。

- 新加入 scope：默认从 membership `effective_from_seq - 1` 开始；如果产品语义是加入后不回放历史，则 `effective_from_seq` 应等于加入时的 `scopes.latest_seq + 1`。
- 重新加入或 private grant 恢复可见：从新的 visibility grant 的 effective cursor 开始，不自动回放之前不可见的历史。
- 被移除、archive 或 private visibility revoke：该 scope 从 active frontier 中移除，并写 dirty scope reason，让客户端清理或隐藏对应缓存。
- notification setting 变化：不改变 message delivery frontier，但必须影响 unread / activity eligibility；如果变化会让已有 tail 从“不计 unread”变为“计 unread”，需要写 dirty scope reason 触发 red dot 重新计算。
- scope cold history / archive 恢复：历史分页使用 explicit history cursor，不通过 live sync frontier 隐式爆量回放。

Cursor snapshot 中的 `membershipVersion` 可以是单调版本、visibility epoch、dirty index watermark 或等价机制。它的作用是让 sync 发现 membership/visibility 变化，并把对应 scope 加入 changed-scope page；不能依赖客户端猜测 scope 集合变化。

### 4.5 Read State

Canonical read state 是 scope-local：

- `scope_memberships.last_read_seq`：该 member 在该 scope 已读到的消息 seq。
- scope-local activity cursor：用于 non-message activity。目标状态下 activity 不再使用 workspace-global activity cursor。
- `scope_memberships.last_read_global_seq`：仅作为兼容字段保留，迁移完成后不得作为新逻辑依赖。

Mark-read 对一个或一组 explicit scopes 更新 `last_read_seq`。Workspace-level "mark all read" 可以遍历当前可见 scopes 的 tail metadata，用 bounded batch 更新每个 scope 的 `last_read_seq = scopes.latest_seq`。

### 4.6 Workspace HasUnread

Workspace red dot 必须表达 policy-filtered unread existence，不是粗略比较 `scopes.latest_seq > scope_memberships.last_read_seq`。粗略比较会把 effective notify principal 自己发起的消息、mute/no-wake、不在 effective membership window、private visibility 不满足的消息或不计 unread 的 activity 误算成红点。普通 message 的 effective notify principal 默认为 author member；workspace 与 platform service-account message 都解析为本地 service author member；其它产品只有在注册契约明确声明时才使用 distinct durable notify principal。

Warm serving 目标是 [`unread-runtime-memory.md`](./unread-runtime-memory.md) 定义的 owner-local UnreadRuntime。实现不能把 workspace list、first paint 或 heartbeat 变成 request-time database query。以下查询形态只允许用于迁移、lazy recovery、operator repair、diagnostics、controlled shadow comparison，或作为 runtime 内部有界恢复输入：

1. Policy-filtered `EXISTS`：直接查是否存在至少一条 unread-eligible message 或 activity。
2. 已有 owning domain sparse facts：例如既有 outbox/dirty frontier。它们可以作为 lazy recovery、operator repair 或 diagnostic 输入，但不能作为本设计新增的 workspace unread warm-serving projection。Removed taskattention task unread facts must not be used as recovery input.

Policy-filtered message EXISTS 的概念修复形态：

```sql
SELECT 1
FROM scope_memberships sm
JOIN scopes s ON s.id = sm.scope_id
JOIN scope_messages msg ON msg.scope_id = sm.scope_id
LEFT JOIN scope_notification_settings sns
  ON sns.scope_id = sm.scope_id AND sns.member_id = sm.member_id
WHERE sm.member_id = ?
  AND s.workspace_id = ?
  AND msg.seq > sm.last_read_seq
  AND msg.seq >= sm.effective_from_seq
  AND COALESCE(msg.notify_principal_member_id, msg.author_member_id) <> sm.member_id
  AND sm.removed_at IS NULL
  AND sm.archived_at IS NULL
  -- visibility predicate
  -- current notification/unread eligibility predicate
LIMIT 1
```

Activity unread 必须单独纳入。目标状态下 activity 使用 scope-local activity seq / cursor；workspace red dot 需要在 UnreadRuntime memory 中 OR 上 activity unread eligibility。若 lazy recovery、operator repair 或 diagnostics 读取 durable facts，也必须保持 existence 语义，而不是 count 聚合。

### 4.7 Agent delivery/read

Agent delivery/read 的普通消息路径应使用 scope-local facts：

- 历史目标设计曾允许请求携带 opaque receive cursor，当前 agent server `/receive` 已删除；message sync/read APIs 应携带 opaque sync cursor 或 `(scope_id, after_seq)` 形式的 explicit scope cursor。
- 候选 scope 来自 `scope_memberships`；当前 notification state 只作为 wake/preference 条件，不作为历史 receive grant 或 offline inbox。
- 每个 scope 内按 `scope_messages.seq` 读取。
- 跨 scope 响应可以按 `created_at` 或 priority merge，但 cursor 正确性必须由 per-scope frontier 保证。

Reaction 等 non-message activity 必须同样收敛到 scope-local activity frontier。可选实现是 `scope_activity_events.seq`（在 `scope_id` 内递增）或统一的 scope event sequence；无论哪种实现，activity 写入不得更新 `workspaces.latest_message_seq` 或其他 workspace-wide allocator。在迁移完成前可以保留 `activity_global_seq` 作为只读兼容字段，但不得成为新写路径或 receive correctness 的依赖。

### 4.8 Search 和 Diagnostics

Search projection 和 diagnostics 不应依赖 workspace-global message order：

- Search doc 使用 `message_id`、`workspace_id`、`scope_id`、`seq`、`created_at`。
- Search pagination 使用 search engine score / created_at / message_id 或 `(scope_id, seq)` 的明确组合 cursor。
- Diagnostics 回链使用 `message_id` 或 `(scope_id, seq)`。
- 任何需要说明“当前 scope tail”或“已读位置”的诊断信息使用 scope-local seq。

当前 search projection 若没有 `seq` 字段，迁移必须显式补 schema/backfill，或者规定 search / diagnostics join `scope_messages` 读取 authoritative `seq`。`scope_message_search_docs.global_seq` 可以在迁移期保留，但目标状态下不是新 projection 的排序或 correctness 依据。

## 5. API 和 Client 迁移

### 5.1 新 API

新增或替换以下 API surface：

- `GET /api/v1/messages/sync?cursor=...`
- `POST /api/v1/messages/sync/cursors` 或等价 bootstrap endpoint，用于初始化当前 member / workspace / client 的 cursor。
- Workspace list / current workspace response 返回 `hasUnread`。
- Mark-read APIs 接受 explicit scope targets，避免隐式依赖 workspace scalar cursor。

`/api/v1/messages/sync` 是 workspace data-plane route，必须和现有 workspace-scoped `/api/v1` route 使用同一套 workspace guard / route profile。旧 `/api/messages/sync` 不是新 cursor API 的 alias，也不能把 `since_seq` 请求隐式 fallback 到新 cursor path。

旧 `/api/messages/sync?since_seq=...` 不作为兼容 API。新协议启用后，默认 server 不应暴露可用的旧 sync path；新 web 客户端、daemon 和自动化测试必须使用 opaque cursor protocol。旧 API 请求应返回明确的 unsupported / upgrade-required 错误，而不是继续维护 scalar cursor 双写。

### 5.2 Daemon Version Gate

Server 和 daemon 的 breaking change 门禁使用 daemon 已有的 ready / session `daemonVersion`，不引入独立 `messageProtocol` 字段，也不依赖每个 HTTP message API 请求上的自定义协议 header。`daemonVersion` 单调递增；server 使用通用的最低 daemon 版本配置（环境变量 `AGENTTEAM_DAEMON_MIN_VERSION` / 配置字段 `MinDaemonVersion`；未设置时不启用 minimum-version hard gate，设置后必须 gate；默认值不应隐式使用 latest/recommended display version）。Server 升级到 scope frontier protocol 后：

- daemon ready / session bootstrap 必须上报 `daemonVersion`。Server 的 gate 优先使用当前 authenticated daemon 连接上的 version/capability state；如需支持离线诊断或注意力提示，可以把 machine last-known daemon version 作为低频 metadata 持久化，但不得依赖 agent runtime session id 或每次 ready/session telemetry 写库来完成 message route gate。
- workspace-scoped Agent Runtime 的 `/send`、`/history`、`/search`、read-check 等 message-facing routes 必须在进入 store read/write 逻辑前统一检查已认证 agent 所属 machine 的 `daemonVersion`；server `/receive` 不再是生产 route。
- `daemonVersion` 为空、格式非法、或低于已配置的 `MinDaemonVersion` 时返回 machine-readable `upgrade_required` 错误和可展示的升级提示，且不能调用 message store。
- `daemonVersion` 来自 daemon binary 的 `-ldflags -X ...Version=...` 注入；release 值是标准 semver（例如 `v1.0.2-rc.11`）。Server 比较前可剥离 ready payload 里的产品前缀（例如当前 desktop 上报的 `agentteam/`）并规范化可选 `v` 前缀，但比较必须遵循 semver 规则，不能做字符串/字典序比较；例如 `v1.0.2-rc.11 > v1.0.2-rc.2` 且 `v1.0.2-rc.11 < v1.0.2`。
- 达到 cutover version 的 daemon release 必须完整支持这组 wire contract：opaque sync cursor、scope-local message/activity cursor、daemon-local queued delivery、`scopeId + seq` realtime 去重、以及不再发送旧 scalar cursor 参数。因为 daemon 版本单调递增，设置 `AGENTTEAM_DAEMON_MIN_VERSION` 后，`daemonVersion >= MinDaemonVersion` 就是 server 的硬门禁条件；未设置该 env 时不应因为 latest/recommended 版本而阻断。
- 旧 daemon 不走 legacy scalar cursor fallback；server 不能只在看到部分 legacy query params 时拒绝。即使旧 daemon 调用 send/history/search，也必须先被 version gate 拦下；旧 `/receive` route 不再存在。
- 不保留或新增 `messageProtocol` / `messageProtocolVersion` ready 字段、machine columns、agent auth 字段或相关 capability token 作为本门禁的一部分。
- 新协议参数必须使用明确名称，例如 `after_seq`、`after_activity_seq`、`before_seq`；旧 `after` / `before` 不能在 scope API 中被静默重解释为 scope-local seq。若旧 daemon 继续传 legacy scalar cursor 参数（`after`、`before`、`after_global_seq`、`before_global_seq` 等），应返回 `upgrade_required`，而不是错页、漏读或重复。
- 不允许新增或依赖 `X-AgentTeam-Message-Protocol`、`X-*` 这类请求 header 作为长期或强制的业务协议版本门禁。`X-*` 自定义 header 命名不是现代新协议字段的推荐做法；本场景也不应让所有 daemon message API 通过 per-request header 证明协议版本。
- public docs / desktop docs 应说明 server upgrade 后 daemon 需要同步升级。

这条规则取代旧客户端兼容期。没有公开兼容承诺时，不需要长期维护旧 daemon protocol 或 workspace scalar cursor 双写。

### 5.3 Client State

Web client 保存 opaque cursor token，不保存 `since_seq` 作为 sync correctness state。Query cache、local storage、realtime resume 和 workspace switcher 都应迁到 opaque cursor protocol。

Client 可以保存 UI hints，例如每个 scope 最近消息时间、滚动位置、局部 unread badge；这些不是 sync frontier 的权威来源。

### 5.4 Realtime

Realtime event 应包含 `workspaceId`、`scopeId`、`seq`、`messageId`。客户端收到 event 后：

- 如果 scope 已加载，可以按 `scopeId + seq` 去重插入。
- 如果 event gap 或缓存不完整，触发 cursor sync。
- 不依赖 workspace-global seq 判断是否漏消息。

## 6. Schema 迁移方向

迁移应分阶段降低风险：

1. 引入 immutable cursor storage、dirty scope index / outbox，并让 web 使用 cursor sync。
2. 引入 daemon capability / version gate；旧 daemon 收到 `upgrade_required`，不进入旧 scalar cursor fallback。门禁使用 ready/session capability 或 server-side daemon version，不使用 `X-*` request header。
3. 停止 workspace unread count，改为 memory-backed policy-filtered `hasUnread`，并让 workspace UI 只展示红点。
4. 为 non-message activity 增加 scope-local activity cursor，并停止 activity 写入 workspace-wide allocator。
5. 让 message write path 停止依赖 `workspaces.latest_message_seq`。新消息只需要 scope-local `seq`。
6. 将 search docs、diagnostics、agent delivery/read surfaces 从 `workspace_global_seq` 迁出；search projection 缺少 `seq` 时需要补 schema/backfill 或 join 规则。
7. 确认没有生产 runtime 依赖后，再删除或放松 `scope_messages.workspace_global_seq`、`scopes.latest_global_seq`、`scope_memberships.last_read_global_seq`、`workspaces.latest_message_seq` 等兼容字段。

删除 schema 字段前必须检查 `internal/tidb/bootstrap.sql` 和所有 migrations 是否一致，并按 `specs/storage/storage.md` 的存储规则处理。

## 7. 方案取舍

| 方案 | 是否推荐 | 原因 |
| --- | --- | --- |
| Scope-local frontier / opaque cursor | 推荐目标方案 | 真正消除 workspace-wide message allocator；与 scope-based message model 一致；趁没有公开兼容承诺时迁移成本最低。 |
| Workspace seq block allocator | 不作为目标方案 | 适合兼容已上线 `since_seq` 客户端，但仍保留 workspace 级线性化点和 owner/queue。 |
| Commit-log / sequencer 保持 scalar | 不推荐 | 可以保留 scalar API，但仍是 workspace 级 order，且引入 outbox/finalizer/恢复复杂度。 |
| TiDB AUTO_INCREMENT / TSO 替代 scalar cursor | 不推荐直接落地 | 不能单独保证客户端需要的 visible commit order，仍需要额外 fence 或 sequencer。 |

## 8. 实现拆分建议

1. **Workspace unread product cut**
   - Workspace response 增加 `hasUnread`。
   - Web workspace switcher / sidebar 改红点，不展示 workspace unread count。
   - 后端 workspace unread warm serving 改为 owner UnreadRuntime memory；policy-filtered existence check 只保留给 migration、lazy recovery、operator repair、diagnostics 或 controlled shadow comparison。

2. **Cursor sync backend**
   - 增加 immutable cursor storage、dirty scope index / outbox 和 cursor sync API。
   - 实现 changed scope discovery、membership/visibility version handling 和 bounded per-scope message page。
   - 确保 dirty scope marker 只在 confirmed frontier 覆盖后清理，不能在响应生成时破坏旧 cursor 重试。
   - 增加 query-shape / amplification tests。

3. **Daemon version gate**
   - 新增 server 支持的通用最低 daemon version 配置，命名为 `MinDaemonVersion` / `AGENTTEAM_DAEMON_MIN_VERSION`，不要使用 `MinScopeFrontierDaemonVersion` / `MIN_SCOPE_FRONTIER_DAEMON_VERSION` 或其他 scope-frontier 专用配置名；该值设置后必须作为 hard gate，且应配置为第一个完整支持 scope frontier wire contract 的 daemon release。
   - 由 daemon ready / session bootstrap 上报。message-facing HTTP route 根据已认证 agent 所属 machine 的当前 daemon connection version/capability 判定；若 route 必须在没有连接上下文时给出解释，可读取低频 last-known `machines.daemon_version` 作为诊断 metadata，但它不是 daemon presence 或 runtime session readiness 的 source of truth。
   - `AGENTTEAM_DAEMON_MIN_VERSION` 设置后，空版本、非法版本或低于 cutover 的 daemon 在 receive / send / search / history route 进入 store 前统一收到 `upgrade_required`；未设置时不启用该 hard gate。
   - 版本比较必须使用 semver comparator：支持 `-ldflags` 注入的 `v1.0.2-rc.11` 这类 prerelease；允许先规范化 `agentteam/` 产品前缀和可选 `v` 前缀；测试必须覆盖 prerelease 数字排序和 prerelease/final 排序，避免 lexicographic compare。
   - 不得引入或保留 `messageProtocol` / `messageProtocolVersion` 字段、machine columns、auth 字段、desktop ready payload 或相关测试作为协议门禁；不要以 capability token 替代版本门禁。
   - 新 scope API 只接受 `before_seq` / `after_seq` 等明确 scope-local cursor 参数；旧 `before` / `after` / global cursor 参数不能被静默重解释。
   - 不引入 `X-AgentTeam-Message-Protocol` 或其他 `X-*` per-request header 作为协议版本开关。
   - 更新 desktop docs，说明 server 升级后 daemon 必须同步升级。

4. **Web sync migration**
   - Web local storage、query cache、realtime resume 使用 opaque cursor。
   - 删除 `since_seq` 作为新客户端同步状态。

5. **Write path cleanup**
   - 停止为新 runtime message 分配 workspace-global message seq。
   - 保留字段写入仅限短期兼容或 migration guard。

6. **Agent delivery / search / diagnostics migration**
   - Agent daemon delivery 使用 outbox payload/status；history/search/read-check 使用 scope-local read predicates，不使用 server receive cursor。
   - Non-message activity 迁到 scope-local activity cursor。
   - Search projection 和 diagnostics 使用 `(scope_id, seq)` / `message_id`；必要时补 search docs `seq` schema/backfill。

7. **Schema cleanup**
   - 删除或放松 global seq 相关字段和索引。
   - 更新 bootstrap schema、migrations、fixtures、guardrails。

## 9. Guardrails 和测试

当前没有单独的 executable guardrail 能完整保护本设计。实现阶段应增加以下长期检查：

- 后端测试：普通 message send path 不更新 `workspaces.latest_message_seq`。
- 后端测试：non-message activity write path 不更新 workspace-wide allocator。
- 后端测试：cursor sync 不使用 `workspace_global_seq` 过滤或排序。
- 后端测试：cursor `C0` 可重试；生成 `C1` 不会让 `C0` 丢增量。
- 后端测试：dirty scope marker 在 `C1` 生成但未确认时仍支持 `C0` 重试。
- 后端测试：设置 `AGENTTEAM_DAEMON_MIN_VERSION` 后，空版本、非法版本或低于 cutover `daemonVersion` 的 machine 调用 receive、send、history、search 都返回 `upgrade_required`，不会进入 store，也不会进入 legacy scalar cursor fallback；新 daemon 的通过条件来自当前 authenticated daemon connection 的 `daemonVersion >= MinDaemonVersion`（或明确的低频 last-known diagnostic metadata fallback），而不是 runtime session DB state、`messageProtocol` 或 `X-*` request header。
- 后端测试：daemon version comparator 按标准 semver 处理 `v1.0.2-rc.11`、`agentteam/v1.0.2-rc.11`、无 `v` 前缀输入、prerelease 数字排序（`rc.11 > rc.2`）和 prerelease/final 排序（`rc.11 < final`）；不能用字符串比较。
- 后端测试：scope API 不再把 legacy `before` / `after` 静默解释成 scope-local cursor；新 cursor 参数使用 `before_seq` / `after_seq` 等明确名称。
- 后端/daemon 测试：代码中不新增也不依赖 `X-AgentTeam-Message-Protocol`；daemon API client 不需要为每个 message request 设置该 header。
- 后端测试：warm workspace unread read 使用 owner UnreadRuntime memory，不做 workspace unread count 聚合或 request-time `EXISTS`；runtime lazy recovery、operator repair 或 shadow query 使用 durable effective notify principal，并保持 self/mute/no-wake/不可见消息 eligibility 一致。
- Query-shape 测试：cursor sync 对 changed scopes 的查询数量受 page size / scope batch size 约束。
- Query-shape 测试：cursor sync 不扫描所有 visible scopes；membership/visibility 变化通过 dirty scope index 或等价机制发现。
- Amplification 测试：dirty scope 索引维护不把单条公开消息写放大为 workspace 全量成员 fanout，除非该 fanout 已由明确投递 policy / ledger 边界限制。
- Frontend 测试：workspace UI 只展示红点，不展示 workspace unread count。
- Search / diagnostics 测试：结果定位使用 `message_id` 或 `(scope_id, seq)`。

如果后续添加 guardrail script，应放在 `guardrails/message/`，并在本 spec 中补充脚本路径。

## 10. 一句话总结

AgentTeam 的目标消息同步模型是 scope-local frontier / opaque cursor：workspace 层只需要 `hasUnread` 红点，不需要精确未读数；新消息写入只分配 scope-local seq，不再维护 workspace-wide scalar visible order。`workspace_global_seq` 和 `workspaces.latest_message_seq` 只属于当前兼容实现，不是目标架构。
