# Rewind 规范

> Canonical path: `specs/message/rewind.md`
> Related specs: [scope 消息模型](scope-model.md), [delivery-runtime.md](delivery-runtime.md), [reaction.md](reaction.md), [../daemon/daemon.md](../daemon/daemon.md)

Date: 2026-06-28
Status: active-spec
Scope: DM、channel、reply-thread scope 的 rewind 产品行为、约束规则、message lifecycle tombstone，以及 runtime native recovery 边界。

## 1. 产品语义

Rewind 是 AgentTeam 的“回到某条消息发送前”操作。它包含两件事：

1. **runtime 真实恢复**：相关 agent runtime 必须先通过各 runtime 原生 rewind / rollback / revert 能力恢复到 target message 进入 runtime 前；runtime 如果需要 recovery point，就使用 `message_recovery_points.recovery_ref` 保存该 runtime 所需 ref。
2. **timeline rewound tombstone**：runtime 全部恢复成功后，AgentTeam 将 target message 及其后的 suffix 标记为 `rewound` lifecycle tombstone，并 scrub 所有能泄露原文的 message-bound projection / sidecar / durable IM projection。

消息侧不新建 hidden range / audit 表，不物理删除 primary suffix `scope_messages` 行。`scope_messages.metadata.messageRecall.status = "rewound"` 表示该消息被 rewind 隐藏：普通 history、by-id、search、sync、source/share/pin/reaction、attachment、diagnostics、stream replay 与 conversation index 都必须把它当作不存在；`recalled` 仍然是单条撤回占位，不能和 `rewound` 混同。

workspace 文件、memory、外部副作用本轮不由 AgentTeam rewind 默认恢复；若未来某 runtime 提供文件回滚能力，必须作为独立 runtime effect policy 设计，不能隐式混入当前 rewind。

## 2. 支持的 scope

| Scope | 支持 | 说明 |
| --- | --- | --- |
| DM scope | ✓ | DM 主线 |
| Channel scope | ✓ | channel 主时间线 |
| Reply-thread scope | ✓ | reply thread 时间线 |

Rewind 以 `scope_id` 为事务边界。若 suffix 中包含 reply-thread fork，fork 处理遵循 §4。

### 2.1 Scope suffix boundary contract

每次 rewind plan 必须显式描述要 tombstone 的 scope-local suffix boundary：

- **primary boundary**：`workspace_id`、`scope_id`、`target_message_id`、`from_seq`、按 `seq ASC` 排序的 `message_ids`；`message_ids[0]` 必须是 `target_message_id`，`from_seq` 必须是 target message 的 `seq`。
- **cascade reply-thread boundary**：当 primary suffix 的尾部 fork 需要级联处理 reply thread 时，必须显式记录 child `scope_id`、`parent_message_id`、`from_seq = 1`、该 child scope 内按 `seq ASC` 排序的 `message_ids`。

Parent/broadcast scope 只能用于权限、summary/realtime 和用户可见刷新依据；除非它以 boundary 形式出现在 plan 中，否则不能被当成 tombstone scope。Plan 内部必须显式区分 `ScopeID`（primary storage/tombstone scope）、`OperationScopeID`（durable operation serialization scope；DB 兼容列名仍可能是 `thread_id`）、`BroadcastScopeID`（post-commit `conversation:rewound` invalidation scope）、`ReplyThreadScopeID`（reply-thread participant/read-state recompute scope）和 cascade reply-thread `ScopeID`。历史字段名如 `thread_id` 只允许停留在现存 DB/recovery 列名边界；rewind plan struct 不再携带 `ChannelID` / `BroadcastChannelID` / `ReplyThreadID` / `ThreadChannelID` / `ThreadID` 等 alias 字段。任何 DB/wire 输入必须先归一成这些 typed scope roles / `scope_id` boundary，再执行锁定、校验、realtime 和 tombstone。

## 3. 发起约束

- 只有 human 可以发起 rewind；
- target message 必须是发起者自己发送的 human-authored message；
- system / service / app-authored message 不能作为 target；
- target message 必须仍是 visible / non-rewound hot `scope_messages`；
- rewind 不能跨越 cold/archive 边界；
- suffix 中不能包含其他 human 发送的消息；
- 相关 agent 必须有 runtime restore capability 和可用 recovery point（如果该 runtime strategy 需要）。

## 4. Reply thread 规则

Rewind 始终沿一条主线执行。当 suffix 中某条消息存在 reply thread 时：

- 若该 fork message 不是 suffix 的最后一条主线消息，rewind 被阻止；
- 若 target 自身有 reply thread 且 target 是尾部 fork，reply thread scope 作为级联 scope 一起 rewind；
- 级联 reply thread 内同样执行 other-human guard、agent recovery point 检查和 runtime restore；
- 级联完成后，child scope 消息走同样 `rewound` tombstone/scrub，并 archive child scope 使父消息入口消失；不复用 channel/DM 删除路径。

错误语义应明确提示用户先从尾部 thread/message rewind。

## 5. Runtime recovery

Daemon 在可能投递给 rewind-capable runtime 前写入 recovery point。Recovery point 是 runtime restore ref，不是消息审计或 hidden range；它由 runtime strategy 决定形态。

当前 rewind-capable runtime implementations 为 `codex`、`claude`、`opencode`，`builtin` 跟随 OpenCode capability。Runtime ID 与 recovery strategy 名称必须分离，并在 `internal/core/rewindcaps` 中以 runtime 常量和 strategy 常量分别建模：策略名不是 runtime ID，不能通过 `ForRuntime` 或 daemon capability negotiation 被当成可投递 runtime。`claude_session_resume_at` 和 `opencode_session_revert` 是正式 runtime recovery strategies；LAN E2E 是发布前验收门槛，而不是把 capability 声明为 unsupported 的理由。

| Runtime | Strategy | Restore 方式 | 说明 |
| --- | --- | --- | --- |
| `codex` | `codex_app_server_turns` | 原生 app-server `thread/rollback` | 当前启用；recovery ref 为 `{threadId, turnCount}`，`turnCount=0` 覆盖 rewind first |
| `claude` | `claude_session_resume_at` | 原生 `--resume-session-at`；before-first-input 使用 fresh session boundary | recovery ref 保存 `{sessionId, boundaryUuid}` 或 zero boundary 信息；不默认 `--rewind-files`；发布前必须通过 LAN E2E |
| `opencode` | `opencode_session_revert` | 原生 `/session/:id/revert` | recovery ref 保存 `{sessionId, messageId}`；`messageId` 必须和实际 prompt delivery 绑定；AgentTeam-managed OpenCode 强制 context-only 配置，发布前必须通过 LAN E2E 验证 |
| `builtin` | `opencode_session_revert` | 继承 OpenCode capability | builtin 作为 OpenCode alias 处理；同样需要通过 LAN E2E 验收 |

`codex_app_server_turns` recovery ref 只是 `{threadId, turnCount}` 恢复坐标，不拥有独立的 runtime artifact。裁剪对应 `message_recovery_points` 行时，服务端和 Daemon 的 GC 都必须将其视为 runtime no-op；不得启动临时 app-server、interrupt 活跃 turn 或调用 `thread/rollback`。Codex transcript mutation 只属于显式 Rewind restore 流程。

Rewind restore 流程：

1. 构建 target scope suffix；
2. 收集 related agents；
3. 验证 runtime capability、recovery strategy、recovery point 和 agent availability；
4. 验证 first-touch 后无 rewind scope 外活动；
5. 调用 runtime native restore 并 verify；
6. 只有全部 restore 成功后，才进入 DB apply。

Runtime evidence 和 diagnostics 由 [delivery-runtime.md](delivery-runtime.md) 定义。

## 6. ApplyRewind 事务

DB apply 必须在一个事务内完成。

### 6.1 Batch tombstone / suppress suffix facts

对目标 scope suffix 的 message ids，事务内必须处理：

- suppress 未消费的 `scope_activity_events`；
- suppress 未执行或未完成的 `scope_fanout_outbox`；
- clamp `scope_sync_dirty` / `member_scope_sync_dirty` 的 `latest_message_seq` 到 rewind 后可见 scope tail，避免 sync dirty index 继续指向 rewound suffix，同时保留仍未同步的非 suffix message；
- scrub `scope_share_cards`；
- scrub `scope_message_reactions`；
- scrub `scope_message_pins` / `scope_message_top_notices`；
- scrub `scope_message_search_docs`；
- scrub 以 suffix message 为 target 或 source 的 `scope_message_sources`；
- scrub message-bound attachments；
- scrub durable `im_stream_events.payload_json` 和 `im_conversation_index.projection_json`，移除 content、attachments、reactions、mentions、source snippets、delivery previews；
- 将 suffix `scope_messages` 行更新为 `rewound` lifecycle tombstone：`content = ''`，metadata 仅保留 `messageRecall.status = "rewound"` 与 `rewoundAt`；
- 删除已被消费完的 suffix `message_recovery_points`，避免未来 plan 使用 tombstoned message 的 restore ref。

Read/search/source/share-card action state 必须先于 transcript `scope_messages` tombstone；`scope_messages` 是 read-visibility tombstone 链路的最后 authority。Task discussion attachments、unclaimed uploads、非 suffix message attachments 不受 scope message rewind 影响。

#### 6.1.1 Projection impact / read-surface boundary

Rewind 的 projection/read-surface contract 分两层：

- **projection-backed surfaces**：`scope_share_cards`、`scope_message_reactions`、`scope_message_pins`、`scope_message_top_notices`、`scope_message_search_docs`、`scope_message_sources`、attachments 必须按显式 tombstone scrub contract 清理对应 suffix rows；这些 projection rows 只是派生读模型，不能作为 transcript 或 rewound-message visibility authority。
- **transcript-authority surfaces**：history/read、message sync、attachment read-check/download、root/source context 和 diagnostics preview 必须以 readable `scope_messages` row + lifecycle metadata 作为最终 authority；即使某个历史 projection、runtime evidence、activity/outbox evidence 或旧 cursor 仍引用 rewound message id，也不能把 rewound tombstone 解释成可见消息。

Projection/read-surface contract 只声明 rewind 后哪些 projection-backed 读面需要失效或重建，以及哪些 transcript-authority 读面必须重新读取 `scope_messages` 并应用 `MessageNotRewoundSQL` / lifecycle gate；它不是 message-sync tombstone payload，也不能让客户端凭 stale projection 局部补丁继续消费 rewound suffix。

Delivery diagnostics / diagnostics preview 的 live message authority 必须来自 readable `scope_messages`。诊断查询必须先命中 readable transcript row 再读取 scope/agent 状态。Rewind 后即使仍存在已 suppress 或已完成的 runtime、activity、outbox、delivery historical evidence，也不得从这些 durable evidence 反向复活 rewound suffix；rewound transcript row 必须返回 not found / invisible。

### 6.2 Recompute / clamp

Tombstone 后必须重算：

- `scopes.latest_seq` / `scopes.latest_global_seq` 作为物理 high-water，保留现有最大 seq/global seq，避免新消息复用旧 suffix seq；
- `scopes.latest_message_at`、`visible_message_count`、`visible_last_message_at` 只基于 human-visible / non-rewound 消息；
- reply-thread summary（若涉及）；
- scope-level read state。

Read-state clamp 必须覆盖：

- `scope_memberships.last_read_seq`；
- `scope_memberships.last_read_global_seq`；
- `scope_memberships.owner_context_dismissed_through_global_seq`；
- read cursor 相关派生状态；
- browser message-sync dirty index。

任何 cursor 或 sync dirty frontier 不得停留在 rewound suffix 的 visible tail 之后。

### 6.3 Reply-thread cascade

若级联 reply thread scope，事务内还必须：

- 对 child scope 从 seq=1 执行同样 `rewound` tombstone/scrub；
- suppress child scope delivery/search/reaction/runtime/share-card state facts；
- archive child scope，使父消息的 reply-thread 入口消失；
- 重算 parent message / parent scope summary。

Channel/DM 删除仍可物理删除其子 reply thread；conversation rewind 不走该删除路径。

### 6.4 Operation lifecycle

`rewind_operations.status` 的稳定状态集合为：

| Status | 含义 |
| --- | --- |
| `preparing` | plan 已持久化，runtime restore / apply 尚未完成；这是唯一会阻止同一 primary `scope_id` 再次开始 rewind 的 active 状态。 |
| `completed` | runtime restore 与 DB apply 均已成功。 |
| `failed` | restore 前或无 restore 场景下失败，DB suffix 未被 tombstone。 |
| `repair_required` | runtime restore 已发生但 DB apply 或后续步骤失败，需要人工诊断恢复。 |

`rewind_operations.thread_id` 是历史列名；scope cutover 后其值语义是 primary rewind `scope_id`。实现不得把 broadcast/parent scope 写入该字段，也不得用错误类型或空字段 fallback 猜测 operation 归属。

## 7. Concurrency

- 同一 scope 同时只能有一个 active rewind operation；
- apply 前必须重新验证 suffix 没有漂移；
- 事务应锁定目标 scope stats/read-state 相关 rows；
- apply 失败时必须保留可诊断的 operation status，不允许半 tombstone 后宣称成功。

## 8. Realtime / API behavior

- 成功后发送 `conversation:rewound`；
- `conversation:rewound` 是 post-commit invalidation event，不是 message append、reaction activity、runtime event 或 outbox delivery；
- `conversation:rewound` payload 只提供 invalidation authority；它不能携带 rewound suffix payload、frontend tombstone patch 或 per-surface replacement facts。
- 成功后还必须在 primary rewind scope 追加一条 human-visible `system` message，提示“发起人回滚了会话”；该 notice 是新的消息行和 `message.created` realtime event，用稳定 `rewind-notice:<operationID>` client message id 幂等写入，不得复用或暴露被 rewind 的 suffix 原文。该 notice 是 intentional projection-only exception：它不进入 ordinary unread、user notification 或 Agent wake；若 direct realtime 丢失，通过 rewind invalidation 后的 conversation/history refetch 恢复。
- API response 可返回 `rewoundMessageIds` 作为 coarse invalidation/debug 信息，但不能返回被 rewound 的 message body、attachments、source snippets 或 per-surface replacement facts。
- 客户端刷新当前 scope、thread summary、unread counts、search result cache、message-sync cursor state 和 relevant task/share-card state。
- Browser 客户端 recovery 顺序必须显式：先推进本地 message-sync cursor generation，并废弃 outstanding websocket `sync:resume:response` generation；再清空内存和持久化 opaque message-sync cursor；然后 invalidate/refetch conversation、summary、unread、search/share/task projection caches；最后用新 generation bootstrap fresh `syncGap`。
- browser 客户端必须把持久化 opaque message-sync cursor 当成 rewind boundary 重建，避免 post-rewind physical seq high-water 和 visible tail 不一致时被旧高水位 cursor 跳过；旧 HTTP `syncGap` / `syncMessages` response 和 websocket `sync:resume:response` 必须受 cursor reset generation/ignore 保护，不得在 rewind 后恢复旧高水位 cursor、merge 旧 payload，或继续 `hasMore` resume 链。
- 任何 stale HTTP / websocket sync response 必须在持久化 cursor、merge payload、或发起 `hasMore` follow-up resume 前 return，并排队一次 fresh-generation sync。
- message sync 不返回 tombstone page；被 rewind 的 suffix 通过 lifecycle metadata、projection scrub、dirty index clamp 和 realtime invalidation 共同保证不可见。
- 失败时返回具体错误，区分权限、other-human guard、reply-thread fork、hot/cold boundary、runtime recovery、concurrent modification。

## 9. 错误语义

| 错误 | 含义 |
| --- | --- |
| `message must be your own human-authored message` | target 不是当前 human 自己发送的 human message |
| `system messages cannot be rewound` | target 是 system/service/app message |
| `rewind would cross the hot/cold boundary` | target 不在 live hot scope history |
| `rewind is blocked because another human sent a later message` | suffix 或级联 scope 中有其他 human message |
| `rewind is blocked because a later message has a reply thread` | 需要先 rewind 更尾部 fork |
| `rewind is blocked because a related agent does not support rewind` | runtime 不支持 |
| `rewind is blocked because a required recovery point is missing` | recovery point 缺失或 strategy 不匹配 |
| `rewind is blocked because a related agent is no longer available for recovery` | agent/machine 已删除 |
| `rewind is blocked because a related agent touched another conversation after first participation` | cross-scope activity |
| `rewind is blocked because newer activity changed the conversation` | apply 前 suffix 漂移 |
| `another rewind is already in progress` | scope 并发 rewind |

## 10. 不变量

- Rewind 是唯一“回到某条消息发送前”的服务端 suffix 操作；
- Rewind 不物理删除 primary suffix `scope_messages` 行，而是写入 `rewound` lifecycle tombstone；
- Rewind 不改变 scope membership 或 private grants；
- Rewind 不创建 non-message activity event、runtime event、message mirror 或 durable fanout row；
- Rewind tombstone/suppress suffix 派生状态后，读路径不得再看到 suffix；
- Rewind 后所有 read/dismiss cursors 不得超过 rewind 后 visible tail；
- Recovery point 生命周期由 runtime/GC 控制，rewind 只消费它们。
