# scope 消息模型

> 规范路径：`specs/message/scope-model.md`
> 相关规范：
> - [`specs/collaboration/source-projection-delivery.md`](../collaboration/source-projection-delivery.md)
> - [`specs/collaboration/collaboration.md`](../collaboration/collaboration.md)
> - [`specs/message/delivery-ledger-optimization.md`](delivery-ledger-optimization.md)
> - [`specs/message/delivery-runtime.md`](delivery-runtime.md)
> - [`specs/storage/storage.md`](../storage/storage.md)
> - [`specs/service-account/service-account.md`](../service-account/service-account.md)
> - [`specs/app/app-notification.md`](../app/app-notification.md)

规范状态：目标规范。

本文定义 AgentTeam 消息域的目标数据模型、投递规则和权限边界。本文只描述目标状态，不描述迁移过程。

## 1. 设计目标

- `scope` 是消息与产品时间线的统一容器；channel、DM、reply thread、
  task discussion、service-account conversation 与 Workflow Builder
  transcript 都使用 scope。
- workspace 表示工作区，scope 表示工作区内的一段会话范围；两者命名和职责分离。
- Conversational scope 的成员使用普通 IM 读写能力；product scope 的 capability registry 定义成员能力和 contribution authority。
- private agent 可以作为具体 scope 的成员出现，并在 UI/API 中以 private 身份展示；private membership 不等于完整 scope 历史读取权。
- private agent 的可见性和可指挥权分离：能看到 private agent，不代表能向它投递指令。
- private agent 必须先加入目标 parent scope，才可以在该 parent scope 及其 reply thread 中被提及、被拉入 thread、接收与该 scope 相关的任务。
- private agent 的输出写入目标 scope 的普通消息流；scope 内其他成员可以看到 owner 正在让 private agent 做什么。
- 消息投递、scope 成员、通知设置、已读状态、附件、reaction、搜索、恢复点、时间线卡片、分享卡片等能力完整覆盖。
- 表结构只包含当前产品能力需要的字段；每个字段必须有明确产品语义。
- 每张核心表都必须有明确字段语义、主键、外键、唯一约束、check 约束和必要查询索引。
- schema SQL、migration SQL、schema verify 和 repository 查询必须统一维护，不能在不同层各自拼出不一致 SQL。
- 行为、API、UI、公共指南和 canonical spec 必须保持同一目标口径，不保留过时文档。
- 除非先形成明确产品决策，本模型不得回退现有功能。
- UI 不是本模型的独立改版目标；除非为了表达 scope/private 语义或保全现有能力，不引入交互和视觉变化。

## 2. 核心概念

### 2.1 scope

scope 是一段会话范围。目标类型包括：

- channel scope；
- DM scope；
- reply thread scope；
- task scope；
- service_account scope。
- workflow scope。

每条可见 message 都属于一个 scope。reply thread 是一个子 scope，记录
parent scope 和 root message。Task 与 workflow 是顶层 product scope：
它们复用消息存储和受信写入基础设施，但读写能力由对应产品 gate
定义，不能仅凭 membership 自动进入普通 conversation 产品面。Service
account profile 由 service-account domain 持有。

### 2.2 membership / participant

membership 表示“某个 member 属于某个 scope，并拥有该 scope 内的参与身份和已读状态”。Channel、DM 与 reply thread 中的 human 和 public agent membership 授予普通读写准入；private agent membership 授予 private participant 准入和 read-state 容器，具体可读消息还必须经过 owner-private visibility filter。

service_account scope 的 Human audience 使用普通 membership，并固定覆盖 Workspace 全部 active Human。Active direct membership 同时授权 message history、search、permalink、source、attachment、reaction read-check、Inbox、通知偏好、read cursor 与 conversation actions；不提供 audience mode、订阅或退订。Workspace 服务号消息创建使用 `service_grant` 和 `service_account_manager` permission bases；Manager 同时拥有管理员名单维护与发布权限，但其增删不改变 audience membership。平台服务号的 workspace-local broadcast materialization 使用 `service_grant` 和 `platform_service_account_delivery` permission bases。两条创建路径都以本地 service author member 作为 author、notify 与 message-idempotency subject。

participant 是 membership 在产品语义上的展示。普通成员展示为 public participant；private agent 展示为 private participant。

private agent 可以出现在具体 scope 的 membership 中：

- private agent 加入 channel 后，在该 channel member 列表中以 private 身份展示；
- private agent 被 thread root 或 thread 内 live mention 拉入后，在该 thread member 列表中以 private 身份展示；
- private agent 加入 thread 不会自动加入其他 thread；
- private agent 加入 thread 的前提是它已经属于 parent scope。

### 2.3 notification setting

notification setting 表示“某个 member 在某个 scope 中的通知偏好”。它不表示成员关系。

membership 和 notification setting 分开存储：

- membership 决定成员列表、参与身份、已读状态；human 和 public agent 的普通读写准入也由 membership 决定；private agent 的可读消息还要经过 owner-private visibility filter；
- notification setting 决定 active、quiet、mute、auto-wake、no-wake 等通知行为。

### 2.4 private authority

private authority 表示“某个 actor 是否有权把本条消息投递给某个 owner 的 private agent”。

private authority 独立于 membership：scope 里展示 private agent，不等于所有 scope 成员都有权指挥它。

有效的 private authority 来源：

- private agent 的 owner human；
- owner 明确授权的 human；
- owner 明确授权的 agent；
- 已经处于同一 owner-private 协作范围内的同 owner private agent；
- 通过显式收件人 API 且通过 owner/grant 校验的调用。

public agent 即使由同一个 owner 创建，也只按普通 public participant 处理；它需要显式授权才可以向 owner 的 private agent 投递指令。

app/service 作者按服务身份处理；它只走自己的显式收件人或普通 scope 投递规则，不自动获得 private authority。

### 2.5 non-message activity event

Non-message activity event 是 scope 内没有独立 message row、但需要进入 notification / wake / diagnostics 语义的活动记录。它是 activity source，不是 membership，不是通知设置，也不是权限来源，也不是 server-side agent receive 队列。

上层 source/projection/delivery 分类见
[`specs/collaboration/source-projection-delivery.md`](../collaboration/source-projection-delivery.md)：
`scope_messages` 承载 `Contribution -> TranscriptMessage`，`scope_activity_events`
承载需要 activity / notification / wake / diagnostics 语义的非消息事实投影。

Non-message activity event 需要持久化，因为 AgentTeam 需要支持：

- reaction 等非消息活动的 agent-facing daemon delivery / diagnostics；
- rewind / remove 时 suppress 未消费活动；
- task typed activity、share/attachment 等非消息活动扩展。

普通 scope timeline 通过 `scope_messages`、scope membership、notification policy 和 cursor 推导，不为 message / mention / explicit recipient / controlled broadcast 写 non-message activity event。agent 对普通 scope 消息的读取通过 history/search/read-check 的 visibility predicate；当前事件投递通过写入时 routing plan + `scope_fanout_outbox` payload/status。Task / execution context ingress 是独立 context/read 输入源；只有没有 message row 的 task typed activity、share/attachment activity 需要进入 timeline、notification、wake 或 diagnostics 时，才写入 non-message activity event。

### 2.6 术语和实现映射

`scope` 是当前 conversation container 的目标术语。目标实现中不保留一套并行的 `space_*` 业务模型；schema convergence 后，消息域只使用 `scope` 命名。

目标映射：

| 目标概念 | 目标表 | 对应实现收敛方向 |
| --- | --- | --- |
| 会话容器 | `scopes` | channel、DM、reply thread、service-account conversation 的公共拓扑收敛到 scope |
| channel 专属展示 | `scope_channel_profiles` | channel slug、名称、描述、公开性等 channel-only 属性 |
| 成员和已读状态 | `scope_memberships` | 统一 human、public agent、private agent 在具体 scope 内的参与身份和 read state |
| 通知偏好 | `scope_notification_settings` | 统一 channel、DM、thread 与 service_account 的通知状态 |
| 消息本体 | `scope_messages` | 所有 scope 的 visible transcript |
| App 交互卡片 | `service_notification_cards` | `app_interactive_card` message 的版本化 render/action sidecar |
| 非消息活动 | `scope_activity_events` | reaction add、task typed activity 等没有 message row、但需要进入 timeline / notification / wake / diagnostics 的 non-message activity log |
| 事务后副作用 | `scope_fanout_outbox` + direct/no-outbox allowlist | ordinary message fanout 的 durable outbox；显式 allowlist 的 direct/no-outbox 例外由对应 spec/guardrail 约束 |

本规范取代消息域中 conversation container 的命名和事实源划分；reaction、rewind、search、delivery ledger 优化的专业语义继续由对应 canonical spec 定义。

取代矩阵：

| 范围 | 本规范优先定义 | 对应 spec 继续定义 |
| --- | --- | --- |
| container/table naming | `scope` / `scope_id` / scope facts | 子域 spec 的业务流程 |
| history/search access control | membership + registered product permission bases + owner-private visibility filter | search 的 TiDB readiness、query contract、projection 行为 |
| reaction storage naming | `scope_id` 命名和 activity 边界 | reaction 唯一键、add/remove、suppression 语义 |
| rewind scope naming | `scope_id` 和 visibility filter | rewind capability、strategy、prepared point、GC 语义 |
| dense/broad delivery | membership cursor + current notification state | delivery-ledger optimization 的 no per-recipient dense ledger 约束 |
| public agent 普通消息投递 | scope membership + notification + cursor | collaboration 旧普通消息 agent 投递段落不再作为消息域目标语义 |


## 3. 目标表结构

核心消息域目标表共 15 张。消息相邻投影/sidecar 表另列 2 张。

### 3.1 `scopes`

所有会话面的公共信息。

字段：

- `id`
- `workspace_id`
- `type`：`channel | dm | thread | task | service_account | workflow`
- `parent_scope_id`
- `root_message_id`
- `root_message_short_id`
- `identity_key`
- `latest_seq`
- `latest_global_seq`
- `latest_message_at`
- `roster_generation`
- `archived_until_seq`
- `status`
- `created_by_member_id`
- `created_at`
- `updated_at`
- `archived_at`

约束：

- channel、DM、task、service_account 和 workflow 的
  `parent_scope_id` / `root_message_id` 为空；
- `roster_generation` 初始为 0，并在 membership、visibility、notification 或其它 message-attention eligibility mutation 中按 scope 单调递增；
- thread 的 `parent_scope_id` 指向 parent scope；
- thread 的 `root_message_id` 指向 parent scope 中打开该 thread 的消息；
- channel 展示信息放在 channel profile 表；
- DM、service_account 与 workflow 身份唯一性通过 `identity_key` 表达；
  service_account 使用全表唯一的
  `service_account:{workspace_id}:{principal_id}`，workflow 使用
  `workflow:{plan_id}`；
- 每个 Workflow Plan 通过 `workflow_plans.scope_id` 显式关联一个
  workflow Scope；Plan 创建事务同时创建 Scope、初始 Draft 和 creator
  membership，Plan archive/restore 不改变 Scope 生命周期；

### 3.2 `scope_channel_profiles`

channel scope 的展示和频道专属属性。

字段：

- `scope_id`
- `slug`
- `display_name`
- `description`
- `visibility`：`public | private`
- `owner_member_id`
- `is_all_channel`
- `created_at`
- `updated_at`

约束：

- 只有 `type='channel'` 的 scope 有 channel profile；
- DM 和 thread 不使用 channel profile。

### 3.3 `scope_memberships`

scope 成员、participant 展示身份和已读状态。

字段：

- `scope_id`
- `member_id`
- `visibility`：`public | private`
- `owner_member_id`
- `last_read_seq`
- `last_read_global_seq`
- `owner_context_dismissed_through_global_seq`
- `effective_from_seq`
- `effective_to_seq`
- `created_at`
- `updated_at`
- `removed_at`
- `archived_at`

约束：

- membership 是带有效区间的状态行；
- 同一 `(scope_id, member_id)` 在同一 seq 上最多只有一个有效 membership；
- `visibility='private'` 时设置 `owner_member_id`；
- member 类型通过 identity/member 表读取，不在本表重复存储；
- private agent 可以成为 channel scope 或 thread scope 的 member；
- private agent 成为 child thread member 前，已经是 parent scope 的 private member；
- member list 按 viewer 和 surface 过滤：公开成员列表只展示 public member；需要展示 private member 的面板必须标记为 private，并显示 owner 语义；
- private member 只在它实际加入的 scope 中展示，不因为 parent/child 关系泄露到其他 scope。

### 3.4 `scope_notification_settings`

scope 内的当前通知偏好。它表达“下一条消息默认怎么处理”的 preference / intent，不表达历史 receive grant，也不保存每条消息写入时的 delivery snapshot。

字段：

- `scope_id`
- `member_id`
- `state`：`active | quiet | mute | auto-wake | no-wake`
- `created_at`
- `updated_at`

约束：

- notification setting 是 current-state 行；
- 同一 `(scope_id, member_id)` 最多一行，更新时直接覆盖当前 state / `updated_at`；
- 需要解释某条消息实际投递给哪些 agent 时，以该消息的 fanout payload/outbox status 为 execution truth，而不是回放 notification setting 历史；
- 该表只表达通知偏好；
- membership 存在时才有 notification setting；
- channel、DM、thread 和 service_account 都使用同一张表。

### 3.5 `scope_private_grants`

owner 授权其他 member 向自己的 private agent 上下文投递消息。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `owner_member_id`
- `grantee_member_id`
- `granted_by_member_id`
- `created_at`
- `expires_at`
- `revoked_at`

约束：

- `grantee_member_id` 可以是 human，也可以是 agent；
- grant 作用在 owner + scope 维度；
- per-agent grant 只有在产品需要按单个 private agent 授权时再加入。

grant 继承规则：

| 场景 | 有效授权来源 | 不继承方向 |
| --- | --- | --- |
| channel mainline | channel scope 上的 owner grant | 不继承 sibling channel / thread grant |
| DM mainline | DM scope 上的 owner grant | 不继承其他 DM / channel grant |
| reply thread root 准入 | parent scope grant；显式收件人授权 | 不继承 sibling thread grant |
| reply thread 内消息 | thread scope grant；parent scope grant | 不继承 sibling thread grant；不回写 parent scope grant |
| explicit context | explicit target scope grant；target 是 thread 时可使用 parent scope grant | 不使用无关 scope grant |

grant 继承只授予 private authority，不自动创建 membership。private agent 进入 thread scope 时，仍必须满足 parent scope 中已经有 private membership 的准入规则。

### 3.6 `scope_messages`

消息本体。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `seq`
- `workspace_global_seq`
- `author_member_id`
- `author_type`
- `notify_principal_member_id`
- `idempotency_subject_member_id`
- `client_message_id`
- `idempotency_payload_digest`
- `message_type`
- `content`
- `metadata`
- `created_at`
- `updated_at`

约束：

- `(scope_id, seq)` 唯一；
- `(workspace_id, workspace_global_seq)` 唯一；
- `workspace_global_seq` 在 workspace 内单调递增；
- human-visible contribution 的 canonical `author_type` 为 `human | agent | service`；`app` 与 `system` sender classifications 在 compatibility/source projection boundary 规范化为 canonical author/source shape；
- `notify_principal_member_id` 是 unread/browser-push self-exclusion 的 durable subject，普通直接发送默认等于 author member；
- `idempotency_subject_member_id` 在普通 direct send 与 workspace service-account direct send 中等于 author member；平台服务号 broadcast delivery 使用目标 workspace 的 service author member。产品注册需要独立命名空间时显式提供 typed override；
- `idempotency_payload_digest` 保存 create command 的 canonical payload digest，并与非空 client_message_id 同时写入；普通 create digest 覆盖 resolved message semantics/message_type、canonical content、ordered attachment ids、reply/source refs 与 client-owned message metadata；platform service-account broadcast delivery 使用 immutable ordered platform asset refs/content hashes 代替 workspace-local attachment ids；server-generated message id、seq、timestamps、workspace-local broadcast attachment ids、AuthorProfile projection 和 message self-source 在 digest 绑定后生成；
- 非空 `client_message_id` 在 `(scope_id, author_member_id, idempotency_subject_member_id, client_message_id)` 下幂等唯一；
- 同一 author、idempotency subject 与 scope 使用同一 `client_message_id` 和 canonical payload 重试时返回同一条消息；同 key 的 payload digest 相异时返回 idempotency conflict；
- private agent 的输出消息写入目标 scope timeline；
- private 输入的安全边界由 authority、membership/grant 和 source/activity visibility 决定，不通过 message-level visibility 字段表达。

### 3.6.1 platform moderation boundary

平台内容安全拦截发生在 `scope_messages` 写入之前。Eligibility 由 authenticated command origin、permission basis、caused-by actor、execution actor 与入口 trust profile 共同决定。浏览器用户可触达的 Human compose、Human-authorized service-account direct post 与 platform-operator-authorized service-account broadcast 在 canonical MessagePlan acceptance 时进入同一 Human-origin moderation boundary。如果命中启用的高危关键词，系统必须：

- 返回稳定错误码，提示本次消息无法发送；
- 不写入 `scope_messages`；
- 不写入 message fanout / wake outbox；
- 不触发 agent wake、app follow-up 或 realtime message-created 副作用；
- 写入一条平台审核日志，记录 typed command target（workspace/scope 或 platform service account/broadcast）、Human user、workspace member ref（存在时）、durable author、原始内容、命中的关键词和时间，供平台运营后台审查。

平台关键词列表是部署级安全配置，不按 workspace 分片。审核日志是平台安全审计事实，不是消息可见性、membership、fanout、delivery、read-state 或 private authority 的事实源；它不改变任何已有 `scope_messages` 行，也不在 `scope_messages.metadata` 中挂接审核状态。

该边界覆盖 authenticated Human 直接提交的可见消息，包括 Human-authored message、Human-authorized service-account send 与 platform-operator-authorized broadcast。平台审核日志记录 service routing principal、Human permission/caused-by/execution actor、workspace member ref（存在时）、durable author 与 broadcast ref（存在时）。Platform broadcast workspace delivery 消费已经通过该边界且 payload digest 匹配的 immutable MessagePlan snapshot。可信内部路径，例如 system wrapper、task dispatch、internal workflow、service callback、provider event 与 agent-authored automation，使用各自注册的信任边界。

### 3.7 `scope_message_sources`

消息来源、引用、转发和显式上下文。

字段：

- `target_scope_id`
- `message_id`
- `ordinal`
- `source_scope_id`
- `source_seq`
- `source_message_id`
- `source_kind`
- `source_dedupe_key`
- `created_at`

约束：

- source 表达 provenance；
- source 不直接决定投递；
- live mention 由发送 pipeline 根据消息形态决定。

### 3.8 `scope_activity_events`

非消息活动事件源。Message 本身由 `scope_messages` 表达；本表不记录 message-created、mention、explicit recipient 或 controlled broadcast。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `activity_type`
- `activity_ref_type`
- `activity_ref_id`
- `activity_global_seq`
- `source_message_id`
- `actor_member_id`
- `reason`
- `payload`
- `created_at`
- `suppressed_at`

约束：

- non-message activity event 只记录非消息活动；
- `activity_ref_type/activity_ref_id` 是 activity 主引用；reaction、task typed activity、share/attachment activity 不需要强造普通 message row；
- `source_message_id` 可为空，只有事件需要展示或解释来源消息时填写；
- `activity_global_seq` 用于 activity 自身排序和消费，不等同于 message global seq；
- server 不再提供 agent `/receive`；daemon delivery 只消费已被 fanout outbox 下发并 accepted 的当前事件 payload；
- dense scope 消息由 membership、notification policy 和 cursor 推导；
- 普通 scope default、mention、explicit recipient、controlled broadcast 不写 non-message activity event；
- read/write access 不从 non-message activity event 推导；
- scope membership 不从 non-message activity event 推导；
- message-backed fanout reason 只存在于当前 outbox payload/可由 message metadata/source 推导，不落本表。

### 3.9 private visibility intervals

private agent 的可读输入由 message-domain SQL readable predicate 统一计算，事实源是 `scope_memberships`、`scope_private_grants`、author effective interval、source refs、non-message activity fact 和 task-native ingress fact：

- private agent 在有效 membership interval 内写出的自己的输出消息可读；
- owner 在该 owner-private membership 覆盖范围内写出的消息可读；这不是跨 owner / workspace role 后门，只适用于 `scope_memberships.owner_member_id` 指向的 owner；
- owner 授权的 grantee / People collaborator（包括已安装 app service collaborator）在授权覆盖范围内写出的消息可读，并按 author + grant/collaborator effective interval 过滤 timeline records；
- thread scope 的 owner-private 可读性使用 thread 自身授权和 parent channel 授权的并集；channel 授权可覆盖该 channel 下的 thread 消息，但不反向回写 thread grant 到 channel，也不继承 sibling thread grant；
- 同 owner private author 只有在同一 owner-private 协作范围内、且 author 与 viewer 都有有效 private membership 时可读；
- explicit source/context/task ingress 使用 `scope_messages`、`scope_message_sources` 和 task-native ingress fact 表达；只有没有 message row 的 activity 才使用 `scope_activity_events`；

所有 ScopeAPI 消息读面必须使用同一个 owner-private visibility contract：history、search、read-check、attachment/source/root-context 检查和 activity source-message check，不能各自实现 Go-side public/private/owner/grant 判断。Production read-row authority is SQL readable predicate；candidate SELECT 必须直接返回 `visibility_reason` / `grant_id`，Go 层只做 scan、dedupe、分页、DTO 映射和 fail-closed invariant check。候选查询可以为了成本先做 bounded pruning，但它必须调用同一 readable predicate，不能在 mapper 之前把 owner 消息、有效 private grant 消息、有效 People collaborator 消息或同 owner private author 消息丢掉。可读 history/search/read-check 不因这些 read authority 自动扩大为 daemon offline inbox；需要上下文时由 bounded history/context 读取补齐。
- diagnostics 输出命中的 membership、grant、author interval、source ref、non-message activity event 或 task ingress reason。

### 3.10 `scope_fanout_outbox`

事务提交后的 ordinary message fanout 和 outbox-backed runtime wake durable outbox。每个 message producer 必须在事务内明确选择 first-delivery owner 和 replay policy；不得仅因为已写入 outbox 就默认有其他 caller 会完成首次 Human fanout。显式 direct/no-outbox 例外（例如 reaction direct fanout、rewind invalidation/notice、channel member system message projection、hidden task-description attachment anchor）不通过本表表达；它们必须有 named policy，并出现在 `guardrails/message/direct-no-outbox-baseline.md` 由 `guardrails/message/direct-no-outbox.sh` enforce。其中 channel member system message projection 的 Agent Exec owner（`internal/service/channeladmin`，`agentteam exec channel` 成员增删）把成员 mutation、system message 与 idempotency receipt 放在同一事务，commit 后仅做 projection-only fanout；receipt replay/no-op/rollback 不 emit。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `message_id`
- `event_ref_type`
- `event_ref_id`
- `fanout_type`
- `dedupe_key`
- `payload`
- `status`：`pending | leased | delivered | failed | suppressed`
- `attempts`
- `max_attempts`
- `next_attempt_at`
- `lease_owner`
- `lease_expires_at`
- `created_at`
- `delivered_at`
- `suppressed_at`
- `suppression_reason`
- `last_error`

约束：

- Applier 在事务内只写 outbox，不直接执行外部副作用；
- ordinary message creation 的 runtime wake、WebSocket/Realtime、daemon handoff 只能在事务提交后，由该 producer 声明的 inline/store owner 使用 committed delivery 执行首次 fanout，或由 outbox worker 执行 worker-first/replay；
- direct/no-outbox allowlist 中的例外如需 direct projection，只能在事务提交后执行；故意不产生 notification/unread/wake 的例外不得被 generic message fanout 捕获，且必须保留对应 durable fact 和 history/sync/refetch recovery story；
- fanout 失败不回滚消息；
- `dedupe_key` 保证同一副作用幂等执行；
- worker 通过 lease 领取任务，失败后按 backoff 写回 `next_attempt_at`；
- 执行 runtime wake、realtime、daemon handoff 前必须二次检查对应 non-message activity event / visibility grant / message 是否已被 suppress 或撤销；
- outbox 只表达副作用执行状态，不作为消息可读性或 membership 的事实源。

#### 3.10.1 Committed ordinary message delivery policy

Ordinary Human-visible message 的 first delivery 和 durable replay 是两个不同契约。Production producer 必须选择下列已审核 policy 之一，generic append helper 不得隐式代选：

| Policy | First-delivery owner | Durable replay | 当前代表路径 |
| --- | --- | --- | --- |
| `inline_ordinary` | API/service/worker caller 在 commit 后立即把完整 committed delivery 交给 shared fanout | ordinary replay 不根据当前 roster 重建历史 unread；未声明 recoverable stream 时也不重放 `message.created` | Human/Agent message API、provider webhook follow-up、Knowledge Q&A answer、share card |
| `autonomous_store_recoverable` | app Store 同时拥有 message transaction 与 commit 后首次 fanout lifecycle | replay 仅修复稳定 `message.created` 与可选 Inbox conversation projection；不重建 ordinary unread | App Notification、external App runtime 非 Task reply、platform Service Account broadcast |
| `inline_targeted` | Task message caller 在 commit 后先尝试投递 | 基于 transaction 内冻结的 Human recipients 做 exact targeted replay | Human/Agent Task discussion；service/app-authored Task discussion（provider service event 经 `AppWebhookResult.Deliveries` 返回 delivery 由 `DeliverFollowupMessages` 执行 first fanout；external App runtime reply 通过 store-owned recoverable post-commit token 调用 `DeliverCommittedMessage`） |
| `worker_first_targeted` | outbox worker | 基于冻结 recipients 的 exact targeted replay | typed Task Activity attention intent |
| `worker_first_mutation` | outbox worker | 只做幂等 browser mutation projection，不新增 unread 或 Agent wake | message update/recall mutation |

`autonomous_store_recoverable` 的 canonical lifecycle 为：

1. app Store 在创建 message 的同一 transaction 内生成完整 `AppMessage` / `MessageDelivery`；
2. Store 把 recoverable stream capability 固化到同一 message outbox payload，并且只为 fresh，非幂等 replay 的 create 保留 committed handoff；
3. transaction 成功提交后，Store 把该 handoff 交给 workspace 已注入的 shared `DeliveryFanout`；
4. first attempt 按 ordinary message 顺序应用当前 warm Human unread、`message.created` / Inbox projection，再执行被 delivery policy 允许的 Agent wake，成功后完成 outbox；
5. rollback、failed materialization、不完整 handoff 和 idempotent replay 都不得再执行这一次 first fanout。

`RecoverableStreamProjection` 只是上述 policy 的 replay capability，不是一个允许 producer 跳过 first delivery 的独立 policy。Ordinary scope 没有在 transaction 内冻结 Human audience；因此若进程在 commit 后、first fanout 完成前退出，outbox 可用稳定 event identity 重放 browser projection，但不承诺精确补齐该 crash window 的 ordinary unread。只有 frozen-recipient targeted delivery 可以跨进程重启完成 exact unread recovery。

Intentional exceptions 必须与上述 ordinary policies 隔离：

| Exception | Human/Agent side effect | Durable recovery |
| --- | --- | --- |
| rewind invalidation 及 rewind system notice | 可发送幂等 invalidation / `message.created` projection，但不进入 ordinary unread、notification 或 Agent wake | rewind facts、system message row 与 conversation refetch |
| channel invite/member system projection（包含 regular create、duplicate 与 role materialization 创建 Agent 时的 explicit initial-channel memberships；隐式 `#all` roster 保持静默） | 自有事务的 `CreateAgentForUser` 收集全部 event results，并在 commit 后仅通过 named projection-only policy 发送 `message.created` 展示更新；不产生 unread、notification 或 Agent wake。caller-owned `CreateWorkspaceAgentInTx` 必须把 transaction-bound results 返回给外层，rollback 丢弃，只有外层 commit 成功后才可逐条 projection-only publish | membership fact、channel history/member list/sync/refetch；rollback 不产生 realtime projection |
| hidden Task description attachment anchor | 不是 Human-visible conversation projection，不产生 realtime、unread、notification 或 wake | hidden message-owned sidecar authority 与 attachment read authorization |

Approval Team 退回 feedback 仍是 [`specs/collaboration/team.md`](../collaboration/team.md) 定义的 required visible message，不是 intentional no-notification exception。它的 producer migration 与 delivery repair 保持 deferred；本节不宣称该路径已纳入 unified committed-message lifecycle，也不允许它借用 NoOutbox 逃逸 ordinary visible-message contract。

### 3.11 `attachments`

通用附件表。附件可以关联 scope message，也可以保持未认领上传状态。Task discussion 也是 task scope 里的 `scope_messages(message_type='task_discussion')`，因此附件不再有 task discussion 专属关联列。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `message_id`
- `uploaded_by_member_id`
- `storage_driver`
- `storage_object_key`
- `filename`
- `content_type`
- `byte_size`
- `payload`
- `created_at`

约束：

- `scope_id` 是上传和读取所属 scope；`message_id` 可为空，以支持该 scope 内的未认领上传；
- message 附件使用 `scope_id/message_id` 关联 scope message；task discussion 附件使用 task scope 的 `scope_id/message_id` 关联对应 `task_discussion` message；
- 附件存储 driver、object key 和 payload 语义由 storage spec 约束；
- 附件不是投递事实源，不参与 visibility 推导；读取附件时使用其关联对象的 visibility。

### 3.13 `scope_message_reactions`

消息 reaction 的目标命名。精确唯一键、可见状态、remove suppression、reaction activity 语义由 [`specs/message/reaction.md`](reaction.md) 定义。

本规范只约束：

- conversation container 字段使用 `scope_id`；
- reaction 不写入 message transcript；
- reaction activity 属于 non-message activity；
- remove 时不保留额外可见状态行。

### 3.14 `message_recovery_points`

消息 rewind 的 recovery point。runtime capability、recovery strategy/ref、prepared point、GC 和 rewind 校验语义由 [`specs/message/rewind.md`](rewind.md) 定义。

本规范只约束：

- recovery point 关联到 `scope_id` 和 `message_id`；
- recovery point 不参与普通消息投递；
- rewind 可读性仍使用 scope membership 和 owner-private visibility filter。

### 3.15 `scope_message_search_docs`

消息搜索索引文档。TiDB FULLTEXT、readiness gate、hot-only 范围、access control 和返回语义由 [`specs/message/search.md`](search.md) 定义。

本规范只约束：

- 搜索文档使用 `scope_id`；
- finalize 必须创建并验证 `MULTILINGUAL` TiDB FULLTEXT artifact，search query 只在该 artifact 已 ready 后使用；
- 搜索结果必须使用与 history 相同的 visibility filter；
- search docs 是投影，不是 read access 的事实源。

### 3.16 `scope_message_search_state`

消息搜索索引状态。read-ready、finalize、backfill 状态由 [`specs/message/search.md`](search.md) 定义。

本规范只约束：

- readiness 按 workspace/scope 维度解释；
- readiness 同时要求 migration/backfill/finalize 均为 complete 且 `read_ready=true`；
- projection 未 ready 或处于 backfill/finalize 进行中时，产品表现为搜索不可用；
- backfill/finalize/promote-ready 运维命令可重复执行，已经 ready 的 projection 不因重跑降级；
- search state 不参与消息投递和权限判断。

## 4. 消息相邻状态表

### 4.1 `scope_share_cards`

scope channel-share message 的 action state。

字段：

- `id`
- `workspace_id`
- `scope_id`
- `message_id`
- `source_scope_id`
- `source_message_id`
- `target_scope_id`
- `created_by_member_id`
- `revoked_at`
- `revoked_by_member_id`
- `payload`
- `created_at`
- `updated_at`

`message_id` 是分享卡片在当前 scope 中的 `channel_share_card` message。`created_by_member_id`、`revoked_at`、`revoked_by_member_id` 是分享卡片创建和撤销生命周期的结构化状态，不放入 `payload` 或消息 metadata。

### 4.2 `service_notification_cards`

App interactive notification 的 message-owned render/action state。

字段：

- `scope_id`
- `message_id`
- `app_notification_id`
- `schema_version`
- `card_revision`
- `render_payload_json`
- `action_schema_json`
- `state`
- `expires_at`
- `created_at`
- `updated_at`

`message_id` 唯一指向同 scope 的
`scope_messages(message_type='app_interactive_card')`。Message content 保存可搜索、
可访问并可用于 push preview 的 fallback；sidecar 保存 registered component schema、
actions 与 current render state。Card update 保持 message id、scope seq、permalink
与 read frontier，并以 card revision 线性化 Human action。

## 5. private agent 规则

### 5.1 parent scope 准入

private agent 先加入 parent scope，成为该 scope 的 private member。parent scope 可以是 channel，也可以是 DM。加入后：

- 需要展示 private membership 的面板展示该 private agent，并标记为 private；
- owner 可以在该 parent scope 中向它投递消息；
- 有 grant 的 member 可以在 grant 范围内向它投递消息；
- 没有 private authority 的 actor 发出的消息不会投递给它，也不会进入它的可读输入上下文；
- scope 里的普通 auto-wake 只按该 private agent 的 notification setting、visibility filter 和 authority 共同决定。

### 5.2 thread 准入

private agent 加入 thread scope 的有效入口：

- thread root message 中的 live mention 提及它；
- thread 内新消息的 live mention 提及它；
- thread root message 的作者就是该 private agent；
- 显式收件人 API 把它作为 recipient，并通过 authority 校验。

准入成功后：

- 写入该 thread scope 的 membership；
- 写入对应 notification setting；
- 后续 thread 消息按 thread membership 和 notification setting 正常续投；
- 不会把 parent scope 中未被准入的 private peer 拉入 thread；
- 不会自动加入 parent scope 之外的其他 scope。

### 5.3 live mention 形态

live mention 解析复用 collaboration 规范中的 canonical routing-token parser。fenced code、inline code、blockquote 内的 routing token 都是 inert text，不触发 routing、wake 或 command 副作用。

- 普通发送：对消息正文运行 canonical parser，解析出的 routing token 是 live mention；
- 单条转发：等价于 owner 发送一条原样消息，对单条转发正文运行 canonical parser；
- 多条/合并转发：转发内容是上下文引用，不生成 routing text，引用中的 routing token 全部 inert；
- 显式上下文投递 API 使用 explicit recipient，不依赖文本 mention 解析。

forward/quote 先决定 `routingMentionMode`，再解析 live mention：`normal` 和 `single_forward` 运行 canonical parser，`multi_forward` 和 `explicit_context` 返回空 live mention 集合。

## 6. 发送实现架构

发送实现采用 `Policy + Planner + Applier` 架构。pipeline 只规定处理顺序；真正的不变量由 policy、plan 和唯一写库入口保证。

### 6.1 Policy

Policy 只回答权限和准入问题，不写库，不产生副作用。

必须集中实现的判断：

- actor 是否可以向目标 scope 写消息；
- actor 是否可以提及目标 member；
- actor 是否可以向某个 owner 的 private agent 投递指令；
- 某个 member 是否可以被新增为目标 scope member；
- app/service 是否只能走普通 scope routing 或显式 recipient routing；
- forward/quote 中的 mention 是否是 live mention。

每个 policy decision 都返回结构化 reason。Message-backed reason（direct mention、explicit recipient、controlled broadcast、task dispatch）必须只作为当前 message fanout / notification 的解释输入，或从 message metadata / source refs 推导；它不进入 `scope_activity_events`，也不创建新的 routing fact 表。

### 6.2 Planner

Planner 把一次发送意图编译成 `MessagePlan`，不直接落库。

输入：

- `MessageIntent`：目标 scope、消息类型、source refs、client id、forward shape、silent/internal 标记；
- `ActorContext`：发送者身份、owner 关系、grant、service 身份；
- `ScopeContext`：目标 scope、parent scope、root message、memberships、notification settings；
- `LiveMentions`：按消息形态解析出的 live mentions。

输出：

- `MessageWrite`：要插入的消息和 sources；
- `MembershipMutations`：要新增或恢复的 scope memberships；
- `NotificationMutations`：要新增或更新的 notification settings；
- private visibility facts：通过 membership、grant、author interval、source refs、message visibility from membership/grants plus non-message activity visibility、task-native ingress 等具体事实建立或引用 private 可见性；
- `ActivityEvents`：要写入的 non-message activity events；
- `TaskContextIngressRefs`：需要关联的 task / execution context ingress refs；
- `OutboxEvents`：事务提交后需要执行的 wake / realtime / daemon handoff 副作用；
- `Explain`：本次计划中每个 recipient 的原因。

约束：

- Planner 只做集合计算和 policy 调用；
- Planner 不执行 SQL；
- Planner 不从 non-message activity event 推导 membership；
- Planner 不因 message type 替换 base scope routing；
- Planner 对所有入口复用同一套 recipient/admission 计算。

### 6.3 Applier

Applier 是发送路径唯一写库入口。Applier 分为事务内写入、durable outbox 和事务后 fanout 三层。

事务内写入顺序：

1. 确保目标 scope 存在；
2. 插入消息和 source refs；
3. 应用 membership mutations；
4. 应用 notification mutations；
5. 写入 private membership / grant / task ingress 等可见性事实；
6. 写入 non-message activity events；
7. 写入 durable outbox / persisted notification intent；
8. 更新 scope counters、cursor 派生状态和 unread 持久状态。

事务提交后执行：

1. 根据 durable outbox 唤醒 runtime；
2. 向 realtime / WebSocket 广播；
3. 执行 daemon handoff / realtime fanout；
4. 标记 outbox 已投递或等待重试。

约束：

- 业务代码不直接绕过 Applier 写 message fanout；
- membership 只能来自 `MembershipMutations`；
- private 可见性只能由 membership、grant、author interval、source refs、message visibility from membership/grants plus non-message activity visibility、task-native ingress 等具体事实表达；
- non-message activity event 只能来自 `ActivityEvents`；
- Applier 不重新做权限判断；
- runtime wake、WebSocket fanout、daemon handoff 不得在可能回滚的事务中直接发出；
- 事务提交后，ordinary message / outbox-backed fanout 失败时通过 durable outbox 重试，不回滚已提交消息；
- direct/no-outbox allowlist 例外不获得普通 message creation 许可，必须依赖 allowlist 记录的 durable recovery。

### 6.4 入口约束

所有会产生可见消息或 agent 收件事件的入口都必须生成 `MessagePlan`：

- human 普通发送；
- agent 发送；
- reply thread 发送；
- DM 发送；
- 单条转发；
- 多条/合并转发；
- app/service 可见消息；
- 显式上下文投递。

入口之间只允许差异化 `MessageIntent` 和 `ActorContext`，不允许各自实现独立 fanout。

### 6.5 规则约束

- 只有 `MembershipMutations` 可以新增 scope membership；
- 只有 `ActivityEvents` 可以产生 non-message activity event；
- non-message activity event 不反向决定 membership；
- fanout/source/task ingress 不能反向创建 membership；
- message type 不替代 base scope routing；
- app/service 消息先走目标 scope 的普通 routing，再叠加服务自己的 explicit routing；
- silent/internal 消息按显式 no-fanout 规则执行。


## 7. 落地实现原则

实现采用停机 cutover 和干净终态：

- 升级期间可以停止服务；
- 这是完整 cutover 重构，不是在旧模型上接功能；
- 不为了滚动升级保留兼容读写路径；
- 不保留旧表、旧 helper、旧 routing 分支和旧测试；
- 不允许为了功能保全继续调用旧 routing / store / helper；
- 不允许通过双读、双写、shadow read、胶水 adapter 或 fallback chain 维持旧模型；
- fresh bootstrap schema 与迁移后 schema 保持一致；
- schema bootstrap、migration、schema verify 必须来自同一目标定义，不能手工分叉维护；
- migration 可以执行破坏性清理，但必须在停机窗口内完成数据搬迁、校验和 schema verify；
- 业务代码只保留 scope 模型、`MessagePlan`、Policy、Planner、Applier 这一套路径；
- 文档同步是 cutover 的一部分；canonical specs、guide、API 文档和公共文档中不能留下旧模型口径；
- 功能保全是硬门槛；除非目标规范明确删除某能力，否则实现不得回退现有行为；
- 功能保全必须由 scope 模型、`MessagePlan`、Policy、Planner、Applier、SQL readable predicate / visible-row mapper、outbox 等新路径承载；新模型表达不了的能力先修改模型，不接回旧代码；
- UI 改动只允许服务于 scope/private 语义展示、旧口径移除或功能保全，不做无关交互/视觉调整；
- checkpoint 可以作为同一 integration branch 上的 review slice；最终以一个 cutover PR 进入主线，避免留下中间态或兼容分支。

推荐升级顺序：

1. 备份数据库；
2. 停止旧服务；
3. 执行 schema migration 和必要数据搬迁；
4. 启动新服务；
5. 执行 schema verify、health check、关键消息投递 smoke test。

## 8. 查询和展示规则

- human 和 public agent 的 conversational scope 消息记录按 scope membership 判断可读性；product scope 消息记录同时应用该 product 注册的 SQL permission bases；
- Workflow Scope 是 Builder product scope。Human 读取必须同时通过当前
  Plan authoring gate 和 Workflow product gate；Agent 即使具有 retained
  membership 也不能使用 generic history、search 或 read-check 读取该
  Scope；
- Workflow Human 请求只能通过 Workflow adapter 写入。adapter 生成
  request metadata 和 exact selected-Agent routing；普通消息 API 不接受
  Browser 自行提供这些受信字段。Agent 回复只能通过当前 wake 的 trusted
  current-target 写回同一 Scope；Workflow Plan management 在 Plan 行锁内
  编排 selected responder、active wake 和 exactly-one final reply，message
  domain 只提供 Scope membership、idempotency、sequence、outbox 和 append
  primitives；
- private agent 的消息记录、搜索结果和 read-check/source/attachment 检查必须经过 owner-private SQL readable predicate；daemon delivery 只来自当前 routed event payload，不能因为一条历史消息可读就把 no-wake backlog 变成待处理队列；
- private agent 可读消息集合来自 membership interval 内自己的输出、有效 membership/grant/collaborator/same-owner private author interval、explicit non-message activity/source refs/task ingress；
- visible-row mapper 分页返回 SQL-visible refs、continuation cursor 和显式 continuation state；candidate budget 耗尽时即使当前 page 为空，也通过 `has_more` / `candidate_budget_exhausted` 要求调用方继续拉取；
- owner 普通消息不为 private roster 批量写逐消息授权索引；
- grant revoke / expire 不 retroactive 改写历史可见性窗口；
- 没有 private authority 的消息不进入 private agent 的可读输入上下文；
- reply thread 消息记录包含 root message 作为上下文展示，但 root message 仍受对应 recipient 的 SQL readable predicate 约束；
- root message 的 parent scope seq 不推进 thread scope 的 read state；
- member list 展示 scope membership，但按 viewer 和 surface 过滤；
- private member 只能以 private 身份展示，并显示 owner 语义；
- 公开成员列表只展示 public member；
- UI/API 不把 private member 伪装成 public participant；
- private authority 不从展示结果推导。

## 9. Guardrail 决策

本规范需要 guardrail。实现落地时必须新增 message-domain guardrail，用来保护下列约束：

- 禁止普通 default/broad routing 写 per-recipient non-message activity event；
- 禁止重新引入 message delivery ledger 类表；
- 禁止在 non-message activity reason 中加入 default/broad reason；
- 禁止 hidden auto-flow copy、private-context creation backfill、source-ref-as-dedupe、attachment copy 等隐藏物化路径；
- 禁止 eager inherited membership / notification rows 自动扩散到未参与的 child scope；
- 禁止 human delivery side ledger、reaction side ledger、task side ledger 重新复制 dense fanout；
- 检查 message-domain schema 不再新增并行 `space_*` 消息事实源；
- 检查发送入口只通过 `MessagePlan` / Applier 写消息 fanout；
- 检查 canonical parser 被复用，不能重新写 mention parser；
- 检查普通消息最多写自身 message row、必要 source/projection 和 outbox，不写 per-recipient dense rows 或 message-backed activity rows。

可执行 guardrail 是 scope cutover 的交付物；对应实现卡片交付 guardrail 并纳入 CI。

## 10. 不变量

- `scope_messages.scope_id` 指向唯一 scope。
- `scope_memberships` 是参与身份和已读状态的事实源，并保留 membership 有效区间。
- `scope_notification_settings` 是当前通知偏好的事实源，不保留 notification 有效区间。
- `scope_activity_events` 是非消息活动源的事实表；message readability 由 `scope_messages` + membership/read cursor + registered product permission bases 表达，agent 当前事件投递由 fanout outbox payload/status 表达。
- `app_interactive_card` 是普通 visible message；`service_notification_cards` 是其 message-owned sidecar，Human callback action 由 App notification action/service-event facts 表达。
- private 可读输入由 membership、grant、SQL-readable author interval、source refs 和 task ingress fact 共同表达。
- `scope_fanout_outbox` 是 ordinary message / outbox-backed 事务后副作用重试的事实源；direct/no-outbox allowlist 例外不通过本表表达。
- private agent 只有在具体 scope 中有 membership 时，才出现在该 scope 的成员列表。
- private agent 的可指挥权和可读输入都只来自 authority/visibility 校验。
- 多条/合并转发中的 mention 不产生 live routing。
- 已记录的 non-message activity event 不改变 scope membership。
- 普通 scope default/broad delivery 不写 per-message/per-recipient non-message activity event。
- private fanout / receive 必须留下可解释的授权来源。
- 每个核心读写查询都有明确 repository owner 和目标索引。
- API、UI、daemon 不直接拼接消息域 SQL。
- `(workspace_id, workspace_global_seq)` 唯一；非空 `client_message_id` 对同一 `(scope_id, author_member_id, idempotency_subject_member_id)` 幂等。
- 每个 Workflow Plan 恰好关联一个同 workspace、顶层、`type=workflow`
  的 Scope；Workflow Scope 不进入普通 Inbox、conversation list、search、
  unread 或 `loop.plan/v1` 可发送 scope 联合类型。
