# Server-side Message Freshness Gate 规范

> Canonical path: `specs/message/server-side-freshness-gate.md`
> Status: target spec
> Related:
> - [`specs/message/scope-model.md`](./scope-model.md)
> - [`specs/message/delivery-runtime.md`](./delivery-runtime.md)
> - [`specs/message/scope-implementation-design.md`](./scope-implementation-design.md)
> - [`specs/daemon/daemon.md`](../daemon/daemon.md)
> - [`specs/daemon/codex-runtime-consolidation.md`](../daemon/codex-runtime-consolidation.md)

规范状态：目标规范。

本文定义 server-side message freshness gate 的目标行为。该 gate 约束 agent-authored visible message creation：channel、DM、reply thread 和 task discussion scope 中由 agent 写入的可见消息，都以目标 scope 的已观察 frontier 作为发送前置条件。

## 1. 目标

- Agent 的可见回复基于已经展示给该 agent runtime 的目标会话消息。
- 同一目标 scope 的并发发送由 server 在消息写入事务内线性化。
- 新消息写入使用 scope-local message sequence 作为目标会话的权威 frontier。
- Freshness 判断与消息追加、搜索投影、fanout outbox 和 wake/realtime side effect 共享同一个事务结果。
- Server 从请求 `target` 解析 canonical target scope 和 canonical `targetKey`，并用 canonical key 执行权限检查、freshness 校验和响应回填。
- Freshness-capable daemon 使用完整 freshness contract；compatibility daemon 继续使用兼容发送路径，并通过 compat metrics / logs 标记。
- Daemon 把 observed frontier 与 latest-known refresh hint 分开维护；只有 runtime-visible 输入边界 会推进 observed frontier。
- 只有 runtime 确认消息正文已经进入新 turn / idle input / direct delivery / runtime acked input 的可见输入边界才推进 observed frontier；active turn 中已被 runtime transport 接受但尚未收到 ack 的消息进入 ack-aware pending runtime input / refresh hint，不进入 daemon-local continuation inbox。
- Held send 以结构化响应表达目标 scope frontier 已推进的状态，并记录 latest-known refresh hint。
- Accepted send 是消息 row、search doc、fanout outbox、realtime fanout 和 wake side effect 的产生来源。
- 幂等重试使用 client message id 得到稳定持久化结果。

## 2. 术语

### 2.1 Target scope

Target scope 是一次 `send_message` 最终写入的 canonical scope。Server 从请求的 `target` 解析 target scope：

- channel target 解析为 channel scope；
- DM target 解析为 DM scope；
- reply thread target 解析为 thread scope；
- raw task scope target 解析为 task discussion scope。

Server 返回的 canonical `targetKey` 是 target scope 的稳定 freshness key。推荐值为 scope id；task discussion 使用 task scope id。

### 2.2 Target sequence

Target sequence 是 target scope 内的 scope-local message sequence，字段语义对应 `scope_messages.seq` 和 `scopes.latest_seq`。Delivery ack sequence、activity sequence、reaction synthetic sequence 与 target sequence 属于独立序列。

### 2.3 Scope frontier

Scope frontier 是 target scope 的最新消息 target sequence。Raw 权威值由 `scopes.latest_seq` 表示。每条写入 `scope_messages` 的可见消息在所属 scope 内获得单调递增的 `seq`。Freshness-capable send 的正常路径先以 raw frontier 做条件写；当 raw CAS miss 且 author 可能存在 private visibility gap 时，server 只在有界尾部窗口内判断是否能证明存在 author 可见的新消息。只有可证明的 author-visible frontier 会触发 `held`；到达扫描预算或无法证明时放行发送，避免不可见消息让 private agent 永久 held 或引入无界 history scan。

### 2.4 Observed frontier

Observed frontier 是 agent runtime 已经实际看到的 target scope 最高 target sequence。Daemon 以 `targetKey -> seenUpToSeq` 记录每个运行中 session 的 observed frontier。Observed frontier 只从 runtime-visible 输入边界 按 high-water 推进；transport accept、pending runtime input / refresh hint 和 search result 不推进 observed frontier。

### 2.5 Latest-known refresh hint

Latest-known refresh hint 是 daemon 已经从 server 获得、但尚未通过 runtime-visible 输入边界 转成 observed frontier 的 target scope 最新 frontier。Held response 会更新 `latestKnownSeq` / `needsRefreshUntilSeq`，并提示 runtime 拉取目标消息上下文。

### 2.6 Fresh send

Fresh send 是带有 freshness constraint 的 agent message send。Freshness constraint 至少包含：

- `targetKey`：daemon 记录的目标 scope freshness key；
- `seenUpToSeq`：agent runtime 已观察到的 target scope frontier；
- `sourceMessageIds`：推进该 observed frontier 的可选消息 id 集合，用于审计和调试；
- `clientMessageId`：一次逻辑发送的幂等 key。

Server 使用请求 `target` 解析 canonical target scope。请求 `freshness.targetKey` 与 canonical `targetKey` 一致时进入 freshness CAS；响应始终回填 canonical `targetKey`。

### 2.7 Send states

`send_message` 的持久化状态包含：

| State | 语义 |
| --- | --- |
| `sent` | 新消息已写入 target scope，并获得 `acceptedSeq`。 |
| `already_sent` | 幂等重试命中已存在消息，响应返回既有 `messageId` 与 `acceptedSeq`。 |
| `held` | Target scope frontier 已推进，响应返回 `latestSeq` 和 refresh hint。 |

## 3. Agent 可见消息发送合同

### 3.1 请求

Agent-authored visible message send 使用内部 API entrypoint；本规范定义该 entrypoint 的完整 request/response contract：

```http
POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/send
```

请求体：

```json
{
  "target": "#channel:threadid",
  "content": "message body",
  "attachmentIds": ["att_..."],
  "clientMessageId": "msg-client-uuid",
  "freshness": {
    "targetKey": "scp_...",
    "seenUpToSeq": 42,
    "sourceMessageIds": ["msg_..."]
  }
}
```

字段语义：

| 字段 | 语义 |
| --- | --- |
| `target` | 发送目标；server 解析为 canonical target scope。 |
| `content` | 消息文本。 |
| `attachmentIds` | 已上传附件 id 列表。 |
| `clientMessageId` | 幂等 key；同一 agent、同一 target scope、同一 key 对应同一逻辑消息。 |
| `freshness.targetKey` | Daemon 记录 frontier 时使用的目标 key；server 用 canonical key 校验该值。 |
| `freshness.seenUpToSeq` | Daemon 从已确认进入输入边界的消息维护并在 HTTP send request 中填入的 observed frontier。 |
| `freshness.sourceMessageIds` | 可解释 observed frontier 来源的消息 id 集合。 |

Request validation 按 daemon capability 分流：

- Freshness-capable daemon request 满足完整 contract：`target` 解析到 agent 可写的 canonical target scope，`clientMessageId` 非空且长度在 server 约束内，`freshness` 对象存在，`freshness.targetKey` 非空且等于 server 解析出的 canonical `targetKey`，`freshness.seenUpToSeq >= 0`，`sourceMessageIds` 是有界集合。
- Compatibility daemon request 继续走兼容发送路径：`target`、`content`、`attachmentIds` 由现有路径解析和写入，server 返回 compatible response shape，并记录 compatibility metric 与 structured log。

Server 对 freshness-capable request 的 targetKey mismatch 返回 structured validation error。Server 对 compatibility request 保持 wire compatibility，并通过 daemon capability / version 标识让新 daemon 使用 freshness path。Compatibility request 保持可用性，同时 freshness guarantee 只适用于 freshness-capable request。Server min daemon version 提升到 freshness-capable version 后，全量 send request 使用 freshness contract。

### 3.2 响应

`sent` 响应：

```json
{
  "state": "sent",
  "messageId": "msg_...",
  "targetKey": "scp_...",
  "acceptedSeq": 43,
  "latestSeq": 43,
  "clientMessageId": "msg-client-uuid"
}
```

`held` 响应：

```json
{
  "state": "held",
  "targetKey": "scp_...",
  "seenUpToSeq": 42,
  "latestSeq": 45,
  "needsRefreshUntilSeq": 45,
  "clientMessageId": "msg-client-uuid"
}
```

Held response 更新 latest-known refresh hint。Observed frontier 继续由 runtime-visible 输入边界 推进。

### 3.3 Freshness 条件

Server 在同一个数据库事务内执行 target scope freshness CAS：

```sql
UPDATE scopes
SET latest_seq = latest_seq + 1
WHERE id = $scope_id
  AND latest_seq = $seen_up_to_seq
```

Server 使用 TiDB-compatible row-count result 解释 raw CAS 结果：affected rows 为 1 时，`acceptedSeq = seenUpToSeq + 1`，该值是写入消息的 `scope_messages.seq`；affected rows 为 0 时，server 刷新幂等 lookup 并进入 CAS miss 分支。

CAS miss 分支按 target 可见性执行有界判断：

- public / no visibility gap target 读取 raw `scopes.latest_seq` 并返回 `held`；
- 可能存在 private visibility gap 的 target 读取 raw `scopes.latest_seq` 与当前 membership visibility，并用现有 `VisibilityFilter` 在有界 newest-first candidate window 内寻找 author-visible frontier；
- 有界检查证明存在 `authorVisibleSeq > seenUpToSeq` 时，server 返回 `held`，`latestSeq` / `needsRefreshUntilSeq` 使用该 author-visible frontier；
- 有界检查没有证明 author-visible 新消息时，server 以当前 raw `latest_seq` 再执行一次条件分配；分配成功时写入下一条 raw scope seq，分配冲突时按同一事务重试规则重新判断。

CAS 结果驱动消息 insert、search doc、outbox 和事务后 fanout。

TiDB retriable transaction conflict 使用同一 `clientMessageId` 重试同一有界事务。事务重试后的 conditional update row count 决定最终 `sent` / `already_sent` / `held` 状态。

### 3.4 Target 隔离

Observed frontier 以 canonical target scope 为 key。每次 send 的 freshness 判断只读取请求 target 解析出的 canonical target scope frontier。

### 3.5 Surfaced frontier

Daemon 在消息已经成功展示给 runtime 后更新 observed frontier。所有 runtime-visible target message 投递 都按 high-water 更新同一 target scope 的 observed frontier：

- 初始 wake prompt、server-directed `agent:deliver`、idle stdin delivery、Codex app-server protocol direct delivery 在消息正文成功交给 runtime 后，可以把 observed frontier 推进到该消息的 `targetSeq`；若 handoff 失败并重新入队，必须回滚该推进。
- daemon 从 continuation inbox 自动递交 pending target messages 后，按已进入 runtime-visible 输入边界 的最高 `targetSeq` 推进 observed frontier。该 inbox 是 agent/session 本地 delivery state；若发现投递乱序或漏投，应修复 delivery/outbox/local-inbox invariant，而不是让 freshness frontier 卡在旧 seq。
- `read_history` 返回 target coverage 时，可以推进 observed frontier 到 `coversThroughSeq`。
- Ack-aware active-turn direct delivery 在 runtime transport accept 后只记录 pending runtime input 和 latest-known refresh hint；只有 runtime 随后以同一 runtime input id / client message id / uuid 发出 `runtime_input_surfaced` ack，证明消息正文已经进入 runtime-visible input boundary 后，才能按该消息 `targetSeq` 推进 observed frontier。若 transport fail、session mismatch 或 runtime 不支持该路径，消息回落到 daemon-local continuation inbox 并由 daemon 自动重试。Transport accepted 后不使用固定 wall-clock ack timeout；长命令期间 pending runtime input 继续保持 pending。
- agent 自己成功发送到该 target scope 的消息可以推进 observed frontier 到 `acceptedSeq`。

Daemon 已接收但还没有通过 runtime-visible 输入边界 展示给 runtime 的消息，属于 pending delivery / pending refresh hint。Ack-aware active-turn transport 已 accepted 的消息作为 pending runtime input 保存，并且必须与 daemon-local continuation inbox 隔离；该 inbox 不得向 agent 暴露这类尚未 ack 的正文。非 ack-aware、不可 steer、transport fail 或 session mismatch 的消息才进入 continuation inbox fallback。Pending delivery 在进入上述 runtime-visible 输入边界 后按 high-water 更新 observed frontier。与 pending delivery 并发发生的 `send_message` 使用该时刻 session observed frontier；daemon 可在本地发现同 target pending refresh 时直接返回 held-like result，否则 server CAS 以 target scope 权威 frontier 返回 `sent` 或 `held`。

Search result 默认作为 sparse source context。`search_messages` 不推进 observed frontier，也不更新 latest-known refresh hint；目标 freshness 刷新闭环由 runtime ack、显式 `read_history`、daemon automatic fallback delivery 和 `send_message` 的 held result 承担。

### 3.6 Active-turn delivery

Active turn 新消息优先走 ack-aware direct delivery：daemon 为每条消息生成 runtime input id，将消息正文交给 runtime transport，并记录 latest-known / needs-refresh hint。Transport accept 只表示 runtime 已收下 pending input，不属于 observed 状态；该消息必须等 runtime 在下一次 input/model boundary 发出 correlated ack 后，才进入 observed frontier。该 ack-aware pending input 不进入 daemon-local continuation inbox，且 runtime 没有主动读取该 inbox 的工具。

若当前 turn 随后基于旧上下文调用 `send_message`，请求使用旧 `seenUpToSeq`；daemon local preflight 或 server CAS 返回 `held`。Held result 可以 best-effort 附带 daemon-local continuation inbox 中同 target 的消息；如果附带消息已覆盖 gap，runtime 读取这些新消息后再判断是否重试；如果本地 inbox 里仍有同 target 消息未附带，文本提示 exact-target `read_history`；如果 gap 已由 ack-aware pending runtime input 覆盖但还没收到 ack，文本提示等待下一次 input boundary 后按需重试，不提示 `read_history`；只有既没有覆盖 gap 的 pending runtime input、也没有可用 fallback inbox 覆盖时，才提示 `read_history(target=exact target)`。Runtime 不支持 ack-aware delivery、当前 turn 不可 steer、transport fail 或 session mismatch 时，消息才回落到当前活进程的 daemon-local continuation inbox，并由 daemon 在后续 safe input boundary / idle 自动投递给 runtime 后进入 observed frontier。Transport accepted 的 pending runtime input 不因长命令或固定时间流逝回落 fallback。

显式打断是独立语义，见 [`agent-interrupt-control.md`](./agent-interrupt-control.md)。普通消息不得通过正文推断为打断；只有 UI / API 显式携带 interrupt policy 时，daemon 才能先停止当前 run，再把该消息作为新的 authoritative current input 投递。

### 3.6.1 Stale delivery suppression

Daemon 在最终唤醒 runtime 前必须重新检查待投递消息是否已经被该 agent session 观察过。对带 `targetKey` / `targetSeq` 的普通 routed message，如果该 session 对同一 `targetKey` 的 `ObservedSeq >= targetSeq`，则该 delivery 已经被更晚的 runtime-visible 输入边界 覆盖，daemon 必须把它 consumed / suppressed，而不是写入 stdin、调用 protocol direct delivery、启动新 turn，或再次从 continuation inbox 递交给 runtime。

该检查必须发生在所有最终投递入口：

- AgentTeam channel 收到 server `agent:deliver` 后、进入 dispatcher/runtime 前；
- `Process.deliverMessage` 选择 protocol direct / stdin / local queue 前；
- local inbox requeue 前；
- `turn_end` / `runtime_idle` 从 local inbox dequeue 前；
- continuation inbox 内容进入 runtime-visible delivery 前。

已经进入 local inbox 的旧 delivery 也要在 dequeue / tool result 时重新判断，因为它可能在入队后、被 runtime 读取前才变 stale。Suppress 时应 ack / consume 对应 delivery，并保留低基数字段日志：delivery id、message id、target、targetKey、targetSeq、observedSeq、latestKnownSeq 和 suppress reason。`targetSeq > observedSeq` 的新消息必须继续正常投递。Reaction / activity delivery 使用独立序列，不能用 message target sequence 规则误判。

### 3.7 Held recovery

`held` 是 `send_message` 的成功 tool result。Runtime 从 tool result 文本中只获得下一步刷新提示和 best-effort 附带的 target messages；`targetKey`、seq frontier、`clientMessageId` 等 freshness internals 留在 daemon/server state、logs 和测试断言中。

Runtime 按 held 文本提示刷新目标 scope：`read_history` 拉取权威历史；当 held gap 已由 ack-aware pending runtime input 覆盖时，文本只提示等待下一次 input boundary。Server 不提供 agent `/receive` refresh 接口，daemon continuation inbox 也不提供 agent pull 接口。`read_history` tool result 和 automatic continuation delivery 在把 target 消息投递给 runtime 后按 high-water 推进 daemon observed frontier；pending runtime input 则只在 correlated ack 到来后推进 observed frontier。随后发起的 `send_message` 使用刷新后的 `seenUpToSeq` 和新的 `clientMessageId`。Pending runtime input / refresh hint 只用于让 stale send 被 held 并指向刷新动作，不能被当作 agent-readable 未读正文提前返回。

MCP `send_message` 的 held tool result 可以 best-effort 附带 daemon-local continuation inbox 中同 `targetKey` 的消息，并返回简短的 `Target context is outdated` 行。Held / partial-held 不返回 structured content；下一步动作只通过文本表达：已附带消息覆盖 held gap 时提示读取新消息后按需重试；本地 inbox 明确还有同 target 消息未附带时提示调用 exact-target `read_history`；ack-aware pending runtime input 覆盖 gap 时提示消息会在下一个 input boundary 被 ack，之后按需重试；否则也提示调用 exact-target `read_history`。Held result 不得隐式调用 `read_history` 或自动重发原始 held content；runtime 必须先执行刷新提示，基于刷新后的上下文重新判断是否还需要 visible reply。

Runtime 在 `held` 后结束 turn 时，目标 scope 的 pending delivery 继续作为后续 runtime-visible input 的来源；ack-aware pending runtime input 若最终收到 ack 则直接推进 observed frontier；若 transport/session 失败则回落为 daemon-local queued fallback；若 runtime 长时间执行工具/命令但尚未 ack，则继续保持 pending。

### 3.8 幂等重试

`clientMessageId` 与 target scope、author member、idempotency subject member 一起构成幂等键。Agent direct send 的 idempotency subject 等于 author member。Server 对同一逻辑发送返回同一持久化结果。幂等命中优先于 freshness held 响应。

`sent` 是 external fanout side effect 的唯一 send state。`already_sent` 返回既有 message id / acceptedSeq，并可用于 local tool confirmation 和 daemon observed frontier update。

### 3.9 多 row 逻辑发送

一个逻辑发送需要写入多条可见消息 row 时，每条 row 都带 freshness constraint。第一条 row 使用 runtime observed frontier；每个已接受 row 的 `acceptedSeq` 成为同一逻辑发送下一条 row 的 `seenUpToSeq`。

每条 row 使用确定性 `clientMessageId` suffix。中间 row 返回 `held` 时，MCP tool result 返回 text-only partial-held guidance，不暴露 accepted chunk message ids、accepted chunk seqs、held chunk index、targetKey、seenUpToSeq 或 latestSeq。Runtime 刷新目标上下文后，后续发送使用同一逻辑 id 和 chunk suffix，使已接受 chunk 通过 `already_sent` 稳定回放，并从 held chunk 继续。第一个未接受 chunk 使用刷新后的 observed frontier；实现使用 `max(refreshedObservedSeq, lastAcceptedSeq)` 作为该 chunk 的 `seenUpToSeq`，使 chunk replay 后的 send frontier 保持单调前进，并反映刷新后的目标上下文。

### 3.10 Task discussion scope

Raw task scope target 的 visible discussion message 使用同一 freshness contract。Server 将 raw task scope target 解析为 task discussion scope，并在 task discussion append 事务内执行 target scope freshness CAS。

Task discussion append 返回 stateful result：`sent` / `already_sent` / `held`。`sent` state 产生 discussion entry、task discussion count、task event、attachment claim、participant mutation、search doc、outbox 和 message fanout；`already_sent` state 返回既有 discussion message；`held` state 返回 latestSeq 和 refresh hint。

### 3.11 Tool result

MCP `send_message` 对 `sent` 和 `already_sent` 返回已发送结果，并回显 destination、message id、targetKey 和 acceptedSeq。对 `held` / `partial_held` 返回 text-only refresh guidance，只暴露下一步动作和必要的已附带消息页，不回显 freshness internals 或 partial-send chunk internals。

## 4. 验收标准

- 两个 agent 基于同一 target scope frontier 并发发送时，事务提交顺序中的一个 send 获得 `sent`，后续基于先前 frontier 的 send 获得 `held`。
- Public / no visibility gap target 在 raw CAS miss 后返回带 raw latest 的 `held`。
- Private visibility gap target 在 raw CAS miss 后执行有界 visible-tail check；证明存在 author-visible unread tail 时返回带 visible latest 的 `held`，无法证明或预算耗尽时通过第二次 raw CAS 分配发送。
- Private-gap `held.latestSeq` 与 `needsRefreshUntilSeq` 只包含已证明 author-visible frontier。
- 同一 agent 在 active turn 中收到 target scope 的新消息时，该消息保持 pending 且不推进 observed；ack-aware accepted pending 不进入 daemon-local continuation inbox，直到 runtime ack 或显式 `read_history` 把该消息投递给 runtime才推进 observed。若 ack-aware delivery 失败 / session mismatch / 不可用，消息回落到 continuation inbox 并自动重试；已 transport accepted 的 pending input 不因 wall-clock timeout 回落。
- Held response 更新 latest-known refresh hint；observed frontier 在 held 后保持原 seenUpToSeq，直到目标消息通过 runtime-visible 输入边界 被看到。
- Held 后直接使用相同 observed frontier 和新 `clientMessageId` 重试会再次获得 `held`。
- 每个 target scope 使用各自的权威 frontier 独立执行 freshness 判断。
- Freshness-capable request 的 `freshness.targetKey` 与 server canonical `targetKey` 匹配时进入 CAS；mismatch 返回 structured validation error。
- Delivery ack sequence 与 target sequence 分别传递；freshness 只使用 target sequence。
- `clientMessageId` 重试返回 `already_sent` 和既有 message id / seq。
- `sent` 产生 `scope_messages` row、search doc、scope fanout outbox、realtime fanout 和 wake side effect。
- `already_sent` 返回既有持久化结果，并保持 external fanout side effects 由原始 `sent` transaction 产生。
- Task discussion send 使用 task discussion scope frontier 执行同一 CAS，并返回 stateful discussion result。
- History tool result 或 automatic continuation delivery 把 target messages 投递给 runtime 后按 high-water 更新 daemon observed frontier；server `/receive` 不属于当前 refresh 接口。Daemon-local continuation inbox 不返回给 agent，也不包含 ack-aware accepted pending runtime input。
- MCP `send_message` held result 会 best-effort 附带同 target daemon-local pending context 并推进 observed frontier；held / partial-held 不返回 structured content，文本提示在已附带 messages 覆盖 gap 时要求读取新消息后按需重试、ack-aware pending runtime input 覆盖 gap 时等待下一个 input boundary、否则要求 exact-target `read_history`，原 held content 不会自动重发。
- 已入队或即将投递的普通 routed message 若满足 `targetSeq <= observedSeq`，daemon 在 channel dispatch、direct/stdin delivery、local inbox dequeue 和 requeue 前 suppress / consume，不唤醒 runtime；`targetSeq > observedSeq` 仍正常投递。
- Search result 只提供 sparse source context，不推进 observed frontier，也不更新 latest-known refresh hint。
- Long content split 成多条 visible message row 时，每条 row 的 accepted seq 串联下一条 row 的 freshness constraint；中间 held 返回 partial result；刷新后首个未接受 chunk 使用 `max(refreshedObservedSeq, lastAcceptedSeq)`。
- Metrics 和 logs 能区分 `sent`、`already_sent`、`held` 和 `partial_held`，并能按低基数维度观测 held rate。
