# v1.5 韧性:durable 消息 · 任务转派 · 执行者离线决策 — 设计规格

> 本文档定义 v1.5 的实现规格,基于对 dsh-agent-teams 的借鉴(用户拍板:借鉴 durable 消息与转派/接管,不借鉴 sub-agent 架构)。
> 后端由宿主开发者实施;前端如需配合由外包实施(契约在本文件)。

---

## 1. 范围与决策

| 借鉴项 | 决策 |
|---|---|
| durable mailbox | ✅ 采纳为 **durable send_note**:离线消息入队,上线补投(不做验收/回执) |
| reassign / takeover | ✅ 采纳为 **reassign_task**:重投 + 更换执行者/验收者(仅 initiator) |
| stranded recovery | ✅ 采纳为 **offlineGrace**:执行者离线超时 → 主动通知 initiator 决策(不自动 fail,超时兜底保留) |
| sub-agent 成员 | ❌ 不借鉴:不适用子代理架构,执行者保持工作区 peer 会话 |
| 船长制/团队容器 | ❌ 不借鉴 |
| 文件目录状态 | ❌ 不借鉴(storage domain 更稳) |

---

## 2. Durable send_note(离线入队 + 上线补投)

### 2.1 语义

- `send_note` 目标离线时,消息**不再直接拒绝**,而是写入 `pending_messages` 表(durable)。
- 补投 sweep(60s,与 DAG sweep 同周期):目标会话 live 时投递并删除。
- 消息保留**原发送时间戳**,补投文本附加「延迟送达」标记(原时间),接收方知道这是旧消息。
- 返回:`{ delivered: true }`(即时)或 `{ delivered: false, queued: true }`(离线入队)。
- 队列上限:每发送方 ≤ 50 条 pending(超出拒绝并报错);pending 消息不占任务队列槽位。

### 2.2 数据模型(schema v10)

```
pending_messages 表:
  id: string          // uuid
  sender: sessionId
  recipient: sessionId
  content: string     // 已过 admitContent
  sentAt: string      // 原始发送时间
  createdAt: string   // 入队时间
```

- domain version v9 → v10(备份 → 改版本号,存量行兼容)。
- 消息投递仍走 `buildMessageMessage`(header 带 `delayed="true"` 标记 + 原时间)。

### 2.3 工具契约

```
send_note(target, content)
→ { delivered: true }                    // 即时送达
→ { delivered: false, queued: true }     // 离线入队,上线补投
→ 报错:限流 / 非 peer / 内容超限 / 队列满(>50)
```

### 2.4 边界

- 补投只发一次:投递成功即删;若投递时又离线,消息**回到队列尾部**重试(上限 3 次,超限丢弃并通知发送方)。
- 无验收/回执:接收方回复仍是自由 prose(与 v1.3 语义一致)。
- 消息渠道依旧**不进 DAG、不进任务面板**。

---

## 3. reassign_task(重投 + 更换执行者/验收者)

### 3.1 语义

```
reassign_task(task_id, new_executor?, new_reviewer?)
```

- 权限:仅 initiator(assignedBy)。
- 适用状态:queued / submitted / working / input-required(**未结算均可转派**);completed(已结算)拒绝。
- `new_executor` 与 `new_reviewer` 至少提供一个;新执行者须 live peer 且 ≠ 自己(除非新验收者独立,复用 create_task 的 self 规则)。
- **换执行者 = 重投递**:任务重新投递给新执行者(投递 header `tool="reassign_task"`),`messageId` 更新,`tokensAtStart` 刷新。
- **换验收者**:更新 `assignedReviewer`(权限随之转移;原验收者不再能 settle——authorizeSettlement 按当前行校验,天然生效)。
- 状态处理:
  - queued → 直接改归属(未投递,无重投)。
  - submitted/working/input-required → 归零为 submitted + 新投递;旧执行者的 in-flight 处理会被状态机拒绝(它已不是 assignedTo)。
- **通知**:
  - 新执行者:任务投递消息(带转派说明)。
  - 旧执行者(若 working):「任务已转派给 X」通知(打断由新投递自然覆盖,旧 attempt 过期)。
  - initiator:转派回执(新归属)。

### 3.2 与现有机制的关系

- 转派 ≠ cancel 重建:任务 id、历史、依赖、验收标准全部保留。
- 依赖/流程:转派不改变 `flowId`/`dependencies`——DAG 拓扑不变,只是执行者/验收者变化。
- 事件:emit `reassigned`(客户端调度器无需动作,DAG 视图刷新人员标签)。

---

## 4. offlineGrace(执行者离线主动决策)

### 4.1 语义

- working 任务 + 执行者离线(agents.get undefined)持续超过 `offlineGraceMs`(默认 15 分钟)→ 通知 initiator:
  「任务 X 的执行方已离线超过 15 分钟。请决策:reassign_task 转派,或 cancel_task 取消,或等待其返回。」
- **不自动 fail**(离线 ≠ 失败,执行方可能稍后返回);2h 超时兜底保留。
- 通知冷却:每任务 15 分钟一次(复用 reminder 冷却模式,基于 updatedAt 防重启重放)。
- 执行者重新上线 → 通知自动停止(检查条件不满足)。

### 4.2 配置

```
offlineGraceMs?: number   // 默认 900_000(15 分钟)
```

---

## 5. 工具面汇总(变更)

| 工具 | 变更 |
|---|---|
| `send_note` | 返回结构扩展(queued 标记);描述补充离线入队语义 |
| `reassign_task` | **新增**:重投 + 换执行者/验收者 |
| `DeliverySource` | 新增 `reassign_task`(转派投递 header) |
| 其余 | 不变 |

工具清单:`list_peers, send_note, create_flow, create_task, edit_task, reassign_task, submit_handoff, list_flows, list_tasks, get_task, report_task, settle_task, cancel_task, request_input, update_card`

---

## 6. 验收清单

- [ ] send_note 目标离线 → `queued: true`,消息落 pending_messages;目标上线后 60s 内补投,header 带延迟标记。
- [ ] 补投重试上限 3 次,超限丢弃并通知发送方;发送方队列 >50 拒绝。
- [ ] reassign_task:仅 initiator;换执行者 → 重投递(新 header)+ 旧执行者报告被状态机拒绝;换验收者 → 权限即时转移;completed 拒绝。
- [ ] reassign 后依赖/流程/验收标准不变,DAG 拓扑不变。
- [ ] offlineGrace:执行者离线超 15 分钟 → initiator 收到决策通知(冷却防轰炸);上线后停止;不自动 fail;超时兜底仍生效。
- [ ] 全量单测通过,构建通过,重启验证。
