> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Task Pipeline Deliverable (Codex)

> **Historical design input（2026-07-23 校准）**：本文保留 Task Orchestrator、0 / 1 / N decisions、`create_closed`、Activity/Outcome 分离等 Task V2 依据，但不再单独作为实施规格。当前路线以 [Task V2+ 工程审计与实施基线](/product-design/v2/tasks-feature/task-v2-plus-engineering-audit.md) 为准；V3 内容仍是待评估 proposal。
>
> **用途**: Codex 独立设计产出,避免和并行 AI 的 `task-lifecycle-gap-analysis.md` / `task-lifecycle-gap-analysis-claude-code.md` 修改冲突。
> **日期**: 2026-05-30
> **范围**: system design only。不改代码、不写 DB、不做 rollout、不设计完整 SMS pipeline。
> **设计口径**: 这里写 **final / long-term-proof target architecture**。实现顺序可以另写 implementation plan,但概念模型先按最终态说清楚。
> **Review basis**: 当前 task pipeline redesign 以这份 Codex 文档作为 review basis。旧 V2 task docs 已移动到 `archive/`,避免和新模型混读。
> **2026-05-31 补充**: 全系统 pipeline 调研后，Task Orchestrator 应视为 unified architecture 的 shared mutation module，而不是 task feature 内部 helper。见 [Task Pipeline Architecture Addendum](/product-design/v2/tasks-feature/design/research/task-pipeline-architecture-addendum.md)。

## How to Read This Doc

这份文档按"先最终 contract,再解释为什么"组织。读者不用先理解所有历史问题,也能先看到最后系统应该长什么样。

| 顺序                                 | 内容                                                                                           | 目的              |
| ---------------------------------- | -------------------------------------------------------------------------------------------- | --------------- |
| Part 1: Final Design Contract      | Executive Summary, object model, workflow, schema, write path, API contract, prompt contract | 直接回答"最终要建什么"    |
| Part 2: Operating Semantics        | Code vs Prompt, UI semantics, scenario coverage, metrics                                     | 说明这些对象在真实产品里怎么用 |
| Part 3: Implementation + Rationale | implementation touchpoints, deep dives, competitor calibration                               | 保留全部设计依据和后续改动清单 |

核心阅读路径:

```text
API contract -> schema -> workflow -> prompt contract
```

原因是 prompt 不应该先被设计出来。prompt 的输入/输出必须由稳定的 task API、schema 和 state machine 反推,否则会变成"AI 想怎么说就怎么说",代码和 UI 很难长期维护。

## Executive Summary

**结论**: Task 是一个统一的 **Work Object / 业务事项对象**。它有二元 lifecycle: `open / closed`;中间执行过程写入 progress log;最终业务结果写入 close outcome。

| 最终决定          | 设计                                                                                                |
| ------------- | ------------------------------------------------------------------------------------------------- |
| Task 是什么      | 一个 customer-work objective,既能是待办,也能是当场完成后的结果记录                                                    |
| Lifecycle     | schema + code + UI 同步用 `open / closed`(测试环境直接迁移,prod 无压力。原 DB `pending` 一次性 `UPDATE → 'open'`)    |
| Progress      | `No Answer`, `Left Voicemail`, `Text Sent`, `Callback Requested` 是 progress,不关闭 task              |
| Close outcome | `booked`, `converted`, `cancel_saved`, `issue_resolved`, `not_interested`, `do_not_contact` 是最终结果 |
| Orchestrator  | 所有 task mutation 统一经过 Task Orchestrator + deterministic state machine                             |
| AI vs Code    | AI 判断语义;代码负责触发、校验、状态迁移、幂等、并发、审计                                                                   |
| API-first     | 数据模型要服务前端、dashboard、OAuth API、CLI、第三方系统和 AI agent                                                 |

### 一句话模型

```text
Task lifecycle:       open / closed
Task progress log:    no_answer / left_voicemail / text_sent / callback_requested / ...
Business outcome:     booked / converted / cancel_saved / issue_resolved / do_not_contact / ...
```

### 为什么用 `open / closed`,不是 `pending / closed`

**结论**: schema + code + UI 同步改名 `open / closed`。测试环境无 prod migration 压力,一次性改干净,不保留 `pending` 映射。

| 命名        | 问题 / 优点                                                                       |
| --------- | ----------------------------------------------------------------------------- |
| `pending` | 听起来像"还没开始/等待中"。但一个 task 可能已经打了 3 次、发了 SMS、留了 voicemail,仍然未完成。此时叫 pending 不够准确 |
| `open`    | 表示"这个事项还没有结束",不关心是否已经尝试过。更适合 API、UI、AI agent 和 manager metrics                |
| `closed`  | 表示 objective 已终止,可能是成功、失败、DNC、wrong number,不等于 business win                   |

实施:

```text
schema/code/API/UI: tasks.status = open | closed (DEFAULT 'open')
Migration: UPDATE tasks SET status='open' WHERE status='pending'
Sweep: 全 repo grep 'pending' 替换为 'open'
       (4 Lambda + studio-api raw SQL + CHECK constraints
        + partial unique index WHERE status='pending')
```

### Current System Context

**结论**: task pipeline 不是单点功能,它处在 per-call AI、contact analyzer、lead pipeline、staff UI/API 和 manager metrics 中间。最终设计必须覆盖所有入口。

| 上下游                   | 它做什么                                             | 现在怎么影响 task                                                                                      | 最终应该怎么影响 task                                                                                    |
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| RingCentral ingress   | 接收电话/SMS webhook,把原始事件送进转录、消息存储和后续分析             | 不直接写 task。它只是把"发生了一通电话/一条短信"带进系统                                                                 | 继续不直接写 task;只提供 activity evidence 给后面的 orchestrator                                              |
| Per-call AI           | 分析单通电话:是否打通、有没有业务结果、是否需要后续跟进                     | 产出 `followUpNeeded`, `primaryOutcomeResult`, `callState` 等 call facts,后续 Contact Analyzer 会读这些信号 | 仍然只写 call facts,不直接 create/close task。它的输出是 task decision 的证据,不是最终 task mutation                 |
| Contact Analyzer      | 聚合一个 contact 的 calls/messages/leads/tasks,做跨互动判断 | 当前 prompt 直接输出 `taskDecisions[]`,代码按 create/close/update 执行;当场完成常被跳过                             | 最终只输出 semantic proposal,例如 `create_closed`, `record_progress`, `close`;由 Task Orchestrator 校验并执行 |
| Lead pipeline         | 新 lead 到达后快速创建 outreach 待办                       | 直接插入 `lead_outreach` open task,绕过 AI,但有 `onConflictDoNothing` 防重复                                | 仍可 deterministic create,但应该走统一 task mutation contract,避免和 AI/员工路径写法不一致                           |
| Staff API/UI          | 员工在 Tasks 页面记录进展或关闭任务                            | 当前 UI 看起来有 Status Update,但后端只有 close;`no_answer` / `left_voicemail` 被误写成 closeResult 并触发关旧建新     | `Progress Update` 写 `task_progress_events`;`Close Task` 只写最终 business outcome                    |
| Dashboard metrics     | 店长看前台/AI agent 完成了多少事项、多少转化/挽留                   | 当前容易把 close task 当 productivity,并被 `closedByStaffName` 过滤污染                                      | 分开统计 completed objectives、business wins、attempt workload,不要把 no-answer 计成完成                      |
| Future API / AI agent | 外部系统、CLI、OAuth client 或 AI agent 读取/操作 task      | 现在没有稳定 API contract,外部调用方会被迫理解 task 表和 timeline 细节                                               | 提供 typed work-object API,让 agent 一次读懂 task 当前状态、进展、下一步和最终结果                                      |

***

## 1. Final Object Model

**结论**: 最终模型分 4 层,每层回答不同问题。不要把它们压进一个字段。

| 层         | 回答的问题              | 存储位置                   | 示例                                                               |
| --------- | ------------------ | ---------------------- | ---------------------------------------------------------------- |
| Activity  | 客观发生了什么互动?         | `calls`, `messages`    | inbound call, outbound call, SMS, voicemail                      |
| Progress  | 这个 task 处理过程中做了什么? | `task_progress_events` | `no_answer`, `left_voicemail`, `text_sent`, `callback_requested` |
| Lifecycle | 这个业务事项还开着吗?        | `tasks.status`         | `open`, `closed`                                                 |
| Outcome   | 这个事项最终业务结果是什么?     | `tasks.closeResult`    | `converted`, `booked`, `cancel_saved`, `issue_resolved`          |

### Task 的边界

| 场景                 | 是否是 task | Lifecycle       | 说明                                      |
| ------------------ | -------: | --------------- | --------------------------------------- |
| 新 lead 需要联系        |        是 | `open`          | 前台待办                                    |
| lead 没接电话          | 不是新 task | 原 task 仍 `open` | 追加 progress                             |
| lead 当场 book intro |        是 | `closed`        | closeResult = `booked`                  |
| inbound 当场成交       |        是 | `closed`        | create-closed,closeResult = `converted` |
| 会员投诉当场解决           |        是 | `closed`        | closeResult = `issue_resolved`          |
| 客户问营业时间,员工已答       |        否 | —               | 普通 service activity,没有持续 objective      |

***

## 2. Final Workflow

**结论**: 目标状态下,所有 task 写操作都经过 Task Orchestrator;AI 只给 proposal,代码执行 state transition。

### Desired Workflow: 每一步在做什么

这张图的重点不是"多一个复杂系统",而是把 task 写入拆成清楚的责任链:入口先过滤,AI 只判断语义,代码最后安全写库。

| 步骤                   | 谁负责                           | 做什么                                                                               | 为什么需要这一步                                           |
| -------------------- | ----------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- |
| 1. 输入事件进入            | 系统事件源                         | 电话结束、SMS 到达、新 lead、员工在 UI 操作                                                      | task 可能从多条入口产生,不能只设计电话或只设计前端                       |
| 2. 触发与过滤层            | 代码                            | 去重、合并短时间内多条 SMS、过滤 `thanks/ok`、exact `STOP` 直接 DNC、判断是否值得调用 AI                    | 控制成本和噪音;简单确定的事不需要大模型判断                             |
| 3. Task Orchestrator | 代码里的统一 task 写入服务              | 统一接收 lead pipeline、contact analyzer、staff API、future AI agent 的 task mutation 请求  | 避免每条路径各自直接改 task,导致语义不一致                           |
| 4. 读取业务上下文           | Orchestrator / repository     | 读取同一个 store 下的 contact、open tasks、recent calls/messages、progress history、timeline | AI 和代码都必须基于同一个上下文判断,不能跨 store 或漏掉已有 open task      |
| 5. AI 语义判断           | Prompt / model                | 只提出 proposal: 建什么目标、记录什么进展、是否完成、建议下一步                                             | 语义判断需要读懂 transcript/SMS/contact history,但 AI 不直接写库 |
| 6. 代码执行层             | Orchestrator state transition | 校验 proposal、检查 taskId/store/status/DNC/enum/idempotency,然后在 transaction 里写库       | 真正改变系统状态必须 deterministic、可审计、能处理并发                 |
| 7. 写入结果              | DB transaction                | 写 `tasks`, `task_progress_events`, `contact_timeline`                             | 让 task 当前状态、执行历史、contact 时间线同时一致                   |
| 8. 下游消费              | UI / metrics / API / playbook | 前台看 open tasks,店长看 completed objectives 和 attempts,agent 读 typed work object      | 同一份数据服务不同使用者,不用各自推断                                |

SMS 在这个 workflow 里的位置:SMS 先作为 `messages` activity 存下来,再进入"触发与过滤层"。`thanks/ok/emoji` 这类 trivial message 不触发 AI;`STOP` 这类确定 DNC 由代码直接处理;购买、取消、投诉、预约、回流、DNC 自然语言表达等 meaningful SMS 才触发 Task Orchestrator。也就是说,SMS 不单独发明一套 task 系统,但它有自己的轻量触发规则。

```mermaid
flowchart TD
  A["输入: 电话/SMS/lead/员工操作"] --> B["过滤: 去重、合并、过滤trivial"]
  B --> C["Task Orchestrator"]
  C --> D["读上下文: contact + open tasks"]
  D --> E["AI: 提 proposal"]
  E --> F["代码: 校验 → 写入 → 幂等"]

  F --> G["创建待办 task"]
  F --> H["记录执行进展"]
  F --> I["更新优先级/dueAt"]
  F --> J["关闭已有 task"]
  F --> K["创建已完成 task"]

  G --> L[("写入: tasks + events + timeline")]
  H --> L
  I --> L
  J --> L
  K --> L

  L --> M["前台 Tasks: open = 待办"]
  L --> N["Manager Metrics: objectives vs workload"]
  L --> O["API / AI Agent: typed work objects"]
  L --> P["Playbook: 执行指南生成"]

  style C fill:#dbeafe,stroke:#2563eb
  style E fill:#fef3c7,stroke:#d97706
  style F fill:#dcfce7,stroke:#16a34a
```

### 现有 Workflow 及问题点

这张图描述的是**当前系统怎么跑,以及为什么这个 flow 会让 task 语义混乱**。它不是 final design。

| 步骤                                     | 现在怎么跑                                                                | 问题在哪里                                                    |
| -------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------- |
| 1. 电话/SMS/lead 进入系统                    | 电话会进入 per-call AI;SMS 当前主要先存储;lead pipeline 可直接建 task                | 入口很多,但 task 写入规则不统一                                      |
| 2. 单通电话 AI 写 call facts                | per-call AI 判断是否打通、是否成功、是否需要后续跟进                                     | 这些 call facts 是证据,但后续 prompt/代码会把它们和 task outcome 混起来    |
| 3. Contact Analyzer 聚合判断               | prompt 看 contact 历史,输出 create/close/update task                      | prompt 当前没有 `create_closed` 和 `record_progress` 这种清晰动作   |
| 4. 代码直接写 `tasks`                       | repository 按 prompt 输出 create/close/update;staff close API 也直接写 task | 缺少统一 Orchestrator,不同入口行为不一致                              |
| 5. 前台 UI 分成 Status Update 和 Close Task | UI 心智其实是"记录进展" vs "关闭事项"                                             | 后端没有真正的 progress update,所以 no-answer/voicemail 被塞进 close |
| 6. 当场完成的事                              | 单通电话 AI 可能判断"业务已完成 + 不需要跟进"                                          | Contact Analyzer 当前规则可能不建 task,导致 completed work 漏记      |

```mermaid
flowchart TD
  A["电话 / SMS / 新 lead 进入系统"] --> B["单通电话 AI: 写 call facts — 打通? 结果? 需跟进?"]
  B --> C["Contact Analyzer: 聚合 contact 历史, 输出 create/close/update"]
  C --> D["执行代码: 按 prompt 结果直接写 tasks"]
  D --> E[("tasks 表: lifecycle + progress + outcome 混在一起")]
  E --> F["前台 Tasks 页面"]
  F --> G["Status Update: No Answer / Voicemail"]
  F --> H["Close Task: Converted / DNC / Other"]
  G --> I["⚠ 问题1: No Answer / Voicemail 被当成 closeResult"]
  I --> J["⚠ 问题2: close.ts 先关旧再建新, 同一事项拆成多个 task"]
  C -. "AI 判断业务已完成 + 不需跟进" .-> K["⚠ 问题3: 不创建 task"]
  K --> Q["结果: 当场办成的 work, manager 看不到"]

  style I fill:#fee2e2,stroke:#dc2626
  style J fill:#fee2e2,stroke:#dc2626
  style K fill:#fee2e2,stroke:#dc2626
  style Q fill:#fee2e2,stroke:#dc2626
```

图里的"业务已完成 + 不需要后续跟进"对应当前代码/prompt 信号里的 `followUpNeeded = false` 和 `primaryOutcomeResult = success`。文档正文避免使用 `fu=no + out=success` 这种内部缩写,因为它对不熟悉 prompt 字段的人不直观。

### Architecture Recommendation

**结论**: 建一个明确的 Task Orchestrator / task domain service,作为所有 task mutation 的唯一协调层。它不是一个大 prompt,也不是一个新 UI 组件;它是代码里的 state machine + policy + transaction boundary。

| 责任                  | 放在 Orchestrator 的原因                                                                                      |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
| 统一入口                | lead pipeline、contact analyzer、staff API、SMS meaningful trigger、future AI agent 都通过同一套 mutation contract |
| 读取上下文               | 统一读取 contact、open tasks、recent calls/messages、progress history、timeline                                  |
| 决定是否调用 AI           | trivial no-op / exact STOP / pure retry 可以代码处理;模糊语义才调 prompt                                             |
| 提供 allowed set      | 给 prompt 注入 `allowedTypeCategories`, `allowedProgressTypes`, `allowedCloseResults`                       |
| 校验 AI proposal      | Zod/schema 校验、taskId/store guard、enum guardrail、confidence policy                                        |
| 执行 state transition | create open、create closed、record progress、update、close 都由代码事务执行                                          |
| 写审计                 | 同步写 `tasks`, `task_progress_events`, `contact_timeline`                                                  |
| 处理并发/幂等             | unique constraint、idempotency key、conditional update、DNC hard stop                                       |

它不负责:

| 不负责                               | 应由谁负责                |
| --------------------------------- | -------------------- |
| 理解 transcript 里客户到底想取消、升级、投诉还是预约  | AI semantic proposal |
| 生成个性化 playbook 文案                 | playbook prompt      |
| 直接改 UI 文案                         | frontend/UI layer    |
| 替代 calls/messages 原始 activity log | activity pipeline    |

***

## 3. Final Schema

**结论**: `tasks` 存当前事项快照和最终结果;`task_progress_events` 存执行过程;`contact_timeline` 存统一时间线投影。

### `tasks`: work object snapshot

| 字段/概念                     | 说明                                                | 现在是否已有                                                              | Final 动作                                                          |
| ------------------------- | ------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `taskId` / `id`           | task 主键                                           | 已有                                                                  | 保留                                                                |
| `contactPhone`, `storeId` | store-scoped contact identity                     | 已有                                                                  | 保留;所有 read/write 继续带 `storeId` guard                              |
| `typeCategory`            | 业务目标类型,如 `lead_follow_up`, `retention`, `upgrade` | 已有                                                                  | 保留;AI 在 code-provided allowed set 内选择                             |
| `status`                  | 事项是否还 open。schema + code + UI 同步用 `open / closed` | 已有 `status` 字段                                                      | 测试环境一次性 `UPDATE` + 全 repo sweep,不保留 `pending` 命名                  |
| `priority`, `dueAt`       | 当前排序和下一次处理时间                                      | 已有                                                                  | 保留;progress update 可调整 `dueAt`                                    |
| `suggestedActions`        | AI 或规则生成的当前建议                                     | 已有                                                                  | 保留;由 prompt 生成,代码校验长度/格式                                          |
| `attemptCount`            | 列表展示用的尝试次数快照                                      | 表里没有字段;当前 task list API 用历史 closed sibling tasks 推导 `attempt_count` | 可新增 denormalized counter;source of truth 是 `task_progress_events` |
| `closeType`               | 任务是人工关闭、AI 自动关闭、还是当场完成创建 closed                   | 已有部分 close type                                                     | 加 `create_closed`;保留 `manual_closed`, `auto_closed`               |
| `closeResult`             | 最终 business outcome                               | 已有,但混入 disposition                                                  | 保留字段,清理语义:只放 outcome,不放 no-answer/voicemail                       |
| `closedAt`, `closedBy...` | close metadata                                    | 已有                                                                  | 保留;补足 actor/executor attribution                                  |
| `executorType`            | 完成/执行者类型: `human`, `ai_agent`, `system`           | 新增                                                                  | 新增,为 future AI agent 和 metrics 归因预留                               |

### `tasks.closeResult`: keep vs move out

**结论**: `closeResult` 不删除这个字段,但要把它从"混合结果桶"改成"最终业务 outcome"。

| 当前/目标值                                                                                                                   | Final 去向                                                         | 原因                                                                             |
| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `booked`, `converted`, `cancel_saved`, `issue_resolved`, `not_interested`, `wrong_number`, `do_not_contact`, `cancelled` | 留在 `tasks.closeResult`                                           | 这些表示事项终局                                                                       |
| `no_answer`, `left_voicemail`                                                                                            | 移到 `task_progress_events.progressType`                           | 这是一次执行进展,不是事项完成                                                                |
| `callback_later`                                                                                                         | 拆分语义后迁移                                                          | 客户要求回拨是 `progressType=callback_requested`;真正完成改约才可能是 `closeResult=rescheduled` |
| `rescheduled`                                                                                                            | 可作为新 closeResult,但只用于"改约这个业务目标已完成"                               | 避免和普通 callback/follow-up 混淆                                                    |
| `attempted`                                                                                                              | Future 不再作为默认 closeResult;改由 progress events + `attemptCount` 表达 | "尝试过"是过程,不是业务终局                                                                |
| `unable_to_reach`                                                                                                        | 建议新增 terminal closeResult,用于达到最大尝试次数后仍联系不上                       | 它表达最终结论"无法联系上",比 `attempted` 更像业务 outcome                                      |
| `already_member`                                                                                                         | 建议保留为 terminal closeResult 或并入 `other` 前先产品拍板                    | 它表示这个 task 目标不成立,不是一次联系进展                                                      |
| `other`                                                                                                                  | 保留,但要求 `closeNote`                                               | 作为无法分类的 terminal outcome fallback                                              |

### `attempted`: final definition

**结论**: `attempted` 不再作为 future 默认 `closeResult`。尝试过程用 `task_progress_events` 和 `attemptCount` 表达;真正终止 task 时,必须写一个 terminal business outcome。

| 场景            | Final 行为                                                                                  |
| ------------- | ----------------------------------------------------------------------------------------- |
| 打了一次没人接       | 不 close task;写 `progressType=no_answer`;`attemptCount += 1`;task 仍 open                   |
| 留了 voicemail  | 不 close task;写 `progressType=left_voicemail`;task 仍 open                                  |
| 发了 SMS        | 不 close task;写 `progressType=text_sent`;等待 meaningful reply 或下一步                          |
| 达到最大尝试次数仍联系不上 | close task;建议 `closeResult=unable_to_reach`;同时保留 progress history 说明尝试过几次                 |
| AI 低置信度、不确定结果 | 不用 `attempted` 兜底 close;输出 `no_op` / `needs_review` / low-confidence proposal,由代码或人决定是否执行 |

历史 `attempted` 数据暂不作为本设计处理范围。这里定义的是 future semantics 和新写入行为。

### `task_progress_events`: progress source of truth

| 字段                        | 说明                                                                                                              | 现在是否已有                         | Final 动作                               |
| ------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------- |
| `taskId`                  | 所属 task                                                                                                         | 新增                             | 新表必需                                   |
| `storeId`, `contactPhone` | 查询和 store guard 用                                                                                               | 新增                             | 建议冗余,避免每次 join tasks                   |
| `progressType`            | `no_answer`, `left_voicemail`, `text_sent`, `callback_requested`, `follow_up_scheduled`, `customer_considering` | 新增;其中部分值现在误在 `closeResult`     | 新增 enum/constant                       |
| `channel`                 | `phone`, `sms`, `voicemail`, `email`, future channel                                                            | 新增                             | 新增                                     |
| `actorType`, `actorId`    | staff / system / ai\_agent / contact\_analysis                                                                  | contact\_timeline 有类似 actor 概念 | 新增到 progress event,便于 task-level query |
| `callId`, `messageId`     | 关联 activity evidence                                                                                            | calls/messages 已有 id           | 新增 nullable FK/reference               |
| `note`                    | 员工或 AI agent 的简短说明                                                                                              | task note 已有,但不是 per-attempt   | 新增,记录这一次进展                             |
| `nextDueAt`               | 这次 progress 后建议/确认的下一次处理时间                                                                                      | `tasks.dueAt` 已有当前值            | 新增,保留历史为什么 dueAt 被改                    |
| `occurredAt`              | 事件发生时间                                                                                                          | 新增                             | 新增                                     |
| `idempotencyKey`          | 防重复写入                                                                                                           | 部分路径有 `onConflictDoNothing`    | 新增,统一去重                                |

### `contact_timeline`: contact-level timeline projection

`contact_timeline` 继续做统一 timeline 和审计,但不承载所有 task progress 查询语义。每次 task progress 应同步写入 timeline event:

```text
task_progress_events: 业务事实 SoT
contact_timeline:     contact 维度 timeline projection / audit feed
tasks:                当前快照
```

| 字段/概念                             | 说明                               | 现在是否已有 | Final 动作                                                |
| --------------------------------- | -------------------------------- | ------ | ------------------------------------------------------- |
| `eventType`                       | timeline 事件类型                    | 已有     | 新增/使用 `task.progress_recorded`                          |
| `entityType`, `entityId`          | 指向 task/call/message 等实体         | 已有     | progress event 投影时 `entityType=task`, `entityId=taskId` |
| actor fields                      | 谁触发了这次变化                         | 已有     | 保留,与 progress event actor 对齐                            |
| AI forensic fields                | AI run/model/confidence/evidence | 已有     | 保留;AI proposal 导致 mutation 时写入                          |
| `oldValue`, `newValue` / metadata | timeline 展示和审计 payload           | 已有     | progress 投影写简洁摘要,不要让 timeline 成为唯一 source of truth      |

***

## 4. Progress Write Path

**结论**: 一次 progress update 应该在一个 transaction 里写 progress event、timeline event,并更新 task 快照。

例子: staff 点击 `Left Voicemail`

```text
1. INSERT task_progress_events
   progressType = left_voicemail
   channel = phone
   actorType = staff
   callId = ...
   occurredAt = ...

2. INSERT contact_timeline
   eventType = task.progress_recorded
   entityType = task
   entityId = taskId
   newValue = { progressType, channel, callId }

3. UPDATE tasks
   attemptCount = attemptCount + 1
   dueAt = next retry window
   updatedAt = now
```

### Timeline event mapping

**结论**: `task_progress_events` 新增后,现有 `contact_timeline` 不废弃。两者并存:progress table 是 task progress 的 SoT,timeline 是 contact-level 展示和审计投影。

| Task 行为                            | Source of truth                    | Timeline event                                          | 说明                                                  |
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
| 创建 open task                       | `tasks`                            | `task.created`                                          | 保留现有语义                                              |
| 创建 closed task                     | `tasks`                            | `task.created` + `task.status_changed` 或 `task.closed`  | 可按现有 timeline 事件模型落地;重点是能审计 create\_closed          |
| 记录 progress                        | `task_progress_events`             | 新增 `task.progress_recorded`                             | 例如 no answer、voicemail、text sent、callback requested |
| 更新 dueAt/priority/suggestedActions | `tasks`                            | `task.due_at_changed` / `task.updated`                  | 保留现有事件                                              |
| 关闭 task                            | `tasks`                            | `task.status_changed` / `task.closed`                   | closeResult 只写 business outcome                     |
| staff override / dismiss AI task   | `tasks` + optional progress/reason | `task.updated` / `task.status_changed` with actor=staff | 显式记录人覆盖 AI 的理由                                      |

Transaction policy:

| 问题                            | 推荐                                                                                                         |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
| progress 写成功但 timeline 写失败怎么办 | 整个 transaction fail;不要留下没有审计投影的 progress                                                                   |
| timeline 是否可反推 progress       | 不作为 source of truth。API 查 task progress 读 `task_progress_events`,timeline 只用于 contact 视图                   |
| progress 是否需要约束               | 需要 CHECK/enum 限制 `progressType/channel/actorType`;`taskId` 必填;`occurredAt` 必填                              |
| progress 是否需要幂等               | 需要 `idempotencyKey` unique,或 `(taskId, callId/messageId, progressType)` partial unique,避免同一 call/SMS 被重复记录 |

### Why not only `contact_timeline`

| 只用 timeline 的问题                        | 独立 progress events 的好处             |
| -------------------------------------- | ---------------------------------- |
| 需要从 JSONB 里 parse progress             | 结构化字段直接查询                          |
| task detail 要过滤跨实体 timeline            | `WHERE task_id = ...` 即可           |
| manager metrics 要从 timeline 推断 attempt | progress 表直接聚合                     |
| API/AI agent 要理解 timeline schema       | endpoint 返回 typed progress objects |

### 查询性能

性能不是反对 `task_progress_events` 的理由。Postgres 很适合这种 append-only event 表,关键是索引:

| 查询          | 索引                                           |
| ----------- | -------------------------------------------- |
| 一个 task 的进展 | `(task_id, occurred_at DESC)`                |
| 门店 workload | `(store_id, occurred_at DESC)`               |
| 员工 attempt  | `(actor_type, actor_id, occurred_at DESC)`   |
| 类型/渠道统计     | `(progress_type, channel, occurred_at DESC)` |

`tasks.attemptCount` 只是列表快照;纠错和历史以 `task_progress_events` 为准。

***

## 5. API Contract

**结论**: 未来 API / CLI / OAuth / AI agent 不应该解析 prompt 文本或 timeline JSONB。它们应该读 typed work objects。

### 推荐 API shape

```http
GET /v3/tasks/:taskId
  -> task snapshot

GET /v3/tasks/:taskId/progress
  -> ordered task_progress_events

POST /v3/tasks/:taskId/progress
  -> record_progress, idempotencyKey required

POST /v3/tasks/:taskId/close
  -> close with business outcome

GET /v3/contacts/:contactId/timeline
  -> cross-entity contact_timeline

GET /v3/contacts/:contactId/work-objects
  -> open/closed objectives + recent progress + recommended next action
```

### API response should separate three concepts

```json
{
  "task": {
    "id": "task_123",
    "status": "open",
    "typeCategory": "lead_follow_up",
    "priority": "high",
    "dueAt": "2026-05-30T21:00:00Z",
    "attemptCount": 2
  },
  "latestProgress": {
    "progressType": "left_voicemail",
    "channel": "phone",
    "occurredAt": "2026-05-30T18:10:00Z"
  },
  "close": null
}
```

Closed example:

```json
{
  "task": {
    "id": "task_456",
    "status": "closed",
    "typeCategory": "lead_follow_up"
  },
  "latestProgress": {
    "progressType": "callback_requested",
    "occurredAt": "2026-05-30T17:20:00Z"
  },
  "close": {
    "closeType": "manual_closed",
    "closeResult": "booked",
    "closedAt": "2026-05-30T18:00:00Z"
  }
}
```

***

## 6. Prompt Contract Derived From API

**结论**: prompt 的职责不是"设计 task 状态",而是在 API/schema 允许的动作空间里提出语义 proposal。

### Prompt input should include

| 输入                      | 说明                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| `contact`               | store-scoped customer identity and lifecycle context                                          |
| `openTasks`             | 当前可操作的 open work objects,带 `taskId`, `typeCategory`, `dueAt`, `attemptCount`, recent progress |
| `recentActivities`      | calls/messages/lead events,只作为 evidence                                                       |
| `recentProgress`        | task-specific progress history,避免重复记录同一件事                                                     |
| `allowedProgressTypes`  | 当前系统支持的 progress enum                                                                         |
| `allowedCloseResults`   | 当前系统支持的 business outcome enum                                                                 |
| `allowedTypeCategories` | 由代码按 customer lifecycle 算出的可选 category set                                                    |
| `storePolicy`           | DNC、SLA、retry threshold、store guard 等 deterministic policy 摘要                                 |

### Prompt output should be proposed mutations

```json
{
  "taskDecisions": [
    {
      "action": "record_progress",
      "taskId": "task_123",
      "progressType": "callback_requested",
      "evidence": "Customer asked to be called back Friday afternoon.",
      "nextDueAtHint": "2026-06-05T18:00:00Z",
      "confidence": 0.88
    },
    {
      "action": "create_closed",
      "typeCategory": "lead_follow_up",
      "closeResult": "booked",
      "evidence": "Staff booked an intro class during the call.",
      "confidence": 0.93
    }
  ]
}
```

### Prompt must not do

| 禁止事项                                                           | 原因                                           |
| -------------------------------------------------------------- | -------------------------------------------- |
| 直接决定 `tasks.status` mutation                                   | state transition 必须由代码执行                     |
| 把 `no_answer`, `left_voicemail`, `text_sent` 输出为 `closeResult` | 这些是 progress,不是 business outcome             |
| invent `taskId`                                                | 只能引用 input 里的 open task                      |
| 绕过 `allowedTypeCategories`                                     | category guardrail 由代码根据 lifecycle 提供        |
| 决定是否通过 DB unique / idempotency / concurrency                   | prompt 不掌握全局并发状态                             |
| 把 timeline 当 source of truth 改写                                | timeline 是 projection/audit,不是 prompt 直接操作对象 |

***

## 7. Code vs Prompt Responsibilities

**结论**: AI 负责语义判断,代码负责世界状态。`typeCategory` 是 hybrid: AI 在代码允许范围内选择。

| 决策点                | 推荐负责层                                   | 为什么                                                | 为什么不放另一层                                                     | 现状                                                              |
| ------------------ | --------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------- |
| 是否触发 task pipeline | Code                                    | event、debounce、cost gate、SQS 幂等都应 deterministic    | prompt 只有被调用后才存在,不能决定是否调用自己                                  | per-call followUp / cron / on-demand / task close 等多入口分散触发      |
| 是否值得深度分析           | Code first,AI second                    | trivial SMS/no-answer/system noise 可代码过滤;边界语义再给 AI | 纯 AI 成本高;纯代码无法判断自然语言购买/取消/投诉意图                               | call 已有 pre-triage;SMS 当前只存储等 batch                             |
| `typeCategory`     | AI choice + code allowed set            | 需要理解 customer lifecycle 和通话语义                      | 纯代码会变硬编码 taxonomy;纯 AI 会越界选不存在/不该选的 category                 | prompt 直接选,代码 guardrail 不够显式                                    |
| 当场完成是否建 task       | Code policy + AI/staff evidence         | create\_closed 是产品规则和状态机能力;完成事实来自语义证据              | 只靠 prompt 会被 repository/schema 卡住;只靠代码读不懂 transcript         | prompt 禁止 resolved interaction 建 task;repository 只能 insert open |
| Progress Update    | Code state machine + staff/AI input     | progress 是同 task 的 append-only record,不是 close     | prompt 不应维护 attempt counter/dueAt;close API 不应承载 progress    | `close.ts` 把 no\_answer/voicemail close + recreate              |
| `priority`         | AI suggestion + code normalization      | 业务紧急度需要语义;SLA/dueAt 上下限要 deterministic             | 纯 AI 不稳定;纯代码不知道取消/投诉/高意向 nuance                              | prompt 输出 priority,代码算 dueAt                                    |
| `suggestedActions` | AI/prompt                               | 下一步话术和行动依赖 transcript/contact context              | 代码模板只能 fallback,不够个性化                                        | lead task 有模板;AI task 有 suggestedActions                        |
| Close              | staff/AI proposal + code execution      | 结果语义由人/AI提出;状态迁移必须原子、幂等、可审计                        | 纯 AI 会 hallucinate taskId/race;纯代码无法判断 cancel saved/resolved | AI auto-close + staff close 两条路径                                |
| DNC                | Code hard stop + AI detection           | exact STOP 可代码判;自然语言 DNC 需要 AI                     | 纯 AI 风险太高;纯代码漏掉 "please stop calling me"                     | doNotContact 在 contact 层,task close 行为不统一                       |
| Playbook           | Dedicated prompt + code context/caching | playbook 是执行指导,不是 task mutation                    | 放 task prompt 会混淆决策和教练;纯代码模板太死                               | 前端 panel 有,后端 prompt 未完整接入                                      |
| 并发/幂等/fallback     | Code/DB                                 | 必须跨 invocation 一致                                  | prompt 没有全局状态和事务能力                                           | 依赖部分 unique/onConflict,但没有统一 task mutation layer                |

### `typeCategory` allowed set

| lifecycle / contact state     | AI 可选 typeCategory                                                 | Code guardrail          |
| ----------------------------- | ------------------------------------------------------------------ | ----------------------- |
| lead active                   | `lead_follow_up`, `booked_not_converted`                           | AI 不得创建 `lead_outreach` |
| member active                 | `cancellation_risk`, `retention`, `upgrade`, `renewal`, `referral` | 必须有对应语义证据               |
| churned re-engagement         | `win_back`                                                         | 只有客户主动回流才允许             |
| terminal / DNC / wrong number | none,只允许 close existing                                            | 禁止创建 outreach task      |

***

## 8. UI Semantics

**结论**: Tasks 页面应明确分成两种操作:记录进展 vs 关闭事项。左侧操作建议命名为 `Progress Update` 或 `Log Progress`,不要叫后端意义上的 status update。

| UI 区域                          | 后端含义               | 允许动作                                                                                                           |
| ------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------- |
| Progress Update / Log Progress | 任务还没完成,记录一次执行进展    | No Answer, Left Voicemail, Text Sent, Follow-up Scheduled, Callback Requested, Note                            |
| Close Task                     | 任务已经完成或终止,记录最终业务结果 | Intro Booked, Converted, Cancel Saved, Issue Resolved, Not Interested, Wrong Number, Do Not Contact, Cancelled |

Prompt 输出也应使用 progress proposal,而不是 "status update":

```json
{
  "action": "record_progress",
  "taskId": "uuid-from-input",
  "progressType": "callback_requested",
  "reason": "Customer asked staff to call back Friday afternoon.",
  "nextDueAtHint": "Friday afternoon",
  "confidence": 0.88
}
```

### Human / AI Race Conditions

**结论**: 人可能比 AI 快,AI 也可能比人快。UI/API 必须让 staff override 成为一等行为,但所有写入仍走同一个 Orchestrator。

| 场景                                        | 正确行为                                                                                                                                   | 设计影响                                                           |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| 人打完电话立即 close,AI 还在分析                     | staff close 先成功;AI later proposal 发现 task 已 closed 后 no-op 或追加 audit note,不能 reopen/重复 close                                           | close API 要 conditional update;AI mutation 要读最新 task status    |
| AI 先创建 open task,人认为不需要                   | staff 可以 dismiss/close with `not_interested`, `wrong_number`, `already_member`, `other` 或 future `not_needed`;记录 staff override reason | UI 需要提供 dismiss/close path;timeline 记录 human override          |
| inbound 当场成交,人先记录 closed work,AI 后分析同一通电话 | 人的 create\_closed/close 是 authoritative;AI 后续只能补 evidence 或 no-op,不能再建重复 task                                                          | idempotency key 应包含 call/message evidence;create\_closed 需要防重复 |
| 人记录 progress,AI 同时建议 close                | Orchestrator 以最新状态和 evidence 决定;如果 close 证据强且 task 仍 open,可 close;否则保留 progress 并进入 review                                             | progress 和 close 都必须走同一状态机                                     |
| AI 低置信度                                   | 不自动 mutation;进入 `needs_review` 或 no-op + audit                                                                                         | 避免用 `attempted`/`other` 作为 AI 万能兜底                             |

***

## 9. Scenario Coverage

**结论**: 同一套 objective/progress/outcome 模型覆盖关键场景,不需要为每个场景写特殊状态。

| 场景                           | 正确行为                                                                  |
| ---------------------------- | --------------------------------------------------------------------- |
| lead no answer               | 原 task 仍 open;写 progressType=`no_answer`;更新 attempt/dueAt             |
| left voicemail               | 原 task 仍 open;写 progressType=`left_voicemail`                         |
| text sent                    | 原 task 仍 open;写 progressType=`text_sent`;meaningful reply 再触发分析       |
| customer asks callback later | 原 task 仍 open;写 progressType=`callback_requested`;设置 nextDueAt        |
| intro booked                 | closeResult=`booked`;无 open task 时 create\_closed                     |
| inbound 当场成交                 | create\_closed,closeResult=`converted`                                |
| cancel saved                 | close existing 或 create\_closed,closeResult=`cancel_saved`            |
| complaint resolved           | closeResult=`issue_resolved`;仍有 promise/escalation 时只 update/progress |
| DNC                          | exact STOP 代码处理;自然语言 DNC 由 AI;关闭 open outreach task                   |
| SMS meaningful reply         | trivial 不触发;购买/取消/投诉/预约/回流/DNC 意图触发 orchestrator                      |
| 跨门店同号码                       | 所有 read/write 以 `(contactPhone, storeId)` 隔离                          |

***

## 10. Explicit Conflict Decisions

**结论**: brief 里的冲突不是二选一,但每个都要有明确边界。推荐方案是统一 work object + progress/outcome 分层。

| 冲突问题                                                       | 明确结论                                                                             | 理由                                                                     |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| "每个当场办成的事都是 task" vs "conversion funnel 不是 completed task" | 当场办成的 **customer-work objective** 是 `create_closed` task;普通 funnel event 不是 task | task 记录前台/AI agent 完成的事项,不替代 conversion funnel                         |
| "只提醒一次" vs "没打通还要回头再试"                                     | 只提醒一次 = 不重复创建同一 objective;不是只尝试一次                                                | no-answer/voicemail 是 progress,原 task 仍 open,用 dueAt/attemptCount 管下一次 |
| "AI 判断灵活" vs "代码状态机 deterministic"                         | AI 提 semantic proposal;代码执行 state transition                                     | AI 适合理解意图,代码负责幂等、并发、审计和 guardrail                                      |
| "SMS 不想触发 AI" vs "有意义 SMS 需要即时跟进"                          | SMS 先走 code gate/debounce;meaningful reply 再触发 orchestrator                      | "thanks/ok" 不花 LLM 成本;购买/取消/投诉/DNC/预约类回复需要及时处理                         |
| "task 是待办" vs "task 是业绩记录"                                 | task 是统一事项对象: open 是待办,closed 是事项结果记录                                            | 用 lifecycle 区分当前状态,用 closeResult 区分业务结果,不会污染待办列表                       |
| activity vs task progress vs business outcome              | 三层不同,不压进同一个字段                                                                    | activity 是客观互动;progress 是任务处理过程;outcome 是最终业务结果                        |

***

## 11. Metrics Impact

**结论**: completion 和 attempt workload 必须分开。

| 指标                   | 推荐口径                                                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Completed objectives | `tasks.status='closed'`                                                                                                                           |
| Business wins        | closed tasks where `closeResult IN ('converted','booked','cancel_saved','issue_resolved','win_back','renewed','upgraded','referral_obtained')`    |
| Attempt workload     | `task_progress_events` count,按 progressType/channel/staff 分组                                                                                      |
| Open workload        | open tasks by dueAt/backlog/priority                                                                                                              |
| Productivity rate    | `completed objectives / (completed objectives + still-open objectives in window)` 或 manager review 后定;不能用 `closed_by_staff_name IS NOT NULL` 过滤分母 |

***

## 12. Implementation Touchpoints

**结论**: 这是 final target 的设计清单,不是阶段拆分。

| 区域                 | 文件/模块                                                      | 改动方向                                                                                                                                                                           |
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Task schema        | `callytics-common/src/db/schema/tasks.ts`                  | 支持 `create_closed`;**`TASK_STATUS = ['open', 'closed']` + DEFAULT `'open'`**(原 pending/closed,测试环境一次性迁移);`closeResult` 保持 business outcome;加 `executorType`;可保留 `attemptCount` |
| Progress schema    | 新 `task_progress_events`                                   | 定义 `progressType`, `channel`, `actorType`, `callId`, `messageId`, `nextDueAt`, `note`, `occurredAt`                                                                            |
| Timeline schema    | `contact_timeline`                                         | 增加/使用 `task.progress_recorded` event projection                                                                                                                                |
| Task UI enum       | `callytics-common/src/db/schema/task-ui.ts`                | `CLOSE_RESULT_OPTIONS` 只展示 outcome;progress options 单独建常量                                                                                                                      |
| AI decision schema | `contacts-analyzer/src/core/models.ts`                     | `TaskDecision` 支持 `create_open`, `create_closed`, `update`, `close`, `record_progress`                                                                                         |
| Contact prompt     | `contacts-analyzer/src/core/prompt-builder.ts`             | SECTION 4 改成 objective/progress/outcome 规则                                                                                                                                     |
| Orchestrator       | `contacts-analyzer` + studio API shared task domain module | 统一执行 task mutation: validate, state transition, idempotency, timeline                                                                                                          |
| Staff API          | `studio-website-monorepo/apps/api/src/routes/tasks/*`      | close API 只 close outcome;新增 progress update API                                                                                                                               |
| Frontend           | `apps/web/src/pages/tasks/components/*`                    | `Status Update` 改成 Progress Update 语义                                                                                                                                          |
| Dashboard          | `apps/api/src/routes/v3/dashboard-staff.ts`                | completion 和 attempt workload 分开                                                                                                                                               |
| External API       | `apps/api/src/routes/v3/tasks/*`, `contacts/*`             | task snapshot、progress history、timeline、work-objects endpoint                                                                                                                  |
| Prompt eval        | `prompt-eval/expected_results.json`                        | 加 create\_closed、record\_progress、DNC、SMS meaningful expected outputs                                                                                                          |
| Playbook           | `playbook-panel.vue` + backend route/prompt                | 用 task + progress history + contact context 生成                                                                                                                                 |

### Implementation Staging, Without Weakening Final Design

**结论**: final model 一次定清楚;实现可以先做最小闭环,但不能把 schema/prompt 设计成只适合短期。

| 类别           | 应先落地                                                                                           | Future-compatible 但 schema/API 要预留                       |
| ------------ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Lifecycle    | schema + code + UI 同步 `open/closed`(测试环境直接迁移)                                                  | 不保留 `pending` 命名                                         |
| Progress     | first-class `task_progress_events`;staff progress API;no-answer/voicemail/text sent 从 close 移出 | AI agent 自动记录 progress;更多 channel/email/chat             |
| Outcome      | close API 只接受 business outcome;支持 `create_closed`                                              | 更细 revenue attribution / outcome taxonomy                |
| Orchestrator | staff API + contact analyzer task mutation 先统一                                                 | SMS meaningful trigger、AI voice agent、third-party API 复用 |
| Prompt       | `TaskDecision` 加 `record_progress`, `create_closed`;allowed set guardrail                      | low-confidence human review, model routing               |
| Timeline     | progress write 同 transaction 投影到 `contact_timeline`                                            | richer audit/event replay                                |
| Metrics      | completed objectives 与 attempt workload 分开                                                     | dashboard 体系后续再整体重做                                      |

***

## 13. Rationale / Deep Dives

### 1. 为什么不是 Agile-style workflow status

Agile task status 管的是工程流程,例如 `Todo -> In Progress -> In Review -> Done`。Retaintive task 管的是客户事项是否还需要处理。

不推荐:

```text
open -> attempted -> voicemail_left -> waiting_callback -> text_sent -> closed
```

推荐:

```text
task.status = open
latestProgress = left_voicemail
attemptCount = 2
dueAt = tomorrow
```

### 2. `callback_later` 怎么拆

| 场景                             | 正确定义                                           |
| ------------------------------ | ---------------------------------------------- |
| 客户说 "Friday call me back"      | progressType=`callback_requested`,task 仍 open  |
| staff 自己安排明天再试                 | progressType=`follow_up_scheduled`,task 仍 open |
| 任务目标就是 reschedule class,且已改好时间 | closeResult=`rescheduled`,task closed          |

### 3. `closeResult` 只保留 business outcome

下面两组不是映射关系,而是两类不同 enum。

Business outcome examples:

```text
converted
booked
cancel_saved
issue_resolved
not_interested
wrong_number
do_not_contact
unable_to_reach
other
```

Progress examples:

```text
no_answer
left_voicemail
text_sent
callback_requested
follow_up_scheduled
customer_considering
```

### 4. 并发 / 幂等 / fallback

| 风险                                  | 代码策略                                                           |
| ----------------------------------- | -------------------------------------------------------------- |
| AI output parse fail                | 不写 mutation;保留 open task;记录 failure/timeline                   |
| AI hallucinated taskId              | 只允许引用同 `(phone, storeId)` 的 open tasks                         |
| staff 和 AI 同时 close                 | `WHERE status='open'` 保证只有一个成功                                 |
| staff record progress 时 AI 正在 close | state machine 先读最新 status;closed 后 progress 只能作为 late activity |
| cron 与 per-call 同时 create           | unique index + idempotency key + conflict no-op                |
| DNC 与新 task 同时出现                    | DNC hard stop 优先;阻止 create outreach,关闭 existing open task      |

### 5. SMS 为什么不单独做一套 task pipeline

**结论**: SMS 应该先作为 activity 存储,再通过 channel-specific trigger/filter 进入同一个 Task Orchestrator。不要为 SMS 发明另一套 task lifecycle。

| 问题                     | 设计判断                                                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| SMS 要不要触发 AI           | 不是"永远不触发",也不是"每条都触发"。先代码过滤 trivial message,meaningful SMS 才触发                                                |
| 什么是 trivial SMS        | `thanks`, `ok`, emoji, 自动回复,没有业务意图的确认                                                                        |
| 什么是 meaningful SMS     | 购买/预约/取消/投诉/回流/DNC/改时间/明确提出下一步                                                                               |
| 多条 SMS 怎么处理            | 短窗口 debounce/merge,合并后再判断,避免客户连续发 3 条导致 3 次 AI 分析                                                            |
| SMS 和 call 是否共用 prompt | 最终共用 task mutation contract,但前置 gate 和输入摘要可以按 channel 区分                                                     |
| 为什么不单独建 SMS task 系统    | task 表达的是 customer-work objective,不是 channel。电话、SMS、future email/chat 都应该汇入同一个 objective/progress/outcome 模型 |

业界老 CRM 常把 call、SMS、email 都挂到同一个 contact activity/timeline 下,但触发 task 的规则常因 channel 不同而不同。这个方向对 Retaintive 有参考价值:统一 objective,但不统一所有触发成本。2026 AI 成本下降后,也不应该简单照搬"SMS 一律等 daily batch";更合理的是 lightweight code/classifier gate + meaningful message 触发 orchestrator。

### 6. 竞品 API 校准

| API / 产品          | 观察                                                          | 对我们的影响                                  |
| ----------------- | ----------------------------------------------------------- | --------------------------------------- |
| HubSpot Tasks API | task status 是 lifecycle,如 `COMPLETED` / `NOT_STARTED`       | 不把 `No Answer` 当 task status            |
| HubSpot Calls API | call outcome/status 放在 call activity 上                      | disposition/progress 从 close outcome 分离 |
| Salesforce Task   | Task 有 `Status`,也有 call fields                              | 可以关联 task 和 call,但不要混语义                 |
| Salesloft         | Calls / Call Data Records / Dispositions / Tasks 分 endpoint | 不同对象有不同 API contract                    |
| Outreach          | call task mapping 特别处理 no-answer 和 duplicate submissions    | duplicate prevention 和 idempotency 放代码层 |

竞品只是校准。很多旧 CRM 设计来自人工录入、AI 不成熟、LLM 成本高的时代。Retaintive 应利用 AI-native 条件,提供更清楚的 `work-objects` API,让外部系统和 AI agent 一次读懂当前事项、进展、结果和下一步。

Official references:

- HubSpot Tasks API: [https://developers.hubspot.com/docs/reference/api/crm/engagements/tasks](https://developers.hubspot.com/docs/reference/api/crm/engagements/tasks)
- Salesforce Task Fields: [https://help.salesforce.com/s/articleView?id=sf.task\_fields.htm\&language=en\_US\&type=5](https://help.salesforce.com/s/articleView?id=sf.task_fields.htm\&language=en_US\&type=5)
- Salesloft Call Dispositions API: [https://developers.salesloft.com/docs/api/call-dispositions/](https://developers.salesloft.com/docs/api/call-dispositions/)
- Outreach Advanced Task Mapping for Call Tasks: [https://support.outreach.io/hc/en-us/articles/360042104133-Microsoft-Dynamics-Advanced-Task-Mapping-Call-Task](https://support.outreach.io/hc/en-us/articles/360042104133-Microsoft-Dynamics-Advanced-Task-Mapping-Call-Task)

***

## 14. Brief Compliance Checklist

**结论**: 当前版本满足 brief 的核心交付和 acceptance criteria。剩余需要 Max review 的不是文档遗漏,而是产品 policy 拍板。

| Brief 要求                          | 覆盖位置                                                         | 状态                                   |
| --------------------------------- | ------------------------------------------------------------ | ------------------------------------ |
| 代码 vs prompt 职责矩阵                 | §7                                                           | Covered                              |
| 现状 vs 设计后 workflow 图              | §2                                                           | Covered                              |
| architecture recommendation       | §2 Architecture Recommendation                               | Covered                              |
| gap 分析文档更新                        | 本文档即为 gap analysis 的 Codex review-basis 版本;旧 V2 docs 已归档避免混读 | Covered                              |
| 需要改的代码/prompt 清单                  | §12                                                          | Covered                              |
| API-first / AI-agent-readable 视角  | §5, §6                                                       | Covered                              |
| task 到底是什么                        | Executive Summary, §1                                        | Covered: unified Work Object         |
| 每个"谁负责"说明为什么不是另一层                 | §7                                                           | Covered                              |
| 关键场景覆盖                            | §9                                                           | Covered                              |
| final target vs future-compatible | §12 staging table                                            | Covered without changing final model |
| six conflict questions            | §10                                                          | Covered                              |
| activity / progress / outcome 分层  | §1, §3, §10                                                  | Covered                              |
| SMS 留接口但不设计完整 SMS pipeline        | §2, §7, §9, §10, §13.5                                       | Covered                              |
| metrics 只说明影响,不重做体系               | §11                                                          | Covered                              |
| future AI agent compatibility     | §3, §5, §6, §12                                              | Covered                              |

Open product policy items:

| 问题                                      | 推荐默认值                                                      |
| --------------------------------------- | ---------------------------------------------------------- |
| `create_closed` 是否计入 productivity       | 计入 completed objectives,按 actor/executorType 归因            |
| `rescheduled` 是 closeResult 还是 progress | 只有业务目标就是 reschedule 且已完成时才是 closeResult;普通"回头再打"是 progress |
| lead retry threshold                    | 默认 3 次 meaningful attempts,但按 task type 可配置                |
| low-confidence AI proposal 怎么处理         | 不自动 mutation;进入 human review 或 no-op + audit               |
