# Mail 失败通知设计

> 版本：v1.3 | 日期：2026-06-07 | 作者：庞统 | 状态：待确认

## 1. 背景

当前 Mail 失败后没有通知发件人。F2 修复让所有 `_mark_task("failed")` 统一 @pangtong-fujunshi，但这对 Mail 不合理：

- Mail 是 A→B 点对点通信，失败应通知发件人 A，不是庞统
- **Mail 唯一特有的失败——no_reply_found（幻觉门控，Agent 没回复 request）——没有任何通知**

### 失败场景分类

| 触发点 | 场景 | 当前通知 | 应该通知谁 |
|--------|------|---------|-----------|
| `_mark_task("failed")` | spawn 级失败：crashed、auth_failed、crash_limit、timeout 等 | @pangtong（F2） | 发件人（不再 @pangtong） |
| `_mail_auto_complete` no_reply_found | Agent 正常退出但没回复 request 类邮件 | 无 | 发件人 |

**所有 Mail failed 都通知发件人**，不管 reason。

## 2. 设计方案

### 2.1 核心机制

Mail 最终标 failed 时，daemon 以 **系统发件人（`system`）** 身份给 from_agent 发一封 inform 邮件，告知原因和处理建议。

### 2.2 系统发件人

新增 `system` 作为 Mail 的特殊发件人：

- **不暴露给外部 API**：`system` 只允许 daemon 内部调用
- **实现方式**：`mail_notify.py` 直接调用 `send_mail` 的内部函数（`_do_send_mail`），不走 FastAPI HTTP 路由
- `system` 不是真实 Agent，没有 main session，只用于发送通知
- 防递归：通知邮件的 must_haves 中加 `"system_notify": true`，触发前检查该标记

### 2.3 判断 _mail 项目

`_mark_task` 签名只有 `(db_path, task_id, status, detail)`，没有 project_id。从 db_path 推断：

```python
def _is_mail_project(db_path: Path) -> bool:
    path_str = str(db_path)
    return "/_mail/" in path_str or path_str.endswith("_mail.db")
```

覆盖两种 _mail db_path：`data/_mail/blackboard.db`（新）和 `data/_mail.db`（旧）。

### 2.4 触发点

**触发点 1**：`_mail_auto_complete` 中 no_reply_found 标 failed 之后（dispatcher.py）

- 位置：标 failed 成功后、return 之前
- 调用：`notify_mail_failed(db_path, task_id, "no_reply_found")`

**触发点 2**：`_mark_task` 中 status=="failed" 且是 _mail 项目时（spawner.py）

- 位置：现有 F2 @pangtong 逻辑处
- **改动**：当 `_is_mail_project(db_path)` 时，**不 @pangtong**，改为调用 `notify_mail_failed`
- 当不是 _mail 项目时，F2 @pangtong 逻辑不变

### 2.5 实现位置

新增 `src/daemon/mail_notify.py`，统一函数 `notify_mail_failed`：

```python
def notify_mail_failed(db_path: Path, original_mail_id: str, reason: str, detail: dict = None):
    """Mail 失败后以 system 身份给发件人发通知邮件
    
    直接调用 _do_send_mail 内部函数，不走 HTTP。
    防递归：检查原邮件 must_haves.system_notify，为 true 则跳过。
    """
```

### 2.6 通知邮件内容

| 字段 | 值 |
|------|------|
| from | `"system"` |
| to | from_agent（原邮件发件人） |
| type | `"inform"` |
| title | `"[投递失败] {原邮件title}"` |
| must_haves.system_notify | `true`（防递归标记） |
| must_haves.in_reply_to | 原邮件 task_id |

通知正文使用**统一模板**，把所有可能的失败原因和建议放在一封邮件里：

```
你的邮件投递失败了。

📧 原始邮件：「{title}」
👤 收件人：{to_agent}
❌ 失败原因：{reason}

常见失败原因及处理建议：
• no_reply_found：收件人未回复。建议重发邮件，或通过黑板任务方式联系
• auth_failed：收件人认证失败。需检查 Agent 配置，联系姜维(jiangwei-infra)排查
• crash_limit：收件人处理时多次崩溃。系统异常，建议稍后重试
• task_timeout：处理超时。建议重发或通过其他方式联系
• 其他原因：建议联系副军师(pangtong-fujunshi)排查

——系统自动通知
```

实现时 `{reason}` 替换为实际 reason 字符串，其余排查指引固定不变。
| 其他 | "邮件「{title}」投递失败（原因：{reason}）" | "建议联系副军师(pangtong-fujunshi)排查" |

### 2.7 防递归

系统通知邮件本身也可能失败（spawn 崩溃等），必须防止无限递归：

```python
# 防递归：系统通知邮件失败不再发通知
meta = json.loads(original_task.must_haves) if original_task.must_haves else {}
if meta.get("system_notify"):
    return  # 系统邮件失败，不再递归通知
```

递归链验证：
1. 原邮件 crash → `_mark_task(failed)` → `_is_mail_project=true` → 不 @pangtong → `notify_mail_failed`
2. 通知邮件(from=system)也 crash → `_mark_task(failed)` → `_is_mail_project=true` → `notify_mail_failed` → 检查 `system_notify=true` → 跳过 ✅

### 2.8 from_agent 获取

原 Mail 的发件人存在两个等价位置：
- `task.assigned_by`
- `must_haves` JSON 的 `"from"` 字段

优先用 `assigned_by`（无需解析 JSON），fallback 解析 must_haves。

## 3. 改动清单

### 3.1 新增文件

| 文件 | 内容 | 预估行数 |
|------|------|---------|
| `src/daemon/mail_notify.py` | `notify_mail_failed` + `_is_mail_project` | ~65 行 |

### 3.2 修改文件

| 文件 | 改动 | 预估行数 |
|------|------|---------|
| `src/daemon/dispatcher.py` | `_mail_auto_complete` 标 failed 后调 notify | ~5 行 |
| `src/daemon/spawner.py` | `_mark_task` failed 分支：_mail 项目不 @pangtong，改调 notify | ~10 行 |
| `src/api/mail_routes.py` | 拆出 `_do_send_mail` 内部函数供 daemon 调用，valid_agents 校验对内部调用跳过 from 检查 | ~15 行 |

总计约 95 行，4 个文件。

### 3.3 不改的

| 项目 | 原因 |
|------|------|
| F2 @pangtong 对 Task 的逻辑 | Task failed 仍 @pangtong，只对 Mail 不适用 |
| no_reply_found 的标 failed 逻辑 | 只在标 failed 之后加通知，不改判定逻辑 |
| inform 类型邮件的完成逻辑 | inform 直接标 done，不存在 no_reply_found 场景 |
| 外部 API 的 from 校验 | system 不走 HTTP 路由，外部无法伪造 |

## 4. 验证方式

| 场景 | 验证 | 预期 |
|------|------|------|
| request 邮件无回复 | Agent 不回复 → _mail_auto_complete 标 failed | 发件人收到 system 通知邮件 |
| spawn 崩溃导致 Mail failed | Agent spawn crash → _mark_task failed | 发件人收到 system 通知，**不** @pangtong |
| Task failed | Task spawn crash → _mark_task failed | @pangtong（F2 不变） |
| 系统通知邮件本身失败 | 通知邮件也 crash | 不再递归（system_notify=true） |
| inform 邮件正常完成 | Agent 收到 inform → 直接 done | 不触发通知 |
| inform 邮件 crash | inform 的 spawn 崩溃 | 发件人收到 system 通知（crash 也通知） |
