From 6eaefe782627b404488fe6d5922b9fd138783af5 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Sat, 22 Aug 2026 01:13:33 +0800 Subject: [PATCH 01/23] =?UTF-8?q?docs(openspec):=20=E8=A7=84=E5=88=92=20Is?= =?UTF-8?q?sue=2034=20=E9=A2=86=E5=9F=9F=E6=A8=A1=E5=9E=8B=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.openspec.yaml | 2 + .../add-conversation-command-api/design.md | 195 +++++++++ .../add-conversation-command-api/proposal.md | 32 ++ .../specs/conversation-command-api/spec.md | 183 ++++++++ .../add-conversation-command-api/tasks.md | 43 ++ .../.openspec.yaml | 2 + .../design.md | 404 ++++++++++++++++++ .../proposal.md | 33 ++ .../specs/domain/spec.md | 162 +++++++ .../define-conversation-domain-model/tasks.md | 37 ++ .../.openspec.yaml | 2 + .../migrate-generation-lifecycle/design.md | 157 +++++++ .../migrate-generation-lifecycle/proposal.md | 32 ++ .../conversation-generation-lifecycle/spec.md | 146 +++++++ .../migrate-generation-lifecycle/tasks.md | 39 ++ .../.openspec.yaml | 2 + .../design.md | 182 ++++++++ .../proposal.md | 33 ++ .../specs/conversation-client-state/spec.md | 165 +++++++ .../tasks.md | 48 +++ .../.openspec.yaml | 2 + .../design.md | 103 +++++ .../proposal.md | 31 ++ .../specs/conversation-persistence/spec.md | 89 ++++ .../tasks.md | 28 ++ .../.openspec.yaml | 2 + .../retire-thread-tree-authority/design.md | 174 ++++++++ .../retire-thread-tree-authority/proposal.md | 33 ++ .../specs/conversation-cutover/spec.md | 155 +++++++ .../retire-thread-tree-authority/tasks.md | 60 +++ 30 files changed, 2576 insertions(+) create mode 100644 openspec/changes/add-conversation-command-api/.openspec.yaml create mode 100644 openspec/changes/add-conversation-command-api/design.md create mode 100644 openspec/changes/add-conversation-command-api/proposal.md create mode 100644 openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md create mode 100644 openspec/changes/add-conversation-command-api/tasks.md create mode 100644 openspec/changes/define-conversation-domain-model/.openspec.yaml create mode 100644 openspec/changes/define-conversation-domain-model/design.md create mode 100644 openspec/changes/define-conversation-domain-model/proposal.md create mode 100644 openspec/changes/define-conversation-domain-model/specs/domain/spec.md create mode 100644 openspec/changes/define-conversation-domain-model/tasks.md create mode 100644 openspec/changes/migrate-generation-lifecycle/.openspec.yaml create mode 100644 openspec/changes/migrate-generation-lifecycle/design.md create mode 100644 openspec/changes/migrate-generation-lifecycle/proposal.md create mode 100644 openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md create mode 100644 openspec/changes/migrate-generation-lifecycle/tasks.md create mode 100644 openspec/changes/normalize-conversation-client-state/.openspec.yaml create mode 100644 openspec/changes/normalize-conversation-client-state/design.md create mode 100644 openspec/changes/normalize-conversation-client-state/proposal.md create mode 100644 openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md create mode 100644 openspec/changes/normalize-conversation-client-state/tasks.md create mode 100644 openspec/changes/normalize-conversation-persistence/.openspec.yaml create mode 100644 openspec/changes/normalize-conversation-persistence/design.md create mode 100644 openspec/changes/normalize-conversation-persistence/proposal.md create mode 100644 openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md create mode 100644 openspec/changes/normalize-conversation-persistence/tasks.md create mode 100644 openspec/changes/retire-thread-tree-authority/.openspec.yaml create mode 100644 openspec/changes/retire-thread-tree-authority/design.md create mode 100644 openspec/changes/retire-thread-tree-authority/proposal.md create mode 100644 openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md create mode 100644 openspec/changes/retire-thread-tree-authority/tasks.md diff --git a/openspec/changes/add-conversation-command-api/.openspec.yaml b/openspec/changes/add-conversation-command-api/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/add-conversation-command-api/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/add-conversation-command-api/design.md b/openspec/changes/add-conversation-command-api/design.md new file mode 100644 index 00000000..cc0c6ab3 --- /dev/null +++ b/openspec/changes/add-conversation-command-api/design.md @@ -0,0 +1,195 @@ +## Context + +规范持久化和 Generation 生命周期已经定义“什么是真实实体”和“执行如何收敛”,但它们不能直接暴露给组件或路由。当前客户端仍倾向于先修改整棵树、PUT 存盘,再发起模型请求;Fork、重试和变体选择也可能在不同模块各自实现事务语义。 + +本设计建立一个传输无关的应用层,并提供首个 HTTP/JSON 适配器。网页、未来 CLI、MCP 和公开 API 可以改变传输方式,但不能绕过同一命令、授权和仓储事务。 + +## Goals / Non-Goals + +**目标:** + +- 为 Conversation/Thread 生命周期、Fork、发送、编辑、重新生成、变体选择和 Generation Stop 建立单一命令入口。 +- 让认证、授权、幂等、版本冲突、事务和事件箱具有统一语义。 +- 返回规范快照和实体增量,支持后续客户端 normalized Store。 +- 给未来 CLI/MCP 留下稳定应用端口,而不是让它们依赖 Next.js 路由或数据库。 + +**非目标:** + +- 在本 change 迁移现有客户端、删除旧 API 或开启生产规范权威。 +- 实现分享 token、公开访问、Project Memory/File/Instruction 接口。 +- 实现 Issue #39 的 Thread 结论发布、跨 Thread 合并或收敛。 +- 规定 UI 布局、轮询视觉或乐观动画。 + +## Decisions + +### D1. 应用层使用显式 Command/Query 端口 + +应用层按用例导出命令与查询,而不是通用 CRUD 仓储: + +```text +Queries + listConversations + getConversationSnapshot + getGeneration + +Commands + createConversation / renameConversation + archiveConversation / restoreConversation / deleteConversation + renameThread / archiveThread / restoreThread + forkThread + sendTurn + editTurnInput + regenerateTurn + selectTurnVariant + stopGeneration +``` + +每个 handler 依赖 `UnitOfWork`、规范仓储、授权策略、时钟、ID、事件箱和 Generation 端口。它们不导入 Next.js Request/Response,也不返回 HTTP 状态。 + +备选方案是暴露通用 `updateEntity`;它会允许调用方组合非法关系并把不变量分散回传输层,因此拒绝。 + +### D2. 写命令使用统一内部 envelope + +内部命令输入包含: + +```text +commandId +actor +scope +targetId +idempotencyKey +expectedRevision +payload +``` + +幂等记录保存 actor、作用域、命令类型、规范化载荷摘要和序列化结果引用。相同摘要重放原结果,不同摘要返回 `idempotency_conflict`。 + +HTTP 适配器优先把 `Idempotency-Key` 映射到幂等键,把 `If-Match`/ETag 映射到预期 revision;JSON 只承载领域载荷。内部 envelope 不强迫未来 MCP 使用 HTTP header。 + +### D3. revision 按最小冲突边界选择 + +```text +Conversation revision:结构、标题、生命周期、ThreadFork 集合 +Thread revision: Thread 生命周期、标题、Turn 追加顺序 +Turn revision: 当前用户/助手变体选择 +Generation version: 执行状态、租约与 checkpoint +``` + +`forkThread` 比较并推进 Conversation revision;发送比较并推进 Thread revision;选择变体比较并推进 Turn revision。事务同时更新多个边界时全部条件必须成功。 + +备选方案是所有命令锁整条 Conversation;实现简单但会让两个独立 Thread 的发送互相冲突,因此拒绝。完全没有 revision 则会把最后写入者胜出当成正确性,因此也拒绝。 + +### D4. Fork 保存来源边界,不复制上游 Message + +`forkThread` 事务执行: + +1. 授权并锁定目标 Conversation revision; +2. 验证来源 Message 属于上游 Thread 且在其当前可用路径中; +3. 创建下游 Thread; +4. 创建唯一 ThreadFork,记录 `sourceMessageId` 与可选 TextAnchor; +5. 将下游提示词上下文边界定义为“沿来源链截至该 Message”的读取投影; +6. 推进 Conversation revision并写入 `ThreadForked` 事件箱事件。 + +不复制上游 Turn/Message;否则同一历史会有多个身份,反馈、分享和后续修改无法判定引用对象。读取模型负责把继承上下文与下游本地 Turns 投影成一列可理解内容。 + +### D5. 发送命令创建完整执行意图 + +`sendTurn` 在事务中创建 Turn、用户 Message、pending 助手 Message、规范 Generation 和 `GenerationRequested` 事件。提交后调度执行器;执行器启动失败由 Generation 生命周期收敛,不回滚已经可查询的意图。 + +`regenerateTurn` 为同 Turn 创建新的助手 Message/Generation。`editTurnInput` 为当前末尾 Turn 创建用户变体和对应助手变体;若目标 Turn 后面已有内容,返回 `fork_required`,避免静默改变历史含义。 + +### D6. 读取快照与写入 delta 使用统一传输版本 + +HTTP 成功响应使用稳定 envelope: + +```json +{ + "schemaVersion": 1, + "data": {}, + "revisions": {}, + "delta": { + "upsert": {}, + "remove": {}, + "invalidate": [] + } +} +``` + +首次 `ConversationSnapshot` 可以包含全部规范实体与明确的派生投影;命令只返回受影响实体、移除 tombstone 和需要重取的投影键。响应中的 JSON schema 通过共享传输契约解析,但领域实体不直接依赖该 schema。 + +### D7. HTTP 资源路径服务于稳定身份 + +首个适配器采用资源和领域动作组合: + +```text +GET/POST /api/projects/{projectId}/conversations +GET/PATCH/DELETE /api/conversations/{conversationId} +POST /api/conversations/{conversationId}/restore +PATCH /api/threads/{threadId} +POST /api/threads/{threadId}/restore +POST /api/threads/{threadId}/forks +POST /api/threads/{threadId}/turns +POST /api/turns/{turnId}/input-edits +POST /api/turns/{turnId}/regenerations +POST /api/turns/{turnId}/active-variant +GET /api/generations/{generationId} +POST /api/generations/{generationId}/stop +``` + +PATCH 只接受明确允许的标题/生命周期字段,不是任意数据库 patch。具体文件可以按 Next.js 当前版本约定组织,但路由只调用应用端口。 + +### D8. 统一授权且不信任归属载荷 + +认证适配器产生 actor;应用授权策略从规范包含关系解析 Workspace/Project/Conversation/Thread/Message/Generation。请求载荷不得决定 owner 或成员身份。不可见资源和不存在资源统一返回 `not_found`,避免 ID 枚举。 + +未来分享 token 会引入另一种 actor 与权限范围,但必须复用同一策略端口;本 change 不提前实现它。 + +### D9. 错误为应用结果,不是散落异常文本 + +应用层稳定分类与 HTTP 映射为: + +```text +invalid_request -> 400 +unauthenticated -> 401 +forbidden -> 403(仅资源存在性已对 actor 可见时) +not_found -> 404 +version_conflict -> 409 +idempotency_conflict -> 409 +state_conflict -> 409 +semantic_validation -> 422 +rate_limited -> 429 +internal -> 500 +``` + +Stop 已终结 Generation 是幂等成功,不是 `state_conflict`。每个错误携带稳定 `code`、`requestId` 和安全 details;日志可以保存内部 cause,但响应不泄漏数据库或模型错误。 + +### D10. 事务事件箱位于同一提交边界 + +会触发执行、标题生成、审计或未来订阅的命令,在规范实体事务内追加 outbox 事件。事件至少包含事件 ID、聚合 ID、聚合 revision、类型、schemaVersion、actor 和发生时间。消费者以事件 ID 幂等;事件不是新的写入权威。 + +### D11. 新 API 默认不承载现有客户端 + +路由可以在隔离测试或显式配置下启用,但旧客户端在 `normalize-conversation-client-state` 完成前不切换。禁止新命令同时调用旧整树 PUT,也禁止从旧树回写规范实体。最终 change 执行一次性 cutover。 + +## Risks / Trade-offs + +- **[风险] 显式命令数量比通用 CRUD 多。** → 这是把业务语义集中并可测试的必要成本;共享 envelope、结果和端口降低样板。 +- **[风险] 多层 revision 会增加客户端处理。** → 每个命令只携带最小边界 revision,响应统一返回更新值,避免全 Conversation 冲突。 +- **[风险] 继承上下文读取投影可能较复杂。** → ThreadFork 保存唯一来源,查询层集中组装并缓存可重建投影,不复制规范 Message。 +- **[风险] outbox 消费存在至少一次投递。** → 事件和消费者使用稳定 ID 幂等,实体事务不依赖同步消费者成功。 +- **[风险] 新旧 API 并存容易误调用。** → 使用互斥 authority 配置、清晰模块命名和契约测试,客户端切换前不启用生产规范写入。 + +## Migration Plan + +1. 建立应用命令/查询、统一结果、授权、幂等与 UnitOfWork 端口。 +2. 实现 Conversation/Thread 生命周期和 `forkThread`,验证事务与 revision。 +3. 接入 `sendTurn`、编辑、重新生成、变体选择与 Generation 查询/Stop。 +4. 增加 HTTP 传输 schema、路由、ETag/Idempotency-Key 和错误映射。 +5. 增加 outbox dispatcher、契约测试和隔离端到端测试。 +6. 保持生产旧客户端未切换,把新契约交给 `normalize-conversation-client-state`。 + +回滚通过关闭规范 API 路由完成;规范数据不可回写旧 JSON,也不启用双写补偿。 + +## Open Questions + +无。公开分享 actor、速率套餐和外部 API 版本生命周期属于后续独立提案,不阻塞内部应用契约和首个网页适配器。 diff --git a/openspec/changes/add-conversation-command-api/proposal.md b/openspec/changes/add-conversation-command-api/proposal.md new file mode 100644 index 00000000..e0b03a18 --- /dev/null +++ b/openspec/changes/add-conversation-command-api/proposal.md @@ -0,0 +1,32 @@ +## Why + +规范表和 Generation 生命周期只能保证持久化层内部正确;如果网页、未来 CLI 或 MCP 仍能分别写 Thread、Message、Fork 或整包快照,重复关系和竞态会从接口重新进入系统。需要一套与传输无关的应用命令和统一 HTTP 适配器,把权限、幂等、版本冲突和事务边界集中为唯一写入入口。 + +## What Changes + +- 新增 Conversation 查询与命令应用层,覆盖 Conversation 创建/列表/读取/重命名/归档/删除,以及 Thread Fork、发送、编辑、重新生成和变体选择。 +- `forkThread` 在一个服务端事务中创建下游 Thread、ThreadFork、上下文边界、Conversation 版本和事务事件;客户端不得分步拼装关系。 +- 发送、编辑与重新生成通过命令原子创建 Turn/Message/Generation,并复用规范 Generation 生命周期;任何付费调用发生在命令事务提交之后。 +- 所有命令使用统一 envelope,包含命令 ID、幂等键、目标 ID、预期版本和载荷;服务端返回规范实体增量与最新版本,而非可回写整树。 +- 新增统一身份认证、所有者/成员授权、稳定领域错误和 HTTP 状态映射;冲突只在预期版本、幂等载荷或当前状态真实冲突时返回。 +- 新增首次加载 `ConversationSnapshot`、增量查询以及 Generation 查询/Stop 端点,供网页与未来公开适配器共用同一应用服务。 +- **BREAKING**:目标接口不再提供整棵 `ThreadTreeState` 的权威 PUT、客户端关系写入或浏览器先存盘屏障;旧路由仅在最终切换前暂存,不能成为新命令的第二权威。 + +## Capabilities + +### New Capabilities + +- `conversation-command-api`:定义 Conversation 查询、原子领域命令、权限、幂等、并发、实体增量和稳定传输结果。 + +### Modified Capabilities + +- 无。遗留路由的删除由 `retire-thread-tree-authority` 负责,本变更建立替代契约。 + +## Impact + +- 应用层:新增 Conversation commands/queries、事务端口、授权策略、事件箱事件和统一结果类型。 +- 接口层:新增 `/api/projects/.../conversations`、`/api/conversations/...`、Thread/Turn/Generation 子资源路由及传输 schema。 +- 持久化:复用规范仓储、Conversation/Thread/Turn revision、Generation 生命周期和事务事件箱。 +- 客户端:后续改用快照初始化和实体增量;本变更只提供接口,不迁移 Store。 +- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`、`migrate-generation-lifecycle`。 +- 后续依赖:`normalize-conversation-client-state`、`retire-thread-tree-authority`,以及未来 CLI/MCP/分享适配器。 diff --git a/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md b/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md new file mode 100644 index 00000000..c0d0dba0 --- /dev/null +++ b/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md @@ -0,0 +1,183 @@ +## Purpose + +为网页、未来 CLI 与 MCP 提供同一套 Conversation 查询和原子命令契约,使授权、幂等、并发、Fork、Message 变体与 Generation 只能通过服务端应用层改变。 + +## ADDED Requirements + +### Requirement: 通过应用命令改变规范实体 + +系统 SHALL 只允许应用命令改变 Conversation、Thread、ThreadFork、Turn、Message、Generation 或当前有效变体。传输层 MUST 只负责认证、解析、调用命令和序列化结果,不得自行拼装关系或接受整包 Conversation 快照覆盖规范实体。 + +#### Scenario: 拒绝整树写入 + +- **WHEN** 客户端尝试提交全部 Thread、Message 与 Fork 以覆盖一条 Conversation +- **THEN** 新接口不提供该写入能力,并要求调用对应实体或领域动作命令 + +#### Scenario: 多种适配器调用相同命令 + +- **WHEN** 网页接口和未来 CLI 对同一 Conversation 执行相同领域动作 +- **THEN** 二者调用同一应用命令并获得相同权限、不变量、幂等与冲突语义 + +### Requirement: 提供 Conversation 生命周期查询和命令 + +系统 SHALL 提供按 Project 列出和创建 Conversation、按 ID 读取快照,以及重命名、归档、恢复和删除 Conversation 的查询或命令。创建 SHALL 在一个事务中创建 Conversation 与根 Thread;删除 SHALL 遵守运行中 Generation 和归属资产保护规则。 + +#### Scenario: 创建 Conversation 与根 Thread + +- **WHEN** 有权限成员在 Project 中创建 Conversation +- **THEN** 系统原子创建 Conversation 和唯一根 Thread,并返回二者的规范增量与初始版本 + +#### Scenario: 归档后从默认列表隐藏 + +- **WHEN** 用户归档 Conversation +- **THEN** Conversation 保持可按 ID 恢复和审计,但默认活跃列表不再返回它 + +#### Scenario: 运行中任务阻止删除 + +- **WHEN** Conversation 仍有非终态 Generation 且用户请求删除 +- **THEN** 系统返回稳定状态冲突且不删除任何实体 + +### Requirement: 提供 Thread 生命周期命令 + +系统 SHALL 提供 Thread 重命名、归档和恢复命令。归档 Thread 不得删除 Message、Generation 或 ThreadFork,也不得改变其他 Thread 的身份;若归档会使当前界面路径不可见,服务端仍 SHALL 保留规范关系,由客户端选择其他可见 Thread。根 Thread 不得脱离 Conversation 单独归档;调用方必须改用 Conversation 归档命令。 + +#### Scenario: 归档非根 Thread + +- **WHEN** 用户归档一个没有非终态 Generation 的分支 Thread +- **THEN** 系统更新该 Thread 生命周期与 revision,保留其 Fork 来源、后代和历史内容 + +#### Scenario: 运行中任务阻止 Thread 归档 + +- **WHEN** Thread 中仍有非终态 Generation +- **THEN** 系统返回稳定状态冲突,Thread 保持活跃 + +#### Scenario: 单独归档根 Thread + +- **WHEN** 用户直接请求归档 Conversation 的根 Thread +- **THEN** 系统返回稳定 `conversation_action_required` 结果,并保持 Conversation 与根 Thread 活跃 + +### Requirement: 原子执行 forkThread + +`forkThread` 命令 SHALL 验证来源 Message 属于上游 Thread,随后在单个事务中创建下游 Thread、唯一 ThreadFork、继承上下文边界、必要版本更新和事务事件箱事件。命令不得复制上游 Message 作为新的规范 Message,也不得要求客户端随后补写 parent、children 或反向 Fork 列表。 + +#### Scenario: 从任意 Thread 的确定 Message Fork + +- **WHEN** 用户以有效上游 Thread、来源 Message、预期 Conversation revision 和幂等键执行 `forkThread` +- **THEN** 系统返回新 Thread、ThreadFork 和版本增量,下游上下文在来源 Message 处截止 + +#### Scenario: Fork 事务中途失败 + +- **WHEN** 来源错配、版本冲突或约束使事务失败 +- **THEN** 系统不留下孤立 Thread、半条 ThreadFork 或事件箱事件 + +### Requirement: 原子执行发送与 Generation 开始 + +发送命令 SHALL 在目标 Thread 中原子创建 Turn、用户 Message、待生成助手 Message 和 Generation,并在事务提交后才开始模型执行。命令 SHALL 使用 Thread revision 保护追加顺序;同一幂等键不得创建重复 Turn 或重复付费执行。 + +#### Scenario: 在 Thread 尾部发送消息 + +- **WHEN** 用户以当前 Thread revision 发送有效输入 +- **THEN** 系统提交新的 Turn、两条 Message 和 Generation,返回规范增量,然后启动服务端执行 + +#### Scenario: 并发发送使用旧 revision + +- **WHEN** 另一个命令已经推进 Thread,而客户端仍以旧 revision 发送 +- **THEN** 系统返回版本冲突和最新可读 revision,不创建第二个不确定顺序的 Turn + +### Requirement: 编辑与重新生成追加 Message 变体 + +编辑当前 Turn 的用户输入和重新生成助手回答 SHALL 在同一 Thread/Turn 中追加新 Message 变体与新 Generation,不得原地覆盖旧 Message。编辑历史位置若已有后续 Turn MUST 被拒绝或要求显式 Fork,不能静默重写后续上下文。 + +#### Scenario: 重新生成当前助手回答 + +- **WHEN** 用户对 Turn 的当前助手 Message 执行重新生成 +- **THEN** 系统追加新的助手 Message 和 Generation,并在合法终结后按版本更新当前助手变体 + +#### Scenario: 编辑有后续内容的历史 Turn + +- **WHEN** 用户尝试直接编辑并续跑一个已有后续 Turn 的历史输入 +- **THEN** 系统返回稳定 `fork_required` 结果,且不修改当前 Thread 历史 + +### Requirement: 持久化选择当前有效变体 + +选择变体命令 SHALL 只允许把同一 Thread/Turn、角色匹配且内容可用的 Message 设为当前有效用户或助手变体。命令 MUST 使用 Turn revision,原子更新选择并返回受影响 Turn 增量。 + +#### Scenario: 切换助手变体 + +- **WHEN** 用户以当前 Turn revision 选择同 Turn 的另一条可用助手 Message +- **THEN** 系统持久化新选择,推进 Turn revision,后续上下文使用该变体 + +#### Scenario: 拒绝跨 Turn 选择 + +- **WHEN** 命令尝试选择另一 Turn 或 Thread 的 Message +- **THEN** 系统拒绝命令且不改变当前选择 + +### Requirement: 提供 Generation 查询和 Stop 命令 + +系统 SHALL 按稳定 Generation ID 提供状态、最新 checkpoint、usage 完整度和计费状态查询,并提供幂等 Stop 命令。Stop 运行中任务 SHALL 返回已接受或当前状态;Stop 已终结任务 SHALL 返回现有终态,不得虚构版本冲突。 + +#### Scenario: 查询进行中 Generation + +- **WHEN** 所有者查询 `running` 或 `stop_requested` Generation +- **THEN** 系统返回当前状态、最新 checkpoint 版本、内容状态与可用轮询提示 + +#### Scenario: Stop 已完成 Generation + +- **WHEN** 所有者对 `completed` Generation 发出 Stop +- **THEN** 系统幂等返回 `completed` 结果,不返回无定义的冲突也不修改计费 + +### Requirement: 统一身份认证和资源授权 + +每个查询和命令 SHALL 先解析认证主体,再通过 Workspace/Project 成员关系检查目标资源。未认证请求 MUST 返回稳定未认证结果;不存在或调用方不可见的目标 MUST 使用不可枚举的未找到结果。客户端提交的 owner、workspace 或 project 字段不得覆盖服务端解析的归属。 + +#### Scenario: 伪造 owner 字段 + +- **WHEN** 已认证用户在载荷中提交另一个 owner ID +- **THEN** 系统忽略或拒绝该字段,并仅使用认证主体和成员关系解析权限 + +#### Scenario: 访问他人不可见 Message + +- **WHEN** 用户按 ID 查询无权访问的 Message 或其 Generation +- **THEN** 系统返回与资源不存在相同的未找到结果,不泄漏资源是否存在 + +### Requirement: 命令具有统一幂等和版本语义 + +每个有副作用命令 SHALL 携带稳定幂等键;会修改既有聚合的命令 SHALL 携带相应 Conversation、Thread 或 Turn 的预期 revision。相同主体、作用域、幂等键和等价载荷 SHALL 返回原结果;同键不同载荷 MUST 返回幂等冲突;版本冲突 MUST 返回最新 revision 与可重试信息。 + +#### Scenario: 网络超时后重放命令 + +- **WHEN** 客户端未收到响应并以相同幂等键重放等价命令 +- **THEN** 系统返回第一次提交的结果,不重复写实体、事件或付费任务 + +#### Scenario: 重用幂等键提交不同载荷 + +- **WHEN** 客户端用已提交的幂等键发送不同命令载荷 +- **THEN** 系统返回稳定幂等冲突且不执行新载荷 + +### Requirement: 返回规范快照和实体增量 + +首次读取 Conversation SHALL 返回带 schema/version 的规范快照;成功命令 SHALL 返回命令结果、最新作用域 revision 和仅受影响的规范实体增量。派生 children、深度、Fork 数量和可见路径可以出现在读取投影中,但 MUST 标记为可重建,不得作为后续写命令输入。 + +#### Scenario: 首次加载 Conversation + +- **WHEN** 客户端读取一条可见 Conversation +- **THEN** 系统返回稳定排序的 Conversation、Threads、ThreadForks、Turns、Messages、相关 Generation 摘要和当前有效选择 + +#### Scenario: Fork 命令只返回受影响实体 + +- **WHEN** `forkThread` 成功 +- **THEN** 响应包含新 Thread、ThreadFork、更新的 Conversation revision 和必要投影失效信息,而不是回传可写整树 + +### Requirement: 使用稳定错误分类映射传输结果 + +应用层 SHALL 返回与 HTTP 无关的稳定结果分类,传输适配器 SHALL 一致映射为状态码和机器可读错误代码。只有真实预期版本冲突、幂等冲突或当前状态冲突可以映射为 `409`;格式错误、未认证、不可见、语义验证和服务器错误 MUST 分别映射,不得用自由文本区分。 + +#### Scenario: Stop 不存在冲突条件 + +- **WHEN** Stop 命令命中已终结 Generation +- **THEN** 应用层返回幂等成功结果,传输层不得为了列出状态码而返回 `409` + +#### Scenario: Fork 使用旧 Conversation revision + +- **WHEN** `forkThread` 的预期 revision 已过期 +- **THEN** 应用层返回 `version_conflict`,HTTP 适配器映射为 `409` 并附最新 revision diff --git a/openspec/changes/add-conversation-command-api/tasks.md b/openspec/changes/add-conversation-command-api/tasks.md new file mode 100644 index 00000000..69798eb1 --- /dev/null +++ b/openspec/changes/add-conversation-command-api/tasks.md @@ -0,0 +1,43 @@ +## 1. 应用层基础契约 + +- [ ] 1.1 定义 Command/Query 输入、actor、幂等键、预期 revision、规范 delta 和稳定错误结果,不导入 HTTP 或数据库行类型。 +- [ ] 1.2 定义 `UnitOfWork`、授权策略、幂等存储、事务事件箱和规范仓储端口,并建立测试替身。 +- [ ] 1.3 实现 Workspace/Project → Conversation/Thread/Message/Generation 的不可枚举授权解析和安全错误 details。 +- [ ] 1.4 实现幂等载荷摘要、等价重放、同键异载荷冲突和命令结果恢复。 + +## 2. Conversation 与 Thread 生命周期 + +- [ ] 2.1 实现 Conversation 列表、规范快照读取以及创建 Conversation + 根 Thread 的原子命令。 +- [ ] 2.2 实现 Conversation 重命名、归档、恢复和受保护删除,覆盖运行中 Generation 冲突。 +- [ ] 2.3 实现 Thread 重命名、非根 Thread 归档和恢复,保留 Fork、后代、Message 与 Generation 历史,并要求根 Thread 使用 Conversation 归档。 +- [ ] 2.4 增加生命周期命令的权限、幂等、revision 和事务回滚测试。 + +## 3. Fork 与上下文边界 + +- [ ] 3.1 实现 `forkThread` 的来源 Message、同 Conversation、当前可用路径和预期 Conversation revision 校验。 +- [ ] 3.2 在单事务创建下游 Thread、ThreadFork、上下文边界、revision 与 `ThreadForked` outbox 事件。 +- [ ] 3.3 实现不复制上游 Message 的继承上下文读取投影,并覆盖 A → B → C 嵌套 Fork。 +- [ ] 3.4 覆盖来源错配、并发 Fork、重复幂等键和事务失败无孤立实体测试。 + +## 4. Turn、Message 变体与 Generation 命令 + +- [ ] 4.1 实现 `sendTurn`,原子创建 Turn、用户/助手 Message、Generation 和事件,并在提交后启动执行器。 +- [ ] 4.2 实现末尾用户输入编辑和助手重新生成,以新 Message/Generation 变体追加而非覆盖旧身份。 +- [ ] 4.3 对已有后续 Turn 的历史编辑返回 `fork_required`,不修改现有上下文。 +- [ ] 4.4 实现带 Turn revision 的用户/助手当前变体选择和角色/归属/内容可用性校验。 +- [ ] 4.5 实现 Generation 查询与幂等 Stop,复用规范 lifecycle 并让已终结 Stop 返回现有结果。 + +## 5. HTTP 传输适配器 + +- [ ] 5.1 定义带 `schemaVersion` 的请求、快照、delta、revision 和错误传输 schema。 +- [ ] 5.2 实现 Project/Conversation/Thread/Turn/Generation 资源路由,只调用应用 Command/Query。 +- [ ] 5.3 将 `Idempotency-Key`、`If-Match`/ETag 和认证会话映射到内部 command envelope。 +- [ ] 5.4 实现 400/401/403/404/409/422/429/500 的稳定错误映射,并验证只有真实冲突返回 409。 +- [ ] 5.5 增加接口契约测试,覆盖 owner 伪造、不可见资源、格式错误、幂等重放和 revision 冲突。 + +## 6. 事件箱与隔离验证 + +- [ ] 6.1 实现 outbox 写入和幂等 dispatcher,验证事件与规范实体在同一事务提交。 +- [ ] 6.2 增加默认关闭的规范 API 开关,拒绝新命令调用旧整树 PUT 或双写旧权威。 +- [ ] 6.3 在隔离环境跑通创建 Conversation、发送、Fork、嵌套 Fork、重新生成、选择变体、Stop、归档与恢复端到端流程。 +- [ ] 6.4 运行相关测试、`pnpm typecheck`、`pnpm openspec:validate` 和接口 schema 检查并记录结果。 diff --git a/openspec/changes/define-conversation-domain-model/.openspec.yaml b/openspec/changes/define-conversation-domain-model/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/define-conversation-domain-model/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/define-conversation-domain-model/design.md b/openspec/changes/define-conversation-domain-model/design.md new file mode 100644 index 00000000..3381b7ba --- /dev/null +++ b/openspec/changes/define-conversation-domain-model/design.md @@ -0,0 +1,404 @@ +## Context + +变更动机见 `proposal.md`,规范语言见 `specs/domain/spec.md`。 + +当前 Thread Chat 模型是一份持久化到 `branch_trees.state` JSONB 的 `ThreadTreeState` 对象。同一个 Fork 事实被重复保存在 `Thread.parentId`、`Thread.children`、`Thread.forkFromMsgId`、`Message.forks`、深度、脚注和锚点等字段中。根节点使用魔法注册项 `threads.main`。Message 变体存在于各 Thread 内部的父子图中,由 `activeLeafMessageId` 选择提示词路径。`branch_generations` 虽然是关系型辅助表,但它的 Thread 和 Message ID 指向 JSON 内部节点,数据库无法通过外键保护。服务端启动和终结 Generation 的仓储因此必须锁定并重写整棵树,再把辅助表结果协调合并回 JSON。 + +Issue #34 要求替换这套权威模型。产品讨论同时确认:Project 必须拥有一整个 Conversation,而用户看到的每一列对话都是一个 Thread。Issue #39 单独记录未来的发散与收敛行为,本变更明确不包含该能力。 + +仓库中仍有活跃 OpenSpec 变更描述旧 JSON 权威模型,特别是 `persist-thread-chat-generations` 和 `add-thread-chat-message-actions`。已经完成的历史行为仍然是有价值的证据,但这些文档不能继续作为目标架构的事实来源。 + +## Goals / Non-Goals + +**目标:** + +- 在提出任何持久化、接口或客户端迁移方案前,建立一套唯一的规范术语。 +- 定义目标逻辑实体、所有权、身份和关系不变量。 +- 收敛 Issue #34 中会改变后续数据库结构和任务顺序的决策。 +- 定义模块边界,使网页界面、未来公开接口、命令行和 MCP 适配器共用同一应用层。 +- 给出由多个可独立审查的小变更组成的明确依赖链。 +- 把遗留 `ThreadTreeState` 隔离为迁移输入,避免旧名称继续泄漏到新模块。 + +**非目标:** + +- 在本变更中创建规范化数据库表或迁移生产、开发数据。 +- 在本变更中替换现有命令接口、Generation 流、协调合并逻辑或客户端状态存储。 +- 重建 Markdown 渲染、流式展示、编辑器、画布或视觉设计。 +- 实现 Project Memory、Project Instruction、Project File、分享、公开令牌、命令行或 MCP。 +- 实现 Issue #39 中的 Thread 结果发布或 Conversation 收敛。 +- 为不存在的外部客户端保留兼容层,或永久保留旧整树写入协议。 + +## Decisions + +### D1. 规范聚合关系:Project → Conversation → Thread + +目标包含关系为: + +```text +Workspace +└── Project + ├── ProjectInstructionVersion + ├── MemoryItem + ├── File + └── Conversation + ├── rootThreadId + └── Thread[] +``` + +`Conversation` 取代 `Thread Tree`。Conversation 是用户可以整体命名、列出、归档、删除并最终分享的聚合。Thread 是 Conversation 内一条可以独立继续的对话列。 + +Workspace 一词专门表示租户、成员和授权边界。现有前端工作台在概念上改称“界面工作区”或“视图状态”,避免与授权聚合混淆。第一次持久化迁移是引入物理 Workspace 行,还是把当前所有者直接映射到默认 Project,由该迁移变更决定;这个实现选择不会改变本设计的包含契约。 + +**备选方案:Project 直接拥有 Thread。** 不采用,因为它无法稳定标识一整个分叉对话,也无法表达其标题、生命周期、加载、分享和画布投影。 + +**备选方案:保留 Thread Tree。** 不采用,因为它描述的是实现拓扑,而不是稳定产品资源,并且会与未来非 Fork 关系发生冲突。 + +### D2. Branch 是关系角色;ThreadFork 是唯一 Fork 事实 + +所有对话列都使用同一种 `Thread` 实体。主线和分支角色通过关系推导: + +```text +主 Thread = Conversation.rootThreadId +分支 Thread = 具有一个入向 ThreadFork 的 Thread +``` + +`ThreadFork` 是独立关系: + +```text +ThreadFork +├── id +├── conversationId +├── parentThreadId +├── sourceMessageId +├── childThreadId # 唯一 +├── anchor # 可选 TextAnchor 值对象 +├── createdBy +└── createdAt +``` + +必须满足以下约束: + +1. 上游 Thread、下游 Thread 和来源 Message 都能在同一 Conversation 中解析; +2. 来源 Message 属于上游 Thread; +3. 下游 Thread 最多只有一个入向 ThreadFork; +4. 根 Thread 没有入向 ThreadFork; +5. 每个非根 Thread 恰好有一个入向 ThreadFork; +6. Fork 有向关系不得形成环; +7. 派生子 Thread、深度、脚注标签和分支数量都不是可写事实。 + +锚点继续作为 ThreadFork 拥有的值对象。它没有独立权限或生命周期,因此不能仅仅为了形式上的规范化而拆成独立实体表。 + +`forkThread` 必须是一个应用命令和一个数据库事务。它创建下游 Thread 与 ThreadFork,推进最小必要版本边界并记录事务事件箱事件。客户端不能分别创建再关联这些对象。 + +**备选方案:在下游 Thread 保存 `parentThreadId + forkFromMessageId`。** 不采用,因为 Fork 自身具有来源、锚点、审计和未来展示语义。独立关系能够清楚表达唯一事实,又不会让 Thread 承担无关字段。 + +**备选方案:为快速读取保留上游 children 和 Message forks 数组。** 不允许作为权威状态。可以存在等价索引或读取模型,但它们必须可以丢弃并重建。 + +### D3. 使用显式 Turn 约束回复变体 + +目标对话图分离四类问题: + +```text +Thread 来源关系: Thread ──ThreadFork──> Thread +Turn 顺序: Thread ──> Turn ──> Turn +Message 变体: Turn ──> Message 变体 +执行尝试: Generation ──> 输入/输出 Message +``` + +逻辑实体为: + +```text +Thread +├── id +├── conversationId +├── modelId / 模型设置 +├── 生命周期 +├── revision +└── Turn[] + +Turn +├── id +├── threadId +├── 前序/顺序 +├── activeUserMessageId +├── activeAssistantMessageId +└── revision + +Message +├── id +├── threadId +├── turnId +├── 角色 +├── 内容/结构化片段 +├── 状态 +├── 变体来源(可选) +└── createdAt + +Generation +├── id +├── threadId +├── turnId +├── inputMessageId +├── outputMessageId +├── sourceAssistantMessageId(仅重新生成) +├── 意图/状态/模型 +├── 心跳/停止 +├── 用量/计费 +└── 幂等身份 +``` + +Turn 为回复变体提供显式稳定键,因此选择器和约束不再通过任意父子链推断“同一个回复位置”。Message 内容可以继续使用带版本的 JSON 或结构化片段值,因为工具片段不会仅仅由于结构化就拥有独立所有权。 + +对于当前已经支持的行为,编辑最后一条用户输入或重新生成最后一条助手输出,会在同一个 Turn 内创建 Message 变体。未来若支持编辑任意历史位置,可以创建新 Thread,而不是静默重写所有后续 Turn;该选择不属于本变更。 + +复合外键或事务检查必须保证 Turn、Message 和 Generation 的 Thread ID 一致。如果普通单列外键仍允许 Generation 组合来自不同 Thread 的 Message,就不足以保护这个不变量。 + +**备选方案:只保留 Message 有向无环图。** 不采用,因为当前错误类型正是由系统反复根据图位置推断 Turn 和变体归属,再把推断结果与另一张 Thread Fork 图组合造成的。 + +**备选方案:把一组用户和助手消息压成一行。** 不采用,因为 Message 需要稳定 ID,以支持分享、反馈、Artifact、Generation 来源和未来公开接口。 + +### D4. 当前有效变体是持久化领域状态 + +当前有效的用户或助手回复变体会决定: + +- 下一次提示词上下文; +- 后续 Turn 跟随哪一个回答; +- ThreadFork 使用的确定来源版本; +- 当前可见的 Message 所属 Artifact 和反馈操作。 + +因此,系统必须在 Turn 边界持久化该选择,并通过携带预期版本的幂等命令更新。不能根据数组顺序或仅存在于浏览器的叶节点推断它。 + +纯界面状态保持分离: + +```text +界面工作区/视图状态 +├── visibleColumns +├── foldedColumns +├── selectedThreadId +├── canvasViewport +├── openPanels +└── 临时草稿与乐观状态 +``` + +初始单所有者模型只有一份规范有效选择。如果未来多人协作需要每位成员拥有个人阅读选择,可以用独立提案增加用户级选择,而不改变 Message 身份或 ThreadFork 来源。 + +**备选方案:当前有效变体完全保留在客户端。** 不采用,因为刷新、服务端提示词组装和多客户端命令会产生分歧。 + +### D5. 每个实体只有一个事实来源;快照只是读取模型 + +规范化写模型拥有实体身份和关系。服务端可以返回为首次加载优化的 `ConversationSnapshot`,但该快照必须由规范实体组装,不能作为整包写入数据被接受。 + +```text +写入路径: +客户端命令 + → 传输校验 + → 应用命令 + → 领域不变量 + → 仓储事务 + → 规范实体增量 + 事务事件箱 + +读取路径: +仓储/读取模型 + → ConversationSnapshot + → 客户端规范化 + → 按 ID 定位的选择器 +``` + +`branch_trees.state` 可以在迁移期暂时作为迁移输入或非权威导出、缓存。任何缓存都必须可丢弃、带版本,并可从规范数据行重建。它不能参与冲突处理,也不能覆盖规范数据行。 + +**备选方案:继续让 JSONB 作为权威来源,再增加更多辅助表。** 不采用,因为辅助表 ID 仍然无法强制保证 Thread/Message 归属,每个新功能也都会增加一条协调合并路径。 + +### D6. 稳定公开身份先于分享、接口、命令行和 MCP + +Workspace、Project、Conversation、Thread、Turn、Message、Generation 和 File 使用不携带角色语义的不透明稳定 ID。公开适配器不得暴露内存数组下标、`main`、树内局部计数器或浏览器复合键作为资源身份。 + +授权包含关系与 Fork 来源关系保持分离: + +```text +授权包含关系: +Workspace → Project → Conversation → Thread → Message + +Conversation 来源关系: +上游 Thread + 来源 Message → ThreadFork → 下游 Thread + +执行关系: +Generation → Turn / Message / 用量 +``` + +未来 REST、命令行和 MCP 适配器调用与网页界面相同的应用命令和查询。它们不能直接查询数据库表,也不能再实现一套所有权规则。分享和令牌权限范围属于独立提案,但本设计确保每个未来分享目标都具有可寻址实体。 + +### D7. Project 资产拥有可跨 Conversation 使用的持久内容 + +Project 不只是 Conversation 文件夹。目标扩展点为: + +```text +ProjectInstructionVersion +├── projectId +├── version/content +└── 审计元数据 + +MemoryItem +├── projectId +├── content/status/version +└── 来源记录 + +File +├── projectId +├── FileVersion[] +└── 来源与引用关系 +``` + +Message 或 Generation 记录自己创建或使用了某个资产,但不能拥有一份可复用 File 的唯一副本。搜索切块、向量和摘要是派生索引,不是规范 File 或 MemoryItem。 + +本变更只定义所有权和术语,不增加这些产品能力,也不强迫现有 Markdown Artifact 在专门的 Project 资产变更之前迁移。 + +**备选方案:所有输出只保存在 Message JSON 内。** 对需要持久复用的文件不采用,因为删除或归档一个 Thread 不应控制无关 Project 资产的生命周期。 + +### D8. 模块边界服从策略,不服从框架目录 + +目标依赖方向为: + +```text +界面组件 + ↓ +客户端应用/状态存储 ──→ 传输契约 + ↓ + 应用命令/查询 + ↓ + 领域模型 + ↑ + 持久化实现 +``` + +规则如下。 + +#### 领域层 + +- 只包含纯 TypeScript 实体、ID、值对象、不变量和派生规则。 +- 不导入 React、DOM、Next.js 请求、Drizzle 数据表或 HTTP 状态。 +- TextAnchor 可以继续作为领域值对象,即使某个适配器会在 Markdown DOM 中解析它。 + +#### 应用层 + +- 包含命令、查询、所有者与权限策略、事务边界、幂等和冲突结果。 +- 依赖仓储、时钟、ID、事件和模型执行端口。 +- `forkThread`、`createTurn`、`regenerate`、`selectVariant`、`archiveConversation`、`stopGeneration` 在概念上属于此层。 + +#### 持久化层 + +- 包含 Drizzle 结构、SQL 约束、仓储适配器、迁移和事务事件箱。 +- 实现应用层端口;不得把数据库行形状泄漏到组件或路由。 + +#### 接口与传输层 + +- 负责身份认证与会话提取、请求结构、错误与状态映射、响应序列化。 +- 不得在路由局部分支中重复实现领域校验。 + +#### 客户端状态层 + +- 保存规范化的规范实体、可丢弃派生索引和乐观命令状态。 +- 应用服务端返回的规范增量;不得自行修改实体关系。 + +#### 界面组件层 + +- 消费按 ID 定位的选择器和命令。 +- 列、画布和 Message 操作组件不能写入 `children`、Fork 反向链接或 Message 所有权。 + +即使物理目录按阶段迁移,也必须遵守这条依赖分离原则。 + +### D9. 隔离遗留术语,不提供永久全局别名 + +迁移期间,专用适配器可以执行以下映射: + +```text +遗留模型 规范模型 +ThreadTreeState ConversationSnapshot / 遗留导入数据 +branch_trees.id Conversation ID 候选值 +threads.main Conversation.rootThreadId 目标 +Thread.parentId ThreadFork.parentThreadId +Thread.forkFromMsgId ThreadFork.sourceMessageId +Thread.children 派生 ThreadFork 索引 +Message.forks 派生 ThreadFork 索引 +Thread.activeLeafMessageId Turn 有效选择/读取投影 +branch_generations.* IDs Generation 外键 +``` + +遗留名称可以保留在名称明确的迁移、导入模块和历史迁移文件中。新的领域层、应用层和接口层代码不能引入 `type Conversation = ThreadTreeState` 一类别名;这种做法只是改名,没有改变所有权。 + +已归档的 OpenSpec 变更继续作为历史记录。依赖旧权威模型的活跃变更必须标记为被替代、受阻或依赖后续迁移,不能通过修改文档假装已完成任务已经符合新模型。 + +### D10. 变更依赖链 + +工作按架构接缝拆分,而不是按任意文件数量拆分: + +```text +define-conversation-domain-model (本变更) + │ + ▼ +normalize-conversation-persistence (实体、约束、仓储、 + │ 迁移与读取快照) + ▼ +migrate-generation-lifecycle (基于真实 Thread/Turn/Message 外键的 + │ 启动、停止、恢复、用量和计费) + ▼ +add-conversation-command-api (实体命令、增量、错误与幂等契约; + │ 退出整树 PUT) + ▼ +normalize-conversation-client-state (实体缓存、选择器、列、画布和 + │ Message 操作) + ▼ +retire-thread-tree-authority (切换、审计、删除遗留写入、 + 适配器和重复事实) +``` + +基础模型完成后可以并行开展: + +```text +normalize-conversation-persistence + ├── project-context-assets (未来独立提案) + └── public-data-access (未来接口、令牌、命令行、MCP 提案) + +retire-thread-tree-authority + └── conversation-convergence (Issue #39,未来提案) +``` + +每个后续变更只负责一个权威边界,并拥有独立验证目标: + +1. 持久化变更证明数据库不变量和规范快照读取正确; +2. Generation 变更证明生命周期和财务语义不再依赖 JSON 协调合并; +3. 接口变更证明只有服务端可以执行变更,并保证幂等; +4. 客户端变更证明规范接口能够产生正确投影; +5. 清理变更证明系统中不再存在遗留权威路径。 + +PR #30 保持 Draft;在 Generation 迁移解决其对 #34 的依赖前,不能视为符合最终架构。已经正确的流式行为可以保留,但 JSON 与辅助表之间的协调机制不是目标契约。 + +## Risks / Trade-offs + +- **[风险] 术语变更影响大量活跃文档,可能产生改了一半的模型。** → 把 `domain` 规范设为唯一正式词汇表,发布遗留映射,并禁止在迁移范围之外新增 `ThreadTree` 术语。 +- **[风险] 显式 Turn 增加一个实体和一组约束。** → 它可以消除重复运行时推断,并把重新生成和编辑不变量限制在局部;Message 内容继续使用灵活 JSON,避免过度拆分结构化片段。 +- **[风险] Conversation 与 Thread 标题可能成为重复事实。** → Conversation 拥有聚合与导航标题。非根 Thread 可以拥有本地列标题;根列使用 Conversation 标题,除非未来产品需求证明必须存在独立根标题。 +- **[风险] 引入 Project 和 Workspace 扩大 #34。** → 本变更只定义包含关系。物理 Workspace、Memory、Instruction、File 和分享能力继续使用独立提案。 +- **[风险] 分阶段迁移会暂时同时存在遗留表示和规范表示。** → 每个阶段只允许一个写入权威,把转换隔离在一个适配器中,增加一致性与审计检查,并设立强制清理变更。 +- **[风险] 前后端一次性切换可能遗留运行中的 Generation。** → 在接口和客户端切换前先迁移生命周期;切换前要求没有活跃 Generation 或具备兼容排空规则,并在每个阶段保留回滚边界。 +- **[风险] 现有 OpenSpec 变更依据已被替代的假设宣称任务完成。** → 明确记录依赖和被替代范围;不静默保留错误声明,也不重写已归档历史。 +- **[风险] 稳定公开 ID 被误认为已经具备授权。** → 在应用命令中保留包含关系和权限检查;来源关系本身永远不授予访问权限。 + +## Migration Plan + +本文定义迁移计划,实际执行由 D10 中的后续变更完成。 + +1. **落地术语与架构决策。** 应用修改后的 `domain` 规范,更新当前架构和术语引用,发布遗留映射,并把冲突的活跃变更标记为依赖本变更或已被替代。 +2. **在读取路径后方引入规范化持久化。** 创建规范数据行和约束;检查真实数据后决定重置还是一次性迁移;生成 `ConversationSnapshot` 读取模型。在一致性检查通过前,不启用规范写入。 +3. **迁移 Generation 所有权。** 把执行尝试、Message 输出、停止与恢复、用量和计费绑定到真实外键,并解决 PR #30 审查中已知的结算、部分结果和过期收敛语义。 +4. **迁移写入命令。** 启用具有幂等和版本冲突语义的规范实体命令。已经迁移的 Conversation 不再接受整棵 Conversation 写入。 +5. **迁移客户端。** 规范化快照,迁移选择器和界面命令,再从浏览器删除防抖保存和协调合并行为。 +6. **切换并清理。** 审计数据、排空活跃 Generation、禁用遗留写入、删除重复事实,最终按照已批准的持久化方案删除 `branch_trees.state` 或将其降级为非权威数据。 + +回滚按阶段进行: + +- 启用规范生产写入前,可以关闭新的读取模型和数据表,继续使用未改变的遗留权威; +- 一旦规范模型接受生产写入,不得把数据反向导出到 ThreadTree 后恢复旧权威;应保持规范数据库,通过回滚兼容客户端/应用、进入只读、恢复规范备份或前滚修复处理; +- 只有当命令接口仍兼容且不会丢失仅存在于规范模型的变更时,才能回滚客户端; +- 破坏性删除遗留列必须最后执行,并以已验证备份和审计报告为前提。 + +目前没有会改变本提案规范、实体选择或任务顺序的未决问题。`normalize-conversation-persistence` 负责取得真实数据审计证据;数据重置还是一次性导入及其部署批次由 `retire-thread-tree-authority` 在目标环境测量后以 ADR 决定。 diff --git a/openspec/changes/define-conversation-domain-model/proposal.md b/openspec/changes/define-conversation-domain-model/proposal.md new file mode 100644 index 00000000..8e4bac29 --- /dev/null +++ b/openspec/changes/define-conversation-domain-model/proposal.md @@ -0,0 +1,33 @@ +## Why + +当前 `Thread Tree / MainThread / ForkedThread` 术语把产品概念绑定到整树 JSON 和当前界面拓扑,也缺少 Project 与单列 Thread 之间的 Conversation 聚合边界。Issue #34 已要求规范化 Thread Chat;在继续设计数据库、命令接口和前端状态存储前,必须先建立唯一、稳定的领域语言与目标实体关系,否则后续变更会继续用同一个词描述不同事物。 + +## What Changes + +- **破坏性变更**:以 `Conversation` 取代 `Thread Tree` 作为完整可分叉对话的领域名称;旧名称只允许出现在迁移说明、遗留适配器或具体树形投影中。 +- **破坏性变更**:统一采用 `Project → Conversation → Thread`。每一列对话都是 Thread;Main Thread 与 Branch Thread 只是由关系推导的角色,不是两套实体,也不持久化 `kind` 或魔法 `id = "main"`。 +- 将 Fork 明确定义为服务端动作,并将 `ThreadFork` 定义为上游 Thread、来源 Message 与下游 Thread 之间的唯一来源关系;`children[]`、`Message.forks[]` 只能是派生索引。 +- 引入显式 `Turn` 作为用户编辑、助手重新生成和回复变体的归属边界;`Message` 与 `Generation` 必须具有稳定身份并严格属于同一 Thread/Turn。 +- 明确当前有效变体会决定后续提示词与 Fork 来源,因此是持久化领域选择;列、折叠、画布和面板等仍属于界面工作区状态。 +- 定义 Workspace、Project、Conversation、Thread、ThreadFork、Turn、Message、Generation、File、MemoryItem 与 ProjectInstruction 的规范术语、生命周期边界和依赖关系。 +- 明确领域层、应用层、持久化层、接口与传输层、客户端状态层和界面组件层的模块职责,禁止 React、HTTP 或 Drizzle 规则渗入纯领域层。 +- 建立从当前 `ThreadTreeState`、`branch_trees`、`branch_generations` 到目标模型的迁移地图,并把后续落地拆为规范化持久化、Generation 生命周期、命令接口、前端规范化状态存储、切换清理等独立变更。 +- 分支结论回流与收敛机制保持在 Issue #39,Project Memory、File、Instruction 的完整产品能力分别后续提案,均不扩大本变更的实现范围。 + +## Capabilities + +### New Capabilities + +- 无。本变更先修正已有 `domain` 能力规范,避免在统一术语前复制出第二套领域规范。 + +### Modified Capabilities + +- `domain`:用 Project、Conversation、Thread、ThreadFork、Turn、Message 与 Generation 的统一语言替换 Thread Tree 模型,并补充角色推导、身份归属、当前有效变体、Project 资产边界和模块依赖要求。 + +## Impact + +- 规范与文档:`openspec/specs/domain/spec.md`、所有仍把 `Thread Tree` 当作产品实体的活跃变更、架构说明和术语引用。 +- 领域代码:`lib/thread-chat/domain/` 中的类型、消息图、重新生成、选择器与遗留 `ThreadTreeState` 边界。 +- 服务端:后续将影响 `branch_trees`、`branch_generations`、Artifact、Feedback、Title 的归属、仓储和事务命令;本变更只建立目标契约与迁移边界,不执行数据库切换。 +- 前端:后续将影响整树状态存储、列与画布选择器、Message 操作与界面工作区状态;本变更不重写现有渲染、流式界面或编辑器。 +- 依赖:后续变更必须依次引用本变更的术语与架构决策记录;Issue #39 在基础模型稳定后再继续设计。 diff --git a/openspec/changes/define-conversation-domain-model/specs/domain/spec.md b/openspec/changes/define-conversation-domain-model/specs/domain/spec.md new file mode 100644 index 00000000..8234f64c --- /dev/null +++ b/openspec/changes/define-conversation-domain-model/specs/domain/spec.md @@ -0,0 +1,162 @@ +## MODIFIED Requirements + +### Requirement: 使用统一的核心术语 + +系统、OpenSpec、接口契约和项目文档 SHALL 使用以下术语: + +- **Workspace**:成员、租户与授权的最外层边界;它可以拥有多个 Project,不得与前端列布局或画布状态混称。 +- **Project**:长期目标、规则、记忆、文件与 Conversation 的上下文和资产边界。 +- **Conversation**:Project 中一整个可分叉对话的聚合,拥有稳定 ID、唯一根 Thread、标题与生命周期;它取代旧领域术语 Thread Tree。 +- **Thread**:Conversation 中一条可独立继续的对话上下文,也是界面中的一栏;所有根与非根对话列使用同一种 Thread 实体。 +- **Main Thread(主 Thread)**:Conversation 的 `rootThreadId` 指向的 Thread,是关系推导角色,不是独立实体类型。 +- **Branch Thread(分支 Thread)**:具有入向 ThreadFork 的非根 Thread,是相对于上游 Thread 的关系角色,不是 Branch 实体或另一套 Thread 类型。 +- **Fork**:从一个 Thread 的确定 Message 创建另一个 Thread 的服务端动作。 +- **ThreadFork**:记录上游 Thread、来源 Message 与下游 Thread 的唯一来源关系。 +- **Turn**:Thread 中一个逻辑交互位置,用于约束用户编辑、助手重新生成和回复变体的归属范围。 +- **Message**:属于一个 Thread 和 Turn、具有稳定 ID 的用户、助手或明确类型的上下文内容。 +- **Generation**:在一个 Thread/Turn 中生成或更新助手输出的一次模型执行尝试;它不是 Message 或 Thread。 +- **File**:Project 内具有独立生命周期并可跨 Conversation 复用的持久资产。 +- **MemoryItem**:Project 内具有来源、状态与版本的记忆条目,不是无边界增长的文本字段。 +- **ProjectInstruction**:Project 级、可版本化的指令;Generation 可以记录实际使用的版本。 +- **Artifact**:由 Message 或 Generation 产生的结构化成果;当其需要独立复用和生命周期时,SHALL 通过 Project 资产及来源记录表达,而不是只存在于消息内部。 +- **Title**:用于识别 Project、Conversation 或 Thread 的人类可读标签。 +- **UI Workspace(界面工作区)**:列、折叠、画布视口、临时选择和打开面板等客户端展示状态;它不是 Workspace 领域实体,也不是 Conversation 事实源。 + +`Thread Tree`、`MainThread` 和 `ForkedThread` 只允许出现在遗留迁移说明或实现适配器中,不得继续作为新产品能力、接口或领域实体的规范名称。 + +#### Scenario: 描述非根对话列 + +- **WHEN** 产品、接口或代码需要描述由 Fork 创建的非根对话列 +- **THEN** 将其描述为 Thread,并在需要强调关系时称为分支 Thread,同时使用 ThreadFork 描述来源关系 + +#### Scenario: 描述完整分叉对话 + +- **WHEN** 产品或接口需要表示一个根 Thread 及其全部派生 Thread 的整体 +- **THEN** 使用 Conversation,而不是 Thread Tree 或 Thread 表示该聚合 + +### Requirement: 明确标题的归属与优先级 + +系统 SHALL 明确 Title 归属 Project、Conversation 或 Thread。Conversation 的自动 Title SHALL 由其根 Thread 的内容派生并作为默认导航标题;根 Thread 列头 SHALL 使用 Conversation Title,避免另存一份同义标题。非根 Thread 可以拥有描述对应列的本地 Title。用户为 Conversation 设置的自定义 Title SHALL 在 Conversation 级导航和根 Thread 列头展示中优先于自动 Title,同时系统 SHALL 保留自动 Title 作为机器派生信息。 + +#### Scenario: Conversation 自动标题与用户重命名并存 + +- **WHEN** Conversation 已由根 Thread 生成自动 Title,且用户随后为该 Conversation 设置自定义 Title +- **THEN** Conversation 导航和根 Thread 列头展示用户自定义 Title,并保留自动 Title + +#### Scenario: 分支 Thread 使用独立标题 + +- **WHEN** 非根 Thread 生成或设置自己的 Title +- **THEN** 该 Title 只描述并标识该 Thread,不覆盖所属 Conversation 的 Title + +## ADDED Requirements + +### Requirement: 维护 Project、Conversation 与 Thread 的包含不变量 + +每个 Conversation SHALL 恰好属于一个 Project,并恰有一个属于自身的根 Thread。每个 Thread SHALL 恰好属于一个 Conversation;Thread 的归属创建后不得改变。根与非根角色 SHALL 由 Conversation 根引用及 ThreadFork 关系推导,系统不得使用持久化 `kind`、`root` 布尔值或具有业务含义的魔法 ID 作为第二事实源。 + +#### Scenario: 创建 Conversation + +- **WHEN** 系统在 Project 中创建 Conversation +- **THEN** 系统为 Conversation 和根 Thread 分配稳定且无业务含义的 ID,并使 `rootThreadId` 指向属于该 Conversation 的 Thread + +#### Scenario: 拒绝跨 Conversation 的 Thread 归属 + +- **WHEN** 一个操作试图把既有 Thread 重新归属到另一个 Conversation +- **THEN** 系统拒绝该操作且不改变两个 Conversation + +### Requirement: 使用 ThreadFork 维护唯一 Fork 来源 + +每个非根 Thread SHALL 恰有一个入向 ThreadFork;根 Thread SHALL 没有入向 ThreadFork。ThreadFork SHALL 指向同一 Conversation 中的上游 Thread、属于上游 Thread 的确定来源 Message,以及唯一的下游 Thread。Fork 来源不得同时以可写 `children`、Message 反向链接或下游来源字段重复保存;这些集合和计数只能是可重建投影。 + +#### Scenario: 从确定消息创建嵌套 Fork + +- **WHEN** 用户从任意 Thread 的确定 Message 创建新 Thread +- **THEN** 系统原子创建下游 Thread 与唯一 ThreadFork,并记录该 Message 所属 Thread 为上游 Thread + +#### Scenario: 拒绝来源消息错配 + +- **WHEN** ThreadFork 的来源 Message 不属于声明的上游 Thread,或任一参与实体不属于同一 Conversation +- **THEN** 系统拒绝该关系且不创建孤立的下游 Thread + +#### Scenario: 派生 Thread 数量来自投影 + +- **WHEN** 界面显示某条 Message 的直接派生 Thread 数量 +- **THEN** 系统从 ThreadFork 查询或派生该数量,而不是读取独立可写的 Message fork 列表 + +### Requirement: 用 Turn 约束 Message 变体 + +每个 Turn SHALL 恰好属于一个 Thread,每个 Message SHALL 恰好属于一个 Thread 和一个 Turn。用户编辑与助手重新生成产生的新 Message 变体 SHALL 留在目标 Thread/Turn 内,不得改变来源 Thread、祖先 Thread 或后代 Thread 的 Message。实现可以保留同一 Turn 内的 Message 因果边,但不得使用 Message 父子边表达 ThreadFork。 + +#### Scenario: 在分支 Thread 中重新生成 + +- **WHEN** 用户对分支 Thread 的助手 Message 执行重新生成 +- **THEN** 系统只在该 Message 所属 Thread/Turn 中追加助手回复变体,其他 Thread 的变体与派生数量保持不变 + +#### Scenario: 拒绝跨 Thread 变体 + +- **WHEN** 操作试图把另一个 Thread 的 Message 加入当前 Turn 的回复变体 +- **THEN** 系统拒绝该操作且不改变当前有效变体 + +### Requirement: 保证 Generation 的执行身份与归属 + +每个 Generation SHALL 具有稳定的执行尝试 ID,并恰好属于一个 Thread 和 Turn。其输入、输出及重新生成来源 Message SHALL 属于相同 Thread,且输入输出关系必须与该 Turn 一致。Generation ID SHALL 只承担执行幂等、状态、停止、恢复、用量与结算身份,不得替代 Thread、Turn 或 Message 的实体身份。 + +#### Scenario: Generation 完成到目标 Message + +- **WHEN** 一个 Generation 成功、停止或失败并产生可持久化结果 +- **THEN** 系统只更新该 Generation 绑定的 Thread/Turn/Message,并保留确定的执行尝试、状态和用量归属 + +#### Scenario: 拒绝跨 Thread Generation + +- **WHEN** Generation 声明的输入或输出 Message 不属于其 Thread +- **THEN** 系统在模型调用或结算前拒绝该 Generation + +### Requirement: 持久化影响后续上下文的当前有效变体 + +Thread/Turn 的当前有效变体会决定后续提示词、Fork 来源与继续生成路径,系统 SHALL 将其作为有版本的领域选择持久化,并将切换限制在同一 Thread/Turn。列可见性、折叠、画布视口、临时选择与面板状态 SHALL 保持在界面工作区,且不得改变实体归属或当前有效变体。 + +#### Scenario: 切换变体后继续对话 + +- **WHEN** 用户在一个 Turn 内切换当前有效助手回复变体并继续发送消息 +- **THEN** 后续 Generation 使用持久化后的有效路径,刷新后仍保持相同选择 + +#### Scenario: UI 布局不改变领域选择 + +- **WHEN** 用户折叠列、切换画布或关闭面板 +- **THEN** Conversation、Thread、Turn、Message 归属和当前有效变体均保持不变 + +### Requirement: 维护 Project 级上下文与资产边界 + +Conversation SHALL 通过 Project 获得可复用上下文和资产边界。ProjectInstruction、MemoryItem 与 File 在存在时 SHALL 归属 Project 并具有独立身份;Message 和 Generation SHALL 通过明确引用或来源记录使用它们,不得把其唯一事实仅嵌入某棵 Conversation JSON。一个 Thread 中产生的 File 在被保存为 Project 资产后 SHALL 可由同一 Project 的其他 Conversation 引用,而不需要复制文件实体。 + +#### Scenario: 跨 Conversation 使用 Project File + +- **WHEN** 一个 Thread 生成的文件被保存为 Project File,随后同一 Project 的另一个 Conversation 引用该文件 +- **THEN** 两个 Conversation 引用同一个稳定 File,并保留原始 Message/Generation 来源记录 + +#### Scenario: Generation 记录 Project Instruction 版本 + +- **WHEN** Project Instruction 在两次 Generation 之间发生修改 +- **THEN** 每次 Generation 都能识别自己实际使用的指令版本,旧回答不会被解释为使用了新版本 + +### Requirement: 分离领域事实、应用命令、传输、持久化与界面状态 + +领域事实 SHALL 独立于 React 组件、HTTP 路由、数据库对象关系映射与客户端布局存在。所有改变 Conversation、Thread、ThreadFork、Turn、Message、Generation 或当前有效变体的操作 SHALL 通过有明确输入、权限、幂等和冲突语义的应用命令执行。服务端可以返回组装后的只读快照,但客户端不得提交可覆盖整个 Conversation 事实状态的整树 JSON。 + +#### Scenario: 客户端执行 Fork + +- **WHEN** 客户端请求从确定 Message 创建 Thread +- **THEN** 服务端命令校验归属并原子提交规范实体增量,客户端只合并受影响实体 + +#### Scenario: 拒绝整 Conversation 覆盖 + +- **WHEN** 客户端提交包含全部 Thread、Message 与 Fork 的可写整树快照以覆盖服务端事实 +- **THEN** 系统拒绝或不提供该权威写入路径,并要求使用实体级命令 + +## REMOVED Requirements + +### Requirement: 维护线程树的层级不变量 + +**Reason**:该 Requirement 把完整对话命名为 Thread Tree,并以 MainThread/ForkedThread 表述两类节点,无法表达 Project → Conversation → Thread 聚合边界,也继续暴露当前整树实现形状。 + +**Migration**:使用新增的“维护 Project、Conversation 与 Thread 的包含不变量”和“使用 ThreadFork 维护唯一 Fork 来源”Requirements。现有 `ThreadTreeState`、MainThread 与 ForkedThread 引用迁移到 Conversation、Thread 角色及 ThreadFork;遗留名称仅可保留在明确标记的迁移适配器中。 diff --git a/openspec/changes/define-conversation-domain-model/tasks.md b/openspec/changes/define-conversation-domain-model/tasks.md new file mode 100644 index 00000000..9f6c3d76 --- /dev/null +++ b/openspec/changes/define-conversation-domain-model/tasks.md @@ -0,0 +1,37 @@ +## 1. 固化规范术语与依赖 + +- [ ] 1.1 将 `domain` 能力规范归档为 Project、Conversation、Thread、ThreadFork、Turn、Message、Generation、File、MemoryItem 与 ProjectInstruction 的唯一正式术语来源,并核对每个角色、身份和包含关系与增量规范一致。 +- [ ] 1.2 新增一份面向开发者的“遗留模型 → 规范模型”迁移表,覆盖 `ThreadTreeState`、`threads.main`、`parentId/children`、`forkFromMsgId/Message.forks`、`activeLeafMessageId`、Artifact 和 `branch_generations` 引用,明确哪些字段只允许存在于迁移适配器。 +- [ ] 1.3 审计 README、CLAUDE、活跃 OpenSpec 和 `lib/thread-chat` 注释中的领域术语;把新设计文档改用 Conversation、Thread、ThreadFork,并对必须保留的历史 `Thread Tree` 引用加上遗留标识,不改写已归档变更的历史事实。 +- [ ] 1.4 在 `persist-thread-chat-generations`、`add-thread-chat-message-actions`、`add-branch-tree-persistence` 及其他仍依赖整树权威模型的活跃变更中记录其与本变更及后续迁移变更的依赖或被替代范围,禁止继续声称整树 JSON 是最终架构。 + +## 2. 建立纯领域契约 + +- [ ] 2.1 在 `lib/thread-chat/domain/` 下建立不依赖 React、Next.js、HTTP 或 Drizzle 的规范 ID 与实体契约,定义 Project、Conversation、Thread、ThreadFork、Turn、Message 和 Generation 的稳定身份与归属字段。 +- [ ] 2.2 实现主 Thread 与分支 Thread 的角色推导,以及 Conversation/ThreadFork 拓扑校验;拒绝魔法 `main` 身份、重复入向 Fork、跨 Conversation Fork、来源 Message 与上游 Thread 错配和 Fork 环。 +- [ ] 2.3 实现 Turn/Message/Generation 归属校验,拒绝跨 Thread 回复变体、跨 Turn 输出和 Generation 输入输出错配,同时允许 Message 的内容与结构化片段保持为可版本化值对象。 +- [ ] 2.4 建立当前有效变体的领域选择契约和版本冲突结果,确保变体只能在同一 Thread/Turn 切换,并与可见列、折叠列、画布视口等界面工作区类型隔离。 +- [ ] 2.5 明确 Conversation/Thread Title 解析契约:Conversation 独占聚合导航标题,根 Thread 列头使用 Conversation Title,非根 Thread 可以使用本地 Title,避免重复持久化同义根标题。 + +## 3. 隔离遗留模型 + +- [ ] 3.1 将当前 `ThreadTreeState`、`Fork`、`threads.main` 和整树解析器明确移动或标记为遗留边界;新的规范领域模块不得通过类型别名把旧整树对象伪装成 Conversation。 +- [ ] 3.2 新增单向、只读的“遗留树 → 规范 Conversation 快照”映射,集中处理根 Thread、Fork、Turn 变体和 Artifact 来源推导;禁止提供“规范模型 → 遗留模型”的权威写回路径。 +- [ ] 3.3 为最小根 Conversation、主线 → A → B 嵌套 Fork、B 重新生成回复变体、非法跨 Thread Generation 和重复 Fork 来源建立确定性固定样例及验证脚本,证明规范投影不会把 B 的回复变体投影到 A。 +- [ ] 3.4 为旧字段建立允许列表或等价审计检查,使新增领域层和应用层代码中的 `ThreadTreeState`、`threads.main`、可写 `children` 与 `Message.forks` 引用能够在代码审查和持续集成中被发现。 + +## 4. 准备小步迁移链 + +- [ ] 4.1 为 `normalize-conversation-persistence` 建立后续变更输入清单:逻辑表、复合外键、不变量、真实数据审计、重置或迁移决策、规范快照和回滚边界。 +- [ ] 4.2 为 `migrate-generation-lifecycle` 建立后续变更输入清单:真实 Thread/Turn/Message 外键、启动、停止、心跳、过期恢复、部分结果、用量不可得、计费幂等以及 PR #30 剩余语义缺口。 +- [ ] 4.3 为 `add-conversation-command-api` 建立后续变更输入清单:Fork、创建 Turn、重新生成、选择变体、归档、停止等命令,以及所有者校验、幂等、预期版本、错误码、实体增量与事务事件箱。 +- [ ] 4.4 为 `normalize-conversation-client-state` 建立后续变更输入清单:规范实体、可重建索引、按 ID 定位的选择器、界面工作区、乐观命令和列、画布、Message 操作迁移。 +- [ ] 4.5 为 `retire-thread-tree-authority` 建立后续变更输入清单:数据审计、运行中 Generation 排空、禁用整树 PUT、删除协调合并与防抖保存、删除重复 Fork 事实和遗留适配器的完成条件。 +- [ ] 4.6 将 Project Memory、File、Instruction、公开接口、令牌、命令行、MCP 和 Issue #39 Conversation 收敛列为基础迁移后的独立提案,明确它们不阻塞 #34 当前链路。 + +## 5. 验证与交接 + +- [ ] 5.1 运行 `pnpm typecheck`,修复规范领域契约、遗留适配器和固定样例的全部类型错误。 +- [ ] 5.2 运行 `pnpm openspec:validate`,确保领域增量规范、依赖说明和所有活跃变更严格验证通过。 +- [ ] 5.3 检查 Git 差异和术语审计结果,确认未修改无关功能、未把界面工作区混入领域实体、未产生第二个 Fork 事实源,也未把 Issue #39 的收敛机制带入本变更。 +- [ ] 5.4 在 Issue #34 回填本变更路径、最终架构决策记录和后续变更依赖链,确保实现者可以从本提案继续而不重新猜测核心数据模型。 diff --git a/openspec/changes/migrate-generation-lifecycle/.openspec.yaml b/openspec/changes/migrate-generation-lifecycle/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/migrate-generation-lifecycle/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/migrate-generation-lifecycle/design.md b/openspec/changes/migrate-generation-lifecycle/design.md new file mode 100644 index 00000000..85ef6295 --- /dev/null +++ b/openspec/changes/migrate-generation-lifecycle/design.md @@ -0,0 +1,157 @@ +## Context + +规范 Conversation 持久化提供了真实 Thread、Turn 与 Message,但现有 Generation 仍通过 `branch_generations` 协调整树 JSON、浏览器保存和流式响应。该结构把“执行是否结束”“内容是否可用”“usage 是否完整”“账单是否已结算”混成一组隐式分支,导致 Stop、崩溃恢复和并发终结无法用数据库约束证明正确。 + +本设计位于规范持久化与命令 API 之间:先建立可独立验证的应用服务、仓储和执行端口,不在本阶段暴露新的公开路由或迁移客户端。 + +## Goals / Non-Goals + +**目标:** + +- 让 Generation 通过真实外键绑定 Thread、Turn 和 Message。 +- 让服务端在浏览器断线后继续执行并留下可恢复 checkpoint。 +- 精确定义 Stop、失败、superseded、僵尸任务收敛和 exactly-once 终结。 +- 分离 Generation 状态、Message 内容状态、usage 完整度与计费状态。 +- 修复 PR #30 审查中已发现的部分 usage、partial 覆盖、联网活动和 Stop 状态漂移。 + +**非目标:** + +- 设计公共 HTTP 命令、错误码或客户端 Store。 +- 正式切换生产流量或删除 `branch_generations`。 +- 引入跨 Conversation Agent 工作流、任务队列产品或 Project 资产模型。 +- 把每个流片段、token 或工具事件永久保存为独立关系表。 + +## Decisions + +### D1. Generation 是执行实体,不是 Message 状态字段 + +新增 `conversation_generations`,核心字段为: + +```text +id +owner_id / workspace_id / project_id +conversation_id / thread_id / turn_id +input_message_id / output_message_id +intent / attempt / idempotency_key +status / content_state +checkpoint_version / checkpoint +usage / usage_completeness / billing_status +lease_owner / heartbeat_at / stop_requested_at +started_at / finished_at / created_at / updated_at +``` + +所有关联使用规范表的复合外键,确保同一 Conversation/Thread/Turn。一个 Message 可以保留多个历史 Generation 尝试,但重新生成默认创建新的输出 Message 变体,不覆盖旧身份。 + +备选方案是继续把 Generation 嵌入 Message 或整树 JSON;这会使执行租约、重试历史和计费幂等无法独立约束,因此拒绝。 + +### D2. 四组状态正交建模 + +```text +Generation.status: + running | stop_requested | completed | stopped | failed | superseded + +Message.contentState: + pending | streaming | complete | incomplete | failed + +usageCompleteness: + complete | partial | unavailable + +billingStatus: + pending | settled | usage_unavailable | not_billable +``` + +`completed` 通常对应 `complete`,但 `stopped`/`failed` 可以对应 `incomplete`;`settled` 只允许搭配 `complete` usage。数据库 CHECK 和应用状态机同时执行这些组合约束。 + +备选方案是从 Generation 终态推导其余状态;这正是当前无法表达“失败但有可用部分内容”和“有部分 usage 但不可完整结算”的根因,因此拒绝。 + +### D3. 开始事务先于任何付费调用 + +应用服务 `startGeneration` 在单个事务中: + +1. 验证所有者、实体归属、意图和相关 Conversation/Thread/Turn 的预期 revision; +2. 追加或确认输出 Message; +3. 创建带幂等键、`running` 状态和初始 checkpoint 的 Generation; +4. 提交后才把执行描述交给模型运行器。 + +相同幂等键和规范化请求摘要返回原 Generation;键相同但摘要不同返回幂等冲突。事务失败没有付费副作用。 + +### D4. 服务端执行所有权与 HTTP 响应解耦 + +执行器独立消费完整模型流。HTTP/SSE 只订阅或转发进度,不把 `request.signal` 作为模型执行的终止信号。进程内 AbortController 注册表只提供低延迟 Stop;持久化 `stop_requested`、心跳和租约支持跨实例观察与恢复。 + +沿用当前经过验证的短周期 Stop 观察和心跳思路,但具体间隔作为运行配置与测试参数,不写死进领域契约。浏览器轮询频率属于后续客户端 change。 + +### D5. checkpoint 是服务端可恢复事实 + +执行器按节流策略把以下规范投影保存到 JSON checkpoint: + +```text +schemaVersion +body +artifactIds +researchPlan +researchActivities[{ id, kind, status, sources, error }] +contentState +knownUsage +``` + +每次更新使用 `checkpoint_version` 比较交换。浏览器收到的流片段可以领先最后一次 checkpoint,但浏览器整树 PUT 不再是恢复权威。工具活动只有在获得输出或错误时才进入 `complete`/`error`;输入阶段保持 `running`。 + +备选方案是保存每个 token 事件;成本和写放大远高于恢复收益,因此使用有版本快照 checkpoint,并允许终结时强制刷新。 + +### D6. Stop 和僵尸任务共用同一终结器 + +Stop 首先将 `running` 条件更新为 `stop_requested`;注册表通知只是加速。执行器观察取消后把最后 checkpoint 交给统一 `finalizeGeneration`。 + +僵尸收敛器按 `status + heartbeat_at + lease` 领取失联任务,并直接读取最新 Generation checkpoint 与输出 Message,不再使用开始时的空 `turnSnapshot` 重建。终结条件更新包含当前版本,健康执行器若已推进或完成,收敛写入自然失败。 + +### D7. usage 完整度决定能否结算 + +终结输入同时携带 `knownUsage` 与 `usageCompleteness`。规则为: + +```text +complete -> 可幂等写 usage_records,billing = settled +partial -> 保留已知值,billing = usage_unavailable +unavailable -> 不伪造零值,billing = usage_unavailable +无付费调用 -> billing = not_billable +``` + +`usage_records.app_generation_id` 保持唯一。若业务需要对 partial usage 先扣已知部分,必须另立计费 ADR;本 change 不把不完整账单称为已结算。 + +### D8. 终结是唯一原子提交边界 + +`finalizeGeneration` 在一个事务中: + +- 以 Generation 状态、租约和版本做比较交换; +- 刷新最终 checkpoint 与 Message 内容状态; +- 仅在仍为当前尝试时更新 Turn 的有效助手 Message; +- 写入或确认唯一 usage 记录与计费状态; +- 记录终态、错误分类和完成时间。 + +较旧尝试晚到时进入 `superseded` 或发现已有终态后幂等返回,不得覆盖当前变体。 + +### D9. 新生命周期默认关闭 + +本阶段通过显式服务端开关或隔离应用组合根启用,只允许集成测试和后续命令 API 使用。遗留路由继续走旧权威路径;禁止把同一次 Generation 同时写入旧 `branch_generations` 和新表。 + +## Risks / Trade-offs + +- **[风险] checkpoint 节流意味着进程硬崩溃时可能丢失最后少量 token。** → 终结前强制刷新,并让节流周期可配置;契约保证不丢已确认 checkpoint,而非承诺逐 token durability。 +- **[风险] 状态维度增加实现复杂度。** → 用类型、数据库 CHECK、集中状态机和表驱动测试限制合法组合,换取语义真实性。 +- **[风险] 服务端继续执行可能占用无消费者资源。** → 使用租约、心跳、超时和持久化 Stop;资源策略不依赖浏览器连接。 +- **[风险] 新旧 Generation 并存期间难以诊断。** → 运行模式互斥、日志带 authority/version 标签,不做隐式双写。 +- **[风险] partial usage 暂不结算可能产生待对账余额。** → 明确 `usage_unavailable` 并保留已知 usage,后续计费策略可以安全重放。 + +## Migration Plan + +1. 增加规范 Generation 表、枚举/约束、复合外键和 usage 唯一关联。 +2. 实现开始、checkpoint、Stop、心跳、stale claim 与统一终结应用服务。 +3. 用模拟执行器验证断线、并发 Stop、崩溃、late result 和 usage 组合。 +4. 在隔离模式将规范生命周期接到测试 Conversation,不承载现有生产路由。 +5. 由 `add-conversation-command-api` 暴露命令,由最终切换 change 决定正式启用和旧表退场。 + +回滚时关闭规范生命周期并删除未承载生产权威的新代码/表;不得在同一运行模式回退为双写。 + +## Open Questions + +无。本 change 不决定 checkpoint 的最终节流数值,也不决定 partial usage 的未来补扣策略;两者分别是运行参数与独立计费决策,不阻塞领域契约。 diff --git a/openspec/changes/migrate-generation-lifecycle/proposal.md b/openspec/changes/migrate-generation-lifecycle/proposal.md new file mode 100644 index 00000000..3e22ea26 --- /dev/null +++ b/openspec/changes/migrate-generation-lifecycle/proposal.md @@ -0,0 +1,32 @@ +## Why + +`normalize-conversation-persistence` 将 Thread、Turn 与 Message 变成可由数据库约束的真实实体,但当前 `branch_generations` 仍把 Generation 作为整树 JSON 的辅助记录运行。Stop、断线续跑、僵尸任务收敛、部分内容、联网活动和计费完整度因此分散在多条路径中,已经出现“部分 usage 被当作完整结算”“失败恢复覆盖已保存内容”等语义错误。必须先把 Generation 收回规范领域模型,后续命令 API 和客户端迁移才能依赖一套可证明的生命周期。 + +## What Changes + +- 新增以真实 Conversation、Thread、Turn、输入 Message 与输出 Message 外键为身份的规范 Generation 记录,明确请求意图、尝试次数、当前变体和幂等键。 +- 定义 `running → stop_requested → stopped`、`running → completed/failed` 与 `superseded` 的合法转换;Generation 必须在任何付费模型调用前以事务方式开始。 +- 将流式执行改为服务端拥有,浏览器断线不得取消后台消费;正文、Artifact、研究活动和 usage 通过有版本的服务端 checkpoint 持久化。 +- 把 Message 内容状态与 Generation 终态分离:有部分输出的 Stop/失败可保存为 `incomplete`,不得统一伪装成 `error` 或清空已有 checkpoint。 +- 明确 usage 完整度与计费状态是两个正交维度;只知道部分 step usage 时必须保留已知值并标记 `usage_unavailable`,不得结算为完整账单。 +- 保留现有服务端 AbortController 注册表、数据库 Stop 观察、心跳和幂等扣费中正确的机制,但以规范实体和单一终结事务重新实现。 +- 本变更不开放新的公共 Conversation HTTP 命令,也不切换生产客户端;规范生命周期默认关闭,供下一步 `add-conversation-command-api` 接入。 + +## Capabilities + +### New Capabilities + +- `conversation-generation-lifecycle`:定义规范 Generation 的身份、状态机、服务端执行、部分 checkpoint、Stop/恢复与计费真实性。 + +### Modified Capabilities + +- 无。遗留 `persist-thread-chat-generations` 的最终替换和归档由切换 change 处理,本变更先建立新的独立契约。 + +## Impact + +- 数据库:新增规范 Generation 表、checkpoint/usage 字段、状态约束、索引及 `usage_records` 的幂等关联。 +- 服务端:聊天流生命周期、Generation 仓储、Stop 注册表、心跳、僵尸任务收敛、结果投影和终结事务。 +- 客户端语义:后续可按 Generation 和 Message 内容状态恢复,但本变更不迁移现有 Store 或路由。 +- 测试:增加状态转换、部分输出、联网活动、断线续跑、Stop、stale recovery、usage 完整度与 exactly-once 计费覆盖。 +- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`。 +- 后续依赖:`add-conversation-command-api`、`normalize-conversation-client-state`、`retire-thread-tree-authority`。 diff --git a/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md b/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md new file mode 100644 index 00000000..fa1ecc82 --- /dev/null +++ b/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md @@ -0,0 +1,146 @@ +## Purpose + +让每次 AI Generation 都绑定规范 Thread/Turn/Message,并以可恢复、可停止、可审计和可正确计费的服务端生命周期保存完整或部分结果。 + +## ADDED Requirements + +### Requirement: Generation 使用规范实体身份 + +系统 SHALL 为每次 AI 尝试创建稳定 Generation ID,并使其同时引用同一 Conversation 中的 Thread、Turn、输入 Message 和输出 Message。Generation SHALL 保存请求意图、尝试序号和幂等键;跨 Conversation、跨 Thread、跨 Turn 或角色不匹配的引用 MUST 被拒绝。 + +#### Scenario: 为重新生成创建同 Turn 新变体 + +- **WHEN** 用户对既有 Turn 请求重新生成助手回答 +- **THEN** 系统创建新的助手 Message 与 Generation,二者属于原 Turn,且不会复用或覆盖旧助手 Message 的身份 + +#### Scenario: 拒绝跨 Thread 输出 Message + +- **WHEN** Generation 尝试把另一条 Thread 的 Message 设为输出 +- **THEN** 系统拒绝开始该 Generation,且不调用付费模型 + +### Requirement: 在付费执行前幂等开始 Generation + +系统 SHALL 在任何付费模型调用前,通过单个事务验证相关聚合或实体的预期 revision、追加或确认目标 Message、创建 Generation 并提交幂等键。重复提交相同幂等键和等价载荷 SHALL 返回同一 Generation;相同键但不同载荷 MUST 被拒绝。 + +#### Scenario: 开始成功后调用模型 + +- **WHEN** 开始事务提交成功 +- **THEN** 执行器可以调用模型,且查询方已能读取状态为 `running` 的 Generation + +#### Scenario: 开始事务失败 + +- **WHEN** 归属、版本或约束验证导致开始事务回滚 +- **THEN** 系统不调用模型、不产生扣费,也不留下孤立 Message 或 Generation + +#### Scenario: 重放相同开始命令 + +- **WHEN** 客户端因超时重放相同幂等键和载荷 +- **THEN** 系统返回已有 Generation,不创建第二次模型执行 + +### Requirement: Generation 由服务端持续执行 + +Generation 开始后 SHALL 由服务端拥有流消费、心跳和终结责任。浏览器断线、页面卸载或单个响应流关闭不得自动中止模型执行;只有持久化 Stop 请求、执行错误、租约收敛或正常完成可以改变执行结果。 + +#### Scenario: 浏览器在流式响应中断线 + +- **WHEN** 响应消费者断开但服务端执行仍健康 +- **THEN** 服务端继续消费模型流、更新心跳和 checkpoint,直至合法终态 + +#### Scenario: 新页面查询进行中任务 + +- **WHEN** 用户刷新后查询仍为 `running` 或 `stop_requested` 的 Generation +- **THEN** 系统返回最新服务端 checkpoint 和状态,而不是要求原浏览器恢复执行所有权 + +### Requirement: 持久化有版本的部分 checkpoint + +系统 SHALL 以单调版本保存正文、Artifact 引用、研究计划、联网活动、来源和内容状态的服务端 checkpoint。checkpoint 更新 MUST 只接受更新版本,且不得把已持久化的非空内容替换为较旧或缺失的投影。 + +#### Scenario: 保存流式部分结果 + +- **WHEN** 执行器获得新的正文片段、Artifact 或研究活动状态 +- **THEN** 系统以更高 checkpoint 版本保存规范结构,并使后续查询可恢复相同部分结果 + +#### Scenario: 拒绝旧 checkpoint 回写 + +- **WHEN** 延迟任务提交低于当前版本的 checkpoint +- **THEN** 系统忽略或拒绝该写入,保留较新的 Message 内容 + +#### Scenario: 保留未完成联网活动 + +- **WHEN** Stop 或失败发生时某项搜索或深读仍处于输入/执行阶段 +- **THEN** checkpoint 保留该活动为 `running` 或明确中止状态,不得无条件投影为 `complete` + +### Requirement: 分离 Generation 终态与 Message 内容状态 + +Generation SHALL 使用 `completed`、`stopped`、`failed` 或 `superseded` 表达执行终态;Message SHALL 独立使用 `complete`、`incomplete` 或 `failed` 表达内容可用性。有可恢复正文、Artifact 或研究结果的 Stop/失败 MUST 保留内容并标记 `incomplete`,不得仅因 Generation 未完成而清空或统一标记 Message 为失败。 + +#### Scenario: Stop 后保留部分回答 + +- **WHEN** Generation 在已有部分输出后进入 `stopped` +- **THEN** 输出 Message 保留最后 checkpoint、标记为 `incomplete`,Generation 保持 `stopped` + +#### Scenario: 无输出执行失败 + +- **WHEN** Generation 在没有任何可恢复输出时失败 +- **THEN** Generation 标记为 `failed`,输出 Message 可以标记为 `failed` 并保存稳定错误分类 + +### Requirement: Stop 使用持久化请求和幂等状态转换 + +Stop SHALL 先以事务方式记录 `stop_requested`,再通过进程内控制器加速取消;数据库状态是跨实例的持久化事实源。重复 Stop SHALL 幂等返回当前状态;已终结 Generation 不得被改写为另一终态。 + +#### Scenario: 停止运行中的 Generation + +- **WHEN** 所有者对 `running` Generation 发出 Stop +- **THEN** 系统持久化 `stop_requested`,执行实例观察后停止消费,并以最后 checkpoint 终结为 `stopped` + +#### Scenario: 重复停止已终结 Generation + +- **WHEN** 所有者再次停止 `completed`、`stopped`、`failed` 或 `superseded` Generation +- **THEN** 系统返回其现有终态且不修改结果、checkpoint 或计费记录 + +### Requirement: 僵尸任务按最后 checkpoint 收敛 + +系统 SHALL 依据租约和心跳识别失联的非终态 Generation,并以最后一个服务端 checkpoint 收敛为失败或停止结果。收敛过程 MUST 使用版本/租约条件写入,不得覆盖已由健康执行器提交的更新,也不得从开始时的空快照重建并覆盖部分内容。 + +#### Scenario: 服务进程在部分输出后崩溃 + +- **WHEN** Generation 心跳超时且已有持久化 checkpoint +- **THEN** 收敛器保留该 checkpoint,将 Generation 终结为 `failed`,并将 Message 标记为 `incomplete` + +#### Scenario: 健康执行器抢先完成 + +- **WHEN** 收敛器准备写入时 Generation 已以更新版本进入终态 +- **THEN** 条件更新失败,收敛器不得覆盖最终结果 + +### Requirement: usage 完整度与计费状态保持真实 + +系统 SHALL 分别保存已知 usage 值、`complete`/`partial`/`unavailable` 完整度和 `pending`/`settled`/`usage_unavailable`/`not_billable` 计费状态。只有完整 usage 才能进入 `settled`;已知部分 step usage 与未知中止 step 同时存在时,系统 MUST 保留已知值并标记 `partial` 和 `usage_unavailable`。 + +#### Scenario: 正常完成且 usage 完整 + +- **WHEN** 所有计费 step 都返回 usage 且 Generation 正常终结 +- **THEN** 系统以 `complete` 完整度幂等写入唯一 usage 记录,并将计费状态设为 `settled` + +#### Scenario: Stop 时只有部分 usage + +- **WHEN** 已完成 step 有 usage,但当前被中止 step 无法取得 usage +- **THEN** 系统保存已知 usage、标记完整度为 `partial`,并将计费状态设为 `usage_unavailable` + +#### Scenario: 重复终结不重复扣费 + +- **WHEN** 响应消费、后台收敛或重试并发终结同一 Generation +- **THEN** Generation ID 的唯一约束只允许一次 usage 入账和一次终态提交 + +### Requirement: 终结事务原子提交规范结果 + +Generation 终结 SHALL 在单个事务中条件更新 Generation 终态、输出 Message 的最终 checkpoint/内容状态、Turn 当前有效助手变体和计费记录。仅当前且尚未终结的 Generation 可以推进当前变体;已被更新尝试取代的执行 SHALL 终结为 `superseded`,不得覆盖新的 Message 选择。 + +#### Scenario: 当前 Generation 正常完成 + +- **WHEN** 当前 Generation 以匹配版本提交完整结果 +- **THEN** 一个事务保存终态、完整 Message、Turn 当前助手变体和唯一计费记录 + +#### Scenario: 旧执行晚到 + +- **WHEN** 较早 Generation 在同一 Turn 的新尝试成为当前后才返回 +- **THEN** 旧执行被标记或保持为 `superseded`,且不得覆盖新变体或重复扣费 diff --git a/openspec/changes/migrate-generation-lifecycle/tasks.md b/openspec/changes/migrate-generation-lifecycle/tasks.md new file mode 100644 index 00000000..570d5b4b --- /dev/null +++ b/openspec/changes/migrate-generation-lifecycle/tasks.md @@ -0,0 +1,39 @@ +## 1. Generation 数据结构 + +- [ ] 1.1 定义 `conversation_generations` 的身份、规范实体复合外键、意图、尝试、幂等键、状态、租约、checkpoint、usage 与时间字段。 +- [ ] 1.2 增加合法状态组合、唯一幂等键、当前尝试和 `usage_records.app_generation_id` 的约束与索引。 +- [ ] 1.3 生成数据库迁移,并用集成用例验证跨 Conversation/Thread/Turn/Message 引用和非法状态组合会被拒绝。 + +## 2. 开始与执行所有权 + +- [ ] 2.1 建立纯应用层 Generation 仓储和执行器端口,不依赖 HTTP、React 或遗留整树类型。 +- [ ] 2.2 实现 `startGeneration` 事务,覆盖预期版本、输出 Message 追加、幂等重放、载荷冲突和提交前禁止模型调用。 +- [ ] 2.3 实现与 HTTP 连接解耦的服务端流消费、AbortController 注册、持久化 Stop 观察、租约和心跳。 +- [ ] 2.4 增加浏览器断线后继续执行以及重新查询可见进行中 Generation 的集成验证。 + +## 3. Checkpoint 与结构化部分结果 + +- [ ] 3.1 定义带 `schemaVersion` 的 checkpoint 解析器,覆盖正文、Artifact、研究计划、联网活动、来源、内容状态和已知 usage。 +- [ ] 3.2 实现按版本比较交换的节流 checkpoint 写入与终结前强制刷新,拒绝旧版本覆盖。 +- [ ] 3.3 修正联网活动投影,使输入/执行阶段保持 `running`,仅输出或错误后进入相应终态。 +- [ ] 3.4 覆盖正文、Artifact、研究活动混合 partial 的保存、刷新恢复和旧 checkpoint 竞态测试。 + +## 4. Stop、失败与僵尸任务收敛 + +- [ ] 4.1 实现幂等 Stop 应用操作:先持久化 `stop_requested`,再通知本地执行器,终态请求只返回既有结果。 +- [ ] 4.2 实现统一 `finalizeGeneration` 事务,原子提交 Generation、Message 内容状态、Turn 当前变体和计费结果。 +- [ ] 4.3 实现基于租约/心跳和条件更新的 stale claim,始终从最新服务端 checkpoint 收敛,不再用开始快照覆盖 partial。 +- [ ] 4.4 覆盖有输出 Stop、无输出失败、进程崩溃、健康执行器抢先完成、旧尝试晚到和重复终结测试。 + +## 5. Usage 与计费真实性 + +- [ ] 5.1 实现 `complete`、`partial`、`unavailable` usage 完整度的聚合和持久化。 +- [ ] 5.2 修改结算规则:仅完整 usage 可 `settled`;partial/unavailable 必须 `usage_unavailable`,无付费调用为 `not_billable`。 +- [ ] 5.3 覆盖“已完成 step 有 usage、当前中止 step 无 usage”的回归用例,并验证保留已知值但不伪装完整结算。 +- [ ] 5.4 覆盖并发响应消费、Stop 和 stale 收敛只产生一次 usage 记录及一次扣费的事务测试。 + +## 6. 隔离接入与验证 + +- [ ] 6.1 增加默认关闭的规范 Generation 组合根,明确拒绝同一次执行双写新旧 Generation 权威。 +- [ ] 6.2 在隔离 Conversation 路径跑通开始、流式 checkpoint、查询、Stop、完成、失败和恢复端到端测试。 +- [ ] 6.3 运行数据库迁移、相关测试、`pnpm typecheck` 与 `pnpm openspec:validate` 并记录结果。 diff --git a/openspec/changes/normalize-conversation-client-state/.openspec.yaml b/openspec/changes/normalize-conversation-client-state/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/normalize-conversation-client-state/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/normalize-conversation-client-state/design.md b/openspec/changes/normalize-conversation-client-state/design.md new file mode 100644 index 00000000..380ff579 --- /dev/null +++ b/openspec/changes/normalize-conversation-client-state/design.md @@ -0,0 +1,182 @@ +## Context + +当前外部 Store 保持一份对象身份稳定、原地修改的 `ThreadTreeState`,每次 mutation 递增全局 version 并通知全部订阅者。Fork 会同时写下游 `parentId`、上游 `children`、来源 Message `forks`、depth 和 footnote;发送和重新生成写 Message 图及 `activeLeafMessageId`。网络层再对整树做 debounce PUT、卸载 flush 和启动 reconcile。 + +新的服务端 API 已把这些关系收敛为规范实体和原子命令。客户端必须同步改变状态形状;否则它仍会在本地维护第二套领域模型。 + +## Goals / Non-Goals + +**目标:** + +- 用 ID map 和服务端版本保存规范实体,避免嵌套副本和全树 mutation。 +- 把派生拓扑、UI Workspace、pending command 和规范实体分层。 +- 用统一网关与 Generation 协调器处理命令、delta、订阅和冲突。 +- 逐组件迁移且保持现有聊天、分叉、画布和 Message actions 行为。 + +**非目标:** + +- 重做视觉设计、Markdown 渲染、编辑器或 React Flow。 +- 在客户端实现离线优先、多用户协同 CRDT 或跨 Conversation 收敛。 +- 让浏览器成为 Generation durability 或关系修复的权威。 +- 在本 change 删除服务端旧表/旧路由;最终 cutover change 负责删除。 + +## Decisions + +### D1. Store 分为四个正交区域 + +```text +canonical + conversationsById + threadsById + threadForksById + turnsById + messagesById + generationsById + revisions/checkpointVersions + +derived + threadIdsByConversation + incomingForkByThread + outgoingForkIdsByThread + turnIdsByThread + messageIdsByTurn + activePath / canvas edges / counts + +uiWorkspace + visibleThreadIds / foldedThreadIds / selectedThreadId + column sizes/policy / canvas viewport + panels / drafts / local hints + +commands + pendingByCommandId / optimisticOverlays / errors +``` + +`canonical` 只接受服务端数据。`derived` 可以按需 memoize 并全部重建。`uiWorkspace` 可以按 Conversation ID 存 localStorage,但不能进入 API 写载荷。`commands` 只描述尚未确认的交互。 + +备选方案是继续保留嵌套树并增加 adapter;这会使 adapter 本身成为第二关系权威,因此拒绝。 + +### D2. 实体更新使用对象替换和细粒度订阅 + +保留零框架依赖的外部 Store 与 React `useSyncExternalStore` 边界,但不再用单一全树 version 触发所有组件。Store 为 `entityType:id`、Conversation 索引和 UI slice 维护订阅 key;更新实体时替换该对象并只通知受影响 key。 + +Selector 返回只读视图或稳定 memoized 结果。开发模式冻结规范实体,以尽早发现组件原地修改。 + +备选方案是立即引入另一套状态库;当前问题是领域形状而不是库能力,引入依赖不会自动消除重复事实,因此不作为前置条件。 + +### D3. 快照安装是事务式 Store commit + +boot 流程先在临时结构中: + +1. 解析传输 schemaVersion; +2. 按 ID 去重并验证复合归属; +3. 验证根 Thread、ThreadFork、Turn/Message 与有效变体引用; +4. 构建基础索引; +5. 一次 commit 替换目标 Conversation 的旧实体集合。 + +失败时保留上一次可用 Store 或展示明确加载错误,不创建空树并静默覆盖远端数据。 + +### D4. delta 合并使用每实体 revision + +命令响应和查询都转换为统一 `CanonicalDelta`: + +```text +schemaVersion +upsert[type][id] +remove[type][id] = tombstoneRevision +revisions +invalidate[] +``` + +Store 逐实体比较 revision;Generation/Message 流 checkpoint 额外比较 checkpointVersion。重复和旧 delta 幂等跳过;检测到未知 schema 或不可解释版本间隙时,把 Conversation 标为 stale 并重取快照。 + +### D5. client gateway 拥有命令协议 + +组件调用类型化意图,例如 `forkThread(input)`、`sendTurn(input)`,gateway 负责: + +- 读取最小作用域 revision; +- 创建/复用 Idempotency-Key; +- 发请求、解析安全错误和 delta; +- 在组件卸载后仍把已提交结果送入共享 Store; +- 对网络不确定结果用同一键重查/重试; +- 对 409 合并最新 revision 或重取,不盲目生成新键重放。 + +旧 `chat-controller.persistNow()` 屏障不进入新 gateway。 + +### D6. 乐观 UI 使用 overlay,不创建规范实体 + +composer 可立即清空/显示 pending bubble,Fork 可显示 pending column shell,但 overlay 使用 `commandId` 和临时 presentation key,不能被 selector 当作真实 Thread/Message,也不能成为下一条领域命令的目标。 + +成功 delta 用服务端稳定 ID 替换 overlay;确定失败回滚并恢复草稿;网络不确定则保持“确认中”,用原幂等键查询。这样既保留响应感,也不在浏览器预演数据库关系。 + +### D7. GenerationCoordinator 按 ID 引用计数 + +每个 loaded Conversation 有一个协调器注册表: + +```text +generationId -> { + subscribers + latestCheckpointVersion + stream/poll handle + visibility mode +} +``` + +同一 Generation 的多个组件共享网络监控。可见运行任务保持较快轮询(初始保持现有约 2 秒行为),页面隐藏时降频(初始约 10 秒);终态停止轮询。SSE 与查询统一转换为 delta,乱序由 checkpointVersion 拒绝。最后订阅释放网络资源,但不会调用 Stop;重新挂载先查询恢复。 + +运行频率是可测配置,不进入领域模型。 + +### D8. 由 ThreadFork 和 Turn 生成列/画布读取模型 + +选择器集中实现: + +- 根 Thread 来自 `Conversation.rootThreadId`; +- parent/children 来自 ThreadFork; +- 深度和画布边通过无环遍历派生; +- Thread 的继承上下文截至 `sourceMessageId`,本地内容来自其 Turns; +- 当前 Message 来自 Turn active variant; +- Fork 数量由 outgoing ThreadFork 计数。 + +组件不再读取 `threads.main`、`parentId`、`children`、Message `forks` 或 `activeLeafMessageId`。 + +### D9. UI Workspace 只保存展示身份 + +现有列放置、替换/细条策略、画布焦点和 panel 状态可以保留算法,但输入改为 Thread ID 与派生 selector。localStorage payload 带 UI schemaVersion 和 Conversation ID;读取时过滤已归档/不存在 Thread。它不能恢复或修改规范关系。 + +### D10. 按竖向能力切片迁移组件 + +迁移顺序为: + +1. 新 Store、snapshot/delta 和 selector 测试; +2. boot/list/title 和 UI Workspace; +3. 只读列、Message、Artifact、画布; +4. Generation 展示与 coordinator; +5. composer/send/stop/regenerate/select variant; +6. selection Fork、反馈和其余 Message actions。 + +每个切片在隔离规范路由下可端到端运行。避免先写一个巨型兼容 adapter 让所有旧组件继续变异整树。 + +### D11. 客户端路径按 Conversation 互斥 + +路由/feature flag 为一次页面会话选择 legacy 或 canonical boot。canonical 路径不会实例化旧 Tree Store、persistence debounce 或 reconcile;legacy 路径不调用新命令。禁止同一 Conversation 页面同时维护两个 Store 并互相同步。 + +## Risks / Trade-offs + +- **[风险] 大量组件依赖旧嵌套类型。** → 按竖向能力切片迁移,先建立 selector facade,但 facade 只读且基于规范实体。 +- **[风险] 细粒度订阅和派生缓存可能产生失效错误。** → 所有索引声明依赖 key,增加重建等价测试和开发模式全量校验。 +- **[风险] overlay 与规范实体视觉切换可能闪烁。** → 以 commandId 关联响应并保持展示 key,规范身份仍只来自服务端。 +- **[风险] 组件卸载后请求回调造成泄漏。** → gateway 属于 Store/application scope,不捕获组件 setState;组件只维护可取消订阅。 +- **[风险] feature flag 并存增加测试矩阵。** → 并存只持续到最终 cutover,模式严格互斥并分别标记 authority。 + +## Migration Plan + +1. 实现 normalized Store、只读 selector、快照安装和 delta 合并。 +2. 实现 client gateway、pending overlay、冲突恢复和 GenerationCoordinator。 +3. 按只读展示 → Generation → 写命令顺序迁移组件。 +4. 在隔离规范路由跑现有行为矩阵和渲染/订阅性能验证。 +5. 把 canonical 客户端置于默认关闭的 Conversation 级开关后,交由最终 change 执行全量 cutover。 + +最终生产 cutover 前,回滚可以关闭试验 canonical 页面并切回 legacy 模式;一旦服务端 canonical authority 接受生产写入,只能回滚到仍使用规范 API/数据的兼容客户端版本或进入只读,任何客户端都不得把本地状态序列化回 `branch_trees.state`。 + +## Open Questions + +无。是否未来引入通用状态库、SSE 还是 WebSocket、以及 UI Workspace 是否跨设备同步,均可在不改变规范实体边界的情况下独立决策。 diff --git a/openspec/changes/normalize-conversation-client-state/proposal.md b/openspec/changes/normalize-conversation-client-state/proposal.md new file mode 100644 index 00000000..846c2495 --- /dev/null +++ b/openspec/changes/normalize-conversation-client-state/proposal.md @@ -0,0 +1,33 @@ +## Why + +服务端命令 API 已能返回规范快照和实体增量,但当前前端仍以可变 `ThreadTreeState` 为中心,组件会直接改 `children`、`activeLeafMessageId` 等关系,再依赖整树存盘和刷新协调。若只替换请求 URL,旧的重复事实和竞态仍会保留在浏览器;必须把客户端改为按稳定 ID 规范化实体,并把领域状态与列/画布视图状态彻底分开。 + +## What Changes + +- 新增按 ID 保存 Conversation、Thread、ThreadFork、Turn、Message 与 Generation 的规范化客户端 Store;实体关系只接受服务端快照或命令 delta。 +- 派生 Thread children、深度、Fork 数量、有效消息路径和列投影;这些索引可丢弃重建,不作为可写事实。 +- 将 visible columns、折叠、选中 Thread、画布 viewport、面板、草稿和 pending command 放入独立 UI Workspace slice。 +- 首次加载从 `ConversationSnapshot` 原子归一化;后续命令通过统一 client gateway 发送幂等键/revision,并合并规范 delta 或处理冲突重取。 +- 流式 Generation 由单一协调器订阅/轮询,按 checkpoint version 更新 Message/Generation;组件挂载只订阅所需 ID,卸载时释放订阅。 +- 乐观体验使用临时展示 overlay 和可回滚 pending command,不允许组件生成权威 Fork/Message 关系或触发整树 PUT。 +- 迁移列、画布、composer、Message actions、标题、反馈和恢复展示到 ID-scoped selector;保留现有 Markdown、Artifact 和流式视觉能力。 +- **BREAKING**:规范客户端路径不再调用 `persistNow`、save debounce、整树 CAS、启动 reconcile 或 `activeLeafMessageId` 写入;正式删除由最终切换 change 完成。 + +## Capabilities + +### New Capabilities + +- `conversation-client-state`:定义规范化实体 Store、派生索引、界面状态、命令 delta、Generation 订阅和组件边界。 + +### Modified Capabilities + +- 无。现有界面能力保持产品行为,旧客户端协议的退场由 `retire-thread-tree-authority` 处理。 + +## Impact + +- 前端核心:`app/thread-chat/core/store.ts`、types/selectors、boot、chat controller、persistence 与 generation reconciliation。 +- 组件:列、画布、composer、selection/fork、Message actions、标题、帮助/反馈和 Artifact 展示。 +- 网络:新增 Conversation client gateway、snapshot/delta parser、命令状态和 Generation polling/subscription coordinator。 +- 测试:Store reducer/selectors、delta/revision、并发冲突、订阅释放、刷新恢复和 UI 回归。 +- 前置依赖:`define-conversation-domain-model`、`add-conversation-command-api`,并间接依赖规范持久化与 Generation 生命周期。 +- 后续依赖:`retire-thread-tree-authority`。 diff --git a/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md b/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md new file mode 100644 index 00000000..f7843839 --- /dev/null +++ b/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md @@ -0,0 +1,165 @@ +## Purpose + +让浏览器按稳定实体 ID 消费 Conversation 快照、命令增量和 Generation checkpoint,同时把领域事实、派生索引、界面工作区与乐观状态分开。 + +## ADDED Requirements + +### Requirement: 按稳定 ID 规范化客户端实体 + +客户端 SHALL 分别按 ID 保存 Conversation、Thread、ThreadFork、Turn、Message 与 Generation,并保存服务端 revision/checkpoint version。一个规范实体在同一加载作用域内 MUST 只有一份客户端对象事实;组件不得保存可独立修改的嵌套实体副本。 + +#### Scenario: 两列引用同一来源 Message + +- **WHEN** 上游 Thread 和下游 Fork 投影都展示同一个来源 Message +- **THEN** 二者通过相同 Message ID 读取同一实体,且任何服务端更新只需合并一次 + +#### Scenario: Generation 更新输出 Message + +- **WHEN** 新 checkpoint 更新某条输出 Message +- **THEN** Store 按 Message ID 和版本替换该实体,所有订阅该 Message 的视图看到同一结果 + +### Requirement: 原子归一化 ConversationSnapshot + +客户端 SHALL 验证 `ConversationSnapshot` 的 schemaVersion、实体归属、引用和 revision 后,在一个 Store 提交中替换目标 Conversation 的规范实体集合。解析或不变量验证失败 MUST 保留现有可用状态并显示可恢复加载错误,不得部分安装快照。 + +#### Scenario: 首次加载有效快照 + +- **WHEN** 接口返回受支持 schemaVersion 的有效 ConversationSnapshot +- **THEN** 客户端原子安装实体、revision 和可重建索引,组件不会观察到半条 Fork 或缺失 Message 的中间状态 + +#### Scenario: 快照包含悬空 ThreadFork + +- **WHEN** 下游 Thread 或来源 Message 无法在快照中解析 +- **THEN** 客户端拒绝整个新快照并触发诊断/重试,而不是自行补造关系 + +### Requirement: 按版本合并规范实体增量 + +客户端 SHALL 只通过服务器命令响应、查询或 Generation checkpoint 合并规范实体 delta。每个 upsert/remove MUST 比较对应 revision 或 checkpoint version;较旧或重复 delta SHALL 幂等忽略,无法安全解释的版本间隙 SHALL 触发作用域重取。 + +#### Scenario: 命令响应晚于更新查询 + +- **WHEN** 一个旧命令响应在更新的实体查询之后到达 +- **THEN** Store 保留较新 revision,不被旧 delta 覆盖 + +#### Scenario: 收到未知 delta schemaVersion + +- **WHEN** 客户端收到不支持的 delta schemaVersion +- **THEN** 客户端不应用该 delta,标记目标 Conversation 需要重新加载 + +### Requirement: Fork 和路径索引必须可重建 + +Thread children、父来源、深度、Fork 数量、Turn 顺序、当前有效 Message 路径和画布边 SHALL 从规范实体派生或缓存为可丢弃索引。客户端不得通过直接修改这些索引来表达 Fork、发送或变体选择。 + +#### Scenario: 合并新 ThreadFork + +- **WHEN** Store 合并服务端返回的新 Thread 与 ThreadFork +- **THEN** children、深度、脚注/Fork 数量和画布边从关系重新派生,不要求响应同时提交多个反向字段 + +#### Scenario: 清空派生缓存 + +- **WHEN** 派生索引缓存被清除 +- **THEN** 客户端能从规范实体重建等价列和画布关系,不向服务端写入任何修复 + +### Requirement: 界面工作区独立于领域事实 + +visible Thread 顺序、折叠、当前选中 Thread、列宽/策略、画布 viewport、打开面板、临时草稿和本地提示 SHALL 保存在独立 UI Workspace slice。改变这些字段不得改变 Conversation revision、ThreadFork、Turn 当前变体或触发领域命令。 + +#### Scenario: 折叠分支列 + +- **WHEN** 用户折叠或关闭一列 +- **THEN** 客户端只更新 UI Workspace,Thread 与 ThreadFork 仍在规范实体 Store 中 + +#### Scenario: 归档当前可见 Thread + +- **WHEN** 归档命令成功并使选中 Thread 不再活跃 +- **THEN** Store 合并服务端生命周期 delta,UI Workspace 选择一个有效可见 Thread,而不删除历史关系 + +### Requirement: 通过统一客户端网关执行命令 + +组件 SHALL 通过统一 Conversation client gateway 执行命令,由网关附加幂等键、预期 revision、取消策略并解析规范响应。组件不得直接调用旧整树 PUT、手动推进 revision 或分别提交 Thread/Fork/Message 关系。 + +#### Scenario: 组件请求 Fork + +- **WHEN** selection UI 请求从 Message 创建 Thread +- **THEN** 网关发送一个 `forkThread` 请求,并只在服务端成功后把规范 delta 合并进 Store + +#### Scenario: 组件卸载时命令已提交 + +- **WHEN** 发起命令的组件卸载但服务端可能已提交请求 +- **THEN** 网关保留幂等身份并允许结果进入共享 Store,组件卸载不触发补偿性整树覆盖 + +### Requirement: 乐观状态不得成为规范关系 + +客户端 MAY 为交互即时性保存草稿、pending command 和临时展示 overlay,但这些状态 SHALL 与规范实体分离并可按命令 ID 回滚。服务端未确认前,客户端不得把临时 ThreadFork、Message 身份或 active variant 当作规范事实用于后续命令。 + +#### Scenario: 乐观发送后版本冲突 + +- **WHEN** composer 显示待发送 overlay,但服务端返回 `version_conflict` +- **THEN** 客户端移除或标记失败 overlay、保留用户草稿、更新 revision,并允许用户明确重试 + +#### Scenario: 幂等重试成功 + +- **WHEN** 网络失败后网关以相同幂等键重试并收到原成功结果 +- **THEN** 客户端用规范 Message/Generation ID 替换 pending overlay,不产生重复可见 Turn + +### Requirement: 统一协调 Generation 订阅与轮询 + +客户端 SHALL 按 Generation ID 维护引用计数的监控协调器,合并流式事件和查询结果,并只应用更高 checkpoint version。视图挂载 SHALL 订阅需要的 Generation,卸载 SHALL 释放订阅;无浏览器订阅不得停止服务端 Generation。 + +#### Scenario: 两个视图观察同一 Generation + +- **WHEN** 列视图和状态面板同时订阅一个运行中 Generation +- **THEN** 客户端只维持一个网络监控实例,并把更新分发给两个订阅者 + +#### Scenario: 最后一个视图卸载 + +- **WHEN** 最后一个订阅者卸载 +- **THEN** 客户端释放对应定时器/事件监听;服务端继续执行,后续重新挂载通过查询恢复最新 checkpoint + +#### Scenario: 轮询和流事件乱序 + +- **WHEN** 旧轮询结果晚于新流式 checkpoint 到达 +- **THEN** 协调器忽略旧版本,不回退 Message 内容或 Generation 状态 + +### Requirement: 组件使用按 ID 定位的选择器 + +列、画布、Message、composer、标题、Artifact、反馈和 Message actions SHALL 通过 ID-scoped selector 读取最小所需实体及派生结果。无关实体更新不得要求所有列重新渲染;selector 不得返回可由组件原地修改的规范对象。 + +#### Scenario: 一个分支流式更新 + +- **WHEN** 某条分支 Thread 的 Message 收到 checkpoint +- **THEN** 订阅该 Message/Thread 的视图更新,未引用它的独立列无需因全局树版本变化重新渲染 + +#### Scenario: Message action 使用稳定身份 + +- **WHEN** 用户复制、反馈、重新生成或 Fork 某条 Message +- **THEN** 操作使用规范 Message/Turn/Thread ID 和 client gateway,不从数组位置或局部树键推断身份 + +### Requirement: 保持当前可见产品行为 + +迁移后的客户端 SHALL 保持现有列/画布导航、嵌套 Fork、Markdown、Artifact、研究活动、流式进度、停止、恢复、标题和 Message actions 的用户可见能力。行为等价不要求保留旧 `ThreadTreeState`、魔法 `main` ID 或整树持久化实现。 + +#### Scenario: 刷新恢复 stopped partial + +- **WHEN** 用户停止带正文、Artifact 或研究活动的 Generation 后刷新 +- **THEN** 客户端从规范快照/checkpoint 恢复相同 `incomplete` 内容和 `stopped` 状态 + +#### Scenario: 打开嵌套 Fork + +- **WHEN** 用户从 A 打开 B,再从 B 打开 C +- **THEN** 列和画布通过 ThreadFork 投影显示正确层级、来源锚点和上下文,不依赖可写 children 数组 + +### Requirement: 规范客户端路径不保存整树或启动协调合并 + +启用规范客户端路径时,系统 MUST 禁用 `persistNow`、save debounce、卸载 flush、整树 CAS、`activeLeafMessageId` 写入以及为修复浏览器 partial 而运行的启动 reconcile。刷新恢复 SHALL 只依赖规范快照、命令结果和服务端 Generation checkpoint。 + +#### Scenario: 流式 token 高频到达 + +- **WHEN** Generation 连续产生流式更新 +- **THEN** 客户端更新展示和规范 checkpoint 版本,但不防抖 PUT 整个 Conversation + +#### Scenario: 页面卸载 + +- **WHEN** 用户在 Generation 运行中关闭页面 +- **THEN** 客户端不 flush 整树,服务端执行和 checkpoint 机制继续承担持久化责任 + diff --git a/openspec/changes/normalize-conversation-client-state/tasks.md b/openspec/changes/normalize-conversation-client-state/tasks.md new file mode 100644 index 00000000..be12fea3 --- /dev/null +++ b/openspec/changes/normalize-conversation-client-state/tasks.md @@ -0,0 +1,48 @@ +## 1. 规范化 Store 基础 + +- [ ] 1.1 定义只读 Conversation/Thread/ThreadFork/Turn/Message/Generation ID maps、revision 和 checkpoint version 状态。 +- [ ] 1.2 实现 ConversationSnapshot 临时解析、归属/引用校验和单次原子安装,失败时不部分覆盖旧状态。 +- [ ] 1.3 实现带 tombstone、schemaVersion 和每实体版本比较的 CanonicalDelta 合并器。 +- [ ] 1.4 增加开发模式实体冻结、重复/旧 delta 幂等、版本间隙重取和未知 schema 拒绝测试。 + +## 2. 派生索引与细粒度订阅 + +- [ ] 2.1 实现 Conversation → Threads、ThreadFork parent/children、Thread → Turns、Turn → Messages 的可重建索引。 +- [ ] 2.2 实现根 Thread、嵌套深度、Fork 数量、继承上下文、当前有效路径和画布边 selector。 +- [ ] 2.3 将 Store 通知拆为实体/索引/UI key,并提供按 ID 定位的 React hooks 与稳定 memoized selector。 +- [ ] 2.4 验证清空派生缓存可重建等价结果,以及一个 Message 流式更新不会通知无关 Thread 订阅。 + +## 3. UI Workspace 与乐观状态 + +- [ ] 3.1 将 visible/folded/selected Thread、列策略、画布 viewport、面板、草稿和本地提示迁入独立 UI Workspace slice。 +- [ ] 3.2 为 UI localStorage payload 增加 schemaVersion/Conversation ID,并在加载时过滤归档或缺失 Thread。 +- [ ] 3.3 实现按 commandId 管理的 pending overlay、草稿恢复、确认中、成功替换和失败回滚。 +- [ ] 3.4 验证折叠/画布/面板操作不改变 canonical revision 或发出领域命令。 + +## 4. Client gateway 与 GenerationCoordinator + +- [ ] 4.1 实现统一 client gateway,附加 Idempotency-Key、最小预期 revision并解析 snapshot/delta/稳定错误。 +- [ ] 4.2 实现网络不确定时复用幂等键、409 revision 恢复和组件卸载后共享结果合并。 +- [ ] 4.3 实现按 Generation ID 引用计数的 stream/poll 协调器、可见/隐藏频率、终态停止和 checkpoint 乱序保护。 +- [ ] 4.4 覆盖多视图共享一个监控、最后订阅释放、重新挂载恢复以及卸载不发送 Stop 的测试。 + +## 5. 只读界面切片迁移 + +- [ ] 5.1 迁移 boot、Conversation 列表/标题和根 Thread 识别,不再使用魔法 `main` ID。 +- [ ] 5.2 迁移列、Thread switcher、tree list、breadcrumb、selection anchor 和嵌套 Fork 展示到 ThreadFork selectors。 +- [ ] 5.3 迁移 Message/Markdown/研究活动/Artifact/反馈展示到稳定 ID selectors。 +- [ ] 5.4 迁移 React Flow 画布节点/边和聚焦行为到可重建投影,并验证懒加载不复制 canonical 实体。 + +## 6. 写交互切片迁移 + +- [ ] 6.1 迁移 composer/send/stop 到 client gateway 和 pending overlay,删除规范路径的 `persistNow` 前置屏障。 +- [ ] 6.2 迁移重新生成、用户输入编辑和变体选择到 Turn/Message 命令与 revision。 +- [ ] 6.3 迁移 selection Fork 到单个 `forkThread` 命令,不再本地写 parent/children/Message forks。 +- [ ] 6.4 迁移复制、反馈、Artifact 定位和其余 Message actions,全部使用稳定 Message/Turn/Thread ID。 + +## 7. 互斥模式与行为验证 + +- [ ] 7.1 增加 Conversation 页面 legacy/canonical 互斥 boot 开关,canonical 模式不实例化旧 Tree Store、整树 persistence 或 reconcile。 +- [ ] 7.2 覆盖发送、流式、Stop partial、刷新恢复、重新生成、选择变体、A → B → C Fork、归档/恢复和标题行为矩阵。 +- [ ] 7.3 增加 selector 通知计数与关键渲染性能验证,确认无关列不会随全局树 version 重渲染。 +- [ ] 7.4 运行相关单元/组件/端到端测试、`pnpm typecheck` 与 `pnpm openspec:validate` 并记录结果。 diff --git a/openspec/changes/normalize-conversation-persistence/.openspec.yaml b/openspec/changes/normalize-conversation-persistence/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/normalize-conversation-persistence/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/normalize-conversation-persistence/design.md b/openspec/changes/normalize-conversation-persistence/design.md new file mode 100644 index 00000000..447130af --- /dev/null +++ b/openspec/changes/normalize-conversation-persistence/design.md @@ -0,0 +1,103 @@ +## Context + +本变更依赖 `define-conversation-domain-model`。当前 `branch_trees.state` 是唯一事实源,`branch_generations`、反馈和 Artifact 只能引用 JSON 内部 ID。本阶段只建立规范结构、仓储和审计能力;在最终切换前不改变线上权威来源。 + +## Goals / Non-Goals + +**目标:** + +- 让 Conversation 核心实体拥有可外键约束的服务端身份。 +- 用数据库约束和事务保护根 Thread、唯一 Fork、Turn/Message 归属与有效变体。 +- 提供稳定的规范快照和可重复遗留审计。 +- 为 Generation 迁移提供真实 Thread/Turn/Message 外键目标。 + +**非目标:** + +- 迁移 Generation 状态、计费和流式终结。 +- 开放新的写命令接口或迁移客户端 Store。 +- 在本阶段启用生产双写或正式删除 `branch_trees`。 +- 实现 Project Memory、File、Instruction 的业务表。 + +## Decisions + +### D1. 物理表按 Conversation 领域命名 + +首批表为: + +```text +workspaces +workspace_members +projects +conversations +conversation_threads +thread_forks +conversation_turns +conversation_messages +``` + +使用 `conversation_*` 前缀避免与其他聊天运行时的线性表混淆。Workspace/Project 只实现最小身份和成员归属;资产能力后续扩展。 + +### D2. 使用复合键保护归属 + +除普通主键外,表提供必要的复合唯一键: + +```text +conversation_threads: unique(id, conversation_id) +conversation_turns: unique(id, thread_id) +conversation_messages: unique(id, thread_id, turn_id) +``` + +ThreadFork 的 parent/child 与 Conversation 使用复合外键;来源 Message 同时携带 parent Thread 约束。Turn 的 active Message 使用可延迟复合外键,并在事务终点验证角色。仅检查单个 ID 不足以阻止跨 Thread 串线。 + +### D3. 根引用和 Fork 完整性使用延迟约束 + +Conversation 与根 Thread、Turn 与 active Message 都存在创建顺序环。迁移使用 PostgreSQL 可延迟约束或等价约束触发器,使一个事务可以先创建双方,再在提交时验证: + +- `rootThreadId` 属于当前 Conversation; +- 根 Thread 没有入向 Fork; +- 每个非根 Thread 恰有一条入向 Fork; +- Fork 图无环; +- active user/assistant Message 属于当前 Turn 且角色匹配。 + +仓储仍执行早期领域校验,以返回可理解错误;数据库约束是最终防线。 + +### D4. Message 内容保持结构化 JSON 值 + +Message 行规范化身份、归属、角色、状态和顺序,但 `content` 继续保存有版本的结构化片段 JSON。工具片段、研究计划和 Markdown 内容没有独立权限时不拆表。File/Artifact 的独立生命周期由后续 Project 资产变更处理。 + +### D5. 快照只读,写入通过仓储端口 + +`ConversationSnapshot` 按稳定顺序组装实体和派生索引,用于测试和后续首次加载。仓储端口按实体/聚合操作,不提供 `saveConversationSnapshot`。这样后续命令接口无法重新引入整树覆盖。 + +### D6. 先审计,再决定重置或导入 + +审计器先对真实数据运行只读报告,输出: + +- 总记录数和所有者分布; +- 可直接映射的 Conversation/Thread/Turn/Message/Fork 数量; +- 魔法根 ID、重复 Message、错误 Fork、跨 Thread active leaf; +- `branch_generations`、反馈和 Artifact 的悬空引用; +- 每棵树的确定性映射摘要和错误代码。 + +根据报告在最终切换变更中选择开发数据重置或一次性导入。本变更不猜测该决定。 + +### D7. 新路径默认关闭 + +规范仓储通过显式服务端配置启用,默认不接受生产写入。测试可以在隔离数据库中直接使用;线上仅允许审计。禁止添加自动双写适配器,因为它会在命令 API 出现前建立两个权威来源。 + +## Risks / Trade-offs + +- **[风险] 延迟约束和触发器增加迁移复杂度。** → 同时提供仓储级早期校验、数据库集成验证和清晰约束命名。 +- **[风险] Workspace/Project 扩大首批表。** → 只实现身份与成员归属,避免以后再次迁移 Conversation 所有权。 +- **[风险] 新表落地但暂不承载流量。** → 通过隔离集成验证和审计证明结构,下一 change 才迁移执行路径。 +- **[风险] JSON 内容仍可能包含无效工具片段。** → 内容使用版本化解析器;关系归属不再依赖片段内部 ID。 + +## Migration Plan + +1. 增加表、复合键、索引、延迟约束和仓储端口。 +2. 在隔离数据库执行最小与非法关系验证。 +3. 实现确定性 `ConversationSnapshot` 读取。 +4. 实现只读遗留审计并对目标环境生成报告。 +5. 保持规范生产写入关闭,把报告交给下一迁移 change。 + +回滚只需删除尚未启用写入的新表和代码;不得修改 `branch_trees` 原数据。 diff --git a/openspec/changes/normalize-conversation-persistence/proposal.md b/openspec/changes/normalize-conversation-persistence/proposal.md new file mode 100644 index 00000000..d7a5dc98 --- /dev/null +++ b/openspec/changes/normalize-conversation-persistence/proposal.md @@ -0,0 +1,31 @@ +## Why + +`define-conversation-domain-model` 已确定 Project、Conversation、Thread、ThreadFork、Turn 与 Message 的身份和归属,但当前数据库仍把整棵 `ThreadTreeState` 存在单个 JSONB 行中,无法用外键拒绝跨 Thread 串线、重复 Fork 来源或错误 active variant。必须先建立规范写模型和可审计迁移入口,后续 Generation、命令接口与客户端才能依赖稳定实体。 + +## What Changes + +- 新增 Workspace/Project、Conversation、Thread、ThreadFork、Turn、Message 的规范化关系表、稳定 ID、复合外键、唯一约束和必要的延迟约束检查。 +- Conversation 独占聚合标题与 `rootThreadId`;根 Thread 不再使用 `id = "main"`,非根 Thread 的来源只由 ThreadFork 表达。 +- Turn 显式保存当前有效用户/助手 Message,Message 内容继续使用带版本的结构化 JSON 值,避免过度拆分工具片段。 +- 新增仓储端口与只读 `ConversationSnapshot` 组装器;快照可供首次加载,但不得作为整包写入数据。 +- 新增对 `branch_trees.state` 的只读审计和导入计划,输出逐实体映射、拒绝原因和可重复运行报告;本变更不启用双写,也不切换生产权威来源。 +- 为下一步 `migrate-generation-lifecycle` 暴露真实 Thread/Turn/Message 身份,但不在本变更迁移 Generation 状态机、计费或流式终结逻辑。 + +## Capabilities + +### New Capabilities + +- `conversation-persistence`:定义 Conversation 核心实体的关系型持久化、不变量、规范快照和遗留数据审计行为。 + +### Modified Capabilities + +- 无。本变更依赖 `define-conversation-domain-model` 的 `domain` 增量规范,不再次复制术语要求。 + +## Impact + +- 数据库:`lib/db/schema.ts`、Drizzle 迁移、约束、索引以及本地重置/迁移脚本。 +- 领域与仓储:`lib/thread-chat/domain/`、新的 Conversation 仓储端口和持久化适配器。 +- 读取路径:新增规范 `ConversationSnapshot`;现有 `branch_trees` 读取在切换前保持权威。 +- 部署:只落表和禁用状态的代码不会改变当前用户行为;正式导入与切换由后续 change 执行。 +- 前置依赖:`define-conversation-domain-model`。 +- 后续依赖:`migrate-generation-lifecycle`、`add-conversation-command-api`。 diff --git a/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md b/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md new file mode 100644 index 00000000..cf4e4f4a --- /dev/null +++ b/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md @@ -0,0 +1,89 @@ +## Purpose + +为 Project、Conversation、Thread、ThreadFork、Turn 与 Message 建立可由数据库和事务强制保护的规范持久化契约,并提供只读快照、遗留数据审计及安全迁移边界。 + +## ADDED Requirements + +### Requirement: 持久化稳定的 Conversation 核心实体 + +系统 SHALL 为 Workspace、Project、Conversation、Thread、Turn 与 Message 分配稳定且无业务含义的服务端 ID,并将每个实体保存为可独立寻址的关系型记录。实体归属创建后不得通过更新外键改变;任何跨所有者、跨 Project 或跨 Conversation 引用 MUST 被拒绝。 + +#### Scenario: 创建根 Conversation + +- **WHEN** 系统在 Project 中创建一条 Conversation +- **THEN** Conversation 与根 Thread 获得独立稳定 ID,且 `rootThreadId` 只能指向属于该 Conversation 的 Thread + +#### Scenario: 拒绝移动既有 Thread + +- **WHEN** 写入尝试把既有 Thread 的 `conversationId` 改为另一条 Conversation +- **THEN** 系统拒绝写入并保持原归属不变 + +### Requirement: 使用唯一 ThreadFork 保存分叉来源 + +系统 SHALL 只通过 ThreadFork 保存上游 Thread、来源 Message 与下游 Thread 的 Fork 事实。来源 Message MUST 属于上游 Thread,三者 MUST 属于同一 Conversation;根 Thread 不得有入向 ThreadFork,非根 Thread MUST 恰有一个入向 ThreadFork,Fork 图不得成环。 + +#### Scenario: 保存有效嵌套 Fork + +- **WHEN** 一条 ThreadFork 指向同一 Conversation 中的上游 Thread、其来源 Message 和尚无来源的下游 Thread +- **THEN** 系统保存该关系,并可从它派生 children、深度与分支数量 + +#### Scenario: 拒绝第二个来源 + +- **WHEN** 写入尝试为同一个非根 Thread 建立第二条入向 ThreadFork +- **THEN** 系统拒绝该写入且保留原唯一来源 + +### Requirement: 使用 Turn 约束 Message 与当前有效变体 + +每个 Turn SHALL 恰好属于一个 Thread,每个 Message SHALL 同时属于该 Thread 和其中一个 Turn。Turn 的当前有效用户与助手 Message MUST 指向自身变体,角色必须匹配;跨 Thread 或跨 Turn 的变体选择 MUST 被拒绝。 + +#### Scenario: 保存重新生成变体 + +- **WHEN** 同一 Turn 产生新的助手 Message 变体并被选为当前有效回答 +- **THEN** 系统追加新 Message、更新该 Turn 的有效助手引用,且不修改其他 Thread/Turn + +#### Scenario: 拒绝跨 Thread 有效选择 + +- **WHEN** Turn 尝试把另一个 Thread 的 Message 设为当前有效变体 +- **THEN** 系统拒绝该选择且原有效路径不变 + +### Requirement: 组装只读规范 Conversation 快照 + +系统 SHALL 能够从规范实体组装包含 Conversation、Threads、ThreadForks、Turns、Messages 和当前有效选择的确定性快照。快照中的派生索引 MUST 可从实体重建;系统不得接受该快照作为覆盖规范行的整包写入。 + +#### Scenario: 两次读取产生稳定快照 + +- **WHEN** 底层规范实体未发生变化并连续读取同一 Conversation +- **THEN** 两次快照具有相同实体关系、排序和当前有效路径 + +#### Scenario: 拒绝整包覆盖 + +- **WHEN** 调用方提交完整 Conversation 快照试图覆盖服务端实体 +- **THEN** 持久化层不提供该权威写入能力,并要求调用实体级仓储或应用命令 + +### Requirement: 审计遗留 ThreadTreeState + +系统 SHALL 提供只读、可重复运行的遗留数据审计,检查所有者、根节点、Thread/Fork、Message 图、当前叶节点、Artifact 来源和辅助 Generation 引用,并输出逐记录的可迁移、需修复或拒绝原因。审计不得修改原始数据。 + +#### Scenario: 审计合法遗留树 + +- **WHEN** 遗留 `ThreadTreeState` 满足已知结构和归属不变量 +- **THEN** 审计输出确定的规范实体映射及数量,不写入规范表或原表 + +#### Scenario: 报告污染引用 + +- **WHEN** 遗留树含缺失来源 Message、重复 Fork、跨 Thread active leaf 或辅助表悬空 ID +- **THEN** 审计将该记录标为不可直接迁移,并给出稳定错误代码和涉及的实体 ID + +### Requirement: 迁移阶段保持唯一写入权威 + +在正式切换前,新增规范表和仓储 SHALL 保持禁用或只读验证状态,旧路径仍是唯一写入权威;切换后则规范实体成为唯一权威,系统不得长期双写 JSON 与规范行。 + +#### Scenario: 未启用规范路径 + +- **WHEN** 规范持久化功能开关未启用 +- **THEN** 当前用户写入行为不变,新表只能用于显式测试、审计或隔离环境 + +#### Scenario: 防止无声明双写 + +- **WHEN** 代码尝试在同一运行模式下同时把一次领域变更写入整树 JSON 与规范实体 +- **THEN** 系统拒绝启动该模式或使验证失败 diff --git a/openspec/changes/normalize-conversation-persistence/tasks.md b/openspec/changes/normalize-conversation-persistence/tasks.md new file mode 100644 index 00000000..19501931 --- /dev/null +++ b/openspec/changes/normalize-conversation-persistence/tasks.md @@ -0,0 +1,28 @@ +## 1. 数据结构与迁移 + +- [ ] 1.1 定义 `workspaces`、`workspace_members`、`projects`、`conversations`、`conversation_threads`、`thread_forks`、`conversation_turns`、`conversation_messages` 的 Drizzle 结构和稳定命名。 +- [ ] 1.2 生成迁移并补充 Drizzle 无法表达的可延迟复合外键、唯一约束与约束触发器。 +- [ ] 1.3 为所有者查询、Conversation 列表、Thread/Turn 顺序、来源 Message 和当前有效变体建立必要索引。 +- [ ] 1.4 更新本地 schema reset 脚本,使新表只在明确目标 schema 中创建或删除,不影响其他数据库。 + +## 2. 规范仓储 + +- [ ] 2.1 建立 Workspace/Project 与 Conversation/Thread 的纯应用仓储端口和 Drizzle 适配器。 +- [ ] 2.2 实现 ThreadFork 事务写入与根、唯一来源、同 Conversation、来源 Message 归属和无环校验。 +- [ ] 2.3 实现 Turn/Message 追加、角色校验和当前有效变体的预期版本更新。 +- [ ] 2.4 实现确定性 `ConversationSnapshot` 组装、稳定排序和可重建派生索引。 +- [ ] 2.5 增加服务端配置边界,默认禁止规范仓储承载生产写入,并拒绝未声明双写模式。 + +## 3. 遗留审计 + +- [ ] 3.1 实现只读 `ThreadTreeState` 审计器和 legacy → canonical ID/实体映射报告。 +- [ ] 3.2 检查根、Fork、Message 图、active leaf、Artifact、反馈和 `branch_generations` 悬空引用,并为每类错误定义稳定代码。 +- [ ] 3.3 增加 dry-run 命令,仅输出汇总与逐树报告,不修改旧表或规范表。 +- [ ] 3.4 在目标环境执行审计并记录“可重置或必须迁移”的数据证据,供最终切换 change 决策。 + +## 4. 约束验证 + +- [ ] 4.1 覆盖最小根 Conversation、A → B → C Fork、同 Turn 回复变体和有效选择的成功用例。 +- [ ] 4.2 覆盖跨 Conversation Fork、来源 Message 错配、第二入向 Fork、Fork 环、跨 Thread Message/Turn 和错误角色 active Message 的拒绝用例。 +- [ ] 4.3 验证两个相同读取产生等价 `ConversationSnapshot`,且仓储没有整包快照写入端口。 +- [ ] 4.4 运行数据库迁移、`pnpm typecheck`、`pnpm openspec:validate` 和相关验证脚本并记录结果。 diff --git a/openspec/changes/retire-thread-tree-authority/.openspec.yaml b/openspec/changes/retire-thread-tree-authority/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/retire-thread-tree-authority/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/retire-thread-tree-authority/design.md b/openspec/changes/retire-thread-tree-authority/design.md new file mode 100644 index 00000000..7070b2c6 --- /dev/null +++ b/openspec/changes/retire-thread-tree-authority/design.md @@ -0,0 +1,174 @@ +## Context + +新领域链完成后,代码库会暂时同时包含可运行的 legacy ThreadTree 栈和默认关闭的 canonical Conversation 栈。并存代码本身可以支持验证,但生产事实不能并存:任何双写、读失败回退或异步同步都会重新创造 Issue #34 的根因。 + +Cutover 是一次数据、应用、客户端和文档共同变更,不是简单切 feature flag。本设计把决定门禁、冻结、导入、互斥切换、观察、回滚和最终删除分成可验证阶段。 + +## Goals / Non-Goals + +**目标:** + +- 基于真实数据选择重置或导入,并保留审计证据。 +- 在没有未确认旧写入时把 canonical 栈切为唯一权威。 +- 保持新 specs 定义的全部用户行为、授权和计费正确性。 +- 提供切换前后不同的安全回滚策略。 +- 删除旧协议和重复领域事实,不留下永久兼容层。 + +**非目标:** + +- 用双写实现无停机迁移。 +- 在 cutover 中实现分享、Project assets、CLI/MCP 或 Issue #39 收敛。 +- 保留旧客户端的长期写兼容。 +- 删除未经依赖审计的其他聊天表或已归档迁移历史。 + +## Decisions + +### D1. Authority 是单值部署状态 + +应用组合根只接受: + +```text +CONVERSATION_AUTHORITY = legacy | canonical +``` + +服务端健康信息发布 authority、schemaVersion 和 cutover epoch;客户端 boot 必须匹配。`legacy` 组合根不能构造 canonical 写 handler,`canonical` 组合根不能构造 branch-tree 写仓储。配置缺失、未知或两套依赖同时可写时健康检查失败。 + +备选方案是按请求或 Conversation 灰度双 authority;跨记录的列表、billing job 和客户端路由会产生不可证明的混合边界,因此不用于首次 cutover。 + +### D2. 数据处置先产出 ADR,再写执行参数 + +使用 persistence change 的审计器在目标环境输出: + +- legacy Conversation/Thread/Message/Generation 数量与所有者分布; +- 合法、需修复、拒绝记录; +- 反馈、Artifact、标题、usage 和其他 sidecar 引用; +- 数据保留义务与环境用途。 + +单独 ADR 选择: + +1. **确定性导入**:存在任何需保留数据时的默认; +2. **受批准重置**:只限明确无保留义务的隔离环境。 + +任务不会预先写死选择。ADR 包含批准者、时间、范围、备份和不可迁移项处置。 + +### D3. Legacy 局部 ID 通过持久映射迁移 + +许多 JSON 内 ID(如 `main`、`m1`、`b2`)只在单棵树内唯一,不能直接成为公开规范 ID。导入器以 `(legacyTreeId, entityType, localId)` 生成或记录稳定 canonical ID,并输出映射清单。所有 ThreadFork、Generation、反馈、Artifact、标题和 usage 引用通过同一映射解析。 + +导入器使用 upsert/幂等 marker,可在冻结数据上重复 dry-run 和正式运行。单条 Conversation 在事务中导入;全局报告校验源/目标计数、唯一约束、哈希摘要和悬空引用。 + +### D4. 用短维护窗口代替双写 + +执行顺序: + +```text +拒绝新的 legacy mutations/generations + → drain/stop/reconcile 非终态 Generation + → 验证 usage/计费终结 + → 备份 legacy 与空/预导入 canonical 数据 + → 正式 import/reset + → 全量 integrity verification + → 切 canonical authority + canonical client +``` + +维护窗口虽然有短暂不可写,但把最后写入边界变成可证明时刻。双写看似减少停机,却需要再次实现两套语义的协调,风险更高。 + +### D5. 行为矩阵是 release gate,不是人工印象 + +矩阵至少覆盖: + +```text +Auth/owner/project isolation +Conversation list/create/title/archive/restore/delete +Thread title/archive/restore +Column/tree-list/canvas navigation +Selection anchor + A→B→C Fork +Send/edit/regenerate/select variant +Markdown/Artifact/research activities +Copy/feedback/message actions +Streaming/disconnect/Stop/stale recovery/refresh +Usage completeness/exactly-once billing +遗留路由拒绝 +``` + +每项关联自动测试、手工 smoke(仅视觉交互)和新 spec Requirement。已确认旧错误不作为“兼容行为”保留。 + +### D6. 切换采用一个不可分割的 release checkpoint + +在数据验证完成且仍处于维护模式时: + +1. 部署/启用 canonical 服务端 authority; +2. 验证规范 API 健康、schema 和 epoch; +3. 启用 canonical 客户端 build; +4. 对内部 canary actor 执行读写 smoke; +5. 开放流量并关闭维护模式。 + +如果平台不能原子切配置与客户端,则先部署能识别服务端 epoch 的客户端壳,只有 epoch 匹配才加载 canonical 页面;不允许新客户端向 legacy 写入或旧客户端向 canonical 写入。 + +### D7. 旧路由立即拒绝,代码和表延后删除 + +切换时旧 mutation routes 返回 `410` + `legacy_protocol_retired`(认证/不可见检查仍按安全策略执行),旧读取不作为 canonical fallback。保留最小拒绝/遥测 handler 一段声明观察期,用于识别陈旧客户端。 + +观察门禁通过后分两步清理: + +1. 删除 Tree Store、`persistNow`、reconcile、legacy repositories/routes 和所有新代码对 ThreadTree 类型的导入; +2. 在备份保留与依赖扫描通过后,用后续不可逆迁移删除确认只服务旧能力的表/列。 + +历史 SQL migration 文件不重写。名称相似的 assistant-ui 线性表先审计用途,不能因为名字相似顺带删除。 + +### D8. 可观测性围绕正确性门禁 + +Dashboard/日志至少包含: + +- authority/schema/epoch mismatch; +- snapshot/command 成功率与稳定错误码; +- revision/idempotency 冲突; +- Generation 非终态年龄、checkpoint age、Stop latency、stale 收敛; +- `usage_unavailable`、唯一 usage 冲突和对账差异; +- legacy route 调用; +- 导入/运行时外键和不变量失败。 + +阈值在 release runbook 中依据当前基线填写,不在设计中伪造通用数字。日志只记录 ID、分类和计数,不记录私密 Message 正文。 + +### D9. 回滚线以首个 canonical 生产写入为界 + +**切换前:** legacy 数据未改变,可退出维护并继续 legacy。 + +**切换后但尚无 canonical 写入:** 可以关闭流量并回到 legacy,但必须用审计证明零 canonical mutation。 + +**已有 canonical 写入:** legacy 已成为落后备份,不得恢复为权威。选择: + +- 回滚客户端/应用版本,但保留兼容 canonical schema; +- 进入 canonical read-only 并前滚修复; +- 从 cutover 后的规范备份/事务日志恢复。 + +绝不实现 canonical → ThreadTree 反向同步,因为它会把删除掉的重复关系语义重新引入。 + +### D10. OpenSpec 按“历史实现 → 目标替代”排序 + +对依赖旧模型的 changes 建立 supersession 清单。完成的旧 changes 保留真实历史;归档时确保最终生效 specs 由 `define-conversation-domain-model` 和四个后续能力覆盖。未完成 `persist-thread-chat-generations` 不补勾任务,记录被 `migrate-generation-lifecycle` 及 cutover 替代的原因和未完成范围。 + +若归档工具会把旧 requirement 重新合并为目标权威,则使用明确的 superseded/skip-specs 历史处置流程,而不是通过修改旧任务伪造一致性。 + +## Risks / Trade-offs + +- **[风险] 维护窗口暂时阻止聊天写入。** → 提前测量导入时间、预生成 dry-run 映射并保留只读访问;用短冻结换取唯一写入边界。 +- **[风险] 遗留污染数据阻塞整批切换。** → 按 Conversation 事务导入并提前修复;所有排除都需 ADR 批准和备份。 +- **[风险] 新客户端与 authority epoch 不匹配。** → boot 强制检查服务端 epoch/schema,不匹配时显示维护/刷新提示而不写入。 +- **[风险] 切换后无法简单回到旧 JSON。** → 这是避免数据丢失的必要限制;通过 canonical 备份、read-only 和前滚演练降低风险。 +- **[风险] 过早删表破坏隐蔽 job。** → 先拒绝路由和删除代码引用,观察零调用,再以 DB dependency/query audit 门禁物理删除。 + +## Migration Plan + +1. 完成并严格验收全部前置 changes,建立行为矩阵和基线。 +2. 在目标环境运行只读审计,批准 import/reset ADR,完成 dry-run 与映射报告。 +3. 演练维护、drain、备份、导入、验证、切换和 canonical 恢复。 +4. 正式进入维护窗口,冻结旧写入并收敛所有 Generation/计费。 +5. 生成验证备份,执行 import/reset 和全量完整性检查。 +6. 切换 authority/client epoch,执行 canary 和 smoke 后开放流量。 +7. 观察门禁通过后删除旧运行时代码;保留备份至声明期限。 +8. 依赖扫描和恢复验证通过后删除遗留表/列,并如实处置旧 OpenSpec changes。 + +## Open Questions + +仅有一个必须由真实审计回答的部署决策:目标环境采用确定性导入还是受批准重置。该问题不改变架构,但必须在执行 cutover 前以 ADR 关闭。 diff --git a/openspec/changes/retire-thread-tree-authority/proposal.md b/openspec/changes/retire-thread-tree-authority/proposal.md new file mode 100644 index 00000000..e94dbddb --- /dev/null +++ b/openspec/changes/retire-thread-tree-authority/proposal.md @@ -0,0 +1,33 @@ +## Why + +前四个 changes 可以把新模型完整实现出来,但只要 `branch_trees.state`、整树 PUT 或旧客户端仍能写入,系统就有两个事实源,任何新关系和修复最终都会再次漂移。最后必须通过有证据、可门禁、不可双写的 cutover,把规范 Conversation 栈切为唯一生产权威,并让旧 ThreadTree 协议和代码安全退场。 + +## What Changes + +- 汇总遗留审计、真实数据量、悬空引用和功能覆盖证据,在 cutover 前明确选择“一次性导入”或“有批准的数据重置”,不得在任务中预设答案。 +- 建立旧/新行为对照与发布门禁,覆盖列表/标题、列/画布、嵌套 Fork、Message 变体/actions、Artifact/反馈、Generation Stop/恢复和计费。 +- 在维护窗口停止旧写入、排空或收敛非终态 Generation、生成可验证备份,并一次性导入/初始化规范实体及所有辅助引用。 +- 以互斥 authority 配置原子切换规范服务端、命令 API 和 normalized 客户端;系统不存在长期双写、双读择优或后台协调模式。 +- **BREAKING**:关闭整树 GET/PUT/DELETE 写协议、遗留 Generation 协调路由和浏览器 save/reconcile 路径;旧客户端收到稳定 retired 响应,不得继续覆盖规范实体。 +- 在观察期后删除 `ThreadTreeState` 新代码依赖、魔法 `main`、重复 parent/children/forks/depth/activeLeaf 字段,以及确认无其他用途的 `branch_trees`/`branch_generations` 结构。 +- 制定不会把规范写入回灌旧 JSON 的回滚方案;切换后问题通过 canonical read-only、前滚修复或规范备份恢复处理。 +- 处理依赖旧权威模型的活跃 OpenSpec changes:完整能力保留为历史,未完成的 `persist-thread-chat-generations` 明确标记被新链替代,不把未完成任务伪装为完成。 + +## Capabilities + +### New Capabilities + +- `conversation-cutover`:定义从 ThreadTree 权威到规范 Conversation 权威的数据决策、发布门禁、互斥切换、旧协议退场、观察与回滚要求。 + +### Modified Capabilities + +- 无。遗留 capabilities 的历史归档顺序由本 change 的迁移任务处理;目标行为由新链的独立 capabilities 定义。 + +## Impact + +- 数据:`branch_trees`、`branch_generations`、Message feedback、Artifact/标题/usage 外键、规范 Conversation 全表和备份。 +- 服务端:旧 branch-tree 路由、Generation sidecar/reconcile、领域 types/selectors/repositories、authority 配置和运维脚本。 +- 客户端:legacy boot/Store/persistence/chat controller 与 canonical 页面开关。 +- 发布:维护窗口、数据审计、canary/smoke、指标、回滚和旧表保留期。 +- OpenSpec:旧 active changes 的历史处置与新 changes 的依赖顺序。 +- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`、`migrate-generation-lifecycle`、`add-conversation-command-api`、`normalize-conversation-client-state` 全部实现并验收。 diff --git a/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md b/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md new file mode 100644 index 00000000..4425d92c --- /dev/null +++ b/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md @@ -0,0 +1,155 @@ +## Purpose + +以可审计、互斥、可回滚且不产生双重事实源的方式,将生产 Conversation 从 ThreadTree JSON 权威切换到规范实体,并安全退役旧协议、代码和数据结构。 + +## ADDED Requirements + +### Requirement: Cutover 必须满足显式前置门禁 + +切换生产权威前,系统 SHALL 证明规范持久化、Generation 生命周期、命令 API 和 normalized 客户端的必需任务及行为矩阵已经完成。发布记录 MUST 包含严格测试结果、数据审计、容量/性能基线、安全检查、备份验证、回滚演练和明确 go/no-go 决策;任一阻塞门禁失败 MUST 取消切换。 + +#### Scenario: Generation partial 回归未通过 + +- **WHEN** 门禁测试仍会在 stale recovery 中覆盖已保存 partial +- **THEN** 发布保持 legacy authority,不进入数据冻结或 cutover + +#### Scenario: 所有门禁满足 + +- **WHEN** 实现、数据、行为、安全、备份和回滚证据均满足记录的验收标准 +- **THEN** 负责人可以批准进入受控维护窗口 + +### Requirement: 用真实审计决定导入或重置 + +系统 SHALL 根据 `normalize-conversation-persistence` 产生的只读审计和目标环境用途,记录选择“一次性导入”或“受批准重置”的决定、影响范围、记录数量、不可迁移项及处理人。存在需保留的用户数据时 MUST 导入或逐项解决,不能为简化开发而静默清空。 + +#### Scenario: 目标环境有需保留用户数据 + +- **WHEN** 审计发现属于真实用户且在保留范围内的 Conversation +- **THEN** cutover 计划选择确定性导入,并为全部不可直接映射记录给出修复或明确排除批准 + +#### Scenario: 隔离开发环境获准重置 + +- **WHEN** 审计证明数据无保留义务且负责人书面批准重置 +- **THEN** 发布记录保存范围、备份与批准证据后,才可初始化空规范数据 + +### Requirement: 一次性导入必须确定且引用完整 + +导入器 SHALL 在旧写入冻结后,把每个 legacy `(tree, entityType, localId)` 确定映射为规范稳定 ID,并迁移 Conversation、Thread、ThreadFork、Turn、Message、Generation、标题、反馈、Artifact 和 usage 关联。导入 MUST 可重复运行、事务安全并输出映射/拒绝报告;任何跨所有者、悬空或计数不一致 MUST 阻止该记录切换。 + +#### Scenario: 导入含嵌套 Fork 的树 + +- **WHEN** 合法遗留树包含 A → B → C 及 Message 反馈、Artifact 和已终结 Generation +- **THEN** 导入产生一个 Conversation、三个 Thread、两条 ThreadFork 和完整辅助外键,重跑不会产生重复实体 + +#### Scenario: 导入发现悬空 Generation Message + +- **WHEN** `branch_generations` 引用无法映射的 JSON Message ID +- **THEN** 导入将该 Conversation 标为阻塞并保持旧备份,不伪造 Message 或忽略计费引用 + +### Requirement: 在无旧写入窗口排空运行任务 + +Cutover SHALL 先进入受控只读/维护模式,拒绝新的 legacy Conversation 写入和 Generation 开始,再等待非终态 Generation 正常完成、停止或由既有规则收敛。直到旧系统没有未确认的模型执行、checkpoint 或计费事务,系统 MUST NOT 执行最终数据导入和 authority 切换。 + +#### Scenario: 维护开始时仍有运行任务 + +- **WHEN** legacy Generation 仍为 `running` 或 `stop_requested` +- **THEN** cutover 等待或按声明策略停止/收敛,并在终态与计费可审计后继续 + +#### Scenario: 维护期间旧客户端发送写入 + +- **WHEN** 旧客户端尝试 PUT 整树或开始 Generation +- **THEN** 系统返回稳定维护响应且不接受新事实 + +### Requirement: 规范 Conversation 必须成为唯一权威 + +Cutover 后,所有读取、命令、Generation、计费和客户端页面 SHALL 只使用规范 Conversation 实体。authority 模式 MUST 在同一部署中互斥;系统不得双写、双读择优、从 legacy 回填缺失规范记录,或运行两个 Store 相互同步。 + +#### Scenario: 规范记录缺失 + +- **WHEN** cutover 后请求的 Conversation 无规范记录但旧 JSON 仍有备份 +- **THEN** 系统返回规范未找到/数据错误并告警,不在请求路径回退读取旧 JSON + +#### Scenario: 配置同时启用两套写入 + +- **WHEN** 部署配置尝试同时启用 legacy 与 canonical authority +- **THEN** 应用启动或健康门禁失败,部署不得接收流量 + +### Requirement: Cutover 后保持目标行为矩阵 + +规范路径 SHALL 跑通 Conversation 列表/标题/归档、Thread 列/画布/归档、A → B → C Fork、锚点、Turn/Message 变体、Message actions、Markdown/Artifact/研究活动、Generation 流式/Stop/刷新恢复、反馈、所有者隔离和计费幂等。行为以新 specs 为准,不要求保留旧数据结构或已确认的错误语义。 + +#### Scenario: 切换后停止部分回答并刷新 + +- **WHEN** 用户在规范路径停止已有 partial 的 Generation 并刷新 +- **THEN** 相同 Thread/Turn/Message、`incomplete` 内容、研究活动、usage 完整度和 `stopped` 状态被恢复 + +#### Scenario: 切换后嵌套 Fork + +- **WHEN** 用户从根 Thread Fork B,再从 B Fork C +- **THEN** 列和画布显示两条 ThreadFork 的正确来源,服务端不存在重复 parent/children/Fork 写入 + +### Requirement: 旧 ThreadTree 协议必须明确退役 + +Authority 切换后,整树写入、旧 Generation 协调、active-leaf 切换和 legacy reconcile 路由 SHALL 被关闭,并对旧客户端返回稳定 `legacy_protocol_retired` 结果或不存在结果。旧协议不得在错误、缺失记录或回滚时重新启用为生产写入入口。 + +#### Scenario: 旧客户端提交整树 PUT + +- **WHEN** cutover 后旧客户端调用 legacy branch-tree 写路由 +- **THEN** 系统拒绝请求并返回稳定 retired 响应,不修改规范或备份数据 + +#### Scenario: 新客户端正常加载 + +- **WHEN** canonical 客户端加载 Conversation +- **THEN** 它只调用规范 snapshot/command/Generation API,不探测或调用旧路由 + +### Requirement: 切换期必须可观测和可审计 + +系统 SHALL 记录 authority epoch/schema、请求路径、命令错误分类、revision 冲突、Generation 终态/checkpoint age、stale 收敛、usage 对账、导入计数和完整性失败。发布 SHALL 使用预先记录的阈值和观察窗口判断继续、只读或前滚修复,日志不得泄漏 Message 内容或凭据。 + +#### Scenario: 切换后 usage_unavailable 异常上升 + +- **WHEN** 计费完整度指标超过批准阈值 +- **THEN** 发布进入声明的保护动作并保留规范记录用于诊断,不切回旧 JSON 结算 + +#### Scenario: 审计查询单次 Fork + +- **WHEN** 运维按 requestId/commandId 调查 Fork +- **THEN** 可以关联 actor、Conversation revision、ThreadFork/outbox 事件和结果,而无需读取整树 mutation diff + +### Requirement: 回滚不得重新引入双重事实源 + +切换前的回滚可以继续使用未修改的 legacy authority;一旦 canonical 接受生产写入,回滚 SHALL 保持规范数据库为事实源,通过关闭写入、回滚应用代码兼容层、恢复规范备份或前滚修复处理。系统 MUST NOT 把新规范写入反向合并回旧 JSON 后恢复旧权威。 + +#### Scenario: 切换后客户端渲染故障 + +- **WHEN** canonical 数据正确但新客户端出现严重回归 +- **THEN** 发布回滚/修复客户端或进入只读,服务端规范实体继续保持权威 + +#### Scenario: 切换后发现规范数据损坏 + +- **WHEN** 完整性检查发现不可接受的规范写入损坏 +- **THEN** 系统停止相关写入并从已验证规范备份恢复或执行审计前滚,不从落后的 legacy JSON 覆盖 + +### Requirement: 遗留代码和表分阶段删除 + +系统 SHALL 在 authority 切换成功后先证明旧路由零调用、无代码写引用且备份可恢复,再按声明保留期删除 ThreadTree 领域类型、浏览器 persistence/reconcile、旧仓储和重复关系字段。物理表删除 MUST 经过依赖清单确认;名称相似但仍服务其他能力的表不得被顺带删除。 + +#### Scenario: 观察期仍有旧路由调用 + +- **WHEN** 指标显示受支持客户端仍调用 branch-tree route +- **THEN** 系统保持路由拒绝状态并追踪调用方,不提前删除诊断信息或备份 + +#### Scenario: 删除旧表前发现外部依赖 + +- **WHEN** 依赖扫描发现某个 billing/feedback job 仍读取 `branch_generations` +- **THEN** 表删除门禁失败,先迁移该 job 并重新验证 + +### Requirement: OpenSpec 历史必须如实标记替代关系 + +依赖 ThreadTree 权威的完成 changes SHALL 作为历史实现记录保留,并在目标 specs 生效顺序中由新链覆盖。未完成的 `persist-thread-chat-generations` MUST 明确标记为被 `migrate-generation-lifecycle` 与本 cutover 替代;系统不得仅为归档而勾选未实现任务或把旧机制继续发布为当前契约。 + +#### Scenario: 处理未完成旧 Generation change + +- **WHEN** cutover 准备归档 OpenSpec 记录 +- **THEN** 文档保留其真实未完成项和 superseded 原因,并使新的 lifecycle spec 成为目标权威 + diff --git a/openspec/changes/retire-thread-tree-authority/tasks.md b/openspec/changes/retire-thread-tree-authority/tasks.md new file mode 100644 index 00000000..3a91d18c --- /dev/null +++ b/openspec/changes/retire-thread-tree-authority/tasks.md @@ -0,0 +1,60 @@ +## 1. 前置实现与发布门禁 + +- [ ] 1.1 验证 `define-conversation-domain-model`、规范持久化、Generation lifecycle、命令 API 和 normalized client 的必需任务及 strict specs 全部完成。 +- [ ] 1.2 建立目标行为矩阵,关联认证、生命周期、列/画布、A → B → C Fork、变体、actions、Artifact/研究、Generation/计费的自动测试和 smoke。 +- [ ] 1.3 记录当前容量/性能/错误/计费基线、cutover 阈值、负责人、观察窗口和 go/no-go checklist。 +- [ ] 1.4 演练 canonical 备份恢复、维护模式、authority epoch mismatch 和切换后只读/前滚回滚流程。 + +## 2. 数据审计与处置 ADR + +- [ ] 2.1 在目标环境运行只读 legacy 审计,输出所有者、Conversation/Thread/Message/Generation、标题、反馈、Artifact 和 usage 数量。 +- [ ] 2.2 逐项处理或明确阻塞悬空、跨所有者、重复 ID、错误 Fork/active path 和计费引用。 +- [ ] 2.3 基于保留义务形成“确定性导入或受批准重置”ADR,记录范围、理由、批准人、备份和排除项。 +- [ ] 2.4 将 ADR 结果转成版本化 cutover 配置,不在代码中隐式猜测环境或数据处置方式。 + +## 3. 确定性导入或重置工具 + +- [ ] 3.1 实现 `(legacyTreeId, entityType, localId) → canonicalId` 的持久映射与幂等 dry-run/report。 +- [ ] 3.2 实现按 Conversation 事务导入 Project/Conversation/Thread/ThreadFork/Turn/Message 和当前有效变体。 +- [ ] 3.3 迁移 Generation、标题、Message feedback、Artifact、usage/billing 和其他审计确认的 sidecar 外键。 +- [ ] 3.4 实现受批准重置路径,仅接受 ADR scope 并在执行前验证目标环境与备份标识。 +- [ ] 3.5 增加 A → B → C、Message 变体、partial Generation、辅助引用、重复运行和故障回滚测试。 +- [ ] 3.6 实现源/目标计数、映射、约束、悬空引用和摘要的全量 post-import verifier。 + +## 4. Authority、维护与旧协议拒绝 + +- [ ] 4.1 实现单值 `legacy | canonical` authority 组合根、schemaVersion/cutover epoch 健康信息和非法双 authority 启动失败。 +- [ ] 4.2 实现维护模式,拒绝新 legacy mutations/Generation,同时保留必要只读和安全认证语义。 +- [ ] 4.3 实现非终态 Generation drain/Stop/stale 收敛与 usage/计费完成检查,未清零时阻止 cutover。 +- [ ] 4.4 为旧整树、active-leaf、Generation sidecar 和 reconcile routes 实现安全 `legacy_protocol_retired` 拒绝与遥测。 +- [ ] 4.5 让 canonical 客户端 boot 校验服务端 authority/schema/epoch,不匹配时不得发送写命令。 + +## 5. Cutover 演练与正式执行 + +- [ ] 5.1 在与生产等价的隔离环境完整演练冻结、drain、备份、import/reset、验证、authority/client 切换和 smoke,并记录耗时。 +- [ ] 5.2 正式进入维护窗口,冻结旧写入并确认所有 Generation、checkpoint、outbox 和计费事务已收敛。 +- [ ] 5.3 创建并验证 legacy/canonical 备份,执行 ADR 选定的数据动作和全量 post-import verifier。 +- [ ] 5.4 原子启用 canonical server/client epoch,对内部 canary 跑行为矩阵关键 smoke 后开放流量。 +- [ ] 5.5 验证 cutover 后所有读取、命令、Generation 和 billing jobs 只访问 canonical repositories。 + +## 6. 观察与回滚保护 + +- [ ] 6.1 建立 authority mismatch、命令错误、revision/idempotency 冲突、Generation age/checkpoint、Stop/stale 和 usage 对账 dashboard/告警。 +- [ ] 6.2 监控旧 route 调用、数据库查询和代码路径,记录陈旧客户端/任务调用方并完成迁移。 +- [ ] 6.3 验证切换后回滚工具不会执行 canonical → ThreadTree 反向同步,也不会把落后 legacy 数据恢复为权威。 +- [ ] 6.4 在声明观察窗口内完成逐日完整性/计费审计,并由负责人批准进入遗留删除阶段。 + +## 7. 删除遗留运行时代码 + +- [ ] 7.1 删除 canonical build 中的 `ThreadTreeState`、魔法 `main`、parent/children/Message forks/depth/activeLeaf 等重复领域事实和 selector。 +- [ ] 7.2 删除 Tree Store mutation、`persistNow`、save debounce、卸载 flush、startup reconcile 和 Generation merge 协调代码。 +- [ ] 7.3 删除旧 branch-tree/Generation 写路由、仓储和新代码对 legacy types 的导入,保留历史迁移文件不重写。 +- [ ] 7.4 运行 `rg`/依赖检查证明运行时代码无旧 authority 读写,并通过完整行为矩阵、typecheck、build 和测试。 + +## 8. 物理清理与 OpenSpec 收口 + +- [ ] 8.1 审计 `branch_trees`、`branch_generations` 及名称相似表的全部 FK、job、查询和备份依赖,形成精确删除清单。 +- [ ] 8.2 在备份保留/恢复验证和零依赖门禁通过后,执行分阶段不可逆迁移删除确认遗留表、列、索引和约束。 +- [ ] 8.3 建立旧 OpenSpec supersession map;保留已完成历史,明确记录 `persist-thread-chat-generations` 未完成项被新 lifecycle/cutover 替代,不补勾任务。 +- [ ] 8.4 按依赖顺序归档新 changes,验证最终 specs 以 Project → Conversation → Thread 模型和 canonical capabilities 为权威。 +- [ ] 8.5 运行最终 `pnpm openspec:validate`、schema 漂移检查、生产 smoke 和数据完整性/计费审计并记录结果。 From a9d30ddec41043d64a40841e7a3749e954f1d45c Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 01:10:43 +0800 Subject: [PATCH 02/23] =?UTF-8?q?feat:=20=E7=AC=AC=E4=B8=80=E7=89=88?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 1124 +++++++++++++---- .../proposal.md | 39 +- 2 files changed, 878 insertions(+), 285 deletions(-) diff --git a/openspec/changes/define-conversation-domain-model/design.md b/openspec/changes/define-conversation-domain-model/design.md index 3381b7ba..39b774a9 100644 --- a/openspec/changes/define-conversation-domain-model/design.md +++ b/openspec/changes/define-conversation-domain-model/design.md @@ -1,404 +1,992 @@ ## Context -变更动机见 `proposal.md`,规范语言见 `specs/domain/spec.md`。 +变更动机见 `proposal.md`。本文只定义目标领域、数据库和服务端模块骨架,不以当前 `ThreadTreeState`、整树 JSON、现有 API 或客户端 Store 的形状为前提。 -当前 Thread Chat 模型是一份持久化到 `branch_trees.state` JSONB 的 `ThreadTreeState` 对象。同一个 Fork 事实被重复保存在 `Thread.parentId`、`Thread.children`、`Thread.forkFromMsgId`、`Message.forks`、深度、脚注和锚点等字段中。根节点使用魔法注册项 `threads.main`。Message 变体存在于各 Thread 内部的父子图中,由 `activeLeafMessageId` 选择提示词路径。`branch_generations` 虽然是关系型辅助表,但它的 Thread 和 Message ID 指向 JSON 内部节点,数据库无法通过外键保护。服务端启动和终结 Generation 的仓储因此必须锁定并重写整棵树,再把辅助表结果协调合并回 JSON。 +此前目标设计同时引入了独立 ThreadFork、Turn、Message Variant、Generation、当前有效变体和多级 revision。它可以表达回答版本树,但产品最终确认的 ThreadChat MVP 1.0 不提供回答版本选择: -Issue #34 要求替换这套权威模型。产品讨论同时确认:Project 必须拥有一整个 Conversation,而用户看到的每一列对话都是一个 Thread。Issue #39 单独记录未来的发散与收敛行为,本变更明确不包含该能力。 +- 用户看到的每一列都是一个 Thread; +- Thread 内 Message 永远严格线性; +- 只有最后一条 user Message 可以 Edit; +- Regenerate 破坏式替换最后一条 assistant Message; +- 需要保留另一条历史时统一使用 Fork Thread; +- Child Thread 创建后必须保持 Fork 当时的语义,不受 Parent 后续破坏式修改影响; +- 模型执行需要后台持久化、Stop、断线恢复、用量与计费,但不应扩张 Chat 内容模型。 -仓库中仍有活跃 OpenSpec 变更描述旧 JSON 权威模型,特别是 `persist-thread-chat-generations` 和 `add-thread-chat-message-actions`。已经完成的历史行为仍然是有价值的证据,但这些文档不能继续作为目标架构的事实来源。 +因此,目标模型收敛为: + +```text +Workspace +└── Project + ├── Conversation + │ └── Thread + │ └── Message[] + ├── ProjectInstructionVersion + ├── MemoryItem + └── ProjectFile + +执行子域: +Assistant Message +└── MessageRun[] +``` + +这次简化不是把运行可靠性删除,而是把“聊天内容事实”和“模型执行事实”分开。Chat Domain 不包含 Turn、Message Variant 或 Generation;执行子域使用 MessageRun 保存每次 attempt。 ## Goals / Non-Goals **目标:** -- 在提出任何持久化、接口或客户端迁移方案前,建立一套唯一的规范术语。 -- 定义目标逻辑实体、所有权、身份和关系不变量。 -- 收敛 Issue #34 中会改变后续数据库结构和任务顺序的决策。 -- 定义模块边界,使网页界面、未来公开接口、命令行和 MCP 适配器共用同一应用层。 -- 给出由多个可独立审查的小变更组成的明确依赖链。 -- 把遗留 `ThreadTreeState` 隔离为迁移输入,避免旧名称继续泄漏到新模块。 +- 建立 Workspace、Project、Conversation、Thread、Message 与 MessageRun 的唯一名称和职责。 +- 定义 Fork、根 Thread、分支 Thread、破坏式 Edit、破坏式 Regenerate、Retry 与 Stop 的语义。 +- 定义 `BaseContext` 与 `ForkSourceSnapshot` 两个值对象及其不可变边界。 +- 给出可由 PostgreSQL/Drizzle 实现的逻辑表、字段、复合约束、索引与事务不变量。 +- 定义 UI Timeline、LLM Prompt、Conversation Bootstrap 等读取投影的事实来源。 +- 定义纯领域、应用命令、执行端口、仓储、HTTP 适配器之间的目标模块骨架和依赖方向。 +- 为后续服务端实现、集成测试、API 测试、Zustand Store、Hooks 与 UI 接入提供稳定基础。 +- 保留 Workspace、Project、Conversation、Thread 和 Message 的稳定公开身份,使未来分享、CLI、MCP 与公开 API 无需重做核心身份。 **非目标:** -- 在本变更中创建规范化数据库表或迁移生产、开发数据。 -- 在本变更中替换现有命令接口、Generation 流、协调合并逻辑或客户端状态存储。 -- 重建 Markdown 渲染、流式展示、编辑器、画布或视觉设计。 -- 实现 Project Memory、Project Instruction、Project File、分享、公开令牌、命令行或 MCP。 -- 实现 Issue #39 中的 Thread 结果发布或 Conversation 收敛。 -- 为不存在的外部客户端保留兼容层,或永久保留旧整树写入协议。 +- 本变更不创建数据库迁移、Drizzle schema、仓储、API 路由或运行时代码。 +- 本变更不设计 Zustand Store、React Hooks、Selectors、组件接入或 UI 交互细节。 +- 本变更不设计现有数据导入、双写、切换、回滚或旧表删除方案。 +- 本变更不保留被覆盖的 user/assistant Message 内容版本,也不提供回答版本选择器。 +- 本变更不支持编辑历史 Message;历史位置的另一条发展路径必须通过 Fork 创建。 +- 本变更不支持多来源 Thread、Thread Merge、跨 Thread 结论收敛或 Issue #39 的发布机制。 +- 本变更不实现 Project Memory、Project Instruction、Project File、分享、公开 Token、CLI 或 MCP 产品功能;只定义所有权和扩展位置。 +- 本变更不实现通用 `Idempotency-Key`、离线队列或命令重放;这些属于 V2。 +- 本变更不决定 SSE、轮询或 WebSocket 的最终传输选择;它只定义可恢复事件需要的运行事实。 ## Decisions -### D1. 规范聚合关系:Project → Conversation → Thread - -目标包含关系为: +### D1. 统一术语与边界 + +| 名称 | 定义 | 是否为核心实体 | +|---|---|---:| +| Workspace | 租户、成员和授权的最外层边界 | 是 | +| WorkspaceMembership | User 与 Workspace 的成员/角色关系 | 关系实体 | +| Project | 长期目标、规则、记忆、文件和 Conversation 的资产边界 | 是 | +| Conversation | 一个根 Thread 及全部派生 Thread 组成的完整讨论 | 是 | +| Thread | 一列可以独立继续的严格线性对话 | 是 | +| Root Thread | Conversation 中唯一没有 Parent 的 Thread;是推导角色 | 否 | +| Branch Thread | 由 Fork 创建且具有 Parent 的 Thread;是推导角色 | 否 | +| Fork | 从确定 Message 创建 Child Thread 的服务端动作 | 否 | +| Message | Thread 中具有稳定 ID 和顺序的用户或助手内容 | 是 | +| MessageRun | 生成/重试一个 assistant Message 的一次后台执行 attempt | 执行子域实体 | +| BaseContext | Child Thread 创建时继承的不可变有效历史投影 | 值对象 | +| ForkSourceSnapshot | Fork 时选区、来源哈希和定位上下文的不可变快照 | 值对象 | +| ProjectInstructionVersion | Project 级指令的不可变版本 | 扩展实体 | +| MemoryItem | Project 级、可追溯的记忆条目 | 扩展实体 | +| ProjectFile / FileVersion | Project 内可跨 Conversation 使用的持久文件及版本 | 扩展实体 | +| UI Workspace | 列、折叠、画布和面板等客户端状态 | 不是后端实体 | + +以下名称不进入目标模型: ```text -Workspace -└── Project - ├── ProjectInstructionVersion - ├── MemoryItem - ├── File - └── Conversation - ├── rootThreadId - └── Thread[] +ThreadTree +MainThread 实体 +ForkedThread 实体 +ThreadFork 实体 +Turn +Generation +Message.parentMessageId +Message Variant +activeGenerationId +Conversation/Thread/Turn revision ``` -`Conversation` 取代 `Thread Tree`。Conversation 是用户可以整体命名、列出、归档、删除并最终分享的聚合。Thread 是 Conversation 内一条可以独立继续的对话列。 +`MessageRun` 在执行/计费子域中有身份和生命周期,因此技术上是运行实体;“不是一等实体”只表示它不是 Chat 内容资源,不表示它可以只存在于进程内存。 + +### D2. 规范包含关系 + +```mermaid +erDiagram + USER ||--o{ WORKSPACE_MEMBERSHIP : joins + WORKSPACE ||--o{ WORKSPACE_MEMBERSHIP : has + WORKSPACE ||--o{ PROJECT : contains + PROJECT ||--o{ CONVERSATION : contains + CONVERSATION ||--|{ THREAD : contains + THREAD ||--o{ MESSAGE : contains + THREAD o|--o{ THREAD : parent_of + MESSAGE o|--o{ THREAD : fork_source_of + MESSAGE ||--o{ MESSAGE_RUN : executed_by + PROJECT ||--o{ PROJECT_INSTRUCTION_VERSION : may_have + PROJECT ||--o{ MEMORY_ITEM : may_have + PROJECT ||--o{ PROJECT_FILE : may_have + PROJECT_FILE ||--o{ FILE_VERSION : versions +``` + +包含关系不等于一个巨型事务聚合: + +- Workspace 是授权解析边界; +- Project 是长期资产边界; +- Conversation 是完整讨论的命名、列表、归档和分享边界; +- Thread 是追加 Message、Edit 和 Regenerate 的主要写入边界; +- Fork 命令跨 Parent Thread 读取并原子创建 Child Thread; +- MessageRun 由执行仓储独立推进,不要求锁定整个 Conversation。 + +**根 Thread 的唯一事实:** 一个 Conversation 恰好有一个 `parentThreadId = null` 的 Thread。目标写模型不同时持久化 `Conversation.rootThreadId`,避免两个字段竞争。读取 DTO 可以派生并返回 `rootThreadId`。 -Workspace 一词专门表示租户、成员和授权边界。现有前端工作台在概念上改称“界面工作区”或“视图状态”,避免与授权聚合混淆。第一次持久化迁移是引入物理 Workspace 行,还是把当前所有者直接映射到默认 Project,由该迁移变更决定;这个实现选择不会改变本设计的包含契约。 +**备选方案:Conversation 持久化 rootThreadId。** 不采用,因为它与 Root Thread 的空 Parent 角色重复,创建时还会产生循环引用。 -**备选方案:Project 直接拥有 Thread。** 不采用,因为它无法稳定标识一整个分叉对话,也无法表达其标题、生命周期、加载、分享和画布投影。 +### D3. Thread 内 Message 严格线性 -**备选方案:保留 Thread Tree。** 不采用,因为它描述的是实现拓扑,而不是稳定产品资源,并且会与未来非 Fork 关系发生冲突。 +Thread 不包含 Turn 或 Message DAG。唯一顺序为服务端分配的 `Message.sequence`: -### D2. Branch 是关系角色;ThreadFork 是唯一 Fork 事实 +```text +Thread A +├── sequence=1 user +├── sequence=2 assistant +├── sequence=3 user +└── sequence=4 assistant +``` -所有对话列都使用同一种 `Thread` 实体。主线和分支角色通过关系推导: +发送一条用户输入时,服务端在同一事务中创建: ```text -主 Thread = Conversation.rootThreadId -分支 Thread = 具有一个入向 ThreadFork 的 Thread +User Message sequence = N +Assistant placeholder sequence = N + 1 +MessageRun attempt = 1 ``` -`ThreadFork` 是独立关系: +`createdAt` 只负责审计,不负责领域排序。`prevMessageId/nextMessageId` 不持久化,因为它们可由 sequence 推导,双向指针会产生重复事实。 + +Message 只允许: ```text -ThreadFork -├── id -├── conversationId -├── parentThreadId -├── sourceMessageId -├── childThreadId # 唯一 -├── anchor # 可选 TextAnchor 值对象 -├── createdBy -└── createdAt +role = user | assistant ``` -必须满足以下约束: +System 指令由服务端 Prompt Builder 注入,不允许作为客户端提交的 Message。工具调用、工具结果、推理、Markdown Artifact 与文件引用使用版本化 Message Part 表达,不为每一种 part 强制创建数据库实体。 + +### D4. Edit 与 Regenerate 是破坏式操作 -1. 上游 Thread、下游 Thread 和来源 Message 都能在同一 Conversation 中解析; -2. 来源 Message 属于上游 Thread; -3. 下游 Thread 最多只有一个入向 ThreadFork; -4. 根 Thread 没有入向 ThreadFork; -5. 每个非根 Thread 恰好有一个入向 ThreadFork; -6. Fork 有向关系不得形成环; -7. 派生子 Thread、深度、脚注标签和分支数量都不是可写事实。 +#### Edit -锚点继续作为 ThreadFork 拥有的值对象。它没有独立权限或生命周期,因此不能仅仅为了形式上的规范化而拆成独立实体表。 +只有当前 Thread 最后一条 user Message 可以 Edit,并且它后面只能是对应的最后一条 assistant Message。事务执行: -`forkThread` 必须是一个应用命令和一个数据库事务。它创建下游 Thread 与 ThreadFork,推进最小必要版本边界并记录事务事件箱事件。客户端不能分别创建再关联这些对象。 +1. 锁定目标 Thread 的末尾 Message; +2. 验证目标 user Message 是最后一个用户输入; +3. 更新该 user Message 的 `parts/contentHash/updatedAt`; +4. 清空或重置紧随其后的 assistant Message 内容; +5. 将旧的非终态 MessageRun 条件更新为 `superseded`; +6. 为同一个 assistantMessageId 创建下一个 MessageRun attempt; +7. 提交后调度执行。 -**备选方案:在下游 Thread 保存 `parentThreadId + forkFromMessageId`。** 不采用,因为 Fork 自身具有来源、锚点、审计和未来展示语义。独立关系能够清楚表达唯一事实,又不会让 Thread 承担无关字段。 +Edit 不创建新的 Message ID、不保存旧输入版本、不产生隐式 Fork。 -**备选方案:为快速读取保留上游 children 和 Message forks 数组。** 不允许作为权威状态。可以存在等价索引或读取模型,但它们必须可以丢弃并重建。 +#### Regenerate -### D3. 使用显式 Turn 约束回复变体 +只有当前 Thread 最后一条 assistant Message 可以 Regenerate。事务执行: -目标对话图分离四类问题: +1. 验证目标是 Thread 最后一条 assistant Message; +2. 保持 assistantMessageId 与 sequence 不变; +3. 清空或重置其可见输出; +4. 将旧非终态 attempt 标记为 `superseded`; +5. 创建下一个 MessageRun attempt; +6. 新结果写回同一个 assistant Message。 + +旧 assistant 内容不保留版本选择。需要保留原回答时,用户必须先 Fork。 + +**备选方案:同一个位置保存多个输入/输出版本。** 不采用,因为它会重新引入 Turn、Generation 配对和 active variant,违背 MVP 的唯一分支机制。 + +### D5. Fork 是语义快照边界 + +Fork 不创建独立 ThreadFork 行。由于 MVP 中每个非根 Thread 恰有一个 Parent、一个来源 Message,且 Fork 元数据与 Child Thread 同生共死,以下字段直接属于 Child Thread: ```text -Thread 来源关系: Thread ──ThreadFork──> Thread -Turn 顺序: Thread ──> Turn ──> Turn -Message 变体: Turn ──> Message 变体 -执行尝试: Generation ──> 输入/输出 Message +parentThreadId +sourceMessageId +forkSourceSnapshot +baseContext ``` -逻辑实体为: +Fork API/应用命令只接受: ```text -Thread -├── id -├── conversationId -├── modelId / 模型设置 -├── 生命周期 -├── revision -└── Turn[] +parentThreadId +sourceMessageId +anchor(exact/prefix/suffix) +可选初始用户输入 +``` -Turn -├── id -├── threadId -├── 前序/顺序 -├── activeUserMessageId -├── activeAssistantMessageId -└── revision - -Message -├── id -├── threadId -├── turnId -├── 角色 -├── 内容/结构化片段 -├── 状态 -├── 变体来源(可选) -└── createdAt +客户端不得提交: -Generation -├── id -├── threadId -├── turnId -├── inputMessageId -├── outputMessageId -├── sourceAssistantMessageId(仅重新生成) -├── 意图/状态/模型 -├── 心跳/停止 -├── 用量/计费 -└── 幂等身份 +```text +childThreadId +baseContext +sourceContentHash +Parent Message history ``` -Turn 为回复变体提供显式稳定键,因此选择器和约束不再通过任意父子链推断“同一个回复位置”。Message 内容可以继续使用带版本的 JSON 或结构化片段值,因为工具片段不会仅仅由于结构化就拥有独立所有权。 +服务端事务执行: -对于当前已经支持的行为,编辑最后一条用户输入或重新生成最后一条助手输出,会在同一个 Turn 内创建 Message 变体。未来若支持编辑任意历史位置,可以创建新 Thread,而不是静默重写所有后续 Turn;该选择不属于本变更。 +1. 解析 actor 与授权; +2. 读取并验证 Parent Thread; +3. 验证 source Message 属于 Parent Thread 且内容可 Fork; +4. 计算 Parent 截至 source Message 的 effective history; +5. 生成不可变 BaseContext; +6. 生成 ForkSourceSnapshot; +7. 创建 Child Thread; +8. 如携带初始输入,则同时创建 user/assistant Message 与 MessageRun; +9. 提交后调度可选执行。 -复合外键或事务检查必须保证 Turn、Message 和 Generation 的 Thread ID 一致。如果普通单列外键仍允许 Generation 组合来自不同 Thread 的 Message,就不足以保护这个不变量。 +Child 创建后: -**备选方案:只保留 Message 有向无环图。** 不采用,因为当前错误类型正是由系统反复根据图位置推断 Turn 和变体归属,再把推断结果与另一张 Thread Fork 图组合造成的。 +- Parent 追加 Message 不改变 Child; +- Parent Edit/Regenerate 不改变 Child; +- Parent 或来源归档不删除 Child; +- 来源内容变化只让 provenance 状态变为 outdated; +- 来源不存在时 provenance 状态为 unavailable; +- BaseContext 永远保持 Fork 时的内容。 -**备选方案:把一组用户和助手消息压成一行。** 不采用,因为 Message 需要稳定 ID,以支持分享、反馈、Artifact、Generation 来源和未来公开接口。 +**备选方案:每次构造 Child Prompt 时递归读取祖先。** 不采用,因为破坏式 Edit/Regenerate 会改变 Child 已经建立的历史语义。 -### D4. 当前有效变体是持久化领域状态 +**备选方案:独立 ThreadFork 实体。** MVP 不采用;只有多 Parent、Merge、关系独立生命周期或关系级分享出现时再升级。 -当前有效的用户或助手回复变体会决定: +### D6. BaseContext 与 ForkSourceSnapshot -- 下一次提示词上下文; -- 后续 Turn 跟随哪一个回答; -- ThreadFork 使用的确定来源版本; -- 当前可见的 Message 所属 Artifact 和反馈操作。 +#### BaseContext -因此,系统必须在 Turn 边界持久化该选择,并通过携带预期版本的幂等命令更新。不能根据数组顺序或仅存在于浏览器的叶节点推断它。 +```ts +interface BaseContextV1 { + schemaVersion: 1 + messages: ContextMessageV1[] +} -纯界面状态保持分离: +interface ContextMessageV1 { + sourceMessageId?: string + role: "user" | "assistant" + parts: MessagePartV1[] +} +``` + +BaseContext 是 Message Content Projection,不是 Message Entity 副本。它不包含: ```text -界面工作区/视图状态 -├── visibleColumns -├── foldedColumns -├── selectedThreadId -├── canvasViewport -├── openPanels -└── 临时草稿与乐观状态 +threadId +sequence +MessageRun +feedback +createdAt / updatedAt +UI 状态 +计费状态 ``` -初始单所有者模型只有一份规范有效选择。如果未来多人协作需要每位成员拥有个人阅读选择,可以用独立提案增加用户级选择,而不改变 Message 身份或 ThreadFork 来源。 +嵌套 Fork 时,服务端先构造 Parent 的完整有效上下文: + +```text +Parent.baseContext ++ +Parent local messages through sourceMessage +``` -**备选方案:当前有效变体完全保留在客户端。** 不采用,因为刷新、服务端提示词组装和多客户端命令会产生分歧。 +再把结果扁平冻结为 Child.baseContext。因此运行时不需要递归读取祖先。 + +#### ForkSourceSnapshot + +```ts +interface ForkSourceSnapshotV1 { + schemaVersion: 1 + sourceMessageId: string + sourceContentHash: string + quote: { + exact: string + prefix?: string + suffix?: string + } +} +``` -### D5. 每个实体只有一个事实来源;快照只是读取模型 +`forkSourceSnapshot` 只回答“从哪里 Fork”,`baseContext` 回答“以后以什么历史继续”。两者不得合并成一个含义模糊的 `forkContext`。 -规范化写模型拥有实体身份和关系。服务端可以返回为首次加载优化的 `ConversationSnapshot`,但该快照必须由规范实体组装,不能作为整包写入数据被接受。 +来源状态是读取投影,不单独持久化: ```text -写入路径: -客户端命令 - → 传输校验 - → 应用命令 - → 领域不变量 - → 仓储事务 - → 规范实体增量 + 事务事件箱 - -读取路径: -仓储/读取模型 - → ConversationSnapshot - → 客户端规范化 - → 按 ID 定位的选择器 +source exists && currentHash == snapshotHash → available +source exists && currentHash != snapshotHash → outdated +source missing/inaccessible → unavailable ``` -`branch_trees.state` 可以在迁移期暂时作为迁移输入或非权威导出、缓存。任何缓存都必须可丢弃、带版本,并可从规范数据行重建。它不能参与冲突处理,也不能覆盖规范数据行。 +### D7. Message Part 协议 -**备选方案:继续让 JSONB 作为权威来源,再增加更多辅助表。** 不采用,因为辅助表 ID 仍然无法强制保证 Thread/Message 归属,每个新功能也都会增加一条协调合并路径。 +Message 内容采用项目自有的版本化 envelope: -### D6. 稳定公开身份先于分享、接口、命令行和 MCP +```ts +interface MessageContentV1 { + schemaVersion: 1 + parts: MessagePartV1[] +} +``` -Workspace、Project、Conversation、Thread、Turn、Message、Generation 和 File 使用不携带角色语义的不透明稳定 ID。公开适配器不得暴露内存数组下标、`main`、树内局部计数器或浏览器复合键作为资源身份。 +`MessagePartV1` 的当前形状必须与仓库安装的 AI SDK v7 `UIMessage.parts` 支持范围兼容,包括当前实际使用的 text、reasoning、tool、data、file/attachment 与 Artifact 表达。领域层不得导入 AI SDK 的运行时对象或 `streamText()` 结果类型。 -授权包含关系与 Fork 来源关系保持分离: +依赖方向: ```text -授权包含关系: -Workspace → Project → Conversation → Thread → Message +AI SDK v7 UIMessage / stream events + ↓ adapter +MessageContentV1 / NormalizedRunEvent + ↓ +Domain / Application +``` + +未来更换模型运行时只替换 adapter;稳定 Message、Thread 与 Conversation ID 不迁移。 + +### D8. MessageRun 是独立执行子域 + +```ts +interface MessageRun { + assistantMessageId: MessageId + attempt: number + status: + | "queued" + | "running" + | "completed" + | "failed" + | "stopped" + | "superseded" + modelId: string + eventSequence: number + checkpointParts: MessagePartV1[] + usage?: ModelUsage + errorCode?: string + queuedAt: Date + startedAt?: Date + finishedAt?: Date + heartbeatAt?: Date + stopRequestedAt?: Date +} +``` + +身份使用 `(assistantMessageId, attempt)`。Regenerate/Retry 创建新 attempt,不复用旧 MessageRun 行;旧 attempt 可以保留用量、计费和审计,但不保留旧 assistant Message 内容版本。 -Conversation 来源关系: -上游 Thread + 来源 Message → ThreadFork → 下游 Thread +状态机: -执行关系: -Generation → Turn / Message / 用量 +```mermaid +stateDiagram-v2 + [*] --> queued + queued --> running + queued --> stopped + running --> completed + running --> failed + running --> stopped + queued --> superseded + running --> superseded ``` -未来 REST、命令行和 MCP 适配器调用与网页界面相同的应用命令和查询。它们不能直接查询数据库表,也不能再实现一套所有权规则。分享和令牌权限范围属于独立提案,但本设计确保每个未来分享目标都具有可寻址实体。 +状态转换使用条件更新,而不是通用 revision: -### D7. Project 资产拥有可跨 Conversation 使用的持久内容 +```sql +UPDATE message_runs +SET status = 'completed', finished_at = now() +WHERE assistant_message_id = $1 + AND attempt = $2 + AND status = 'running'; +``` + +同一个 assistant Message 同时最多只有一个 `queued/running` attempt。 -Project 不只是 Conversation 文件夹。目标扩展点为: +浏览器刷新、关闭或流连接断开只释放订阅,不调用 Stop。重载后,读取模型按 assistantMessageId 返回最新 attempt 的: ```text -ProjectInstructionVersion -├── projectId -├── version/content -└── 审计元数据 +status +attempt +checkpointParts +eventSequence +``` + +传输层可以用 SSE、轮询或未来 WebSocket 交付同一规范运行事件。P0 不要求永久保存每个 token event;服务端可以在重连时先发送最新 checkpoint/reset 事件,再继续递增 eventSequence。 -MemoryItem -├── projectId -├── content/status/version -└── 来源记录 +Message 表不重复保存 `running/failed/stopped`。UI 中的 assistant runtime status 是 Message 与最新 MessageRun join 后的读取投影。 -File -├── projectId -├── FileVersion[] -└── 来源与引用关系 +### D9. Prompt 与 UI Timeline 是不同读取投影 + +#### UI Timeline + +按 Message.sequence 返回全部本地 Message,并把最新 MessageRun 状态投影到 assistant Message: + +```text +U1 +[A1 生成失败] +U2 +A2 ``` -Message 或 Generation 记录自己创建或使用了某个资产,但不能拥有一份可复用 File 的唯一副本。搜索切块、向量和摘要是派生索引,不是规范 File 或 MemoryItem。 +#### Prompt History -本变更只定义所有权和术语,不增加这些产品能力,也不强迫现有 Markdown Artifact 在专门的 Project 资产变更之前迁移。 +Prompt Builder 使用: -**备选方案:所有输出只保存在 Message JSON 内。** 对需要持久复用的文件不采用,因为删除或归档一个 Thread 不应控制无关 Project 资产的生命周期。 +```text +Project/System Context ++ +Thread.baseContext ++ +Thread local effective messages +``` -### D8. 模块边界服从策略,不服从框架目录 +P0 规则: -目标依赖方向为: +| 本地事实 | 进入 Prompt | +|---|---:| +| user Message | 是 | +| assistant 最新 attempt completed | 是 | +| assistant 最新 attempt queued/running | 否 | +| assistant 最新 attempt failed | 否 | +| assistant 最新 attempt stopped | 否 | +| assistant 最新 attempt superseded | 否 | + +因此上面的 Prompt 为: ```text -界面组件 - ↓ -客户端应用/状态存储 ──→ 传输契约 - ↓ - 应用命令/查询 - ↓ - 领域模型 - ↑ - 持久化实现 +U1 +U2 +A2 ``` -规则如下。 +Project Instruction、Memory 与 File retrieval 属于 Project/System Context,不复制进 BaseContext。若未来需要执行可复现性,MessageRun 记录实际使用的 instruction version、file version 与 retrieval provenance;这不改变 Chat 内容实体。 -#### 领域层 +### D10. PostgreSQL 核心 Schema -- 只包含纯 TypeScript 实体、ID、值对象、不变量和派生规则。 -- 不导入 React、DOM、Next.js 请求、Drizzle 数据表或 HTTP 状态。 -- TextAnchor 可以继续作为领域值对象,即使某个适配器会在 Markdown DOM 中解析它。 +下列是目标逻辑 schema。具体 Drizzle 文件拆分、迁移编号和既有 User 表名由后续持久化变更决定。所有 ID 由服务端生成,使用 UUID 或等价不透明稳定 ID;时间统一存 `timestamptz`。 -#### 应用层 +#### 枚举 -- 包含命令、查询、所有者与权限策略、事务边界、幂等和冲突结果。 -- 依赖仓储、时钟、ID、事件和模型执行端口。 -- `forkThread`、`createTurn`、`regenerate`、`selectVariant`、`archiveConversation`、`stopGeneration` 在概念上属于此层。 +```sql +CREATE TYPE workspace_role AS ENUM ('owner', 'admin', 'member'); +CREATE TYPE record_status AS ENUM ('active', 'archived'); +CREATE TYPE message_role AS ENUM ('user', 'assistant'); +CREATE TYPE message_run_status AS ENUM ( + 'queued', + 'running', + 'completed', + 'failed', + 'stopped', + 'superseded' +); +``` -#### 持久化层 +实际实现可以使用受约束 text 替代 PostgreSQL enum,避免枚举迁移成本;无论采用哪种物理表示,领域值集合必须相同。 -- 包含 Drizzle 结构、SQL 约束、仓储适配器、迁移和事务事件箱。 -- 实现应用层端口;不得把数据库行形状泄漏到组件或路由。 +#### workspaces -#### 接口与传输层 +```sql +CREATE TABLE workspaces ( + id uuid PRIMARY KEY, + name text NOT NULL, + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +``` -- 负责身份认证与会话提取、请求结构、错误与状态映射、响应序列化。 -- 不得在路由局部分支中重复实现领域校验。 +Owner 角色以 membership 为唯一事实;`created_by` 只表示审计,不表示当前 Owner。 -#### 客户端状态层 +#### workspace_memberships -- 保存规范化的规范实体、可丢弃派生索引和乐观命令状态。 -- 应用服务端返回的规范增量;不得自行修改实体关系。 +```sql +CREATE TABLE workspace_memberships ( + workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + role workspace_role NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (workspace_id, user_id) +); -#### 界面组件层 +CREATE INDEX workspace_memberships_user_idx + ON workspace_memberships(user_id, workspace_id); +``` -- 消费按 ID 定位的选择器和命令。 -- 列、画布和 Message 操作组件不能写入 `children`、Fork 反向链接或 Message 所有权。 +“至少一个 owner”需要应用事务或延迟约束保证,不能仅靠单行 CHECK。 + +#### projects + +```sql +CREATE TABLE projects ( + id uuid PRIMARY KEY, + workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT, + name text NOT NULL, + description text, + status record_status NOT NULL DEFAULT 'active', + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + archived_at timestamptz +); + +CREATE INDEX projects_workspace_status_idx + ON projects(workspace_id, status, updated_at DESC); +``` -即使物理目录按阶段迁移,也必须遵守这条依赖分离原则。 +Project 默认继承 Workspace 成员权限;MVP 不创建 ProjectMembership。 + +#### conversations + +```sql +CREATE TABLE conversations ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, + title text, + title_source text CHECK (title_source IN ('auto', 'custom')), + status record_status NOT NULL DEFAULT 'active', + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + archived_at timestamptz +); + +CREATE INDEX conversations_project_status_idx + ON conversations(project_id, status, updated_at DESC); +``` -### D9. 隔离遗留术语,不提供永久全局别名 +`title_source` 用于阻止后续自动标题覆盖用户自定义标题。它与 revision 无关。 + +#### threads + +```sql +CREATE TABLE threads ( + id uuid PRIMARY KEY, + conversation_id uuid NOT NULL REFERENCES conversations(id) ON DELETE RESTRICT, + + parent_thread_id uuid, + source_message_id uuid, + fork_source_snapshot jsonb, + base_context jsonb NOT NULL, + + title text, + title_source text CHECK (title_source IN ('auto', 'custom')), + default_model_id text, + status record_status NOT NULL DEFAULT 'active', + + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + archived_at timestamptz, + + UNIQUE (id, conversation_id), + + CHECK (jsonb_typeof(base_context) = 'object'), + CHECK ( + ( + parent_thread_id IS NULL + AND source_message_id IS NULL + AND fork_source_snapshot IS NULL + ) + OR + ( + parent_thread_id IS NOT NULL + AND source_message_id IS NOT NULL + AND fork_source_snapshot IS NOT NULL + ) + ), + + FOREIGN KEY (parent_thread_id, conversation_id) + REFERENCES threads(id, conversation_id) + ON DELETE RESTRICT + DEFERRABLE INITIALLY IMMEDIATE +); + +CREATE UNIQUE INDEX threads_one_root_per_conversation_idx + ON threads(conversation_id) + WHERE parent_thread_id IS NULL; + +CREATE INDEX threads_parent_idx + ON threads(parent_thread_id, created_at); + +CREATE INDEX threads_conversation_status_idx + ON threads(conversation_id, status, created_at); +``` -迁移期间,专用适配器可以执行以下映射: +根 Thread 的 `base_context` 必须是 `{ "schemaVersion": 1, "messages": [] }`。JSON 内部 schema 由应用层 codec 验证;数据库只做最低 JSON 类型约束。 -```text -遗留模型 规范模型 -ThreadTreeState ConversationSnapshot / 遗留导入数据 -branch_trees.id Conversation ID 候选值 -threads.main Conversation.rootThreadId 目标 -Thread.parentId ThreadFork.parentThreadId -Thread.forkFromMsgId ThreadFork.sourceMessageId -Thread.children 派生 ThreadFork 索引 -Message.forks 派生 ThreadFork 索引 -Thread.activeLeafMessageId Turn 有效选择/读取投影 -branch_generations.* IDs Generation 外键 +#### messages + +```sql +CREATE TABLE messages ( + id uuid PRIMARY KEY, + thread_id uuid NOT NULL REFERENCES threads(id) ON DELETE RESTRICT, + sequence bigint NOT NULL CHECK (sequence > 0), + role message_role NOT NULL, + + content_schema_version smallint NOT NULL DEFAULT 1, + parts jsonb NOT NULL, + content_hash text NOT NULL, + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + UNIQUE (thread_id, sequence), + UNIQUE (id, thread_id), + CHECK (jsonb_typeof(parts) = 'array') +); + +CREATE INDEX messages_thread_sequence_idx + ON messages(thread_id, sequence); +``` + +`content_hash` 由服务端对规范化后的 MessageContent 计算,用于来源过期判断;客户端不能提交该值。 + +在 threads/messages 都存在后增加来源复合外键: + +```sql +ALTER TABLE threads + ADD CONSTRAINT threads_source_message_belongs_to_parent_fk + FOREIGN KEY (source_message_id, parent_thread_id) + REFERENCES messages(id, thread_id) + ON DELETE RESTRICT + DEFERRABLE INITIALLY IMMEDIATE; +``` + +它在数据库层保证 source Message 属于声明的 Parent Thread。Parent/Child 同 Conversation 已由 threads 的复合 Parent 外键保证。 + +MVP 不提供独立 Message hard delete。归档 Conversation/Thread 不删除 Message;未来硬删除必须以显式应用事务处理整个 Conversation,不能依赖来源 Message 的级联删除。 + +#### message_runs + +```sql +CREATE TABLE message_runs ( + assistant_message_id uuid NOT NULL REFERENCES messages(id) ON DELETE RESTRICT, + attempt integer NOT NULL CHECK (attempt > 0), + status message_run_status NOT NULL, + + model_id text NOT NULL, + provider_id text, + event_sequence bigint NOT NULL DEFAULT 0 CHECK (event_sequence >= 0), + checkpoint_schema_version smallint NOT NULL DEFAULT 1, + checkpoint_parts jsonb NOT NULL DEFAULT '[]'::jsonb, + + usage jsonb, + error_code text, + billing_state text, + + queued_at timestamptz NOT NULL DEFAULT now(), + started_at timestamptz, + heartbeat_at timestamptz, + stop_requested_at timestamptz, + finished_at timestamptz, + + PRIMARY KEY (assistant_message_id, attempt), + CHECK (jsonb_typeof(checkpoint_parts) = 'array') +); + +CREATE UNIQUE INDEX message_runs_one_active_attempt_idx + ON message_runs(assistant_message_id) + WHERE status IN ('queued', 'running'); + +CREATE INDEX message_runs_latest_attempt_idx + ON message_runs(assistant_message_id, attempt DESC); + +CREATE INDEX message_runs_recovery_idx + ON message_runs(status, heartbeat_at) + WHERE status IN ('queued', 'running'); +``` + +“assistant_message_id 必须指向 role=assistant”是跨行约束,由应用命令在事务中验证并由集成测试覆盖;如后续需要数据库强制,可增加约束触发器,不在首版使用复杂触发器。 + +`usage` 与 `billing_state` 的精确 schema 由计费能力拥有;MessageRun 只保留稳定承载位置,不让计费字段进入 Message。 + +#### message_feedbacks(附属关系) + +```sql +CREATE TABLE message_feedbacks ( + user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + message_id uuid NOT NULL REFERENCES messages(id) ON DELETE RESTRICT, + value text NOT NULL CHECK (value IN ('positive', 'negative')), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (user_id, message_id) +); ``` -遗留名称可以保留在名称明确的迁移、导入模块和历史迁移文件中。新的领域层、应用层和接口层代码不能引入 `type Conversation = ThreadTreeState` 一类别名;这种做法只是改名,没有改变所有权。 +Feedback 是 User 与 Message 的关系,不嵌入 Message JSON,也不复制进 BaseContext。 + +### D11. Project 扩展 Schema + +这些表属于稳定扩展方向,但不要求与 Chat MVP 核心表在同一实现批次完成。 + +#### project_instruction_versions -已归档的 OpenSpec 变更继续作为历史记录。依赖旧权威模型的活跃变更必须标记为被替代、受阻或依赖后续迁移,不能通过修改文档假装已完成任务已经符合新模型。 +```sql +CREATE TABLE project_instruction_versions ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, + version integer NOT NULL CHECK (version > 0), + content text NOT NULL, + is_active boolean NOT NULL DEFAULT false, + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (project_id, version) +); -### D10. 变更依赖链 +CREATE UNIQUE INDEX project_instruction_one_active_idx + ON project_instruction_versions(project_id) + WHERE is_active; +``` + +切换 active version 必须在事务内先后更新旧、新版本。MessageRun 后续可以记录实际使用的 instructionVersionId。 + +#### memory_items + +```sql +CREATE TABLE memory_items ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, + content jsonb NOT NULL, + source jsonb, + status record_status NOT NULL DEFAULT 'active', + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CHECK (jsonb_typeof(content) = 'object') +); + +CREATE INDEX memory_items_project_status_idx + ON memory_items(project_id, status, updated_at DESC); +``` -工作按架构接缝拆分,而不是按任意文件数量拆分: +向量、切块、摘要与搜索索引是可重建派生数据,不是 MemoryItem 的唯一事实。 + +#### project_files / file_versions + +```sql +CREATE TABLE project_files ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, + name text NOT NULL, + status record_status NOT NULL DEFAULT 'active', + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE file_versions ( + id uuid PRIMARY KEY, + file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE RESTRICT, + version integer NOT NULL CHECK (version > 0), + storage_key text NOT NULL, + content_type text NOT NULL, + size_bytes bigint NOT NULL CHECK (size_bytes >= 0), + content_hash text NOT NULL, + created_by uuid NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (file_id, version) +); +``` + +Message 中的临时 Artifact 可以继续存在于 parts。只有用户明确“保存到 Project Files”时才创建 ProjectFile/FileVersion,并用后续来源关系记录 originating Message;不会把每个工具输出自动提升为 Project 资产。 + +### D12. 数据库不变量与并发规则 + +数据库与应用层共同保证: + +1. Project 恰好属于一个 Workspace; +2. Conversation 恰好属于一个 Project; +3. Conversation 恰好有一个 Root Thread; +4. Thread 归属 Conversation 后不可移动; +5. 非根 Thread 的 parent/source/snapshots 必须同时存在; +6. Parent、Child 和 source Message 必须位于同一 Conversation; +7. Child 创建时只能指向已经存在的 Parent,因此 Fork 事务不会创建环;额外领域校验拒绝任何导入环; +8. Thread 内 `(threadId, sequence)` 唯一; +9. 普通发送在同一事务中分配连续 user/assistant sequence; +10. 只有最后一条 user 可以 Edit; +11. 只有最后一条 assistant 可以 Regenerate/Retry; +12. 同一个 assistant Message 最多一个非终态 MessageRun; +13. MessageRun 状态通过条件更新推进; +14. BaseContext 创建后不可通过公开仓储更新; +15. 客户端不能提交 owner、contentHash、baseContext、run status 或新实体 ID; +16. 归档不级联删除 Child Thread、Message 或 MessageRun; +17. 通用 revision 不用于发送、Fork、运行订阅或状态转换。 + +Message sequence 分配属于 Thread 写入事务。实现可以锁定 Thread 行,或使用数据库原子计数器/最大 sequence 保护;选择由持久化实现提案决定。无论采用哪一种,客户端不生成 sequence,也不依赖时间排序。 + +P0 使用前端 single-flight 防止重复点击,但网络结果不确定后的重复提交仍是已知限制。V2 再增加 Idempotency-Key 和命令结果重放,不在当前表中预建半成品幂等结构。 + +### D13. 应用命令与事务边界 + +应用层使用明确用例,不暴露通用 `updateEntity`: + +| 命令 | 主要事务结果 | +|---|---| +| createWorkspace | Workspace + Owner Membership | +| createProject | Project | +| createConversation | Conversation + Root Thread | +| sendMessage | user Message + assistant placeholder + MessageRun | +| editLastUserMessage | 更新最后 user/assistant + 新 MessageRun attempt | +| regenerateLastAssistantMessage | 重置最后 assistant + 新 MessageRun attempt | +| retryAssistantMessage | 验证 failed/stopped 后创建新 attempt | +| forkThread | Child Thread + BaseContext + ForkSourceSnapshot,可选初始消息/run | +| archiveConversation | 只更新 Conversation 生命周期 | +| archiveThread | 只更新目标非根 Thread 生命周期,不删除后代 | +| stopMessageRun | 条件推进最新 active attempt | +| submitMessageFeedback | upsert/delete 用户反馈关系 | + +命令不返回可写整棵 Conversation。成功响应返回受影响实体或读取 DTO;客户端不能把 Bootstrap 原样 PUT 回服务端。 + +模型执行必须在 user/assistant Message 与 queued MessageRun 事务提交后开始。执行器启动失败时,MessageRun 最终收敛为 failed;不能回滚已经被用户提交并可查询的输入意图。 + +### D14. 读取模型 + +#### ConversationBootstrap + +用于 ThreadChat URL 首次装载,至少包含: ```text -define-conversation-domain-model (本变更) - │ - ▼ -normalize-conversation-persistence (实体、约束、仓储、 - │ 迁移与读取快照) - ▼ -migrate-generation-lifecycle (基于真实 Thread/Turn/Message 外键的 - │ 启动、停止、恢复、用量和计费) - ▼ -add-conversation-command-api (实体命令、增量、错误与幂等契约; - │ 退出整树 PUT) - ▼ -normalize-conversation-client-state (实体缓存、选择器、列、画布和 - │ Message 操作) - ▼ -retire-thread-tree-authority (切换、审计、删除遗留写入、 - 适配器和重复事实) -``` - -基础模型完成后可以并行开展: +schemaVersion +conversation +threads +每个 Thread 最近最多 200 条 Messages +每条 assistant Message 的最新 MessageRun 摘要 +当前用户 feedback +hasMoreOlder / olderCursor +``` + +它是只读 BFF DTO,不是数据库实体、备份或可写快照。P0 正常路径一次返回每 Thread 最近 200 条 Message,避免客户端一开始实现复杂的树内增量分页。 + +#### ConversationGraph + +只返回 Conversation、Thread parent/source 与标题摘要,用于 Canvas 或导航,不下载 Message parts、BaseContext 全文或 Artifact 内容。 + +#### ThreadTimeline + +按 sequence 返回本地 Message,并 join 最新 MessageRun 状态。BaseContext 不作为本地 Message 重复渲染;UI 可以通过 provenance 单独展示 Fork 来源。 + +#### ThreadPrompt + +只在服务端构造,使用 Project/System Context、BaseContext 与本地有效 Message。客户端提交的 messages 数组永远不是权威 Prompt 来源。 + +### D15. 目标服务端模块骨架 + +目标目录表达依赖边界,不要求本变更创建文件: ```text -normalize-conversation-persistence - ├── project-context-assets (未来独立提案) - └── public-data-access (未来接口、令牌、命令行、MCP 提案) +lib/thread-chat/ +├── domain/ +│ ├── ids.ts +│ ├── workspace.ts +│ ├── project.ts +│ ├── conversation.ts +│ ├── thread.ts +│ ├── message.ts +│ ├── value-objects/ +│ │ ├── message-content.ts +│ │ ├── base-context.ts +│ │ └── fork-source-snapshot.ts +│ └── policies/ +│ ├── thread-message-policy.ts +│ ├── fork-policy.ts +│ └── prompt-policy.ts +│ +├── application/ +│ ├── commands/ +│ │ ├── create-conversation.ts +│ │ ├── send-message.ts +│ │ ├── edit-last-user-message.ts +│ │ ├── regenerate-last-assistant-message.ts +│ │ ├── fork-thread.ts +│ │ └── stop-message-run.ts +│ ├── queries/ +│ │ ├── get-conversation-bootstrap.ts +│ │ ├── get-conversation-graph.ts +│ │ └── get-thread-timeline.ts +│ └── ports/ +│ ├── unit-of-work.ts +│ ├── repositories.ts +│ ├── authorization.ts +│ ├── id-generator.ts +│ ├── clock.ts +│ └── model-runtime.ts +│ +├── execution/ +│ ├── message-run.ts +│ ├── message-run-state-machine.ts +│ ├── message-run-service.ts +│ └── normalized-run-event.ts +│ +├── infrastructure/ +│ ├── db/ +│ │ ├── schema/ +│ │ ├── repositories/ +│ │ └── transaction.ts +│ ├── runtime/ +│ │ └── ai-sdk-v7-adapter.ts +│ └── authorization/ +│ +└── transport/ + ├── schemas/ + ├── mappers/ + └── errors.ts + +app/api/v1/ +└── ... HTTP route adapters only +``` + +依赖方向: -retire-thread-tree-authority - └── conversation-convergence (Issue #39,未来提案) +```text +HTTP / future CLI / future MCP + ↓ +Application Commands & Queries + ↓ +Domain Policies + Execution Ports + ↓ +Infrastructure Adapters ``` -每个后续变更只负责一个权威边界,并拥有独立验证目标: +约束: + +- domain 不导入 React、Next.js、HTTP、Drizzle、AI SDK 或数据库 row; +- application 不导入 Next.js Request/Response; +- infrastructure 实现 ports,不反向定义领域规则; +- route 只负责认证适配、schema 解析、调用用例和错误映射; +- Prompt Builder 只在服务端; +- CLI/MCP 未来复用 application,不直接访问表。 -1. 持久化变更证明数据库不变量和规范快照读取正确; -2. Generation 变更证明生命周期和财务语义不再依赖 JSON 协调合并; -3. 接口变更证明只有服务端可以执行变更,并保证幂等; -4. 客户端变更证明规范接口能够产生正确投影; -5. 清理变更证明系统中不再存在遗留权威路径。 +### D16. 后续拆分链 -PR #30 保持 Draft;在 Generation 迁移解决其对 #34 的依赖前,不能视为符合最终架构。已经正确的流式行为可以保留,但 JSON 与辅助表之间的协调机制不是目标契约。 +本设计通过后,后续按以下顺序建立独立 OpenSpec: + +1. **服务端领域与数据库骨架**:创建纯领域类型、值对象 codec、Drizzle schema、迁移、仓储和数据库约束测试。 +2. **MessageRun 生命周期**:实现后台执行、状态机、checkpoint、Stop、断线恢复、用量和计费挂点。 +3. **应用命令与 API v1**:实现授权、事务命令、查询 DTO、错误协议和路由;通用幂等仍不进入 P0。 +4. **服务端验收**:领域策略测试、数据库集成测试、API 契约测试、并发/断线/恢复测试。 +5. **前端领域与 Zustand**:设计 Entity Store、MessageRun Stream 状态、UI State、Selectors 和 Conversation Runtime。 +6. **Hooks 与 UI 接入**:Bootstrap、发送、Fork、Edit、Regenerate、Stop、反馈、Canvas 和列视图 Hooks。 +7. **整体链路与权威切换**:端到端验证后再处理旧数据、旧 API、旧整树权威和清理。 + +后续变更不得在没有显式修改本基础设计的情况下重新引入 Turn、Message Variant、独立 ThreadFork 或 Generation Chat 实体。 ## Risks / Trade-offs -- **[风险] 术语变更影响大量活跃文档,可能产生改了一半的模型。** → 把 `domain` 规范设为唯一正式词汇表,发布遗留映射,并禁止在迁移范围之外新增 `ThreadTree` 术语。 -- **[风险] 显式 Turn 增加一个实体和一组约束。** → 它可以消除重复运行时推断,并把重新生成和编辑不变量限制在局部;Message 内容继续使用灵活 JSON,避免过度拆分结构化片段。 -- **[风险] Conversation 与 Thread 标题可能成为重复事实。** → Conversation 拥有聚合与导航标题。非根 Thread 可以拥有本地列标题;根列使用 Conversation 标题,除非未来产品需求证明必须存在独立根标题。 -- **[风险] 引入 Project 和 Workspace 扩大 #34。** → 本变更只定义包含关系。物理 Workspace、Memory、Instruction、File 和分享能力继续使用独立提案。 -- **[风险] 分阶段迁移会暂时同时存在遗留表示和规范表示。** → 每个阶段只允许一个写入权威,把转换隔离在一个适配器中,增加一致性与审计检查,并设立强制清理变更。 -- **[风险] 前后端一次性切换可能遗留运行中的 Generation。** → 在接口和客户端切换前先迁移生命周期;切换前要求没有活跃 Generation 或具备兼容排空规则,并在每个阶段保留回滚边界。 -- **[风险] 现有 OpenSpec 变更依据已被替代的假设宣称任务完成。** → 明确记录依赖和被替代范围;不静默保留错误声明,也不重写已归档历史。 -- **[风险] 稳定公开 ID 被误认为已经具备授权。** → 在应用命令中保留包含关系和权限检查;来源关系本身永远不授予访问权限。 +- **[BaseContext 会重复历史内容并随 Fork 深度增加存储]** → MVP 接受以换取破坏式编辑下的语义独立;只保存精简 ContextMessage Projection。真实规模达到阈值后可以引入内容寻址或快照去重,不改变逻辑模型。 +- **[破坏式 Edit/Regenerate 永久丢失旧可见内容]** → 这是明确产品决策;需要保留时使用 Fork。MessageRun 只保留执行审计,不承诺恢复旧回答正文。 +- **[来源 Message 内容变化后 sourceMessageId 仍指向同一可变实体]** → ForkSourceSnapshot 的 content hash 和 quote 保留当时 provenance;状态读取为 outdated,BaseContext 不变。 +- **[JSONB parts/baseContext 缺少数据库内部结构约束]** → 使用共享版本化 codec 在写入和读取时验证,数据库保留 schemaVersion、JSON 类型和关系约束;对未知版本 fail closed。 +- **[Message 与 MessageRun 分离增加 join]** → 使用 latest-attempt 索引和 Bootstrap/Timeline 读取模型集中 join,避免把运行状态重复写入 Message。 +- **[不使用通用 revision 时多标签页可能发生最后写入者胜出]** → 追加顺序由事务和唯一 sequence 保护,运行状态由条件更新保护;低风险元数据 P0 接受 last-write-wins,需要覆盖保护时再局部增加 ETag。 +- **[P0 无幂等键时网络不确定可能重复发送]** → 前端 single-flight 降低重复点击;将可靠重放作为 V2 独立能力,不在当前模型中加入半套命令系统。 +- **[没有独立 ThreadFork 会限制未来多来源关系]** → 当前每个 Child 唯一 Parent,字段与 Thread 同生命周期;真正出现 Merge/多来源时再提升为关系实体并迁移,避免为未知需求预建。 +- **[Project 扩展表让范围看起来扩大]** → 本设计只固定所有权与 schema 方向;实现批次可以只落核心 Workspace/Project/Conversation/Thread/Message/MessageRun。 +- **[忽略现有实现会把迁移风险延后]** → 这是本阶段的有意边界;先批准正确目标,再用单独提案审计真实数据和切换路径,避免旧结构反向塑造新模型。 ## Migration Plan -本文定义迁移计划,实际执行由 D10 中的后续变更完成。 +本变更不执行现有数据迁移。它定义目标状态和后续落地顺序: + +1. 审查并批准本 proposal/design,确认简化模型为 Issue #34 唯一目标。 +2. 在独立变更中同步修改正式 domain spec 和所有依赖旧模型的活跃 OpenSpec。 +3. 建立全新服务端领域与数据库 schema,不在实现初期加入旧整树兼容逻辑。 +4. 通过数据库约束、领域策略、事务集成和 API 契约测试完成后端验收。 +5. 另行设计前端 Zustand Store、Selectors、Hooks 与 Conversation Runtime。 +6. 前后端新链路端到端通过后,再审计旧数据并决定重置、一次性导入或只读保留。 +7. 最终切换时只允许一个写入权威;禁止新旧模型双向同步或把规范实体反序列化回旧整树 JSON。 -1. **落地术语与架构决策。** 应用修改后的 `domain` 规范,更新当前架构和术语引用,发布遗留映射,并把冲突的活跃变更标记为依赖本变更或已被替代。 -2. **在读取路径后方引入规范化持久化。** 创建规范数据行和约束;检查真实数据后决定重置还是一次性迁移;生成 `ConversationSnapshot` 读取模型。在一致性检查通过前,不启用规范写入。 -3. **迁移 Generation 所有权。** 把执行尝试、Message 输出、停止与恢复、用量和计费绑定到真实外键,并解决 PR #30 审查中已知的结算、部分结果和过期收敛语义。 -4. **迁移写入命令。** 启用具有幂等和版本冲突语义的规范实体命令。已经迁移的 Conversation 不再接受整棵 Conversation 写入。 -5. **迁移客户端。** 规范化快照,迁移选择器和界面命令,再从浏览器删除防抖保存和协调合并行为。 -6. **切换并清理。** 审计数据、排空活跃 Generation、禁用遗留写入、删除重复事实,最终按照已批准的持久化方案删除 `branch_trees.state` 或将其降级为非权威数据。 +回滚、真实数据导入、运行中旧 generation 排空和旧表删除,必须基于部署环境数据另写 ADR,不在本基础设计中猜测。 -回滚按阶段进行: +## Open Questions -- 启用规范生产写入前,可以关闭新的读取模型和数据表,继续使用未改变的遗留权威; -- 一旦规范模型接受生产写入,不得把数据反向导出到 ThreadTree 后恢复旧权威;应保持规范数据库,通过回滚兼容客户端/应用、进入只读、恢复规范备份或前滚修复处理; -- 只有当命令接口仍兼容且不会丢失仅存在于规范模型的变更时,才能回滚客户端; -- 破坏性删除遗留列必须最后执行,并以已验证备份和审计报告为前提。 +目前没有会改变核心实体、关系或数据库主干的未决问题。以下实现选择有意推迟到对应后续提案: -目前没有会改变本提案规范、实体选择或任务顺序的未决问题。`normalize-conversation-persistence` 负责取得真实数据审计证据;数据重置还是一次性导入及其部署批次由 `retire-thread-tree-authority` 在目标环境测量后以 ADR 决定。 +- Message sequence 使用 Thread 行锁、独立计数器还是等价数据库原子策略; +- MessageRun 事件使用 SSE、轮询还是 WebSocket,以及是否需要短期事件表; +- Project Instruction、Memory、File 的首个产品范围和 retrieval 策略; +- 真实旧数据采用重置、一次性迁移还是只读保留; +- V2 Idempotency-Key、离线重放和外部 API 生命周期。 diff --git a/openspec/changes/define-conversation-domain-model/proposal.md b/openspec/changes/define-conversation-domain-model/proposal.md index 8e4bac29..dd8aa4a9 100644 --- a/openspec/changes/define-conversation-domain-model/proposal.md +++ b/openspec/changes/define-conversation-domain-model/proposal.md @@ -1,33 +1,38 @@ ## Why -当前 `Thread Tree / MainThread / ForkedThread` 术语把产品概念绑定到整树 JSON 和当前界面拓扑,也缺少 Project 与单列 Thread 之间的 Conversation 聚合边界。Issue #34 已要求规范化 Thread Chat;在继续设计数据库、命令接口和前端状态存储前,必须先建立唯一、稳定的领域语言与目标实体关系,否则后续变更会继续用同一个词描述不同事物。 +Issue #34 的目标模型经过多轮讨论后已经收敛:当前 MVP 不需要同时维护 Thread 分支、Message 变体、Turn 选择和 Generation 领域实体四套关系。继续沿用此前的 `ThreadFork + Turn + Message Variant + Generation` 方案会把重新生成、编辑、提示词构造、客户端状态和数据库约束都建立在不必要的复杂度上,也会偏离“每一列就是一条严格线性 Thread”的产品心智模型。 + +在设计 API、前端 Zustand Store、Hooks 和迁移实现之前,需要先用一份独立于现有代码的 OpenSpec 固化最终领域语言、实体关系、数据库目标结构和模块边界。后续服务端实现、集成测试、API 测试与前端接入都必须以这份模型为唯一基础。 ## What Changes -- **破坏性变更**:以 `Conversation` 取代 `Thread Tree` 作为完整可分叉对话的领域名称;旧名称只允许出现在迁移说明、遗留适配器或具体树形投影中。 -- **破坏性变更**:统一采用 `Project → Conversation → Thread`。每一列对话都是 Thread;Main Thread 与 Branch Thread 只是由关系推导的角色,不是两套实体,也不持久化 `kind` 或魔法 `id = "main"`。 -- 将 Fork 明确定义为服务端动作,并将 `ThreadFork` 定义为上游 Thread、来源 Message 与下游 Thread 之间的唯一来源关系;`children[]`、`Message.forks[]` 只能是派生索引。 -- 引入显式 `Turn` 作为用户编辑、助手重新生成和回复变体的归属边界;`Message` 与 `Generation` 必须具有稳定身份并严格属于同一 Thread/Turn。 -- 明确当前有效变体会决定后续提示词与 Fork 来源,因此是持久化领域选择;列、折叠、画布和面板等仍属于界面工作区状态。 -- 定义 Workspace、Project、Conversation、Thread、ThreadFork、Turn、Message、Generation、File、MemoryItem 与 ProjectInstruction 的规范术语、生命周期边界和依赖关系。 -- 明确领域层、应用层、持久化层、接口与传输层、客户端状态层和界面组件层的模块职责,禁止 React、HTTP 或 Drizzle 规则渗入纯领域层。 -- 建立从当前 `ThreadTreeState`、`branch_trees`、`branch_generations` 到目标模型的迁移地图,并把后续落地拆为规范化持久化、Generation 生命周期、命令接口、前端规范化状态存储、切换清理等独立变更。 -- 分支结论回流与收敛机制保持在 Issue #39,Project Memory、File、Instruction 的完整产品能力分别后续提案,均不扩大本变更的实现范围。 +- **BREAKING**:将 Chat Domain 收敛为 `Project → Conversation → Thread → Message[]`;Thread 内 Message 永远严格线性,不再引入 Turn、Message DAG、Message Variant 或 `activeGenerationId`。 +- **BREAKING**:Edit 和 Regenerate 改为破坏式操作。只有当前 Thread 最后一条 user Message 可以 Edit,只有最后一条 assistant Message 可以 Regenerate;需要保留另一条历史时必须创建新 Thread。 +- **BREAKING**:不再把 ThreadFork 建模为独立实体。Fork 是服务端领域命令,唯一入向来源直接保存在 Child Thread 的 `parentThreadId`、`sourceMessageId`、`forkSourceSnapshot` 与 `baseContext` 中。 +- 引入不可变 `baseContext` 值对象:服务端在 Fork 事务中冻结 Parent Thread 截至来源 Message 的有效历史,保证 Child Thread 不受 Parent 后续 Edit、Regenerate、追加、归档或来源失效影响。 +- 引入 `forkSourceSnapshot` 值对象,独立记录选区、上下文片段和来源内容哈希;来源变化只产生 `outdated/unavailable` 读取状态,不删除或改写 Child Thread。 +- 将模型执行生命周期收敛到持久化运行记录 `MessageRun`。它属于执行与计费子域,不是 Chat 内容实体;浏览器断开只取消订阅,不终止后台执行。 +- 明确 Workspace 是成员与授权边界,Project 是 Conversation、项目指令、记忆和文件的长期资产边界;Project 资产能力只定义关系与扩展位置,不在本变更实现完整产品功能。 +- 明确 Message 内容使用项目版本化协议并与 AI SDK v7 `UIMessage.parts` 兼容,同时禁止 AI SDK、React、Next.js、HTTP 或 Drizzle 类型渗入纯领域层。 +- 定义目标 PostgreSQL schema、复合约束、索引、事务边界、读取投影和服务端模块骨架,但不编写迁移、仓储、API、前端 Store、Hooks 或 UI 代码。 +- 将后续落地拆为:服务端领域与持久化、应用命令与 API、服务端集成/API 验收、前端 Zustand/Selectors/Hooks、UI 接入与遗留权威退出。 +- 幂等命令、离线重放、Message 版本保留、跨 Thread 收敛与多来源合并不进入 MVP;其中通用幂等机制明确留待 V2。 ## Capabilities ### New Capabilities -- 无。本变更先修正已有 `domain` 能力规范,避免在统一术语前复制出第二套领域规范。 +- 无。本变更继续以既有 `domain` 能力作为唯一领域规范入口,避免产生第二套相互竞争的术语规范。 ### Modified Capabilities -- `domain`:用 Project、Conversation、Thread、ThreadFork、Turn、Message 与 Generation 的统一语言替换 Thread Tree 模型,并补充角色推导、身份归属、当前有效变体、Project 资产边界和模块依赖要求。 +- `domain`:以 Workspace、Project、Conversation、线性 Thread、Message、MessageRun、BaseContext 和 ForkSourceSnapshot 的简化模型,替换此前的 ThreadFork、Turn、Message Variant 与 Generation 领域模型,并明确 Project 扩展资产、运行子域和模块依赖边界。 ## Impact -- 规范与文档:`openspec/specs/domain/spec.md`、所有仍把 `Thread Tree` 当作产品实体的活跃变更、架构说明和术语引用。 -- 领域代码:`lib/thread-chat/domain/` 中的类型、消息图、重新生成、选择器与遗留 `ThreadTreeState` 边界。 -- 服务端:后续将影响 `branch_trees`、`branch_generations`、Artifact、Feedback、Title 的归属、仓储和事务命令;本变更只建立目标契约与迁移边界,不执行数据库切换。 -- 前端:后续将影响整树状态存储、列与画布选择器、Message 操作与界面工作区状态;本变更不重写现有渲染、流式界面或编辑器。 -- 依赖:后续变更必须依次引用本变更的术语与架构决策记录;Issue #39 在基础模型稳定后再继续设计。 +- OpenSpec:本变更重定义 `define-conversation-domain-model` 的目标模型;仍依赖 Turn、Generation、独立 ThreadFork、revision 或幂等命令的后续活跃变更,在继续实现前必须基于本设计重新审查。 +- 后端目标模块:Workspace/Project、Conversation/Thread/Message 领域契约,Fork/Edit/Regenerate/Prompt 规则,MessageRun 执行端口,PostgreSQL schema 与仓储事务。 +- 服务端后续验收:数据库约束测试、领域策略测试、事务集成测试、API 契约测试、后台运行与断线恢复测试。 +- 前端后续工作:Conversation Bootstrap、规范化 Entity Store、MessageRun Stream 状态、Zustand Store、Selectors、Hooks 与现有 ThreadChat UI 接入;本变更不提前规定实现细节。 +- 现有实现:当前 `ThreadTreeState`、整树 JSON、branch generation 协调和客户端生成实体 ID 均不作为目标设计依据;数据迁移和切换策略由后续独立变更处理。 +- 外部扩展:稳定的 Workspace、Project、Conversation、Thread 和 Message ID 为未来分享、CLI、MCP 与公开 Token API 保留寻址能力,但本变更不实现这些入口。 From 90c6e9b853808a65b628c33237690a64010e64ac Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 13:06:40 +0800 Subject: [PATCH 03/23] chore: delete useless docs --- .../.openspec.yaml | 2 - .../add-conversation-command-api/design.md | 195 ---- .../add-conversation-command-api/proposal.md | 32 - .../specs/conversation-command-api/spec.md | 183 ---- .../add-conversation-command-api/tasks.md | 43 - .../.openspec.yaml | 2 - .../design.md | 992 ------------------ .../proposal.md | 38 - .../specs/domain/spec.md | 162 --- .../define-conversation-domain-model/tasks.md | 37 - .../.openspec.yaml | 2 - .../migrate-generation-lifecycle/design.md | 157 --- .../migrate-generation-lifecycle/proposal.md | 32 - .../conversation-generation-lifecycle/spec.md | 146 --- .../migrate-generation-lifecycle/tasks.md | 39 - .../.openspec.yaml | 2 - .../design.md | 182 ---- .../proposal.md | 33 - .../specs/conversation-client-state/spec.md | 165 --- .../tasks.md | 48 - .../.openspec.yaml | 2 - .../design.md | 103 -- .../proposal.md | 31 - .../specs/conversation-persistence/spec.md | 89 -- .../tasks.md | 28 - .../.openspec.yaml | 2 - .../retire-thread-tree-authority/design.md | 174 --- .../retire-thread-tree-authority/proposal.md | 33 - .../specs/conversation-cutover/spec.md | 155 --- .../retire-thread-tree-authority/tasks.md | 60 -- 30 files changed, 3169 deletions(-) delete mode 100644 openspec/changes/add-conversation-command-api/.openspec.yaml delete mode 100644 openspec/changes/add-conversation-command-api/design.md delete mode 100644 openspec/changes/add-conversation-command-api/proposal.md delete mode 100644 openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md delete mode 100644 openspec/changes/add-conversation-command-api/tasks.md delete mode 100644 openspec/changes/define-conversation-domain-model/.openspec.yaml delete mode 100644 openspec/changes/define-conversation-domain-model/design.md delete mode 100644 openspec/changes/define-conversation-domain-model/proposal.md delete mode 100644 openspec/changes/define-conversation-domain-model/specs/domain/spec.md delete mode 100644 openspec/changes/define-conversation-domain-model/tasks.md delete mode 100644 openspec/changes/migrate-generation-lifecycle/.openspec.yaml delete mode 100644 openspec/changes/migrate-generation-lifecycle/design.md delete mode 100644 openspec/changes/migrate-generation-lifecycle/proposal.md delete mode 100644 openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md delete mode 100644 openspec/changes/migrate-generation-lifecycle/tasks.md delete mode 100644 openspec/changes/normalize-conversation-client-state/.openspec.yaml delete mode 100644 openspec/changes/normalize-conversation-client-state/design.md delete mode 100644 openspec/changes/normalize-conversation-client-state/proposal.md delete mode 100644 openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md delete mode 100644 openspec/changes/normalize-conversation-client-state/tasks.md delete mode 100644 openspec/changes/normalize-conversation-persistence/.openspec.yaml delete mode 100644 openspec/changes/normalize-conversation-persistence/design.md delete mode 100644 openspec/changes/normalize-conversation-persistence/proposal.md delete mode 100644 openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md delete mode 100644 openspec/changes/normalize-conversation-persistence/tasks.md delete mode 100644 openspec/changes/retire-thread-tree-authority/.openspec.yaml delete mode 100644 openspec/changes/retire-thread-tree-authority/design.md delete mode 100644 openspec/changes/retire-thread-tree-authority/proposal.md delete mode 100644 openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md delete mode 100644 openspec/changes/retire-thread-tree-authority/tasks.md diff --git a/openspec/changes/add-conversation-command-api/.openspec.yaml b/openspec/changes/add-conversation-command-api/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/add-conversation-command-api/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/add-conversation-command-api/design.md b/openspec/changes/add-conversation-command-api/design.md deleted file mode 100644 index cc0c6ab3..00000000 --- a/openspec/changes/add-conversation-command-api/design.md +++ /dev/null @@ -1,195 +0,0 @@ -## Context - -规范持久化和 Generation 生命周期已经定义“什么是真实实体”和“执行如何收敛”,但它们不能直接暴露给组件或路由。当前客户端仍倾向于先修改整棵树、PUT 存盘,再发起模型请求;Fork、重试和变体选择也可能在不同模块各自实现事务语义。 - -本设计建立一个传输无关的应用层,并提供首个 HTTP/JSON 适配器。网页、未来 CLI、MCP 和公开 API 可以改变传输方式,但不能绕过同一命令、授权和仓储事务。 - -## Goals / Non-Goals - -**目标:** - -- 为 Conversation/Thread 生命周期、Fork、发送、编辑、重新生成、变体选择和 Generation Stop 建立单一命令入口。 -- 让认证、授权、幂等、版本冲突、事务和事件箱具有统一语义。 -- 返回规范快照和实体增量,支持后续客户端 normalized Store。 -- 给未来 CLI/MCP 留下稳定应用端口,而不是让它们依赖 Next.js 路由或数据库。 - -**非目标:** - -- 在本 change 迁移现有客户端、删除旧 API 或开启生产规范权威。 -- 实现分享 token、公开访问、Project Memory/File/Instruction 接口。 -- 实现 Issue #39 的 Thread 结论发布、跨 Thread 合并或收敛。 -- 规定 UI 布局、轮询视觉或乐观动画。 - -## Decisions - -### D1. 应用层使用显式 Command/Query 端口 - -应用层按用例导出命令与查询,而不是通用 CRUD 仓储: - -```text -Queries - listConversations - getConversationSnapshot - getGeneration - -Commands - createConversation / renameConversation - archiveConversation / restoreConversation / deleteConversation - renameThread / archiveThread / restoreThread - forkThread - sendTurn - editTurnInput - regenerateTurn - selectTurnVariant - stopGeneration -``` - -每个 handler 依赖 `UnitOfWork`、规范仓储、授权策略、时钟、ID、事件箱和 Generation 端口。它们不导入 Next.js Request/Response,也不返回 HTTP 状态。 - -备选方案是暴露通用 `updateEntity`;它会允许调用方组合非法关系并把不变量分散回传输层,因此拒绝。 - -### D2. 写命令使用统一内部 envelope - -内部命令输入包含: - -```text -commandId -actor -scope -targetId -idempotencyKey -expectedRevision -payload -``` - -幂等记录保存 actor、作用域、命令类型、规范化载荷摘要和序列化结果引用。相同摘要重放原结果,不同摘要返回 `idempotency_conflict`。 - -HTTP 适配器优先把 `Idempotency-Key` 映射到幂等键,把 `If-Match`/ETag 映射到预期 revision;JSON 只承载领域载荷。内部 envelope 不强迫未来 MCP 使用 HTTP header。 - -### D3. revision 按最小冲突边界选择 - -```text -Conversation revision:结构、标题、生命周期、ThreadFork 集合 -Thread revision: Thread 生命周期、标题、Turn 追加顺序 -Turn revision: 当前用户/助手变体选择 -Generation version: 执行状态、租约与 checkpoint -``` - -`forkThread` 比较并推进 Conversation revision;发送比较并推进 Thread revision;选择变体比较并推进 Turn revision。事务同时更新多个边界时全部条件必须成功。 - -备选方案是所有命令锁整条 Conversation;实现简单但会让两个独立 Thread 的发送互相冲突,因此拒绝。完全没有 revision 则会把最后写入者胜出当成正确性,因此也拒绝。 - -### D4. Fork 保存来源边界,不复制上游 Message - -`forkThread` 事务执行: - -1. 授权并锁定目标 Conversation revision; -2. 验证来源 Message 属于上游 Thread 且在其当前可用路径中; -3. 创建下游 Thread; -4. 创建唯一 ThreadFork,记录 `sourceMessageId` 与可选 TextAnchor; -5. 将下游提示词上下文边界定义为“沿来源链截至该 Message”的读取投影; -6. 推进 Conversation revision并写入 `ThreadForked` 事件箱事件。 - -不复制上游 Turn/Message;否则同一历史会有多个身份,反馈、分享和后续修改无法判定引用对象。读取模型负责把继承上下文与下游本地 Turns 投影成一列可理解内容。 - -### D5. 发送命令创建完整执行意图 - -`sendTurn` 在事务中创建 Turn、用户 Message、pending 助手 Message、规范 Generation 和 `GenerationRequested` 事件。提交后调度执行器;执行器启动失败由 Generation 生命周期收敛,不回滚已经可查询的意图。 - -`regenerateTurn` 为同 Turn 创建新的助手 Message/Generation。`editTurnInput` 为当前末尾 Turn 创建用户变体和对应助手变体;若目标 Turn 后面已有内容,返回 `fork_required`,避免静默改变历史含义。 - -### D6. 读取快照与写入 delta 使用统一传输版本 - -HTTP 成功响应使用稳定 envelope: - -```json -{ - "schemaVersion": 1, - "data": {}, - "revisions": {}, - "delta": { - "upsert": {}, - "remove": {}, - "invalidate": [] - } -} -``` - -首次 `ConversationSnapshot` 可以包含全部规范实体与明确的派生投影;命令只返回受影响实体、移除 tombstone 和需要重取的投影键。响应中的 JSON schema 通过共享传输契约解析,但领域实体不直接依赖该 schema。 - -### D7. HTTP 资源路径服务于稳定身份 - -首个适配器采用资源和领域动作组合: - -```text -GET/POST /api/projects/{projectId}/conversations -GET/PATCH/DELETE /api/conversations/{conversationId} -POST /api/conversations/{conversationId}/restore -PATCH /api/threads/{threadId} -POST /api/threads/{threadId}/restore -POST /api/threads/{threadId}/forks -POST /api/threads/{threadId}/turns -POST /api/turns/{turnId}/input-edits -POST /api/turns/{turnId}/regenerations -POST /api/turns/{turnId}/active-variant -GET /api/generations/{generationId} -POST /api/generations/{generationId}/stop -``` - -PATCH 只接受明确允许的标题/生命周期字段,不是任意数据库 patch。具体文件可以按 Next.js 当前版本约定组织,但路由只调用应用端口。 - -### D8. 统一授权且不信任归属载荷 - -认证适配器产生 actor;应用授权策略从规范包含关系解析 Workspace/Project/Conversation/Thread/Message/Generation。请求载荷不得决定 owner 或成员身份。不可见资源和不存在资源统一返回 `not_found`,避免 ID 枚举。 - -未来分享 token 会引入另一种 actor 与权限范围,但必须复用同一策略端口;本 change 不提前实现它。 - -### D9. 错误为应用结果,不是散落异常文本 - -应用层稳定分类与 HTTP 映射为: - -```text -invalid_request -> 400 -unauthenticated -> 401 -forbidden -> 403(仅资源存在性已对 actor 可见时) -not_found -> 404 -version_conflict -> 409 -idempotency_conflict -> 409 -state_conflict -> 409 -semantic_validation -> 422 -rate_limited -> 429 -internal -> 500 -``` - -Stop 已终结 Generation 是幂等成功,不是 `state_conflict`。每个错误携带稳定 `code`、`requestId` 和安全 details;日志可以保存内部 cause,但响应不泄漏数据库或模型错误。 - -### D10. 事务事件箱位于同一提交边界 - -会触发执行、标题生成、审计或未来订阅的命令,在规范实体事务内追加 outbox 事件。事件至少包含事件 ID、聚合 ID、聚合 revision、类型、schemaVersion、actor 和发生时间。消费者以事件 ID 幂等;事件不是新的写入权威。 - -### D11. 新 API 默认不承载现有客户端 - -路由可以在隔离测试或显式配置下启用,但旧客户端在 `normalize-conversation-client-state` 完成前不切换。禁止新命令同时调用旧整树 PUT,也禁止从旧树回写规范实体。最终 change 执行一次性 cutover。 - -## Risks / Trade-offs - -- **[风险] 显式命令数量比通用 CRUD 多。** → 这是把业务语义集中并可测试的必要成本;共享 envelope、结果和端口降低样板。 -- **[风险] 多层 revision 会增加客户端处理。** → 每个命令只携带最小边界 revision,响应统一返回更新值,避免全 Conversation 冲突。 -- **[风险] 继承上下文读取投影可能较复杂。** → ThreadFork 保存唯一来源,查询层集中组装并缓存可重建投影,不复制规范 Message。 -- **[风险] outbox 消费存在至少一次投递。** → 事件和消费者使用稳定 ID 幂等,实体事务不依赖同步消费者成功。 -- **[风险] 新旧 API 并存容易误调用。** → 使用互斥 authority 配置、清晰模块命名和契约测试,客户端切换前不启用生产规范写入。 - -## Migration Plan - -1. 建立应用命令/查询、统一结果、授权、幂等与 UnitOfWork 端口。 -2. 实现 Conversation/Thread 生命周期和 `forkThread`,验证事务与 revision。 -3. 接入 `sendTurn`、编辑、重新生成、变体选择与 Generation 查询/Stop。 -4. 增加 HTTP 传输 schema、路由、ETag/Idempotency-Key 和错误映射。 -5. 增加 outbox dispatcher、契约测试和隔离端到端测试。 -6. 保持生产旧客户端未切换,把新契约交给 `normalize-conversation-client-state`。 - -回滚通过关闭规范 API 路由完成;规范数据不可回写旧 JSON,也不启用双写补偿。 - -## Open Questions - -无。公开分享 actor、速率套餐和外部 API 版本生命周期属于后续独立提案,不阻塞内部应用契约和首个网页适配器。 diff --git a/openspec/changes/add-conversation-command-api/proposal.md b/openspec/changes/add-conversation-command-api/proposal.md deleted file mode 100644 index e0b03a18..00000000 --- a/openspec/changes/add-conversation-command-api/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -## Why - -规范表和 Generation 生命周期只能保证持久化层内部正确;如果网页、未来 CLI 或 MCP 仍能分别写 Thread、Message、Fork 或整包快照,重复关系和竞态会从接口重新进入系统。需要一套与传输无关的应用命令和统一 HTTP 适配器,把权限、幂等、版本冲突和事务边界集中为唯一写入入口。 - -## What Changes - -- 新增 Conversation 查询与命令应用层,覆盖 Conversation 创建/列表/读取/重命名/归档/删除,以及 Thread Fork、发送、编辑、重新生成和变体选择。 -- `forkThread` 在一个服务端事务中创建下游 Thread、ThreadFork、上下文边界、Conversation 版本和事务事件;客户端不得分步拼装关系。 -- 发送、编辑与重新生成通过命令原子创建 Turn/Message/Generation,并复用规范 Generation 生命周期;任何付费调用发生在命令事务提交之后。 -- 所有命令使用统一 envelope,包含命令 ID、幂等键、目标 ID、预期版本和载荷;服务端返回规范实体增量与最新版本,而非可回写整树。 -- 新增统一身份认证、所有者/成员授权、稳定领域错误和 HTTP 状态映射;冲突只在预期版本、幂等载荷或当前状态真实冲突时返回。 -- 新增首次加载 `ConversationSnapshot`、增量查询以及 Generation 查询/Stop 端点,供网页与未来公开适配器共用同一应用服务。 -- **BREAKING**:目标接口不再提供整棵 `ThreadTreeState` 的权威 PUT、客户端关系写入或浏览器先存盘屏障;旧路由仅在最终切换前暂存,不能成为新命令的第二权威。 - -## Capabilities - -### New Capabilities - -- `conversation-command-api`:定义 Conversation 查询、原子领域命令、权限、幂等、并发、实体增量和稳定传输结果。 - -### Modified Capabilities - -- 无。遗留路由的删除由 `retire-thread-tree-authority` 负责,本变更建立替代契约。 - -## Impact - -- 应用层:新增 Conversation commands/queries、事务端口、授权策略、事件箱事件和统一结果类型。 -- 接口层:新增 `/api/projects/.../conversations`、`/api/conversations/...`、Thread/Turn/Generation 子资源路由及传输 schema。 -- 持久化:复用规范仓储、Conversation/Thread/Turn revision、Generation 生命周期和事务事件箱。 -- 客户端:后续改用快照初始化和实体增量;本变更只提供接口,不迁移 Store。 -- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`、`migrate-generation-lifecycle`。 -- 后续依赖:`normalize-conversation-client-state`、`retire-thread-tree-authority`,以及未来 CLI/MCP/分享适配器。 diff --git a/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md b/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md deleted file mode 100644 index c0d0dba0..00000000 --- a/openspec/changes/add-conversation-command-api/specs/conversation-command-api/spec.md +++ /dev/null @@ -1,183 +0,0 @@ -## Purpose - -为网页、未来 CLI 与 MCP 提供同一套 Conversation 查询和原子命令契约,使授权、幂等、并发、Fork、Message 变体与 Generation 只能通过服务端应用层改变。 - -## ADDED Requirements - -### Requirement: 通过应用命令改变规范实体 - -系统 SHALL 只允许应用命令改变 Conversation、Thread、ThreadFork、Turn、Message、Generation 或当前有效变体。传输层 MUST 只负责认证、解析、调用命令和序列化结果,不得自行拼装关系或接受整包 Conversation 快照覆盖规范实体。 - -#### Scenario: 拒绝整树写入 - -- **WHEN** 客户端尝试提交全部 Thread、Message 与 Fork 以覆盖一条 Conversation -- **THEN** 新接口不提供该写入能力,并要求调用对应实体或领域动作命令 - -#### Scenario: 多种适配器调用相同命令 - -- **WHEN** 网页接口和未来 CLI 对同一 Conversation 执行相同领域动作 -- **THEN** 二者调用同一应用命令并获得相同权限、不变量、幂等与冲突语义 - -### Requirement: 提供 Conversation 生命周期查询和命令 - -系统 SHALL 提供按 Project 列出和创建 Conversation、按 ID 读取快照,以及重命名、归档、恢复和删除 Conversation 的查询或命令。创建 SHALL 在一个事务中创建 Conversation 与根 Thread;删除 SHALL 遵守运行中 Generation 和归属资产保护规则。 - -#### Scenario: 创建 Conversation 与根 Thread - -- **WHEN** 有权限成员在 Project 中创建 Conversation -- **THEN** 系统原子创建 Conversation 和唯一根 Thread,并返回二者的规范增量与初始版本 - -#### Scenario: 归档后从默认列表隐藏 - -- **WHEN** 用户归档 Conversation -- **THEN** Conversation 保持可按 ID 恢复和审计,但默认活跃列表不再返回它 - -#### Scenario: 运行中任务阻止删除 - -- **WHEN** Conversation 仍有非终态 Generation 且用户请求删除 -- **THEN** 系统返回稳定状态冲突且不删除任何实体 - -### Requirement: 提供 Thread 生命周期命令 - -系统 SHALL 提供 Thread 重命名、归档和恢复命令。归档 Thread 不得删除 Message、Generation 或 ThreadFork,也不得改变其他 Thread 的身份;若归档会使当前界面路径不可见,服务端仍 SHALL 保留规范关系,由客户端选择其他可见 Thread。根 Thread 不得脱离 Conversation 单独归档;调用方必须改用 Conversation 归档命令。 - -#### Scenario: 归档非根 Thread - -- **WHEN** 用户归档一个没有非终态 Generation 的分支 Thread -- **THEN** 系统更新该 Thread 生命周期与 revision,保留其 Fork 来源、后代和历史内容 - -#### Scenario: 运行中任务阻止 Thread 归档 - -- **WHEN** Thread 中仍有非终态 Generation -- **THEN** 系统返回稳定状态冲突,Thread 保持活跃 - -#### Scenario: 单独归档根 Thread - -- **WHEN** 用户直接请求归档 Conversation 的根 Thread -- **THEN** 系统返回稳定 `conversation_action_required` 结果,并保持 Conversation 与根 Thread 活跃 - -### Requirement: 原子执行 forkThread - -`forkThread` 命令 SHALL 验证来源 Message 属于上游 Thread,随后在单个事务中创建下游 Thread、唯一 ThreadFork、继承上下文边界、必要版本更新和事务事件箱事件。命令不得复制上游 Message 作为新的规范 Message,也不得要求客户端随后补写 parent、children 或反向 Fork 列表。 - -#### Scenario: 从任意 Thread 的确定 Message Fork - -- **WHEN** 用户以有效上游 Thread、来源 Message、预期 Conversation revision 和幂等键执行 `forkThread` -- **THEN** 系统返回新 Thread、ThreadFork 和版本增量,下游上下文在来源 Message 处截止 - -#### Scenario: Fork 事务中途失败 - -- **WHEN** 来源错配、版本冲突或约束使事务失败 -- **THEN** 系统不留下孤立 Thread、半条 ThreadFork 或事件箱事件 - -### Requirement: 原子执行发送与 Generation 开始 - -发送命令 SHALL 在目标 Thread 中原子创建 Turn、用户 Message、待生成助手 Message 和 Generation,并在事务提交后才开始模型执行。命令 SHALL 使用 Thread revision 保护追加顺序;同一幂等键不得创建重复 Turn 或重复付费执行。 - -#### Scenario: 在 Thread 尾部发送消息 - -- **WHEN** 用户以当前 Thread revision 发送有效输入 -- **THEN** 系统提交新的 Turn、两条 Message 和 Generation,返回规范增量,然后启动服务端执行 - -#### Scenario: 并发发送使用旧 revision - -- **WHEN** 另一个命令已经推进 Thread,而客户端仍以旧 revision 发送 -- **THEN** 系统返回版本冲突和最新可读 revision,不创建第二个不确定顺序的 Turn - -### Requirement: 编辑与重新生成追加 Message 变体 - -编辑当前 Turn 的用户输入和重新生成助手回答 SHALL 在同一 Thread/Turn 中追加新 Message 变体与新 Generation,不得原地覆盖旧 Message。编辑历史位置若已有后续 Turn MUST 被拒绝或要求显式 Fork,不能静默重写后续上下文。 - -#### Scenario: 重新生成当前助手回答 - -- **WHEN** 用户对 Turn 的当前助手 Message 执行重新生成 -- **THEN** 系统追加新的助手 Message 和 Generation,并在合法终结后按版本更新当前助手变体 - -#### Scenario: 编辑有后续内容的历史 Turn - -- **WHEN** 用户尝试直接编辑并续跑一个已有后续 Turn 的历史输入 -- **THEN** 系统返回稳定 `fork_required` 结果,且不修改当前 Thread 历史 - -### Requirement: 持久化选择当前有效变体 - -选择变体命令 SHALL 只允许把同一 Thread/Turn、角色匹配且内容可用的 Message 设为当前有效用户或助手变体。命令 MUST 使用 Turn revision,原子更新选择并返回受影响 Turn 增量。 - -#### Scenario: 切换助手变体 - -- **WHEN** 用户以当前 Turn revision 选择同 Turn 的另一条可用助手 Message -- **THEN** 系统持久化新选择,推进 Turn revision,后续上下文使用该变体 - -#### Scenario: 拒绝跨 Turn 选择 - -- **WHEN** 命令尝试选择另一 Turn 或 Thread 的 Message -- **THEN** 系统拒绝命令且不改变当前选择 - -### Requirement: 提供 Generation 查询和 Stop 命令 - -系统 SHALL 按稳定 Generation ID 提供状态、最新 checkpoint、usage 完整度和计费状态查询,并提供幂等 Stop 命令。Stop 运行中任务 SHALL 返回已接受或当前状态;Stop 已终结任务 SHALL 返回现有终态,不得虚构版本冲突。 - -#### Scenario: 查询进行中 Generation - -- **WHEN** 所有者查询 `running` 或 `stop_requested` Generation -- **THEN** 系统返回当前状态、最新 checkpoint 版本、内容状态与可用轮询提示 - -#### Scenario: Stop 已完成 Generation - -- **WHEN** 所有者对 `completed` Generation 发出 Stop -- **THEN** 系统幂等返回 `completed` 结果,不返回无定义的冲突也不修改计费 - -### Requirement: 统一身份认证和资源授权 - -每个查询和命令 SHALL 先解析认证主体,再通过 Workspace/Project 成员关系检查目标资源。未认证请求 MUST 返回稳定未认证结果;不存在或调用方不可见的目标 MUST 使用不可枚举的未找到结果。客户端提交的 owner、workspace 或 project 字段不得覆盖服务端解析的归属。 - -#### Scenario: 伪造 owner 字段 - -- **WHEN** 已认证用户在载荷中提交另一个 owner ID -- **THEN** 系统忽略或拒绝该字段,并仅使用认证主体和成员关系解析权限 - -#### Scenario: 访问他人不可见 Message - -- **WHEN** 用户按 ID 查询无权访问的 Message 或其 Generation -- **THEN** 系统返回与资源不存在相同的未找到结果,不泄漏资源是否存在 - -### Requirement: 命令具有统一幂等和版本语义 - -每个有副作用命令 SHALL 携带稳定幂等键;会修改既有聚合的命令 SHALL 携带相应 Conversation、Thread 或 Turn 的预期 revision。相同主体、作用域、幂等键和等价载荷 SHALL 返回原结果;同键不同载荷 MUST 返回幂等冲突;版本冲突 MUST 返回最新 revision 与可重试信息。 - -#### Scenario: 网络超时后重放命令 - -- **WHEN** 客户端未收到响应并以相同幂等键重放等价命令 -- **THEN** 系统返回第一次提交的结果,不重复写实体、事件或付费任务 - -#### Scenario: 重用幂等键提交不同载荷 - -- **WHEN** 客户端用已提交的幂等键发送不同命令载荷 -- **THEN** 系统返回稳定幂等冲突且不执行新载荷 - -### Requirement: 返回规范快照和实体增量 - -首次读取 Conversation SHALL 返回带 schema/version 的规范快照;成功命令 SHALL 返回命令结果、最新作用域 revision 和仅受影响的规范实体增量。派生 children、深度、Fork 数量和可见路径可以出现在读取投影中,但 MUST 标记为可重建,不得作为后续写命令输入。 - -#### Scenario: 首次加载 Conversation - -- **WHEN** 客户端读取一条可见 Conversation -- **THEN** 系统返回稳定排序的 Conversation、Threads、ThreadForks、Turns、Messages、相关 Generation 摘要和当前有效选择 - -#### Scenario: Fork 命令只返回受影响实体 - -- **WHEN** `forkThread` 成功 -- **THEN** 响应包含新 Thread、ThreadFork、更新的 Conversation revision 和必要投影失效信息,而不是回传可写整树 - -### Requirement: 使用稳定错误分类映射传输结果 - -应用层 SHALL 返回与 HTTP 无关的稳定结果分类,传输适配器 SHALL 一致映射为状态码和机器可读错误代码。只有真实预期版本冲突、幂等冲突或当前状态冲突可以映射为 `409`;格式错误、未认证、不可见、语义验证和服务器错误 MUST 分别映射,不得用自由文本区分。 - -#### Scenario: Stop 不存在冲突条件 - -- **WHEN** Stop 命令命中已终结 Generation -- **THEN** 应用层返回幂等成功结果,传输层不得为了列出状态码而返回 `409` - -#### Scenario: Fork 使用旧 Conversation revision - -- **WHEN** `forkThread` 的预期 revision 已过期 -- **THEN** 应用层返回 `version_conflict`,HTTP 适配器映射为 `409` 并附最新 revision diff --git a/openspec/changes/add-conversation-command-api/tasks.md b/openspec/changes/add-conversation-command-api/tasks.md deleted file mode 100644 index 69798eb1..00000000 --- a/openspec/changes/add-conversation-command-api/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -## 1. 应用层基础契约 - -- [ ] 1.1 定义 Command/Query 输入、actor、幂等键、预期 revision、规范 delta 和稳定错误结果,不导入 HTTP 或数据库行类型。 -- [ ] 1.2 定义 `UnitOfWork`、授权策略、幂等存储、事务事件箱和规范仓储端口,并建立测试替身。 -- [ ] 1.3 实现 Workspace/Project → Conversation/Thread/Message/Generation 的不可枚举授权解析和安全错误 details。 -- [ ] 1.4 实现幂等载荷摘要、等价重放、同键异载荷冲突和命令结果恢复。 - -## 2. Conversation 与 Thread 生命周期 - -- [ ] 2.1 实现 Conversation 列表、规范快照读取以及创建 Conversation + 根 Thread 的原子命令。 -- [ ] 2.2 实现 Conversation 重命名、归档、恢复和受保护删除,覆盖运行中 Generation 冲突。 -- [ ] 2.3 实现 Thread 重命名、非根 Thread 归档和恢复,保留 Fork、后代、Message 与 Generation 历史,并要求根 Thread 使用 Conversation 归档。 -- [ ] 2.4 增加生命周期命令的权限、幂等、revision 和事务回滚测试。 - -## 3. Fork 与上下文边界 - -- [ ] 3.1 实现 `forkThread` 的来源 Message、同 Conversation、当前可用路径和预期 Conversation revision 校验。 -- [ ] 3.2 在单事务创建下游 Thread、ThreadFork、上下文边界、revision 与 `ThreadForked` outbox 事件。 -- [ ] 3.3 实现不复制上游 Message 的继承上下文读取投影,并覆盖 A → B → C 嵌套 Fork。 -- [ ] 3.4 覆盖来源错配、并发 Fork、重复幂等键和事务失败无孤立实体测试。 - -## 4. Turn、Message 变体与 Generation 命令 - -- [ ] 4.1 实现 `sendTurn`,原子创建 Turn、用户/助手 Message、Generation 和事件,并在提交后启动执行器。 -- [ ] 4.2 实现末尾用户输入编辑和助手重新生成,以新 Message/Generation 变体追加而非覆盖旧身份。 -- [ ] 4.3 对已有后续 Turn 的历史编辑返回 `fork_required`,不修改现有上下文。 -- [ ] 4.4 实现带 Turn revision 的用户/助手当前变体选择和角色/归属/内容可用性校验。 -- [ ] 4.5 实现 Generation 查询与幂等 Stop,复用规范 lifecycle 并让已终结 Stop 返回现有结果。 - -## 5. HTTP 传输适配器 - -- [ ] 5.1 定义带 `schemaVersion` 的请求、快照、delta、revision 和错误传输 schema。 -- [ ] 5.2 实现 Project/Conversation/Thread/Turn/Generation 资源路由,只调用应用 Command/Query。 -- [ ] 5.3 将 `Idempotency-Key`、`If-Match`/ETag 和认证会话映射到内部 command envelope。 -- [ ] 5.4 实现 400/401/403/404/409/422/429/500 的稳定错误映射,并验证只有真实冲突返回 409。 -- [ ] 5.5 增加接口契约测试,覆盖 owner 伪造、不可见资源、格式错误、幂等重放和 revision 冲突。 - -## 6. 事件箱与隔离验证 - -- [ ] 6.1 实现 outbox 写入和幂等 dispatcher,验证事件与规范实体在同一事务提交。 -- [ ] 6.2 增加默认关闭的规范 API 开关,拒绝新命令调用旧整树 PUT 或双写旧权威。 -- [ ] 6.3 在隔离环境跑通创建 Conversation、发送、Fork、嵌套 Fork、重新生成、选择变体、Stop、归档与恢复端到端流程。 -- [ ] 6.4 运行相关测试、`pnpm typecheck`、`pnpm openspec:validate` 和接口 schema 检查并记录结果。 diff --git a/openspec/changes/define-conversation-domain-model/.openspec.yaml b/openspec/changes/define-conversation-domain-model/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/define-conversation-domain-model/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/define-conversation-domain-model/design.md b/openspec/changes/define-conversation-domain-model/design.md deleted file mode 100644 index 39b774a9..00000000 --- a/openspec/changes/define-conversation-domain-model/design.md +++ /dev/null @@ -1,992 +0,0 @@ -## Context - -变更动机见 `proposal.md`。本文只定义目标领域、数据库和服务端模块骨架,不以当前 `ThreadTreeState`、整树 JSON、现有 API 或客户端 Store 的形状为前提。 - -此前目标设计同时引入了独立 ThreadFork、Turn、Message Variant、Generation、当前有效变体和多级 revision。它可以表达回答版本树,但产品最终确认的 ThreadChat MVP 1.0 不提供回答版本选择: - -- 用户看到的每一列都是一个 Thread; -- Thread 内 Message 永远严格线性; -- 只有最后一条 user Message 可以 Edit; -- Regenerate 破坏式替换最后一条 assistant Message; -- 需要保留另一条历史时统一使用 Fork Thread; -- Child Thread 创建后必须保持 Fork 当时的语义,不受 Parent 后续破坏式修改影响; -- 模型执行需要后台持久化、Stop、断线恢复、用量与计费,但不应扩张 Chat 内容模型。 - -因此,目标模型收敛为: - -```text -Workspace -└── Project - ├── Conversation - │ └── Thread - │ └── Message[] - ├── ProjectInstructionVersion - ├── MemoryItem - └── ProjectFile - -执行子域: -Assistant Message -└── MessageRun[] -``` - -这次简化不是把运行可靠性删除,而是把“聊天内容事实”和“模型执行事实”分开。Chat Domain 不包含 Turn、Message Variant 或 Generation;执行子域使用 MessageRun 保存每次 attempt。 - -## Goals / Non-Goals - -**目标:** - -- 建立 Workspace、Project、Conversation、Thread、Message 与 MessageRun 的唯一名称和职责。 -- 定义 Fork、根 Thread、分支 Thread、破坏式 Edit、破坏式 Regenerate、Retry 与 Stop 的语义。 -- 定义 `BaseContext` 与 `ForkSourceSnapshot` 两个值对象及其不可变边界。 -- 给出可由 PostgreSQL/Drizzle 实现的逻辑表、字段、复合约束、索引与事务不变量。 -- 定义 UI Timeline、LLM Prompt、Conversation Bootstrap 等读取投影的事实来源。 -- 定义纯领域、应用命令、执行端口、仓储、HTTP 适配器之间的目标模块骨架和依赖方向。 -- 为后续服务端实现、集成测试、API 测试、Zustand Store、Hooks 与 UI 接入提供稳定基础。 -- 保留 Workspace、Project、Conversation、Thread 和 Message 的稳定公开身份,使未来分享、CLI、MCP 与公开 API 无需重做核心身份。 - -**非目标:** - -- 本变更不创建数据库迁移、Drizzle schema、仓储、API 路由或运行时代码。 -- 本变更不设计 Zustand Store、React Hooks、Selectors、组件接入或 UI 交互细节。 -- 本变更不设计现有数据导入、双写、切换、回滚或旧表删除方案。 -- 本变更不保留被覆盖的 user/assistant Message 内容版本,也不提供回答版本选择器。 -- 本变更不支持编辑历史 Message;历史位置的另一条发展路径必须通过 Fork 创建。 -- 本变更不支持多来源 Thread、Thread Merge、跨 Thread 结论收敛或 Issue #39 的发布机制。 -- 本变更不实现 Project Memory、Project Instruction、Project File、分享、公开 Token、CLI 或 MCP 产品功能;只定义所有权和扩展位置。 -- 本变更不实现通用 `Idempotency-Key`、离线队列或命令重放;这些属于 V2。 -- 本变更不决定 SSE、轮询或 WebSocket 的最终传输选择;它只定义可恢复事件需要的运行事实。 - -## Decisions - -### D1. 统一术语与边界 - -| 名称 | 定义 | 是否为核心实体 | -|---|---|---:| -| Workspace | 租户、成员和授权的最外层边界 | 是 | -| WorkspaceMembership | User 与 Workspace 的成员/角色关系 | 关系实体 | -| Project | 长期目标、规则、记忆、文件和 Conversation 的资产边界 | 是 | -| Conversation | 一个根 Thread 及全部派生 Thread 组成的完整讨论 | 是 | -| Thread | 一列可以独立继续的严格线性对话 | 是 | -| Root Thread | Conversation 中唯一没有 Parent 的 Thread;是推导角色 | 否 | -| Branch Thread | 由 Fork 创建且具有 Parent 的 Thread;是推导角色 | 否 | -| Fork | 从确定 Message 创建 Child Thread 的服务端动作 | 否 | -| Message | Thread 中具有稳定 ID 和顺序的用户或助手内容 | 是 | -| MessageRun | 生成/重试一个 assistant Message 的一次后台执行 attempt | 执行子域实体 | -| BaseContext | Child Thread 创建时继承的不可变有效历史投影 | 值对象 | -| ForkSourceSnapshot | Fork 时选区、来源哈希和定位上下文的不可变快照 | 值对象 | -| ProjectInstructionVersion | Project 级指令的不可变版本 | 扩展实体 | -| MemoryItem | Project 级、可追溯的记忆条目 | 扩展实体 | -| ProjectFile / FileVersion | Project 内可跨 Conversation 使用的持久文件及版本 | 扩展实体 | -| UI Workspace | 列、折叠、画布和面板等客户端状态 | 不是后端实体 | - -以下名称不进入目标模型: - -```text -ThreadTree -MainThread 实体 -ForkedThread 实体 -ThreadFork 实体 -Turn -Generation -Message.parentMessageId -Message Variant -activeGenerationId -Conversation/Thread/Turn revision -``` - -`MessageRun` 在执行/计费子域中有身份和生命周期,因此技术上是运行实体;“不是一等实体”只表示它不是 Chat 内容资源,不表示它可以只存在于进程内存。 - -### D2. 规范包含关系 - -```mermaid -erDiagram - USER ||--o{ WORKSPACE_MEMBERSHIP : joins - WORKSPACE ||--o{ WORKSPACE_MEMBERSHIP : has - WORKSPACE ||--o{ PROJECT : contains - PROJECT ||--o{ CONVERSATION : contains - CONVERSATION ||--|{ THREAD : contains - THREAD ||--o{ MESSAGE : contains - THREAD o|--o{ THREAD : parent_of - MESSAGE o|--o{ THREAD : fork_source_of - MESSAGE ||--o{ MESSAGE_RUN : executed_by - PROJECT ||--o{ PROJECT_INSTRUCTION_VERSION : may_have - PROJECT ||--o{ MEMORY_ITEM : may_have - PROJECT ||--o{ PROJECT_FILE : may_have - PROJECT_FILE ||--o{ FILE_VERSION : versions -``` - -包含关系不等于一个巨型事务聚合: - -- Workspace 是授权解析边界; -- Project 是长期资产边界; -- Conversation 是完整讨论的命名、列表、归档和分享边界; -- Thread 是追加 Message、Edit 和 Regenerate 的主要写入边界; -- Fork 命令跨 Parent Thread 读取并原子创建 Child Thread; -- MessageRun 由执行仓储独立推进,不要求锁定整个 Conversation。 - -**根 Thread 的唯一事实:** 一个 Conversation 恰好有一个 `parentThreadId = null` 的 Thread。目标写模型不同时持久化 `Conversation.rootThreadId`,避免两个字段竞争。读取 DTO 可以派生并返回 `rootThreadId`。 - -**备选方案:Conversation 持久化 rootThreadId。** 不采用,因为它与 Root Thread 的空 Parent 角色重复,创建时还会产生循环引用。 - -### D3. Thread 内 Message 严格线性 - -Thread 不包含 Turn 或 Message DAG。唯一顺序为服务端分配的 `Message.sequence`: - -```text -Thread A -├── sequence=1 user -├── sequence=2 assistant -├── sequence=3 user -└── sequence=4 assistant -``` - -发送一条用户输入时,服务端在同一事务中创建: - -```text -User Message sequence = N -Assistant placeholder sequence = N + 1 -MessageRun attempt = 1 -``` - -`createdAt` 只负责审计,不负责领域排序。`prevMessageId/nextMessageId` 不持久化,因为它们可由 sequence 推导,双向指针会产生重复事实。 - -Message 只允许: - -```text -role = user | assistant -``` - -System 指令由服务端 Prompt Builder 注入,不允许作为客户端提交的 Message。工具调用、工具结果、推理、Markdown Artifact 与文件引用使用版本化 Message Part 表达,不为每一种 part 强制创建数据库实体。 - -### D4. Edit 与 Regenerate 是破坏式操作 - -#### Edit - -只有当前 Thread 最后一条 user Message 可以 Edit,并且它后面只能是对应的最后一条 assistant Message。事务执行: - -1. 锁定目标 Thread 的末尾 Message; -2. 验证目标 user Message 是最后一个用户输入; -3. 更新该 user Message 的 `parts/contentHash/updatedAt`; -4. 清空或重置紧随其后的 assistant Message 内容; -5. 将旧的非终态 MessageRun 条件更新为 `superseded`; -6. 为同一个 assistantMessageId 创建下一个 MessageRun attempt; -7. 提交后调度执行。 - -Edit 不创建新的 Message ID、不保存旧输入版本、不产生隐式 Fork。 - -#### Regenerate - -只有当前 Thread 最后一条 assistant Message 可以 Regenerate。事务执行: - -1. 验证目标是 Thread 最后一条 assistant Message; -2. 保持 assistantMessageId 与 sequence 不变; -3. 清空或重置其可见输出; -4. 将旧非终态 attempt 标记为 `superseded`; -5. 创建下一个 MessageRun attempt; -6. 新结果写回同一个 assistant Message。 - -旧 assistant 内容不保留版本选择。需要保留原回答时,用户必须先 Fork。 - -**备选方案:同一个位置保存多个输入/输出版本。** 不采用,因为它会重新引入 Turn、Generation 配对和 active variant,违背 MVP 的唯一分支机制。 - -### D5. Fork 是语义快照边界 - -Fork 不创建独立 ThreadFork 行。由于 MVP 中每个非根 Thread 恰有一个 Parent、一个来源 Message,且 Fork 元数据与 Child Thread 同生共死,以下字段直接属于 Child Thread: - -```text -parentThreadId -sourceMessageId -forkSourceSnapshot -baseContext -``` - -Fork API/应用命令只接受: - -```text -parentThreadId -sourceMessageId -anchor(exact/prefix/suffix) -可选初始用户输入 -``` - -客户端不得提交: - -```text -childThreadId -baseContext -sourceContentHash -Parent Message history -``` - -服务端事务执行: - -1. 解析 actor 与授权; -2. 读取并验证 Parent Thread; -3. 验证 source Message 属于 Parent Thread 且内容可 Fork; -4. 计算 Parent 截至 source Message 的 effective history; -5. 生成不可变 BaseContext; -6. 生成 ForkSourceSnapshot; -7. 创建 Child Thread; -8. 如携带初始输入,则同时创建 user/assistant Message 与 MessageRun; -9. 提交后调度可选执行。 - -Child 创建后: - -- Parent 追加 Message 不改变 Child; -- Parent Edit/Regenerate 不改变 Child; -- Parent 或来源归档不删除 Child; -- 来源内容变化只让 provenance 状态变为 outdated; -- 来源不存在时 provenance 状态为 unavailable; -- BaseContext 永远保持 Fork 时的内容。 - -**备选方案:每次构造 Child Prompt 时递归读取祖先。** 不采用,因为破坏式 Edit/Regenerate 会改变 Child 已经建立的历史语义。 - -**备选方案:独立 ThreadFork 实体。** MVP 不采用;只有多 Parent、Merge、关系独立生命周期或关系级分享出现时再升级。 - -### D6. BaseContext 与 ForkSourceSnapshot - -#### BaseContext - -```ts -interface BaseContextV1 { - schemaVersion: 1 - messages: ContextMessageV1[] -} - -interface ContextMessageV1 { - sourceMessageId?: string - role: "user" | "assistant" - parts: MessagePartV1[] -} -``` - -BaseContext 是 Message Content Projection,不是 Message Entity 副本。它不包含: - -```text -threadId -sequence -MessageRun -feedback -createdAt / updatedAt -UI 状态 -计费状态 -``` - -嵌套 Fork 时,服务端先构造 Parent 的完整有效上下文: - -```text -Parent.baseContext -+ -Parent local messages through sourceMessage -``` - -再把结果扁平冻结为 Child.baseContext。因此运行时不需要递归读取祖先。 - -#### ForkSourceSnapshot - -```ts -interface ForkSourceSnapshotV1 { - schemaVersion: 1 - sourceMessageId: string - sourceContentHash: string - quote: { - exact: string - prefix?: string - suffix?: string - } -} -``` - -`forkSourceSnapshot` 只回答“从哪里 Fork”,`baseContext` 回答“以后以什么历史继续”。两者不得合并成一个含义模糊的 `forkContext`。 - -来源状态是读取投影,不单独持久化: - -```text -source exists && currentHash == snapshotHash → available -source exists && currentHash != snapshotHash → outdated -source missing/inaccessible → unavailable -``` - -### D7. Message Part 协议 - -Message 内容采用项目自有的版本化 envelope: - -```ts -interface MessageContentV1 { - schemaVersion: 1 - parts: MessagePartV1[] -} -``` - -`MessagePartV1` 的当前形状必须与仓库安装的 AI SDK v7 `UIMessage.parts` 支持范围兼容,包括当前实际使用的 text、reasoning、tool、data、file/attachment 与 Artifact 表达。领域层不得导入 AI SDK 的运行时对象或 `streamText()` 结果类型。 - -依赖方向: - -```text -AI SDK v7 UIMessage / stream events - ↓ adapter -MessageContentV1 / NormalizedRunEvent - ↓ -Domain / Application -``` - -未来更换模型运行时只替换 adapter;稳定 Message、Thread 与 Conversation ID 不迁移。 - -### D8. MessageRun 是独立执行子域 - -```ts -interface MessageRun { - assistantMessageId: MessageId - attempt: number - status: - | "queued" - | "running" - | "completed" - | "failed" - | "stopped" - | "superseded" - modelId: string - eventSequence: number - checkpointParts: MessagePartV1[] - usage?: ModelUsage - errorCode?: string - queuedAt: Date - startedAt?: Date - finishedAt?: Date - heartbeatAt?: Date - stopRequestedAt?: Date -} -``` - -身份使用 `(assistantMessageId, attempt)`。Regenerate/Retry 创建新 attempt,不复用旧 MessageRun 行;旧 attempt 可以保留用量、计费和审计,但不保留旧 assistant Message 内容版本。 - -状态机: - -```mermaid -stateDiagram-v2 - [*] --> queued - queued --> running - queued --> stopped - running --> completed - running --> failed - running --> stopped - queued --> superseded - running --> superseded -``` - -状态转换使用条件更新,而不是通用 revision: - -```sql -UPDATE message_runs -SET status = 'completed', finished_at = now() -WHERE assistant_message_id = $1 - AND attempt = $2 - AND status = 'running'; -``` - -同一个 assistant Message 同时最多只有一个 `queued/running` attempt。 - -浏览器刷新、关闭或流连接断开只释放订阅,不调用 Stop。重载后,读取模型按 assistantMessageId 返回最新 attempt 的: - -```text -status -attempt -checkpointParts -eventSequence -``` - -传输层可以用 SSE、轮询或未来 WebSocket 交付同一规范运行事件。P0 不要求永久保存每个 token event;服务端可以在重连时先发送最新 checkpoint/reset 事件,再继续递增 eventSequence。 - -Message 表不重复保存 `running/failed/stopped`。UI 中的 assistant runtime status 是 Message 与最新 MessageRun join 后的读取投影。 - -### D9. Prompt 与 UI Timeline 是不同读取投影 - -#### UI Timeline - -按 Message.sequence 返回全部本地 Message,并把最新 MessageRun 状态投影到 assistant Message: - -```text -U1 -[A1 生成失败] -U2 -A2 -``` - -#### Prompt History - -Prompt Builder 使用: - -```text -Project/System Context -+ -Thread.baseContext -+ -Thread local effective messages -``` - -P0 规则: - -| 本地事实 | 进入 Prompt | -|---|---:| -| user Message | 是 | -| assistant 最新 attempt completed | 是 | -| assistant 最新 attempt queued/running | 否 | -| assistant 最新 attempt failed | 否 | -| assistant 最新 attempt stopped | 否 | -| assistant 最新 attempt superseded | 否 | - -因此上面的 Prompt 为: - -```text -U1 -U2 -A2 -``` - -Project Instruction、Memory 与 File retrieval 属于 Project/System Context,不复制进 BaseContext。若未来需要执行可复现性,MessageRun 记录实际使用的 instruction version、file version 与 retrieval provenance;这不改变 Chat 内容实体。 - -### D10. PostgreSQL 核心 Schema - -下列是目标逻辑 schema。具体 Drizzle 文件拆分、迁移编号和既有 User 表名由后续持久化变更决定。所有 ID 由服务端生成,使用 UUID 或等价不透明稳定 ID;时间统一存 `timestamptz`。 - -#### 枚举 - -```sql -CREATE TYPE workspace_role AS ENUM ('owner', 'admin', 'member'); -CREATE TYPE record_status AS ENUM ('active', 'archived'); -CREATE TYPE message_role AS ENUM ('user', 'assistant'); -CREATE TYPE message_run_status AS ENUM ( - 'queued', - 'running', - 'completed', - 'failed', - 'stopped', - 'superseded' -); -``` - -实际实现可以使用受约束 text 替代 PostgreSQL enum,避免枚举迁移成本;无论采用哪种物理表示,领域值集合必须相同。 - -#### workspaces - -```sql -CREATE TABLE workspaces ( - id uuid PRIMARY KEY, - name text NOT NULL, - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() -); -``` - -Owner 角色以 membership 为唯一事实;`created_by` 只表示审计,不表示当前 Owner。 - -#### workspace_memberships - -```sql -CREATE TABLE workspace_memberships ( - workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT, - user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT, - role workspace_role NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (workspace_id, user_id) -); - -CREATE INDEX workspace_memberships_user_idx - ON workspace_memberships(user_id, workspace_id); -``` - -“至少一个 owner”需要应用事务或延迟约束保证,不能仅靠单行 CHECK。 - -#### projects - -```sql -CREATE TABLE projects ( - id uuid PRIMARY KEY, - workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT, - name text NOT NULL, - description text, - status record_status NOT NULL DEFAULT 'active', - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - archived_at timestamptz -); - -CREATE INDEX projects_workspace_status_idx - ON projects(workspace_id, status, updated_at DESC); -``` - -Project 默认继承 Workspace 成员权限;MVP 不创建 ProjectMembership。 - -#### conversations - -```sql -CREATE TABLE conversations ( - id uuid PRIMARY KEY, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, - title text, - title_source text CHECK (title_source IN ('auto', 'custom')), - status record_status NOT NULL DEFAULT 'active', - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - archived_at timestamptz -); - -CREATE INDEX conversations_project_status_idx - ON conversations(project_id, status, updated_at DESC); -``` - -`title_source` 用于阻止后续自动标题覆盖用户自定义标题。它与 revision 无关。 - -#### threads - -```sql -CREATE TABLE threads ( - id uuid PRIMARY KEY, - conversation_id uuid NOT NULL REFERENCES conversations(id) ON DELETE RESTRICT, - - parent_thread_id uuid, - source_message_id uuid, - fork_source_snapshot jsonb, - base_context jsonb NOT NULL, - - title text, - title_source text CHECK (title_source IN ('auto', 'custom')), - default_model_id text, - status record_status NOT NULL DEFAULT 'active', - - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - archived_at timestamptz, - - UNIQUE (id, conversation_id), - - CHECK (jsonb_typeof(base_context) = 'object'), - CHECK ( - ( - parent_thread_id IS NULL - AND source_message_id IS NULL - AND fork_source_snapshot IS NULL - ) - OR - ( - parent_thread_id IS NOT NULL - AND source_message_id IS NOT NULL - AND fork_source_snapshot IS NOT NULL - ) - ), - - FOREIGN KEY (parent_thread_id, conversation_id) - REFERENCES threads(id, conversation_id) - ON DELETE RESTRICT - DEFERRABLE INITIALLY IMMEDIATE -); - -CREATE UNIQUE INDEX threads_one_root_per_conversation_idx - ON threads(conversation_id) - WHERE parent_thread_id IS NULL; - -CREATE INDEX threads_parent_idx - ON threads(parent_thread_id, created_at); - -CREATE INDEX threads_conversation_status_idx - ON threads(conversation_id, status, created_at); -``` - -根 Thread 的 `base_context` 必须是 `{ "schemaVersion": 1, "messages": [] }`。JSON 内部 schema 由应用层 codec 验证;数据库只做最低 JSON 类型约束。 - -#### messages - -```sql -CREATE TABLE messages ( - id uuid PRIMARY KEY, - thread_id uuid NOT NULL REFERENCES threads(id) ON DELETE RESTRICT, - sequence bigint NOT NULL CHECK (sequence > 0), - role message_role NOT NULL, - - content_schema_version smallint NOT NULL DEFAULT 1, - parts jsonb NOT NULL, - content_hash text NOT NULL, - - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - - UNIQUE (thread_id, sequence), - UNIQUE (id, thread_id), - CHECK (jsonb_typeof(parts) = 'array') -); - -CREATE INDEX messages_thread_sequence_idx - ON messages(thread_id, sequence); -``` - -`content_hash` 由服务端对规范化后的 MessageContent 计算,用于来源过期判断;客户端不能提交该值。 - -在 threads/messages 都存在后增加来源复合外键: - -```sql -ALTER TABLE threads - ADD CONSTRAINT threads_source_message_belongs_to_parent_fk - FOREIGN KEY (source_message_id, parent_thread_id) - REFERENCES messages(id, thread_id) - ON DELETE RESTRICT - DEFERRABLE INITIALLY IMMEDIATE; -``` - -它在数据库层保证 source Message 属于声明的 Parent Thread。Parent/Child 同 Conversation 已由 threads 的复合 Parent 外键保证。 - -MVP 不提供独立 Message hard delete。归档 Conversation/Thread 不删除 Message;未来硬删除必须以显式应用事务处理整个 Conversation,不能依赖来源 Message 的级联删除。 - -#### message_runs - -```sql -CREATE TABLE message_runs ( - assistant_message_id uuid NOT NULL REFERENCES messages(id) ON DELETE RESTRICT, - attempt integer NOT NULL CHECK (attempt > 0), - status message_run_status NOT NULL, - - model_id text NOT NULL, - provider_id text, - event_sequence bigint NOT NULL DEFAULT 0 CHECK (event_sequence >= 0), - checkpoint_schema_version smallint NOT NULL DEFAULT 1, - checkpoint_parts jsonb NOT NULL DEFAULT '[]'::jsonb, - - usage jsonb, - error_code text, - billing_state text, - - queued_at timestamptz NOT NULL DEFAULT now(), - started_at timestamptz, - heartbeat_at timestamptz, - stop_requested_at timestamptz, - finished_at timestamptz, - - PRIMARY KEY (assistant_message_id, attempt), - CHECK (jsonb_typeof(checkpoint_parts) = 'array') -); - -CREATE UNIQUE INDEX message_runs_one_active_attempt_idx - ON message_runs(assistant_message_id) - WHERE status IN ('queued', 'running'); - -CREATE INDEX message_runs_latest_attempt_idx - ON message_runs(assistant_message_id, attempt DESC); - -CREATE INDEX message_runs_recovery_idx - ON message_runs(status, heartbeat_at) - WHERE status IN ('queued', 'running'); -``` - -“assistant_message_id 必须指向 role=assistant”是跨行约束,由应用命令在事务中验证并由集成测试覆盖;如后续需要数据库强制,可增加约束触发器,不在首版使用复杂触发器。 - -`usage` 与 `billing_state` 的精确 schema 由计费能力拥有;MessageRun 只保留稳定承载位置,不让计费字段进入 Message。 - -#### message_feedbacks(附属关系) - -```sql -CREATE TABLE message_feedbacks ( - user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT, - message_id uuid NOT NULL REFERENCES messages(id) ON DELETE RESTRICT, - value text NOT NULL CHECK (value IN ('positive', 'negative')), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (user_id, message_id) -); -``` - -Feedback 是 User 与 Message 的关系,不嵌入 Message JSON,也不复制进 BaseContext。 - -### D11. Project 扩展 Schema - -这些表属于稳定扩展方向,但不要求与 Chat MVP 核心表在同一实现批次完成。 - -#### project_instruction_versions - -```sql -CREATE TABLE project_instruction_versions ( - id uuid PRIMARY KEY, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, - version integer NOT NULL CHECK (version > 0), - content text NOT NULL, - is_active boolean NOT NULL DEFAULT false, - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (project_id, version) -); - -CREATE UNIQUE INDEX project_instruction_one_active_idx - ON project_instruction_versions(project_id) - WHERE is_active; -``` - -切换 active version 必须在事务内先后更新旧、新版本。MessageRun 后续可以记录实际使用的 instructionVersionId。 - -#### memory_items - -```sql -CREATE TABLE memory_items ( - id uuid PRIMARY KEY, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, - content jsonb NOT NULL, - source jsonb, - status record_status NOT NULL DEFAULT 'active', - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - CHECK (jsonb_typeof(content) = 'object') -); - -CREATE INDEX memory_items_project_status_idx - ON memory_items(project_id, status, updated_at DESC); -``` - -向量、切块、摘要与搜索索引是可重建派生数据,不是 MemoryItem 的唯一事实。 - -#### project_files / file_versions - -```sql -CREATE TABLE project_files ( - id uuid PRIMARY KEY, - project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT, - name text NOT NULL, - status record_status NOT NULL DEFAULT 'active', - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() -); - -CREATE TABLE file_versions ( - id uuid PRIMARY KEY, - file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE RESTRICT, - version integer NOT NULL CHECK (version > 0), - storage_key text NOT NULL, - content_type text NOT NULL, - size_bytes bigint NOT NULL CHECK (size_bytes >= 0), - content_hash text NOT NULL, - created_by uuid NOT NULL REFERENCES users(id), - created_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (file_id, version) -); -``` - -Message 中的临时 Artifact 可以继续存在于 parts。只有用户明确“保存到 Project Files”时才创建 ProjectFile/FileVersion,并用后续来源关系记录 originating Message;不会把每个工具输出自动提升为 Project 资产。 - -### D12. 数据库不变量与并发规则 - -数据库与应用层共同保证: - -1. Project 恰好属于一个 Workspace; -2. Conversation 恰好属于一个 Project; -3. Conversation 恰好有一个 Root Thread; -4. Thread 归属 Conversation 后不可移动; -5. 非根 Thread 的 parent/source/snapshots 必须同时存在; -6. Parent、Child 和 source Message 必须位于同一 Conversation; -7. Child 创建时只能指向已经存在的 Parent,因此 Fork 事务不会创建环;额外领域校验拒绝任何导入环; -8. Thread 内 `(threadId, sequence)` 唯一; -9. 普通发送在同一事务中分配连续 user/assistant sequence; -10. 只有最后一条 user 可以 Edit; -11. 只有最后一条 assistant 可以 Regenerate/Retry; -12. 同一个 assistant Message 最多一个非终态 MessageRun; -13. MessageRun 状态通过条件更新推进; -14. BaseContext 创建后不可通过公开仓储更新; -15. 客户端不能提交 owner、contentHash、baseContext、run status 或新实体 ID; -16. 归档不级联删除 Child Thread、Message 或 MessageRun; -17. 通用 revision 不用于发送、Fork、运行订阅或状态转换。 - -Message sequence 分配属于 Thread 写入事务。实现可以锁定 Thread 行,或使用数据库原子计数器/最大 sequence 保护;选择由持久化实现提案决定。无论采用哪一种,客户端不生成 sequence,也不依赖时间排序。 - -P0 使用前端 single-flight 防止重复点击,但网络结果不确定后的重复提交仍是已知限制。V2 再增加 Idempotency-Key 和命令结果重放,不在当前表中预建半成品幂等结构。 - -### D13. 应用命令与事务边界 - -应用层使用明确用例,不暴露通用 `updateEntity`: - -| 命令 | 主要事务结果 | -|---|---| -| createWorkspace | Workspace + Owner Membership | -| createProject | Project | -| createConversation | Conversation + Root Thread | -| sendMessage | user Message + assistant placeholder + MessageRun | -| editLastUserMessage | 更新最后 user/assistant + 新 MessageRun attempt | -| regenerateLastAssistantMessage | 重置最后 assistant + 新 MessageRun attempt | -| retryAssistantMessage | 验证 failed/stopped 后创建新 attempt | -| forkThread | Child Thread + BaseContext + ForkSourceSnapshot,可选初始消息/run | -| archiveConversation | 只更新 Conversation 生命周期 | -| archiveThread | 只更新目标非根 Thread 生命周期,不删除后代 | -| stopMessageRun | 条件推进最新 active attempt | -| submitMessageFeedback | upsert/delete 用户反馈关系 | - -命令不返回可写整棵 Conversation。成功响应返回受影响实体或读取 DTO;客户端不能把 Bootstrap 原样 PUT 回服务端。 - -模型执行必须在 user/assistant Message 与 queued MessageRun 事务提交后开始。执行器启动失败时,MessageRun 最终收敛为 failed;不能回滚已经被用户提交并可查询的输入意图。 - -### D14. 读取模型 - -#### ConversationBootstrap - -用于 ThreadChat URL 首次装载,至少包含: - -```text -schemaVersion -conversation -threads -每个 Thread 最近最多 200 条 Messages -每条 assistant Message 的最新 MessageRun 摘要 -当前用户 feedback -hasMoreOlder / olderCursor -``` - -它是只读 BFF DTO,不是数据库实体、备份或可写快照。P0 正常路径一次返回每 Thread 最近 200 条 Message,避免客户端一开始实现复杂的树内增量分页。 - -#### ConversationGraph - -只返回 Conversation、Thread parent/source 与标题摘要,用于 Canvas 或导航,不下载 Message parts、BaseContext 全文或 Artifact 内容。 - -#### ThreadTimeline - -按 sequence 返回本地 Message,并 join 最新 MessageRun 状态。BaseContext 不作为本地 Message 重复渲染;UI 可以通过 provenance 单独展示 Fork 来源。 - -#### ThreadPrompt - -只在服务端构造,使用 Project/System Context、BaseContext 与本地有效 Message。客户端提交的 messages 数组永远不是权威 Prompt 来源。 - -### D15. 目标服务端模块骨架 - -目标目录表达依赖边界,不要求本变更创建文件: - -```text -lib/thread-chat/ -├── domain/ -│ ├── ids.ts -│ ├── workspace.ts -│ ├── project.ts -│ ├── conversation.ts -│ ├── thread.ts -│ ├── message.ts -│ ├── value-objects/ -│ │ ├── message-content.ts -│ │ ├── base-context.ts -│ │ └── fork-source-snapshot.ts -│ └── policies/ -│ ├── thread-message-policy.ts -│ ├── fork-policy.ts -│ └── prompt-policy.ts -│ -├── application/ -│ ├── commands/ -│ │ ├── create-conversation.ts -│ │ ├── send-message.ts -│ │ ├── edit-last-user-message.ts -│ │ ├── regenerate-last-assistant-message.ts -│ │ ├── fork-thread.ts -│ │ └── stop-message-run.ts -│ ├── queries/ -│ │ ├── get-conversation-bootstrap.ts -│ │ ├── get-conversation-graph.ts -│ │ └── get-thread-timeline.ts -│ └── ports/ -│ ├── unit-of-work.ts -│ ├── repositories.ts -│ ├── authorization.ts -│ ├── id-generator.ts -│ ├── clock.ts -│ └── model-runtime.ts -│ -├── execution/ -│ ├── message-run.ts -│ ├── message-run-state-machine.ts -│ ├── message-run-service.ts -│ └── normalized-run-event.ts -│ -├── infrastructure/ -│ ├── db/ -│ │ ├── schema/ -│ │ ├── repositories/ -│ │ └── transaction.ts -│ ├── runtime/ -│ │ └── ai-sdk-v7-adapter.ts -│ └── authorization/ -│ -└── transport/ - ├── schemas/ - ├── mappers/ - └── errors.ts - -app/api/v1/ -└── ... HTTP route adapters only -``` - -依赖方向: - -```text -HTTP / future CLI / future MCP - ↓ -Application Commands & Queries - ↓ -Domain Policies + Execution Ports - ↓ -Infrastructure Adapters -``` - -约束: - -- domain 不导入 React、Next.js、HTTP、Drizzle、AI SDK 或数据库 row; -- application 不导入 Next.js Request/Response; -- infrastructure 实现 ports,不反向定义领域规则; -- route 只负责认证适配、schema 解析、调用用例和错误映射; -- Prompt Builder 只在服务端; -- CLI/MCP 未来复用 application,不直接访问表。 - -### D16. 后续拆分链 - -本设计通过后,后续按以下顺序建立独立 OpenSpec: - -1. **服务端领域与数据库骨架**:创建纯领域类型、值对象 codec、Drizzle schema、迁移、仓储和数据库约束测试。 -2. **MessageRun 生命周期**:实现后台执行、状态机、checkpoint、Stop、断线恢复、用量和计费挂点。 -3. **应用命令与 API v1**:实现授权、事务命令、查询 DTO、错误协议和路由;通用幂等仍不进入 P0。 -4. **服务端验收**:领域策略测试、数据库集成测试、API 契约测试、并发/断线/恢复测试。 -5. **前端领域与 Zustand**:设计 Entity Store、MessageRun Stream 状态、UI State、Selectors 和 Conversation Runtime。 -6. **Hooks 与 UI 接入**:Bootstrap、发送、Fork、Edit、Regenerate、Stop、反馈、Canvas 和列视图 Hooks。 -7. **整体链路与权威切换**:端到端验证后再处理旧数据、旧 API、旧整树权威和清理。 - -后续变更不得在没有显式修改本基础设计的情况下重新引入 Turn、Message Variant、独立 ThreadFork 或 Generation Chat 实体。 - -## Risks / Trade-offs - -- **[BaseContext 会重复历史内容并随 Fork 深度增加存储]** → MVP 接受以换取破坏式编辑下的语义独立;只保存精简 ContextMessage Projection。真实规模达到阈值后可以引入内容寻址或快照去重,不改变逻辑模型。 -- **[破坏式 Edit/Regenerate 永久丢失旧可见内容]** → 这是明确产品决策;需要保留时使用 Fork。MessageRun 只保留执行审计,不承诺恢复旧回答正文。 -- **[来源 Message 内容变化后 sourceMessageId 仍指向同一可变实体]** → ForkSourceSnapshot 的 content hash 和 quote 保留当时 provenance;状态读取为 outdated,BaseContext 不变。 -- **[JSONB parts/baseContext 缺少数据库内部结构约束]** → 使用共享版本化 codec 在写入和读取时验证,数据库保留 schemaVersion、JSON 类型和关系约束;对未知版本 fail closed。 -- **[Message 与 MessageRun 分离增加 join]** → 使用 latest-attempt 索引和 Bootstrap/Timeline 读取模型集中 join,避免把运行状态重复写入 Message。 -- **[不使用通用 revision 时多标签页可能发生最后写入者胜出]** → 追加顺序由事务和唯一 sequence 保护,运行状态由条件更新保护;低风险元数据 P0 接受 last-write-wins,需要覆盖保护时再局部增加 ETag。 -- **[P0 无幂等键时网络不确定可能重复发送]** → 前端 single-flight 降低重复点击;将可靠重放作为 V2 独立能力,不在当前模型中加入半套命令系统。 -- **[没有独立 ThreadFork 会限制未来多来源关系]** → 当前每个 Child 唯一 Parent,字段与 Thread 同生命周期;真正出现 Merge/多来源时再提升为关系实体并迁移,避免为未知需求预建。 -- **[Project 扩展表让范围看起来扩大]** → 本设计只固定所有权与 schema 方向;实现批次可以只落核心 Workspace/Project/Conversation/Thread/Message/MessageRun。 -- **[忽略现有实现会把迁移风险延后]** → 这是本阶段的有意边界;先批准正确目标,再用单独提案审计真实数据和切换路径,避免旧结构反向塑造新模型。 - -## Migration Plan - -本变更不执行现有数据迁移。它定义目标状态和后续落地顺序: - -1. 审查并批准本 proposal/design,确认简化模型为 Issue #34 唯一目标。 -2. 在独立变更中同步修改正式 domain spec 和所有依赖旧模型的活跃 OpenSpec。 -3. 建立全新服务端领域与数据库 schema,不在实现初期加入旧整树兼容逻辑。 -4. 通过数据库约束、领域策略、事务集成和 API 契约测试完成后端验收。 -5. 另行设计前端 Zustand Store、Selectors、Hooks 与 Conversation Runtime。 -6. 前后端新链路端到端通过后,再审计旧数据并决定重置、一次性导入或只读保留。 -7. 最终切换时只允许一个写入权威;禁止新旧模型双向同步或把规范实体反序列化回旧整树 JSON。 - -回滚、真实数据导入、运行中旧 generation 排空和旧表删除,必须基于部署环境数据另写 ADR,不在本基础设计中猜测。 - -## Open Questions - -目前没有会改变核心实体、关系或数据库主干的未决问题。以下实现选择有意推迟到对应后续提案: - -- Message sequence 使用 Thread 行锁、独立计数器还是等价数据库原子策略; -- MessageRun 事件使用 SSE、轮询还是 WebSocket,以及是否需要短期事件表; -- Project Instruction、Memory、File 的首个产品范围和 retrieval 策略; -- 真实旧数据采用重置、一次性迁移还是只读保留; -- V2 Idempotency-Key、离线重放和外部 API 生命周期。 diff --git a/openspec/changes/define-conversation-domain-model/proposal.md b/openspec/changes/define-conversation-domain-model/proposal.md deleted file mode 100644 index dd8aa4a9..00000000 --- a/openspec/changes/define-conversation-domain-model/proposal.md +++ /dev/null @@ -1,38 +0,0 @@ -## Why - -Issue #34 的目标模型经过多轮讨论后已经收敛:当前 MVP 不需要同时维护 Thread 分支、Message 变体、Turn 选择和 Generation 领域实体四套关系。继续沿用此前的 `ThreadFork + Turn + Message Variant + Generation` 方案会把重新生成、编辑、提示词构造、客户端状态和数据库约束都建立在不必要的复杂度上,也会偏离“每一列就是一条严格线性 Thread”的产品心智模型。 - -在设计 API、前端 Zustand Store、Hooks 和迁移实现之前,需要先用一份独立于现有代码的 OpenSpec 固化最终领域语言、实体关系、数据库目标结构和模块边界。后续服务端实现、集成测试、API 测试与前端接入都必须以这份模型为唯一基础。 - -## What Changes - -- **BREAKING**:将 Chat Domain 收敛为 `Project → Conversation → Thread → Message[]`;Thread 内 Message 永远严格线性,不再引入 Turn、Message DAG、Message Variant 或 `activeGenerationId`。 -- **BREAKING**:Edit 和 Regenerate 改为破坏式操作。只有当前 Thread 最后一条 user Message 可以 Edit,只有最后一条 assistant Message 可以 Regenerate;需要保留另一条历史时必须创建新 Thread。 -- **BREAKING**:不再把 ThreadFork 建模为独立实体。Fork 是服务端领域命令,唯一入向来源直接保存在 Child Thread 的 `parentThreadId`、`sourceMessageId`、`forkSourceSnapshot` 与 `baseContext` 中。 -- 引入不可变 `baseContext` 值对象:服务端在 Fork 事务中冻结 Parent Thread 截至来源 Message 的有效历史,保证 Child Thread 不受 Parent 后续 Edit、Regenerate、追加、归档或来源失效影响。 -- 引入 `forkSourceSnapshot` 值对象,独立记录选区、上下文片段和来源内容哈希;来源变化只产生 `outdated/unavailable` 读取状态,不删除或改写 Child Thread。 -- 将模型执行生命周期收敛到持久化运行记录 `MessageRun`。它属于执行与计费子域,不是 Chat 内容实体;浏览器断开只取消订阅,不终止后台执行。 -- 明确 Workspace 是成员与授权边界,Project 是 Conversation、项目指令、记忆和文件的长期资产边界;Project 资产能力只定义关系与扩展位置,不在本变更实现完整产品功能。 -- 明确 Message 内容使用项目版本化协议并与 AI SDK v7 `UIMessage.parts` 兼容,同时禁止 AI SDK、React、Next.js、HTTP 或 Drizzle 类型渗入纯领域层。 -- 定义目标 PostgreSQL schema、复合约束、索引、事务边界、读取投影和服务端模块骨架,但不编写迁移、仓储、API、前端 Store、Hooks 或 UI 代码。 -- 将后续落地拆为:服务端领域与持久化、应用命令与 API、服务端集成/API 验收、前端 Zustand/Selectors/Hooks、UI 接入与遗留权威退出。 -- 幂等命令、离线重放、Message 版本保留、跨 Thread 收敛与多来源合并不进入 MVP;其中通用幂等机制明确留待 V2。 - -## Capabilities - -### New Capabilities - -- 无。本变更继续以既有 `domain` 能力作为唯一领域规范入口,避免产生第二套相互竞争的术语规范。 - -### Modified Capabilities - -- `domain`:以 Workspace、Project、Conversation、线性 Thread、Message、MessageRun、BaseContext 和 ForkSourceSnapshot 的简化模型,替换此前的 ThreadFork、Turn、Message Variant 与 Generation 领域模型,并明确 Project 扩展资产、运行子域和模块依赖边界。 - -## Impact - -- OpenSpec:本变更重定义 `define-conversation-domain-model` 的目标模型;仍依赖 Turn、Generation、独立 ThreadFork、revision 或幂等命令的后续活跃变更,在继续实现前必须基于本设计重新审查。 -- 后端目标模块:Workspace/Project、Conversation/Thread/Message 领域契约,Fork/Edit/Regenerate/Prompt 规则,MessageRun 执行端口,PostgreSQL schema 与仓储事务。 -- 服务端后续验收:数据库约束测试、领域策略测试、事务集成测试、API 契约测试、后台运行与断线恢复测试。 -- 前端后续工作:Conversation Bootstrap、规范化 Entity Store、MessageRun Stream 状态、Zustand Store、Selectors、Hooks 与现有 ThreadChat UI 接入;本变更不提前规定实现细节。 -- 现有实现:当前 `ThreadTreeState`、整树 JSON、branch generation 协调和客户端生成实体 ID 均不作为目标设计依据;数据迁移和切换策略由后续独立变更处理。 -- 外部扩展:稳定的 Workspace、Project、Conversation、Thread 和 Message ID 为未来分享、CLI、MCP 与公开 Token API 保留寻址能力,但本变更不实现这些入口。 diff --git a/openspec/changes/define-conversation-domain-model/specs/domain/spec.md b/openspec/changes/define-conversation-domain-model/specs/domain/spec.md deleted file mode 100644 index 8234f64c..00000000 --- a/openspec/changes/define-conversation-domain-model/specs/domain/spec.md +++ /dev/null @@ -1,162 +0,0 @@ -## MODIFIED Requirements - -### Requirement: 使用统一的核心术语 - -系统、OpenSpec、接口契约和项目文档 SHALL 使用以下术语: - -- **Workspace**:成员、租户与授权的最外层边界;它可以拥有多个 Project,不得与前端列布局或画布状态混称。 -- **Project**:长期目标、规则、记忆、文件与 Conversation 的上下文和资产边界。 -- **Conversation**:Project 中一整个可分叉对话的聚合,拥有稳定 ID、唯一根 Thread、标题与生命周期;它取代旧领域术语 Thread Tree。 -- **Thread**:Conversation 中一条可独立继续的对话上下文,也是界面中的一栏;所有根与非根对话列使用同一种 Thread 实体。 -- **Main Thread(主 Thread)**:Conversation 的 `rootThreadId` 指向的 Thread,是关系推导角色,不是独立实体类型。 -- **Branch Thread(分支 Thread)**:具有入向 ThreadFork 的非根 Thread,是相对于上游 Thread 的关系角色,不是 Branch 实体或另一套 Thread 类型。 -- **Fork**:从一个 Thread 的确定 Message 创建另一个 Thread 的服务端动作。 -- **ThreadFork**:记录上游 Thread、来源 Message 与下游 Thread 的唯一来源关系。 -- **Turn**:Thread 中一个逻辑交互位置,用于约束用户编辑、助手重新生成和回复变体的归属范围。 -- **Message**:属于一个 Thread 和 Turn、具有稳定 ID 的用户、助手或明确类型的上下文内容。 -- **Generation**:在一个 Thread/Turn 中生成或更新助手输出的一次模型执行尝试;它不是 Message 或 Thread。 -- **File**:Project 内具有独立生命周期并可跨 Conversation 复用的持久资产。 -- **MemoryItem**:Project 内具有来源、状态与版本的记忆条目,不是无边界增长的文本字段。 -- **ProjectInstruction**:Project 级、可版本化的指令;Generation 可以记录实际使用的版本。 -- **Artifact**:由 Message 或 Generation 产生的结构化成果;当其需要独立复用和生命周期时,SHALL 通过 Project 资产及来源记录表达,而不是只存在于消息内部。 -- **Title**:用于识别 Project、Conversation 或 Thread 的人类可读标签。 -- **UI Workspace(界面工作区)**:列、折叠、画布视口、临时选择和打开面板等客户端展示状态;它不是 Workspace 领域实体,也不是 Conversation 事实源。 - -`Thread Tree`、`MainThread` 和 `ForkedThread` 只允许出现在遗留迁移说明或实现适配器中,不得继续作为新产品能力、接口或领域实体的规范名称。 - -#### Scenario: 描述非根对话列 - -- **WHEN** 产品、接口或代码需要描述由 Fork 创建的非根对话列 -- **THEN** 将其描述为 Thread,并在需要强调关系时称为分支 Thread,同时使用 ThreadFork 描述来源关系 - -#### Scenario: 描述完整分叉对话 - -- **WHEN** 产品或接口需要表示一个根 Thread 及其全部派生 Thread 的整体 -- **THEN** 使用 Conversation,而不是 Thread Tree 或 Thread 表示该聚合 - -### Requirement: 明确标题的归属与优先级 - -系统 SHALL 明确 Title 归属 Project、Conversation 或 Thread。Conversation 的自动 Title SHALL 由其根 Thread 的内容派生并作为默认导航标题;根 Thread 列头 SHALL 使用 Conversation Title,避免另存一份同义标题。非根 Thread 可以拥有描述对应列的本地 Title。用户为 Conversation 设置的自定义 Title SHALL 在 Conversation 级导航和根 Thread 列头展示中优先于自动 Title,同时系统 SHALL 保留自动 Title 作为机器派生信息。 - -#### Scenario: Conversation 自动标题与用户重命名并存 - -- **WHEN** Conversation 已由根 Thread 生成自动 Title,且用户随后为该 Conversation 设置自定义 Title -- **THEN** Conversation 导航和根 Thread 列头展示用户自定义 Title,并保留自动 Title - -#### Scenario: 分支 Thread 使用独立标题 - -- **WHEN** 非根 Thread 生成或设置自己的 Title -- **THEN** 该 Title 只描述并标识该 Thread,不覆盖所属 Conversation 的 Title - -## ADDED Requirements - -### Requirement: 维护 Project、Conversation 与 Thread 的包含不变量 - -每个 Conversation SHALL 恰好属于一个 Project,并恰有一个属于自身的根 Thread。每个 Thread SHALL 恰好属于一个 Conversation;Thread 的归属创建后不得改变。根与非根角色 SHALL 由 Conversation 根引用及 ThreadFork 关系推导,系统不得使用持久化 `kind`、`root` 布尔值或具有业务含义的魔法 ID 作为第二事实源。 - -#### Scenario: 创建 Conversation - -- **WHEN** 系统在 Project 中创建 Conversation -- **THEN** 系统为 Conversation 和根 Thread 分配稳定且无业务含义的 ID,并使 `rootThreadId` 指向属于该 Conversation 的 Thread - -#### Scenario: 拒绝跨 Conversation 的 Thread 归属 - -- **WHEN** 一个操作试图把既有 Thread 重新归属到另一个 Conversation -- **THEN** 系统拒绝该操作且不改变两个 Conversation - -### Requirement: 使用 ThreadFork 维护唯一 Fork 来源 - -每个非根 Thread SHALL 恰有一个入向 ThreadFork;根 Thread SHALL 没有入向 ThreadFork。ThreadFork SHALL 指向同一 Conversation 中的上游 Thread、属于上游 Thread 的确定来源 Message,以及唯一的下游 Thread。Fork 来源不得同时以可写 `children`、Message 反向链接或下游来源字段重复保存;这些集合和计数只能是可重建投影。 - -#### Scenario: 从确定消息创建嵌套 Fork - -- **WHEN** 用户从任意 Thread 的确定 Message 创建新 Thread -- **THEN** 系统原子创建下游 Thread 与唯一 ThreadFork,并记录该 Message 所属 Thread 为上游 Thread - -#### Scenario: 拒绝来源消息错配 - -- **WHEN** ThreadFork 的来源 Message 不属于声明的上游 Thread,或任一参与实体不属于同一 Conversation -- **THEN** 系统拒绝该关系且不创建孤立的下游 Thread - -#### Scenario: 派生 Thread 数量来自投影 - -- **WHEN** 界面显示某条 Message 的直接派生 Thread 数量 -- **THEN** 系统从 ThreadFork 查询或派生该数量,而不是读取独立可写的 Message fork 列表 - -### Requirement: 用 Turn 约束 Message 变体 - -每个 Turn SHALL 恰好属于一个 Thread,每个 Message SHALL 恰好属于一个 Thread 和一个 Turn。用户编辑与助手重新生成产生的新 Message 变体 SHALL 留在目标 Thread/Turn 内,不得改变来源 Thread、祖先 Thread 或后代 Thread 的 Message。实现可以保留同一 Turn 内的 Message 因果边,但不得使用 Message 父子边表达 ThreadFork。 - -#### Scenario: 在分支 Thread 中重新生成 - -- **WHEN** 用户对分支 Thread 的助手 Message 执行重新生成 -- **THEN** 系统只在该 Message 所属 Thread/Turn 中追加助手回复变体,其他 Thread 的变体与派生数量保持不变 - -#### Scenario: 拒绝跨 Thread 变体 - -- **WHEN** 操作试图把另一个 Thread 的 Message 加入当前 Turn 的回复变体 -- **THEN** 系统拒绝该操作且不改变当前有效变体 - -### Requirement: 保证 Generation 的执行身份与归属 - -每个 Generation SHALL 具有稳定的执行尝试 ID,并恰好属于一个 Thread 和 Turn。其输入、输出及重新生成来源 Message SHALL 属于相同 Thread,且输入输出关系必须与该 Turn 一致。Generation ID SHALL 只承担执行幂等、状态、停止、恢复、用量与结算身份,不得替代 Thread、Turn 或 Message 的实体身份。 - -#### Scenario: Generation 完成到目标 Message - -- **WHEN** 一个 Generation 成功、停止或失败并产生可持久化结果 -- **THEN** 系统只更新该 Generation 绑定的 Thread/Turn/Message,并保留确定的执行尝试、状态和用量归属 - -#### Scenario: 拒绝跨 Thread Generation - -- **WHEN** Generation 声明的输入或输出 Message 不属于其 Thread -- **THEN** 系统在模型调用或结算前拒绝该 Generation - -### Requirement: 持久化影响后续上下文的当前有效变体 - -Thread/Turn 的当前有效变体会决定后续提示词、Fork 来源与继续生成路径,系统 SHALL 将其作为有版本的领域选择持久化,并将切换限制在同一 Thread/Turn。列可见性、折叠、画布视口、临时选择与面板状态 SHALL 保持在界面工作区,且不得改变实体归属或当前有效变体。 - -#### Scenario: 切换变体后继续对话 - -- **WHEN** 用户在一个 Turn 内切换当前有效助手回复变体并继续发送消息 -- **THEN** 后续 Generation 使用持久化后的有效路径,刷新后仍保持相同选择 - -#### Scenario: UI 布局不改变领域选择 - -- **WHEN** 用户折叠列、切换画布或关闭面板 -- **THEN** Conversation、Thread、Turn、Message 归属和当前有效变体均保持不变 - -### Requirement: 维护 Project 级上下文与资产边界 - -Conversation SHALL 通过 Project 获得可复用上下文和资产边界。ProjectInstruction、MemoryItem 与 File 在存在时 SHALL 归属 Project 并具有独立身份;Message 和 Generation SHALL 通过明确引用或来源记录使用它们,不得把其唯一事实仅嵌入某棵 Conversation JSON。一个 Thread 中产生的 File 在被保存为 Project 资产后 SHALL 可由同一 Project 的其他 Conversation 引用,而不需要复制文件实体。 - -#### Scenario: 跨 Conversation 使用 Project File - -- **WHEN** 一个 Thread 生成的文件被保存为 Project File,随后同一 Project 的另一个 Conversation 引用该文件 -- **THEN** 两个 Conversation 引用同一个稳定 File,并保留原始 Message/Generation 来源记录 - -#### Scenario: Generation 记录 Project Instruction 版本 - -- **WHEN** Project Instruction 在两次 Generation 之间发生修改 -- **THEN** 每次 Generation 都能识别自己实际使用的指令版本,旧回答不会被解释为使用了新版本 - -### Requirement: 分离领域事实、应用命令、传输、持久化与界面状态 - -领域事实 SHALL 独立于 React 组件、HTTP 路由、数据库对象关系映射与客户端布局存在。所有改变 Conversation、Thread、ThreadFork、Turn、Message、Generation 或当前有效变体的操作 SHALL 通过有明确输入、权限、幂等和冲突语义的应用命令执行。服务端可以返回组装后的只读快照,但客户端不得提交可覆盖整个 Conversation 事实状态的整树 JSON。 - -#### Scenario: 客户端执行 Fork - -- **WHEN** 客户端请求从确定 Message 创建 Thread -- **THEN** 服务端命令校验归属并原子提交规范实体增量,客户端只合并受影响实体 - -#### Scenario: 拒绝整 Conversation 覆盖 - -- **WHEN** 客户端提交包含全部 Thread、Message 与 Fork 的可写整树快照以覆盖服务端事实 -- **THEN** 系统拒绝或不提供该权威写入路径,并要求使用实体级命令 - -## REMOVED Requirements - -### Requirement: 维护线程树的层级不变量 - -**Reason**:该 Requirement 把完整对话命名为 Thread Tree,并以 MainThread/ForkedThread 表述两类节点,无法表达 Project → Conversation → Thread 聚合边界,也继续暴露当前整树实现形状。 - -**Migration**:使用新增的“维护 Project、Conversation 与 Thread 的包含不变量”和“使用 ThreadFork 维护唯一 Fork 来源”Requirements。现有 `ThreadTreeState`、MainThread 与 ForkedThread 引用迁移到 Conversation、Thread 角色及 ThreadFork;遗留名称仅可保留在明确标记的迁移适配器中。 diff --git a/openspec/changes/define-conversation-domain-model/tasks.md b/openspec/changes/define-conversation-domain-model/tasks.md deleted file mode 100644 index 9f6c3d76..00000000 --- a/openspec/changes/define-conversation-domain-model/tasks.md +++ /dev/null @@ -1,37 +0,0 @@ -## 1. 固化规范术语与依赖 - -- [ ] 1.1 将 `domain` 能力规范归档为 Project、Conversation、Thread、ThreadFork、Turn、Message、Generation、File、MemoryItem 与 ProjectInstruction 的唯一正式术语来源,并核对每个角色、身份和包含关系与增量规范一致。 -- [ ] 1.2 新增一份面向开发者的“遗留模型 → 规范模型”迁移表,覆盖 `ThreadTreeState`、`threads.main`、`parentId/children`、`forkFromMsgId/Message.forks`、`activeLeafMessageId`、Artifact 和 `branch_generations` 引用,明确哪些字段只允许存在于迁移适配器。 -- [ ] 1.3 审计 README、CLAUDE、活跃 OpenSpec 和 `lib/thread-chat` 注释中的领域术语;把新设计文档改用 Conversation、Thread、ThreadFork,并对必须保留的历史 `Thread Tree` 引用加上遗留标识,不改写已归档变更的历史事实。 -- [ ] 1.4 在 `persist-thread-chat-generations`、`add-thread-chat-message-actions`、`add-branch-tree-persistence` 及其他仍依赖整树权威模型的活跃变更中记录其与本变更及后续迁移变更的依赖或被替代范围,禁止继续声称整树 JSON 是最终架构。 - -## 2. 建立纯领域契约 - -- [ ] 2.1 在 `lib/thread-chat/domain/` 下建立不依赖 React、Next.js、HTTP 或 Drizzle 的规范 ID 与实体契约,定义 Project、Conversation、Thread、ThreadFork、Turn、Message 和 Generation 的稳定身份与归属字段。 -- [ ] 2.2 实现主 Thread 与分支 Thread 的角色推导,以及 Conversation/ThreadFork 拓扑校验;拒绝魔法 `main` 身份、重复入向 Fork、跨 Conversation Fork、来源 Message 与上游 Thread 错配和 Fork 环。 -- [ ] 2.3 实现 Turn/Message/Generation 归属校验,拒绝跨 Thread 回复变体、跨 Turn 输出和 Generation 输入输出错配,同时允许 Message 的内容与结构化片段保持为可版本化值对象。 -- [ ] 2.4 建立当前有效变体的领域选择契约和版本冲突结果,确保变体只能在同一 Thread/Turn 切换,并与可见列、折叠列、画布视口等界面工作区类型隔离。 -- [ ] 2.5 明确 Conversation/Thread Title 解析契约:Conversation 独占聚合导航标题,根 Thread 列头使用 Conversation Title,非根 Thread 可以使用本地 Title,避免重复持久化同义根标题。 - -## 3. 隔离遗留模型 - -- [ ] 3.1 将当前 `ThreadTreeState`、`Fork`、`threads.main` 和整树解析器明确移动或标记为遗留边界;新的规范领域模块不得通过类型别名把旧整树对象伪装成 Conversation。 -- [ ] 3.2 新增单向、只读的“遗留树 → 规范 Conversation 快照”映射,集中处理根 Thread、Fork、Turn 变体和 Artifact 来源推导;禁止提供“规范模型 → 遗留模型”的权威写回路径。 -- [ ] 3.3 为最小根 Conversation、主线 → A → B 嵌套 Fork、B 重新生成回复变体、非法跨 Thread Generation 和重复 Fork 来源建立确定性固定样例及验证脚本,证明规范投影不会把 B 的回复变体投影到 A。 -- [ ] 3.4 为旧字段建立允许列表或等价审计检查,使新增领域层和应用层代码中的 `ThreadTreeState`、`threads.main`、可写 `children` 与 `Message.forks` 引用能够在代码审查和持续集成中被发现。 - -## 4. 准备小步迁移链 - -- [ ] 4.1 为 `normalize-conversation-persistence` 建立后续变更输入清单:逻辑表、复合外键、不变量、真实数据审计、重置或迁移决策、规范快照和回滚边界。 -- [ ] 4.2 为 `migrate-generation-lifecycle` 建立后续变更输入清单:真实 Thread/Turn/Message 外键、启动、停止、心跳、过期恢复、部分结果、用量不可得、计费幂等以及 PR #30 剩余语义缺口。 -- [ ] 4.3 为 `add-conversation-command-api` 建立后续变更输入清单:Fork、创建 Turn、重新生成、选择变体、归档、停止等命令,以及所有者校验、幂等、预期版本、错误码、实体增量与事务事件箱。 -- [ ] 4.4 为 `normalize-conversation-client-state` 建立后续变更输入清单:规范实体、可重建索引、按 ID 定位的选择器、界面工作区、乐观命令和列、画布、Message 操作迁移。 -- [ ] 4.5 为 `retire-thread-tree-authority` 建立后续变更输入清单:数据审计、运行中 Generation 排空、禁用整树 PUT、删除协调合并与防抖保存、删除重复 Fork 事实和遗留适配器的完成条件。 -- [ ] 4.6 将 Project Memory、File、Instruction、公开接口、令牌、命令行、MCP 和 Issue #39 Conversation 收敛列为基础迁移后的独立提案,明确它们不阻塞 #34 当前链路。 - -## 5. 验证与交接 - -- [ ] 5.1 运行 `pnpm typecheck`,修复规范领域契约、遗留适配器和固定样例的全部类型错误。 -- [ ] 5.2 运行 `pnpm openspec:validate`,确保领域增量规范、依赖说明和所有活跃变更严格验证通过。 -- [ ] 5.3 检查 Git 差异和术语审计结果,确认未修改无关功能、未把界面工作区混入领域实体、未产生第二个 Fork 事实源,也未把 Issue #39 的收敛机制带入本变更。 -- [ ] 5.4 在 Issue #34 回填本变更路径、最终架构决策记录和后续变更依赖链,确保实现者可以从本提案继续而不重新猜测核心数据模型。 diff --git a/openspec/changes/migrate-generation-lifecycle/.openspec.yaml b/openspec/changes/migrate-generation-lifecycle/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/migrate-generation-lifecycle/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/migrate-generation-lifecycle/design.md b/openspec/changes/migrate-generation-lifecycle/design.md deleted file mode 100644 index 85ef6295..00000000 --- a/openspec/changes/migrate-generation-lifecycle/design.md +++ /dev/null @@ -1,157 +0,0 @@ -## Context - -规范 Conversation 持久化提供了真实 Thread、Turn 与 Message,但现有 Generation 仍通过 `branch_generations` 协调整树 JSON、浏览器保存和流式响应。该结构把“执行是否结束”“内容是否可用”“usage 是否完整”“账单是否已结算”混成一组隐式分支,导致 Stop、崩溃恢复和并发终结无法用数据库约束证明正确。 - -本设计位于规范持久化与命令 API 之间:先建立可独立验证的应用服务、仓储和执行端口,不在本阶段暴露新的公开路由或迁移客户端。 - -## Goals / Non-Goals - -**目标:** - -- 让 Generation 通过真实外键绑定 Thread、Turn 和 Message。 -- 让服务端在浏览器断线后继续执行并留下可恢复 checkpoint。 -- 精确定义 Stop、失败、superseded、僵尸任务收敛和 exactly-once 终结。 -- 分离 Generation 状态、Message 内容状态、usage 完整度与计费状态。 -- 修复 PR #30 审查中已发现的部分 usage、partial 覆盖、联网活动和 Stop 状态漂移。 - -**非目标:** - -- 设计公共 HTTP 命令、错误码或客户端 Store。 -- 正式切换生产流量或删除 `branch_generations`。 -- 引入跨 Conversation Agent 工作流、任务队列产品或 Project 资产模型。 -- 把每个流片段、token 或工具事件永久保存为独立关系表。 - -## Decisions - -### D1. Generation 是执行实体,不是 Message 状态字段 - -新增 `conversation_generations`,核心字段为: - -```text -id -owner_id / workspace_id / project_id -conversation_id / thread_id / turn_id -input_message_id / output_message_id -intent / attempt / idempotency_key -status / content_state -checkpoint_version / checkpoint -usage / usage_completeness / billing_status -lease_owner / heartbeat_at / stop_requested_at -started_at / finished_at / created_at / updated_at -``` - -所有关联使用规范表的复合外键,确保同一 Conversation/Thread/Turn。一个 Message 可以保留多个历史 Generation 尝试,但重新生成默认创建新的输出 Message 变体,不覆盖旧身份。 - -备选方案是继续把 Generation 嵌入 Message 或整树 JSON;这会使执行租约、重试历史和计费幂等无法独立约束,因此拒绝。 - -### D2. 四组状态正交建模 - -```text -Generation.status: - running | stop_requested | completed | stopped | failed | superseded - -Message.contentState: - pending | streaming | complete | incomplete | failed - -usageCompleteness: - complete | partial | unavailable - -billingStatus: - pending | settled | usage_unavailable | not_billable -``` - -`completed` 通常对应 `complete`,但 `stopped`/`failed` 可以对应 `incomplete`;`settled` 只允许搭配 `complete` usage。数据库 CHECK 和应用状态机同时执行这些组合约束。 - -备选方案是从 Generation 终态推导其余状态;这正是当前无法表达“失败但有可用部分内容”和“有部分 usage 但不可完整结算”的根因,因此拒绝。 - -### D3. 开始事务先于任何付费调用 - -应用服务 `startGeneration` 在单个事务中: - -1. 验证所有者、实体归属、意图和相关 Conversation/Thread/Turn 的预期 revision; -2. 追加或确认输出 Message; -3. 创建带幂等键、`running` 状态和初始 checkpoint 的 Generation; -4. 提交后才把执行描述交给模型运行器。 - -相同幂等键和规范化请求摘要返回原 Generation;键相同但摘要不同返回幂等冲突。事务失败没有付费副作用。 - -### D4. 服务端执行所有权与 HTTP 响应解耦 - -执行器独立消费完整模型流。HTTP/SSE 只订阅或转发进度,不把 `request.signal` 作为模型执行的终止信号。进程内 AbortController 注册表只提供低延迟 Stop;持久化 `stop_requested`、心跳和租约支持跨实例观察与恢复。 - -沿用当前经过验证的短周期 Stop 观察和心跳思路,但具体间隔作为运行配置与测试参数,不写死进领域契约。浏览器轮询频率属于后续客户端 change。 - -### D5. checkpoint 是服务端可恢复事实 - -执行器按节流策略把以下规范投影保存到 JSON checkpoint: - -```text -schemaVersion -body -artifactIds -researchPlan -researchActivities[{ id, kind, status, sources, error }] -contentState -knownUsage -``` - -每次更新使用 `checkpoint_version` 比较交换。浏览器收到的流片段可以领先最后一次 checkpoint,但浏览器整树 PUT 不再是恢复权威。工具活动只有在获得输出或错误时才进入 `complete`/`error`;输入阶段保持 `running`。 - -备选方案是保存每个 token 事件;成本和写放大远高于恢复收益,因此使用有版本快照 checkpoint,并允许终结时强制刷新。 - -### D6. Stop 和僵尸任务共用同一终结器 - -Stop 首先将 `running` 条件更新为 `stop_requested`;注册表通知只是加速。执行器观察取消后把最后 checkpoint 交给统一 `finalizeGeneration`。 - -僵尸收敛器按 `status + heartbeat_at + lease` 领取失联任务,并直接读取最新 Generation checkpoint 与输出 Message,不再使用开始时的空 `turnSnapshot` 重建。终结条件更新包含当前版本,健康执行器若已推进或完成,收敛写入自然失败。 - -### D7. usage 完整度决定能否结算 - -终结输入同时携带 `knownUsage` 与 `usageCompleteness`。规则为: - -```text -complete -> 可幂等写 usage_records,billing = settled -partial -> 保留已知值,billing = usage_unavailable -unavailable -> 不伪造零值,billing = usage_unavailable -无付费调用 -> billing = not_billable -``` - -`usage_records.app_generation_id` 保持唯一。若业务需要对 partial usage 先扣已知部分,必须另立计费 ADR;本 change 不把不完整账单称为已结算。 - -### D8. 终结是唯一原子提交边界 - -`finalizeGeneration` 在一个事务中: - -- 以 Generation 状态、租约和版本做比较交换; -- 刷新最终 checkpoint 与 Message 内容状态; -- 仅在仍为当前尝试时更新 Turn 的有效助手 Message; -- 写入或确认唯一 usage 记录与计费状态; -- 记录终态、错误分类和完成时间。 - -较旧尝试晚到时进入 `superseded` 或发现已有终态后幂等返回,不得覆盖当前变体。 - -### D9. 新生命周期默认关闭 - -本阶段通过显式服务端开关或隔离应用组合根启用,只允许集成测试和后续命令 API 使用。遗留路由继续走旧权威路径;禁止把同一次 Generation 同时写入旧 `branch_generations` 和新表。 - -## Risks / Trade-offs - -- **[风险] checkpoint 节流意味着进程硬崩溃时可能丢失最后少量 token。** → 终结前强制刷新,并让节流周期可配置;契约保证不丢已确认 checkpoint,而非承诺逐 token durability。 -- **[风险] 状态维度增加实现复杂度。** → 用类型、数据库 CHECK、集中状态机和表驱动测试限制合法组合,换取语义真实性。 -- **[风险] 服务端继续执行可能占用无消费者资源。** → 使用租约、心跳、超时和持久化 Stop;资源策略不依赖浏览器连接。 -- **[风险] 新旧 Generation 并存期间难以诊断。** → 运行模式互斥、日志带 authority/version 标签,不做隐式双写。 -- **[风险] partial usage 暂不结算可能产生待对账余额。** → 明确 `usage_unavailable` 并保留已知 usage,后续计费策略可以安全重放。 - -## Migration Plan - -1. 增加规范 Generation 表、枚举/约束、复合外键和 usage 唯一关联。 -2. 实现开始、checkpoint、Stop、心跳、stale claim 与统一终结应用服务。 -3. 用模拟执行器验证断线、并发 Stop、崩溃、late result 和 usage 组合。 -4. 在隔离模式将规范生命周期接到测试 Conversation,不承载现有生产路由。 -5. 由 `add-conversation-command-api` 暴露命令,由最终切换 change 决定正式启用和旧表退场。 - -回滚时关闭规范生命周期并删除未承载生产权威的新代码/表;不得在同一运行模式回退为双写。 - -## Open Questions - -无。本 change 不决定 checkpoint 的最终节流数值,也不决定 partial usage 的未来补扣策略;两者分别是运行参数与独立计费决策,不阻塞领域契约。 diff --git a/openspec/changes/migrate-generation-lifecycle/proposal.md b/openspec/changes/migrate-generation-lifecycle/proposal.md deleted file mode 100644 index 3e22ea26..00000000 --- a/openspec/changes/migrate-generation-lifecycle/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -## Why - -`normalize-conversation-persistence` 将 Thread、Turn 与 Message 变成可由数据库约束的真实实体,但当前 `branch_generations` 仍把 Generation 作为整树 JSON 的辅助记录运行。Stop、断线续跑、僵尸任务收敛、部分内容、联网活动和计费完整度因此分散在多条路径中,已经出现“部分 usage 被当作完整结算”“失败恢复覆盖已保存内容”等语义错误。必须先把 Generation 收回规范领域模型,后续命令 API 和客户端迁移才能依赖一套可证明的生命周期。 - -## What Changes - -- 新增以真实 Conversation、Thread、Turn、输入 Message 与输出 Message 外键为身份的规范 Generation 记录,明确请求意图、尝试次数、当前变体和幂等键。 -- 定义 `running → stop_requested → stopped`、`running → completed/failed` 与 `superseded` 的合法转换;Generation 必须在任何付费模型调用前以事务方式开始。 -- 将流式执行改为服务端拥有,浏览器断线不得取消后台消费;正文、Artifact、研究活动和 usage 通过有版本的服务端 checkpoint 持久化。 -- 把 Message 内容状态与 Generation 终态分离:有部分输出的 Stop/失败可保存为 `incomplete`,不得统一伪装成 `error` 或清空已有 checkpoint。 -- 明确 usage 完整度与计费状态是两个正交维度;只知道部分 step usage 时必须保留已知值并标记 `usage_unavailable`,不得结算为完整账单。 -- 保留现有服务端 AbortController 注册表、数据库 Stop 观察、心跳和幂等扣费中正确的机制,但以规范实体和单一终结事务重新实现。 -- 本变更不开放新的公共 Conversation HTTP 命令,也不切换生产客户端;规范生命周期默认关闭,供下一步 `add-conversation-command-api` 接入。 - -## Capabilities - -### New Capabilities - -- `conversation-generation-lifecycle`:定义规范 Generation 的身份、状态机、服务端执行、部分 checkpoint、Stop/恢复与计费真实性。 - -### Modified Capabilities - -- 无。遗留 `persist-thread-chat-generations` 的最终替换和归档由切换 change 处理,本变更先建立新的独立契约。 - -## Impact - -- 数据库:新增规范 Generation 表、checkpoint/usage 字段、状态约束、索引及 `usage_records` 的幂等关联。 -- 服务端:聊天流生命周期、Generation 仓储、Stop 注册表、心跳、僵尸任务收敛、结果投影和终结事务。 -- 客户端语义:后续可按 Generation 和 Message 内容状态恢复,但本变更不迁移现有 Store 或路由。 -- 测试:增加状态转换、部分输出、联网活动、断线续跑、Stop、stale recovery、usage 完整度与 exactly-once 计费覆盖。 -- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`。 -- 后续依赖:`add-conversation-command-api`、`normalize-conversation-client-state`、`retire-thread-tree-authority`。 diff --git a/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md b/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md deleted file mode 100644 index fa1ecc82..00000000 --- a/openspec/changes/migrate-generation-lifecycle/specs/conversation-generation-lifecycle/spec.md +++ /dev/null @@ -1,146 +0,0 @@ -## Purpose - -让每次 AI Generation 都绑定规范 Thread/Turn/Message,并以可恢复、可停止、可审计和可正确计费的服务端生命周期保存完整或部分结果。 - -## ADDED Requirements - -### Requirement: Generation 使用规范实体身份 - -系统 SHALL 为每次 AI 尝试创建稳定 Generation ID,并使其同时引用同一 Conversation 中的 Thread、Turn、输入 Message 和输出 Message。Generation SHALL 保存请求意图、尝试序号和幂等键;跨 Conversation、跨 Thread、跨 Turn 或角色不匹配的引用 MUST 被拒绝。 - -#### Scenario: 为重新生成创建同 Turn 新变体 - -- **WHEN** 用户对既有 Turn 请求重新生成助手回答 -- **THEN** 系统创建新的助手 Message 与 Generation,二者属于原 Turn,且不会复用或覆盖旧助手 Message 的身份 - -#### Scenario: 拒绝跨 Thread 输出 Message - -- **WHEN** Generation 尝试把另一条 Thread 的 Message 设为输出 -- **THEN** 系统拒绝开始该 Generation,且不调用付费模型 - -### Requirement: 在付费执行前幂等开始 Generation - -系统 SHALL 在任何付费模型调用前,通过单个事务验证相关聚合或实体的预期 revision、追加或确认目标 Message、创建 Generation 并提交幂等键。重复提交相同幂等键和等价载荷 SHALL 返回同一 Generation;相同键但不同载荷 MUST 被拒绝。 - -#### Scenario: 开始成功后调用模型 - -- **WHEN** 开始事务提交成功 -- **THEN** 执行器可以调用模型,且查询方已能读取状态为 `running` 的 Generation - -#### Scenario: 开始事务失败 - -- **WHEN** 归属、版本或约束验证导致开始事务回滚 -- **THEN** 系统不调用模型、不产生扣费,也不留下孤立 Message 或 Generation - -#### Scenario: 重放相同开始命令 - -- **WHEN** 客户端因超时重放相同幂等键和载荷 -- **THEN** 系统返回已有 Generation,不创建第二次模型执行 - -### Requirement: Generation 由服务端持续执行 - -Generation 开始后 SHALL 由服务端拥有流消费、心跳和终结责任。浏览器断线、页面卸载或单个响应流关闭不得自动中止模型执行;只有持久化 Stop 请求、执行错误、租约收敛或正常完成可以改变执行结果。 - -#### Scenario: 浏览器在流式响应中断线 - -- **WHEN** 响应消费者断开但服务端执行仍健康 -- **THEN** 服务端继续消费模型流、更新心跳和 checkpoint,直至合法终态 - -#### Scenario: 新页面查询进行中任务 - -- **WHEN** 用户刷新后查询仍为 `running` 或 `stop_requested` 的 Generation -- **THEN** 系统返回最新服务端 checkpoint 和状态,而不是要求原浏览器恢复执行所有权 - -### Requirement: 持久化有版本的部分 checkpoint - -系统 SHALL 以单调版本保存正文、Artifact 引用、研究计划、联网活动、来源和内容状态的服务端 checkpoint。checkpoint 更新 MUST 只接受更新版本,且不得把已持久化的非空内容替换为较旧或缺失的投影。 - -#### Scenario: 保存流式部分结果 - -- **WHEN** 执行器获得新的正文片段、Artifact 或研究活动状态 -- **THEN** 系统以更高 checkpoint 版本保存规范结构,并使后续查询可恢复相同部分结果 - -#### Scenario: 拒绝旧 checkpoint 回写 - -- **WHEN** 延迟任务提交低于当前版本的 checkpoint -- **THEN** 系统忽略或拒绝该写入,保留较新的 Message 内容 - -#### Scenario: 保留未完成联网活动 - -- **WHEN** Stop 或失败发生时某项搜索或深读仍处于输入/执行阶段 -- **THEN** checkpoint 保留该活动为 `running` 或明确中止状态,不得无条件投影为 `complete` - -### Requirement: 分离 Generation 终态与 Message 内容状态 - -Generation SHALL 使用 `completed`、`stopped`、`failed` 或 `superseded` 表达执行终态;Message SHALL 独立使用 `complete`、`incomplete` 或 `failed` 表达内容可用性。有可恢复正文、Artifact 或研究结果的 Stop/失败 MUST 保留内容并标记 `incomplete`,不得仅因 Generation 未完成而清空或统一标记 Message 为失败。 - -#### Scenario: Stop 后保留部分回答 - -- **WHEN** Generation 在已有部分输出后进入 `stopped` -- **THEN** 输出 Message 保留最后 checkpoint、标记为 `incomplete`,Generation 保持 `stopped` - -#### Scenario: 无输出执行失败 - -- **WHEN** Generation 在没有任何可恢复输出时失败 -- **THEN** Generation 标记为 `failed`,输出 Message 可以标记为 `failed` 并保存稳定错误分类 - -### Requirement: Stop 使用持久化请求和幂等状态转换 - -Stop SHALL 先以事务方式记录 `stop_requested`,再通过进程内控制器加速取消;数据库状态是跨实例的持久化事实源。重复 Stop SHALL 幂等返回当前状态;已终结 Generation 不得被改写为另一终态。 - -#### Scenario: 停止运行中的 Generation - -- **WHEN** 所有者对 `running` Generation 发出 Stop -- **THEN** 系统持久化 `stop_requested`,执行实例观察后停止消费,并以最后 checkpoint 终结为 `stopped` - -#### Scenario: 重复停止已终结 Generation - -- **WHEN** 所有者再次停止 `completed`、`stopped`、`failed` 或 `superseded` Generation -- **THEN** 系统返回其现有终态且不修改结果、checkpoint 或计费记录 - -### Requirement: 僵尸任务按最后 checkpoint 收敛 - -系统 SHALL 依据租约和心跳识别失联的非终态 Generation,并以最后一个服务端 checkpoint 收敛为失败或停止结果。收敛过程 MUST 使用版本/租约条件写入,不得覆盖已由健康执行器提交的更新,也不得从开始时的空快照重建并覆盖部分内容。 - -#### Scenario: 服务进程在部分输出后崩溃 - -- **WHEN** Generation 心跳超时且已有持久化 checkpoint -- **THEN** 收敛器保留该 checkpoint,将 Generation 终结为 `failed`,并将 Message 标记为 `incomplete` - -#### Scenario: 健康执行器抢先完成 - -- **WHEN** 收敛器准备写入时 Generation 已以更新版本进入终态 -- **THEN** 条件更新失败,收敛器不得覆盖最终结果 - -### Requirement: usage 完整度与计费状态保持真实 - -系统 SHALL 分别保存已知 usage 值、`complete`/`partial`/`unavailable` 完整度和 `pending`/`settled`/`usage_unavailable`/`not_billable` 计费状态。只有完整 usage 才能进入 `settled`;已知部分 step usage 与未知中止 step 同时存在时,系统 MUST 保留已知值并标记 `partial` 和 `usage_unavailable`。 - -#### Scenario: 正常完成且 usage 完整 - -- **WHEN** 所有计费 step 都返回 usage 且 Generation 正常终结 -- **THEN** 系统以 `complete` 完整度幂等写入唯一 usage 记录,并将计费状态设为 `settled` - -#### Scenario: Stop 时只有部分 usage - -- **WHEN** 已完成 step 有 usage,但当前被中止 step 无法取得 usage -- **THEN** 系统保存已知 usage、标记完整度为 `partial`,并将计费状态设为 `usage_unavailable` - -#### Scenario: 重复终结不重复扣费 - -- **WHEN** 响应消费、后台收敛或重试并发终结同一 Generation -- **THEN** Generation ID 的唯一约束只允许一次 usage 入账和一次终态提交 - -### Requirement: 终结事务原子提交规范结果 - -Generation 终结 SHALL 在单个事务中条件更新 Generation 终态、输出 Message 的最终 checkpoint/内容状态、Turn 当前有效助手变体和计费记录。仅当前且尚未终结的 Generation 可以推进当前变体;已被更新尝试取代的执行 SHALL 终结为 `superseded`,不得覆盖新的 Message 选择。 - -#### Scenario: 当前 Generation 正常完成 - -- **WHEN** 当前 Generation 以匹配版本提交完整结果 -- **THEN** 一个事务保存终态、完整 Message、Turn 当前助手变体和唯一计费记录 - -#### Scenario: 旧执行晚到 - -- **WHEN** 较早 Generation 在同一 Turn 的新尝试成为当前后才返回 -- **THEN** 旧执行被标记或保持为 `superseded`,且不得覆盖新变体或重复扣费 diff --git a/openspec/changes/migrate-generation-lifecycle/tasks.md b/openspec/changes/migrate-generation-lifecycle/tasks.md deleted file mode 100644 index 570d5b4b..00000000 --- a/openspec/changes/migrate-generation-lifecycle/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -## 1. Generation 数据结构 - -- [ ] 1.1 定义 `conversation_generations` 的身份、规范实体复合外键、意图、尝试、幂等键、状态、租约、checkpoint、usage 与时间字段。 -- [ ] 1.2 增加合法状态组合、唯一幂等键、当前尝试和 `usage_records.app_generation_id` 的约束与索引。 -- [ ] 1.3 生成数据库迁移,并用集成用例验证跨 Conversation/Thread/Turn/Message 引用和非法状态组合会被拒绝。 - -## 2. 开始与执行所有权 - -- [ ] 2.1 建立纯应用层 Generation 仓储和执行器端口,不依赖 HTTP、React 或遗留整树类型。 -- [ ] 2.2 实现 `startGeneration` 事务,覆盖预期版本、输出 Message 追加、幂等重放、载荷冲突和提交前禁止模型调用。 -- [ ] 2.3 实现与 HTTP 连接解耦的服务端流消费、AbortController 注册、持久化 Stop 观察、租约和心跳。 -- [ ] 2.4 增加浏览器断线后继续执行以及重新查询可见进行中 Generation 的集成验证。 - -## 3. Checkpoint 与结构化部分结果 - -- [ ] 3.1 定义带 `schemaVersion` 的 checkpoint 解析器,覆盖正文、Artifact、研究计划、联网活动、来源、内容状态和已知 usage。 -- [ ] 3.2 实现按版本比较交换的节流 checkpoint 写入与终结前强制刷新,拒绝旧版本覆盖。 -- [ ] 3.3 修正联网活动投影,使输入/执行阶段保持 `running`,仅输出或错误后进入相应终态。 -- [ ] 3.4 覆盖正文、Artifact、研究活动混合 partial 的保存、刷新恢复和旧 checkpoint 竞态测试。 - -## 4. Stop、失败与僵尸任务收敛 - -- [ ] 4.1 实现幂等 Stop 应用操作:先持久化 `stop_requested`,再通知本地执行器,终态请求只返回既有结果。 -- [ ] 4.2 实现统一 `finalizeGeneration` 事务,原子提交 Generation、Message 内容状态、Turn 当前变体和计费结果。 -- [ ] 4.3 实现基于租约/心跳和条件更新的 stale claim,始终从最新服务端 checkpoint 收敛,不再用开始快照覆盖 partial。 -- [ ] 4.4 覆盖有输出 Stop、无输出失败、进程崩溃、健康执行器抢先完成、旧尝试晚到和重复终结测试。 - -## 5. Usage 与计费真实性 - -- [ ] 5.1 实现 `complete`、`partial`、`unavailable` usage 完整度的聚合和持久化。 -- [ ] 5.2 修改结算规则:仅完整 usage 可 `settled`;partial/unavailable 必须 `usage_unavailable`,无付费调用为 `not_billable`。 -- [ ] 5.3 覆盖“已完成 step 有 usage、当前中止 step 无 usage”的回归用例,并验证保留已知值但不伪装完整结算。 -- [ ] 5.4 覆盖并发响应消费、Stop 和 stale 收敛只产生一次 usage 记录及一次扣费的事务测试。 - -## 6. 隔离接入与验证 - -- [ ] 6.1 增加默认关闭的规范 Generation 组合根,明确拒绝同一次执行双写新旧 Generation 权威。 -- [ ] 6.2 在隔离 Conversation 路径跑通开始、流式 checkpoint、查询、Stop、完成、失败和恢复端到端测试。 -- [ ] 6.3 运行数据库迁移、相关测试、`pnpm typecheck` 与 `pnpm openspec:validate` 并记录结果。 diff --git a/openspec/changes/normalize-conversation-client-state/.openspec.yaml b/openspec/changes/normalize-conversation-client-state/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/normalize-conversation-client-state/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/normalize-conversation-client-state/design.md b/openspec/changes/normalize-conversation-client-state/design.md deleted file mode 100644 index 380ff579..00000000 --- a/openspec/changes/normalize-conversation-client-state/design.md +++ /dev/null @@ -1,182 +0,0 @@ -## Context - -当前外部 Store 保持一份对象身份稳定、原地修改的 `ThreadTreeState`,每次 mutation 递增全局 version 并通知全部订阅者。Fork 会同时写下游 `parentId`、上游 `children`、来源 Message `forks`、depth 和 footnote;发送和重新生成写 Message 图及 `activeLeafMessageId`。网络层再对整树做 debounce PUT、卸载 flush 和启动 reconcile。 - -新的服务端 API 已把这些关系收敛为规范实体和原子命令。客户端必须同步改变状态形状;否则它仍会在本地维护第二套领域模型。 - -## Goals / Non-Goals - -**目标:** - -- 用 ID map 和服务端版本保存规范实体,避免嵌套副本和全树 mutation。 -- 把派生拓扑、UI Workspace、pending command 和规范实体分层。 -- 用统一网关与 Generation 协调器处理命令、delta、订阅和冲突。 -- 逐组件迁移且保持现有聊天、分叉、画布和 Message actions 行为。 - -**非目标:** - -- 重做视觉设计、Markdown 渲染、编辑器或 React Flow。 -- 在客户端实现离线优先、多用户协同 CRDT 或跨 Conversation 收敛。 -- 让浏览器成为 Generation durability 或关系修复的权威。 -- 在本 change 删除服务端旧表/旧路由;最终 cutover change 负责删除。 - -## Decisions - -### D1. Store 分为四个正交区域 - -```text -canonical - conversationsById - threadsById - threadForksById - turnsById - messagesById - generationsById - revisions/checkpointVersions - -derived - threadIdsByConversation - incomingForkByThread - outgoingForkIdsByThread - turnIdsByThread - messageIdsByTurn - activePath / canvas edges / counts - -uiWorkspace - visibleThreadIds / foldedThreadIds / selectedThreadId - column sizes/policy / canvas viewport - panels / drafts / local hints - -commands - pendingByCommandId / optimisticOverlays / errors -``` - -`canonical` 只接受服务端数据。`derived` 可以按需 memoize 并全部重建。`uiWorkspace` 可以按 Conversation ID 存 localStorage,但不能进入 API 写载荷。`commands` 只描述尚未确认的交互。 - -备选方案是继续保留嵌套树并增加 adapter;这会使 adapter 本身成为第二关系权威,因此拒绝。 - -### D2. 实体更新使用对象替换和细粒度订阅 - -保留零框架依赖的外部 Store 与 React `useSyncExternalStore` 边界,但不再用单一全树 version 触发所有组件。Store 为 `entityType:id`、Conversation 索引和 UI slice 维护订阅 key;更新实体时替换该对象并只通知受影响 key。 - -Selector 返回只读视图或稳定 memoized 结果。开发模式冻结规范实体,以尽早发现组件原地修改。 - -备选方案是立即引入另一套状态库;当前问题是领域形状而不是库能力,引入依赖不会自动消除重复事实,因此不作为前置条件。 - -### D3. 快照安装是事务式 Store commit - -boot 流程先在临时结构中: - -1. 解析传输 schemaVersion; -2. 按 ID 去重并验证复合归属; -3. 验证根 Thread、ThreadFork、Turn/Message 与有效变体引用; -4. 构建基础索引; -5. 一次 commit 替换目标 Conversation 的旧实体集合。 - -失败时保留上一次可用 Store 或展示明确加载错误,不创建空树并静默覆盖远端数据。 - -### D4. delta 合并使用每实体 revision - -命令响应和查询都转换为统一 `CanonicalDelta`: - -```text -schemaVersion -upsert[type][id] -remove[type][id] = tombstoneRevision -revisions -invalidate[] -``` - -Store 逐实体比较 revision;Generation/Message 流 checkpoint 额外比较 checkpointVersion。重复和旧 delta 幂等跳过;检测到未知 schema 或不可解释版本间隙时,把 Conversation 标为 stale 并重取快照。 - -### D5. client gateway 拥有命令协议 - -组件调用类型化意图,例如 `forkThread(input)`、`sendTurn(input)`,gateway 负责: - -- 读取最小作用域 revision; -- 创建/复用 Idempotency-Key; -- 发请求、解析安全错误和 delta; -- 在组件卸载后仍把已提交结果送入共享 Store; -- 对网络不确定结果用同一键重查/重试; -- 对 409 合并最新 revision 或重取,不盲目生成新键重放。 - -旧 `chat-controller.persistNow()` 屏障不进入新 gateway。 - -### D6. 乐观 UI 使用 overlay,不创建规范实体 - -composer 可立即清空/显示 pending bubble,Fork 可显示 pending column shell,但 overlay 使用 `commandId` 和临时 presentation key,不能被 selector 当作真实 Thread/Message,也不能成为下一条领域命令的目标。 - -成功 delta 用服务端稳定 ID 替换 overlay;确定失败回滚并恢复草稿;网络不确定则保持“确认中”,用原幂等键查询。这样既保留响应感,也不在浏览器预演数据库关系。 - -### D7. GenerationCoordinator 按 ID 引用计数 - -每个 loaded Conversation 有一个协调器注册表: - -```text -generationId -> { - subscribers - latestCheckpointVersion - stream/poll handle - visibility mode -} -``` - -同一 Generation 的多个组件共享网络监控。可见运行任务保持较快轮询(初始保持现有约 2 秒行为),页面隐藏时降频(初始约 10 秒);终态停止轮询。SSE 与查询统一转换为 delta,乱序由 checkpointVersion 拒绝。最后订阅释放网络资源,但不会调用 Stop;重新挂载先查询恢复。 - -运行频率是可测配置,不进入领域模型。 - -### D8. 由 ThreadFork 和 Turn 生成列/画布读取模型 - -选择器集中实现: - -- 根 Thread 来自 `Conversation.rootThreadId`; -- parent/children 来自 ThreadFork; -- 深度和画布边通过无环遍历派生; -- Thread 的继承上下文截至 `sourceMessageId`,本地内容来自其 Turns; -- 当前 Message 来自 Turn active variant; -- Fork 数量由 outgoing ThreadFork 计数。 - -组件不再读取 `threads.main`、`parentId`、`children`、Message `forks` 或 `activeLeafMessageId`。 - -### D9. UI Workspace 只保存展示身份 - -现有列放置、替换/细条策略、画布焦点和 panel 状态可以保留算法,但输入改为 Thread ID 与派生 selector。localStorage payload 带 UI schemaVersion 和 Conversation ID;读取时过滤已归档/不存在 Thread。它不能恢复或修改规范关系。 - -### D10. 按竖向能力切片迁移组件 - -迁移顺序为: - -1. 新 Store、snapshot/delta 和 selector 测试; -2. boot/list/title 和 UI Workspace; -3. 只读列、Message、Artifact、画布; -4. Generation 展示与 coordinator; -5. composer/send/stop/regenerate/select variant; -6. selection Fork、反馈和其余 Message actions。 - -每个切片在隔离规范路由下可端到端运行。避免先写一个巨型兼容 adapter 让所有旧组件继续变异整树。 - -### D11. 客户端路径按 Conversation 互斥 - -路由/feature flag 为一次页面会话选择 legacy 或 canonical boot。canonical 路径不会实例化旧 Tree Store、persistence debounce 或 reconcile;legacy 路径不调用新命令。禁止同一 Conversation 页面同时维护两个 Store 并互相同步。 - -## Risks / Trade-offs - -- **[风险] 大量组件依赖旧嵌套类型。** → 按竖向能力切片迁移,先建立 selector facade,但 facade 只读且基于规范实体。 -- **[风险] 细粒度订阅和派生缓存可能产生失效错误。** → 所有索引声明依赖 key,增加重建等价测试和开发模式全量校验。 -- **[风险] overlay 与规范实体视觉切换可能闪烁。** → 以 commandId 关联响应并保持展示 key,规范身份仍只来自服务端。 -- **[风险] 组件卸载后请求回调造成泄漏。** → gateway 属于 Store/application scope,不捕获组件 setState;组件只维护可取消订阅。 -- **[风险] feature flag 并存增加测试矩阵。** → 并存只持续到最终 cutover,模式严格互斥并分别标记 authority。 - -## Migration Plan - -1. 实现 normalized Store、只读 selector、快照安装和 delta 合并。 -2. 实现 client gateway、pending overlay、冲突恢复和 GenerationCoordinator。 -3. 按只读展示 → Generation → 写命令顺序迁移组件。 -4. 在隔离规范路由跑现有行为矩阵和渲染/订阅性能验证。 -5. 把 canonical 客户端置于默认关闭的 Conversation 级开关后,交由最终 change 执行全量 cutover。 - -最终生产 cutover 前,回滚可以关闭试验 canonical 页面并切回 legacy 模式;一旦服务端 canonical authority 接受生产写入,只能回滚到仍使用规范 API/数据的兼容客户端版本或进入只读,任何客户端都不得把本地状态序列化回 `branch_trees.state`。 - -## Open Questions - -无。是否未来引入通用状态库、SSE 还是 WebSocket、以及 UI Workspace 是否跨设备同步,均可在不改变规范实体边界的情况下独立决策。 diff --git a/openspec/changes/normalize-conversation-client-state/proposal.md b/openspec/changes/normalize-conversation-client-state/proposal.md deleted file mode 100644 index 846c2495..00000000 --- a/openspec/changes/normalize-conversation-client-state/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -## Why - -服务端命令 API 已能返回规范快照和实体增量,但当前前端仍以可变 `ThreadTreeState` 为中心,组件会直接改 `children`、`activeLeafMessageId` 等关系,再依赖整树存盘和刷新协调。若只替换请求 URL,旧的重复事实和竞态仍会保留在浏览器;必须把客户端改为按稳定 ID 规范化实体,并把领域状态与列/画布视图状态彻底分开。 - -## What Changes - -- 新增按 ID 保存 Conversation、Thread、ThreadFork、Turn、Message 与 Generation 的规范化客户端 Store;实体关系只接受服务端快照或命令 delta。 -- 派生 Thread children、深度、Fork 数量、有效消息路径和列投影;这些索引可丢弃重建,不作为可写事实。 -- 将 visible columns、折叠、选中 Thread、画布 viewport、面板、草稿和 pending command 放入独立 UI Workspace slice。 -- 首次加载从 `ConversationSnapshot` 原子归一化;后续命令通过统一 client gateway 发送幂等键/revision,并合并规范 delta 或处理冲突重取。 -- 流式 Generation 由单一协调器订阅/轮询,按 checkpoint version 更新 Message/Generation;组件挂载只订阅所需 ID,卸载时释放订阅。 -- 乐观体验使用临时展示 overlay 和可回滚 pending command,不允许组件生成权威 Fork/Message 关系或触发整树 PUT。 -- 迁移列、画布、composer、Message actions、标题、反馈和恢复展示到 ID-scoped selector;保留现有 Markdown、Artifact 和流式视觉能力。 -- **BREAKING**:规范客户端路径不再调用 `persistNow`、save debounce、整树 CAS、启动 reconcile 或 `activeLeafMessageId` 写入;正式删除由最终切换 change 完成。 - -## Capabilities - -### New Capabilities - -- `conversation-client-state`:定义规范化实体 Store、派生索引、界面状态、命令 delta、Generation 订阅和组件边界。 - -### Modified Capabilities - -- 无。现有界面能力保持产品行为,旧客户端协议的退场由 `retire-thread-tree-authority` 处理。 - -## Impact - -- 前端核心:`app/thread-chat/core/store.ts`、types/selectors、boot、chat controller、persistence 与 generation reconciliation。 -- 组件:列、画布、composer、selection/fork、Message actions、标题、帮助/反馈和 Artifact 展示。 -- 网络:新增 Conversation client gateway、snapshot/delta parser、命令状态和 Generation polling/subscription coordinator。 -- 测试:Store reducer/selectors、delta/revision、并发冲突、订阅释放、刷新恢复和 UI 回归。 -- 前置依赖:`define-conversation-domain-model`、`add-conversation-command-api`,并间接依赖规范持久化与 Generation 生命周期。 -- 后续依赖:`retire-thread-tree-authority`。 diff --git a/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md b/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md deleted file mode 100644 index f7843839..00000000 --- a/openspec/changes/normalize-conversation-client-state/specs/conversation-client-state/spec.md +++ /dev/null @@ -1,165 +0,0 @@ -## Purpose - -让浏览器按稳定实体 ID 消费 Conversation 快照、命令增量和 Generation checkpoint,同时把领域事实、派生索引、界面工作区与乐观状态分开。 - -## ADDED Requirements - -### Requirement: 按稳定 ID 规范化客户端实体 - -客户端 SHALL 分别按 ID 保存 Conversation、Thread、ThreadFork、Turn、Message 与 Generation,并保存服务端 revision/checkpoint version。一个规范实体在同一加载作用域内 MUST 只有一份客户端对象事实;组件不得保存可独立修改的嵌套实体副本。 - -#### Scenario: 两列引用同一来源 Message - -- **WHEN** 上游 Thread 和下游 Fork 投影都展示同一个来源 Message -- **THEN** 二者通过相同 Message ID 读取同一实体,且任何服务端更新只需合并一次 - -#### Scenario: Generation 更新输出 Message - -- **WHEN** 新 checkpoint 更新某条输出 Message -- **THEN** Store 按 Message ID 和版本替换该实体,所有订阅该 Message 的视图看到同一结果 - -### Requirement: 原子归一化 ConversationSnapshot - -客户端 SHALL 验证 `ConversationSnapshot` 的 schemaVersion、实体归属、引用和 revision 后,在一个 Store 提交中替换目标 Conversation 的规范实体集合。解析或不变量验证失败 MUST 保留现有可用状态并显示可恢复加载错误,不得部分安装快照。 - -#### Scenario: 首次加载有效快照 - -- **WHEN** 接口返回受支持 schemaVersion 的有效 ConversationSnapshot -- **THEN** 客户端原子安装实体、revision 和可重建索引,组件不会观察到半条 Fork 或缺失 Message 的中间状态 - -#### Scenario: 快照包含悬空 ThreadFork - -- **WHEN** 下游 Thread 或来源 Message 无法在快照中解析 -- **THEN** 客户端拒绝整个新快照并触发诊断/重试,而不是自行补造关系 - -### Requirement: 按版本合并规范实体增量 - -客户端 SHALL 只通过服务器命令响应、查询或 Generation checkpoint 合并规范实体 delta。每个 upsert/remove MUST 比较对应 revision 或 checkpoint version;较旧或重复 delta SHALL 幂等忽略,无法安全解释的版本间隙 SHALL 触发作用域重取。 - -#### Scenario: 命令响应晚于更新查询 - -- **WHEN** 一个旧命令响应在更新的实体查询之后到达 -- **THEN** Store 保留较新 revision,不被旧 delta 覆盖 - -#### Scenario: 收到未知 delta schemaVersion - -- **WHEN** 客户端收到不支持的 delta schemaVersion -- **THEN** 客户端不应用该 delta,标记目标 Conversation 需要重新加载 - -### Requirement: Fork 和路径索引必须可重建 - -Thread children、父来源、深度、Fork 数量、Turn 顺序、当前有效 Message 路径和画布边 SHALL 从规范实体派生或缓存为可丢弃索引。客户端不得通过直接修改这些索引来表达 Fork、发送或变体选择。 - -#### Scenario: 合并新 ThreadFork - -- **WHEN** Store 合并服务端返回的新 Thread 与 ThreadFork -- **THEN** children、深度、脚注/Fork 数量和画布边从关系重新派生,不要求响应同时提交多个反向字段 - -#### Scenario: 清空派生缓存 - -- **WHEN** 派生索引缓存被清除 -- **THEN** 客户端能从规范实体重建等价列和画布关系,不向服务端写入任何修复 - -### Requirement: 界面工作区独立于领域事实 - -visible Thread 顺序、折叠、当前选中 Thread、列宽/策略、画布 viewport、打开面板、临时草稿和本地提示 SHALL 保存在独立 UI Workspace slice。改变这些字段不得改变 Conversation revision、ThreadFork、Turn 当前变体或触发领域命令。 - -#### Scenario: 折叠分支列 - -- **WHEN** 用户折叠或关闭一列 -- **THEN** 客户端只更新 UI Workspace,Thread 与 ThreadFork 仍在规范实体 Store 中 - -#### Scenario: 归档当前可见 Thread - -- **WHEN** 归档命令成功并使选中 Thread 不再活跃 -- **THEN** Store 合并服务端生命周期 delta,UI Workspace 选择一个有效可见 Thread,而不删除历史关系 - -### Requirement: 通过统一客户端网关执行命令 - -组件 SHALL 通过统一 Conversation client gateway 执行命令,由网关附加幂等键、预期 revision、取消策略并解析规范响应。组件不得直接调用旧整树 PUT、手动推进 revision 或分别提交 Thread/Fork/Message 关系。 - -#### Scenario: 组件请求 Fork - -- **WHEN** selection UI 请求从 Message 创建 Thread -- **THEN** 网关发送一个 `forkThread` 请求,并只在服务端成功后把规范 delta 合并进 Store - -#### Scenario: 组件卸载时命令已提交 - -- **WHEN** 发起命令的组件卸载但服务端可能已提交请求 -- **THEN** 网关保留幂等身份并允许结果进入共享 Store,组件卸载不触发补偿性整树覆盖 - -### Requirement: 乐观状态不得成为规范关系 - -客户端 MAY 为交互即时性保存草稿、pending command 和临时展示 overlay,但这些状态 SHALL 与规范实体分离并可按命令 ID 回滚。服务端未确认前,客户端不得把临时 ThreadFork、Message 身份或 active variant 当作规范事实用于后续命令。 - -#### Scenario: 乐观发送后版本冲突 - -- **WHEN** composer 显示待发送 overlay,但服务端返回 `version_conflict` -- **THEN** 客户端移除或标记失败 overlay、保留用户草稿、更新 revision,并允许用户明确重试 - -#### Scenario: 幂等重试成功 - -- **WHEN** 网络失败后网关以相同幂等键重试并收到原成功结果 -- **THEN** 客户端用规范 Message/Generation ID 替换 pending overlay,不产生重复可见 Turn - -### Requirement: 统一协调 Generation 订阅与轮询 - -客户端 SHALL 按 Generation ID 维护引用计数的监控协调器,合并流式事件和查询结果,并只应用更高 checkpoint version。视图挂载 SHALL 订阅需要的 Generation,卸载 SHALL 释放订阅;无浏览器订阅不得停止服务端 Generation。 - -#### Scenario: 两个视图观察同一 Generation - -- **WHEN** 列视图和状态面板同时订阅一个运行中 Generation -- **THEN** 客户端只维持一个网络监控实例,并把更新分发给两个订阅者 - -#### Scenario: 最后一个视图卸载 - -- **WHEN** 最后一个订阅者卸载 -- **THEN** 客户端释放对应定时器/事件监听;服务端继续执行,后续重新挂载通过查询恢复最新 checkpoint - -#### Scenario: 轮询和流事件乱序 - -- **WHEN** 旧轮询结果晚于新流式 checkpoint 到达 -- **THEN** 协调器忽略旧版本,不回退 Message 内容或 Generation 状态 - -### Requirement: 组件使用按 ID 定位的选择器 - -列、画布、Message、composer、标题、Artifact、反馈和 Message actions SHALL 通过 ID-scoped selector 读取最小所需实体及派生结果。无关实体更新不得要求所有列重新渲染;selector 不得返回可由组件原地修改的规范对象。 - -#### Scenario: 一个分支流式更新 - -- **WHEN** 某条分支 Thread 的 Message 收到 checkpoint -- **THEN** 订阅该 Message/Thread 的视图更新,未引用它的独立列无需因全局树版本变化重新渲染 - -#### Scenario: Message action 使用稳定身份 - -- **WHEN** 用户复制、反馈、重新生成或 Fork 某条 Message -- **THEN** 操作使用规范 Message/Turn/Thread ID 和 client gateway,不从数组位置或局部树键推断身份 - -### Requirement: 保持当前可见产品行为 - -迁移后的客户端 SHALL 保持现有列/画布导航、嵌套 Fork、Markdown、Artifact、研究活动、流式进度、停止、恢复、标题和 Message actions 的用户可见能力。行为等价不要求保留旧 `ThreadTreeState`、魔法 `main` ID 或整树持久化实现。 - -#### Scenario: 刷新恢复 stopped partial - -- **WHEN** 用户停止带正文、Artifact 或研究活动的 Generation 后刷新 -- **THEN** 客户端从规范快照/checkpoint 恢复相同 `incomplete` 内容和 `stopped` 状态 - -#### Scenario: 打开嵌套 Fork - -- **WHEN** 用户从 A 打开 B,再从 B 打开 C -- **THEN** 列和画布通过 ThreadFork 投影显示正确层级、来源锚点和上下文,不依赖可写 children 数组 - -### Requirement: 规范客户端路径不保存整树或启动协调合并 - -启用规范客户端路径时,系统 MUST 禁用 `persistNow`、save debounce、卸载 flush、整树 CAS、`activeLeafMessageId` 写入以及为修复浏览器 partial 而运行的启动 reconcile。刷新恢复 SHALL 只依赖规范快照、命令结果和服务端 Generation checkpoint。 - -#### Scenario: 流式 token 高频到达 - -- **WHEN** Generation 连续产生流式更新 -- **THEN** 客户端更新展示和规范 checkpoint 版本,但不防抖 PUT 整个 Conversation - -#### Scenario: 页面卸载 - -- **WHEN** 用户在 Generation 运行中关闭页面 -- **THEN** 客户端不 flush 整树,服务端执行和 checkpoint 机制继续承担持久化责任 - diff --git a/openspec/changes/normalize-conversation-client-state/tasks.md b/openspec/changes/normalize-conversation-client-state/tasks.md deleted file mode 100644 index be12fea3..00000000 --- a/openspec/changes/normalize-conversation-client-state/tasks.md +++ /dev/null @@ -1,48 +0,0 @@ -## 1. 规范化 Store 基础 - -- [ ] 1.1 定义只读 Conversation/Thread/ThreadFork/Turn/Message/Generation ID maps、revision 和 checkpoint version 状态。 -- [ ] 1.2 实现 ConversationSnapshot 临时解析、归属/引用校验和单次原子安装,失败时不部分覆盖旧状态。 -- [ ] 1.3 实现带 tombstone、schemaVersion 和每实体版本比较的 CanonicalDelta 合并器。 -- [ ] 1.4 增加开发模式实体冻结、重复/旧 delta 幂等、版本间隙重取和未知 schema 拒绝测试。 - -## 2. 派生索引与细粒度订阅 - -- [ ] 2.1 实现 Conversation → Threads、ThreadFork parent/children、Thread → Turns、Turn → Messages 的可重建索引。 -- [ ] 2.2 实现根 Thread、嵌套深度、Fork 数量、继承上下文、当前有效路径和画布边 selector。 -- [ ] 2.3 将 Store 通知拆为实体/索引/UI key,并提供按 ID 定位的 React hooks 与稳定 memoized selector。 -- [ ] 2.4 验证清空派生缓存可重建等价结果,以及一个 Message 流式更新不会通知无关 Thread 订阅。 - -## 3. UI Workspace 与乐观状态 - -- [ ] 3.1 将 visible/folded/selected Thread、列策略、画布 viewport、面板、草稿和本地提示迁入独立 UI Workspace slice。 -- [ ] 3.2 为 UI localStorage payload 增加 schemaVersion/Conversation ID,并在加载时过滤归档或缺失 Thread。 -- [ ] 3.3 实现按 commandId 管理的 pending overlay、草稿恢复、确认中、成功替换和失败回滚。 -- [ ] 3.4 验证折叠/画布/面板操作不改变 canonical revision 或发出领域命令。 - -## 4. Client gateway 与 GenerationCoordinator - -- [ ] 4.1 实现统一 client gateway,附加 Idempotency-Key、最小预期 revision并解析 snapshot/delta/稳定错误。 -- [ ] 4.2 实现网络不确定时复用幂等键、409 revision 恢复和组件卸载后共享结果合并。 -- [ ] 4.3 实现按 Generation ID 引用计数的 stream/poll 协调器、可见/隐藏频率、终态停止和 checkpoint 乱序保护。 -- [ ] 4.4 覆盖多视图共享一个监控、最后订阅释放、重新挂载恢复以及卸载不发送 Stop 的测试。 - -## 5. 只读界面切片迁移 - -- [ ] 5.1 迁移 boot、Conversation 列表/标题和根 Thread 识别,不再使用魔法 `main` ID。 -- [ ] 5.2 迁移列、Thread switcher、tree list、breadcrumb、selection anchor 和嵌套 Fork 展示到 ThreadFork selectors。 -- [ ] 5.3 迁移 Message/Markdown/研究活动/Artifact/反馈展示到稳定 ID selectors。 -- [ ] 5.4 迁移 React Flow 画布节点/边和聚焦行为到可重建投影,并验证懒加载不复制 canonical 实体。 - -## 6. 写交互切片迁移 - -- [ ] 6.1 迁移 composer/send/stop 到 client gateway 和 pending overlay,删除规范路径的 `persistNow` 前置屏障。 -- [ ] 6.2 迁移重新生成、用户输入编辑和变体选择到 Turn/Message 命令与 revision。 -- [ ] 6.3 迁移 selection Fork 到单个 `forkThread` 命令,不再本地写 parent/children/Message forks。 -- [ ] 6.4 迁移复制、反馈、Artifact 定位和其余 Message actions,全部使用稳定 Message/Turn/Thread ID。 - -## 7. 互斥模式与行为验证 - -- [ ] 7.1 增加 Conversation 页面 legacy/canonical 互斥 boot 开关,canonical 模式不实例化旧 Tree Store、整树 persistence 或 reconcile。 -- [ ] 7.2 覆盖发送、流式、Stop partial、刷新恢复、重新生成、选择变体、A → B → C Fork、归档/恢复和标题行为矩阵。 -- [ ] 7.3 增加 selector 通知计数与关键渲染性能验证,确认无关列不会随全局树 version 重渲染。 -- [ ] 7.4 运行相关单元/组件/端到端测试、`pnpm typecheck` 与 `pnpm openspec:validate` 并记录结果。 diff --git a/openspec/changes/normalize-conversation-persistence/.openspec.yaml b/openspec/changes/normalize-conversation-persistence/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/normalize-conversation-persistence/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/normalize-conversation-persistence/design.md b/openspec/changes/normalize-conversation-persistence/design.md deleted file mode 100644 index 447130af..00000000 --- a/openspec/changes/normalize-conversation-persistence/design.md +++ /dev/null @@ -1,103 +0,0 @@ -## Context - -本变更依赖 `define-conversation-domain-model`。当前 `branch_trees.state` 是唯一事实源,`branch_generations`、反馈和 Artifact 只能引用 JSON 内部 ID。本阶段只建立规范结构、仓储和审计能力;在最终切换前不改变线上权威来源。 - -## Goals / Non-Goals - -**目标:** - -- 让 Conversation 核心实体拥有可外键约束的服务端身份。 -- 用数据库约束和事务保护根 Thread、唯一 Fork、Turn/Message 归属与有效变体。 -- 提供稳定的规范快照和可重复遗留审计。 -- 为 Generation 迁移提供真实 Thread/Turn/Message 外键目标。 - -**非目标:** - -- 迁移 Generation 状态、计费和流式终结。 -- 开放新的写命令接口或迁移客户端 Store。 -- 在本阶段启用生产双写或正式删除 `branch_trees`。 -- 实现 Project Memory、File、Instruction 的业务表。 - -## Decisions - -### D1. 物理表按 Conversation 领域命名 - -首批表为: - -```text -workspaces -workspace_members -projects -conversations -conversation_threads -thread_forks -conversation_turns -conversation_messages -``` - -使用 `conversation_*` 前缀避免与其他聊天运行时的线性表混淆。Workspace/Project 只实现最小身份和成员归属;资产能力后续扩展。 - -### D2. 使用复合键保护归属 - -除普通主键外,表提供必要的复合唯一键: - -```text -conversation_threads: unique(id, conversation_id) -conversation_turns: unique(id, thread_id) -conversation_messages: unique(id, thread_id, turn_id) -``` - -ThreadFork 的 parent/child 与 Conversation 使用复合外键;来源 Message 同时携带 parent Thread 约束。Turn 的 active Message 使用可延迟复合外键,并在事务终点验证角色。仅检查单个 ID 不足以阻止跨 Thread 串线。 - -### D3. 根引用和 Fork 完整性使用延迟约束 - -Conversation 与根 Thread、Turn 与 active Message 都存在创建顺序环。迁移使用 PostgreSQL 可延迟约束或等价约束触发器,使一个事务可以先创建双方,再在提交时验证: - -- `rootThreadId` 属于当前 Conversation; -- 根 Thread 没有入向 Fork; -- 每个非根 Thread 恰有一条入向 Fork; -- Fork 图无环; -- active user/assistant Message 属于当前 Turn 且角色匹配。 - -仓储仍执行早期领域校验,以返回可理解错误;数据库约束是最终防线。 - -### D4. Message 内容保持结构化 JSON 值 - -Message 行规范化身份、归属、角色、状态和顺序,但 `content` 继续保存有版本的结构化片段 JSON。工具片段、研究计划和 Markdown 内容没有独立权限时不拆表。File/Artifact 的独立生命周期由后续 Project 资产变更处理。 - -### D5. 快照只读,写入通过仓储端口 - -`ConversationSnapshot` 按稳定顺序组装实体和派生索引,用于测试和后续首次加载。仓储端口按实体/聚合操作,不提供 `saveConversationSnapshot`。这样后续命令接口无法重新引入整树覆盖。 - -### D6. 先审计,再决定重置或导入 - -审计器先对真实数据运行只读报告,输出: - -- 总记录数和所有者分布; -- 可直接映射的 Conversation/Thread/Turn/Message/Fork 数量; -- 魔法根 ID、重复 Message、错误 Fork、跨 Thread active leaf; -- `branch_generations`、反馈和 Artifact 的悬空引用; -- 每棵树的确定性映射摘要和错误代码。 - -根据报告在最终切换变更中选择开发数据重置或一次性导入。本变更不猜测该决定。 - -### D7. 新路径默认关闭 - -规范仓储通过显式服务端配置启用,默认不接受生产写入。测试可以在隔离数据库中直接使用;线上仅允许审计。禁止添加自动双写适配器,因为它会在命令 API 出现前建立两个权威来源。 - -## Risks / Trade-offs - -- **[风险] 延迟约束和触发器增加迁移复杂度。** → 同时提供仓储级早期校验、数据库集成验证和清晰约束命名。 -- **[风险] Workspace/Project 扩大首批表。** → 只实现身份与成员归属,避免以后再次迁移 Conversation 所有权。 -- **[风险] 新表落地但暂不承载流量。** → 通过隔离集成验证和审计证明结构,下一 change 才迁移执行路径。 -- **[风险] JSON 内容仍可能包含无效工具片段。** → 内容使用版本化解析器;关系归属不再依赖片段内部 ID。 - -## Migration Plan - -1. 增加表、复合键、索引、延迟约束和仓储端口。 -2. 在隔离数据库执行最小与非法关系验证。 -3. 实现确定性 `ConversationSnapshot` 读取。 -4. 实现只读遗留审计并对目标环境生成报告。 -5. 保持规范生产写入关闭,把报告交给下一迁移 change。 - -回滚只需删除尚未启用写入的新表和代码;不得修改 `branch_trees` 原数据。 diff --git a/openspec/changes/normalize-conversation-persistence/proposal.md b/openspec/changes/normalize-conversation-persistence/proposal.md deleted file mode 100644 index d7a5dc98..00000000 --- a/openspec/changes/normalize-conversation-persistence/proposal.md +++ /dev/null @@ -1,31 +0,0 @@ -## Why - -`define-conversation-domain-model` 已确定 Project、Conversation、Thread、ThreadFork、Turn 与 Message 的身份和归属,但当前数据库仍把整棵 `ThreadTreeState` 存在单个 JSONB 行中,无法用外键拒绝跨 Thread 串线、重复 Fork 来源或错误 active variant。必须先建立规范写模型和可审计迁移入口,后续 Generation、命令接口与客户端才能依赖稳定实体。 - -## What Changes - -- 新增 Workspace/Project、Conversation、Thread、ThreadFork、Turn、Message 的规范化关系表、稳定 ID、复合外键、唯一约束和必要的延迟约束检查。 -- Conversation 独占聚合标题与 `rootThreadId`;根 Thread 不再使用 `id = "main"`,非根 Thread 的来源只由 ThreadFork 表达。 -- Turn 显式保存当前有效用户/助手 Message,Message 内容继续使用带版本的结构化 JSON 值,避免过度拆分工具片段。 -- 新增仓储端口与只读 `ConversationSnapshot` 组装器;快照可供首次加载,但不得作为整包写入数据。 -- 新增对 `branch_trees.state` 的只读审计和导入计划,输出逐实体映射、拒绝原因和可重复运行报告;本变更不启用双写,也不切换生产权威来源。 -- 为下一步 `migrate-generation-lifecycle` 暴露真实 Thread/Turn/Message 身份,但不在本变更迁移 Generation 状态机、计费或流式终结逻辑。 - -## Capabilities - -### New Capabilities - -- `conversation-persistence`:定义 Conversation 核心实体的关系型持久化、不变量、规范快照和遗留数据审计行为。 - -### Modified Capabilities - -- 无。本变更依赖 `define-conversation-domain-model` 的 `domain` 增量规范,不再次复制术语要求。 - -## Impact - -- 数据库:`lib/db/schema.ts`、Drizzle 迁移、约束、索引以及本地重置/迁移脚本。 -- 领域与仓储:`lib/thread-chat/domain/`、新的 Conversation 仓储端口和持久化适配器。 -- 读取路径:新增规范 `ConversationSnapshot`;现有 `branch_trees` 读取在切换前保持权威。 -- 部署:只落表和禁用状态的代码不会改变当前用户行为;正式导入与切换由后续 change 执行。 -- 前置依赖:`define-conversation-domain-model`。 -- 后续依赖:`migrate-generation-lifecycle`、`add-conversation-command-api`。 diff --git a/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md b/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md deleted file mode 100644 index cf4e4f4a..00000000 --- a/openspec/changes/normalize-conversation-persistence/specs/conversation-persistence/spec.md +++ /dev/null @@ -1,89 +0,0 @@ -## Purpose - -为 Project、Conversation、Thread、ThreadFork、Turn 与 Message 建立可由数据库和事务强制保护的规范持久化契约,并提供只读快照、遗留数据审计及安全迁移边界。 - -## ADDED Requirements - -### Requirement: 持久化稳定的 Conversation 核心实体 - -系统 SHALL 为 Workspace、Project、Conversation、Thread、Turn 与 Message 分配稳定且无业务含义的服务端 ID,并将每个实体保存为可独立寻址的关系型记录。实体归属创建后不得通过更新外键改变;任何跨所有者、跨 Project 或跨 Conversation 引用 MUST 被拒绝。 - -#### Scenario: 创建根 Conversation - -- **WHEN** 系统在 Project 中创建一条 Conversation -- **THEN** Conversation 与根 Thread 获得独立稳定 ID,且 `rootThreadId` 只能指向属于该 Conversation 的 Thread - -#### Scenario: 拒绝移动既有 Thread - -- **WHEN** 写入尝试把既有 Thread 的 `conversationId` 改为另一条 Conversation -- **THEN** 系统拒绝写入并保持原归属不变 - -### Requirement: 使用唯一 ThreadFork 保存分叉来源 - -系统 SHALL 只通过 ThreadFork 保存上游 Thread、来源 Message 与下游 Thread 的 Fork 事实。来源 Message MUST 属于上游 Thread,三者 MUST 属于同一 Conversation;根 Thread 不得有入向 ThreadFork,非根 Thread MUST 恰有一个入向 ThreadFork,Fork 图不得成环。 - -#### Scenario: 保存有效嵌套 Fork - -- **WHEN** 一条 ThreadFork 指向同一 Conversation 中的上游 Thread、其来源 Message 和尚无来源的下游 Thread -- **THEN** 系统保存该关系,并可从它派生 children、深度与分支数量 - -#### Scenario: 拒绝第二个来源 - -- **WHEN** 写入尝试为同一个非根 Thread 建立第二条入向 ThreadFork -- **THEN** 系统拒绝该写入且保留原唯一来源 - -### Requirement: 使用 Turn 约束 Message 与当前有效变体 - -每个 Turn SHALL 恰好属于一个 Thread,每个 Message SHALL 同时属于该 Thread 和其中一个 Turn。Turn 的当前有效用户与助手 Message MUST 指向自身变体,角色必须匹配;跨 Thread 或跨 Turn 的变体选择 MUST 被拒绝。 - -#### Scenario: 保存重新生成变体 - -- **WHEN** 同一 Turn 产生新的助手 Message 变体并被选为当前有效回答 -- **THEN** 系统追加新 Message、更新该 Turn 的有效助手引用,且不修改其他 Thread/Turn - -#### Scenario: 拒绝跨 Thread 有效选择 - -- **WHEN** Turn 尝试把另一个 Thread 的 Message 设为当前有效变体 -- **THEN** 系统拒绝该选择且原有效路径不变 - -### Requirement: 组装只读规范 Conversation 快照 - -系统 SHALL 能够从规范实体组装包含 Conversation、Threads、ThreadForks、Turns、Messages 和当前有效选择的确定性快照。快照中的派生索引 MUST 可从实体重建;系统不得接受该快照作为覆盖规范行的整包写入。 - -#### Scenario: 两次读取产生稳定快照 - -- **WHEN** 底层规范实体未发生变化并连续读取同一 Conversation -- **THEN** 两次快照具有相同实体关系、排序和当前有效路径 - -#### Scenario: 拒绝整包覆盖 - -- **WHEN** 调用方提交完整 Conversation 快照试图覆盖服务端实体 -- **THEN** 持久化层不提供该权威写入能力,并要求调用实体级仓储或应用命令 - -### Requirement: 审计遗留 ThreadTreeState - -系统 SHALL 提供只读、可重复运行的遗留数据审计,检查所有者、根节点、Thread/Fork、Message 图、当前叶节点、Artifact 来源和辅助 Generation 引用,并输出逐记录的可迁移、需修复或拒绝原因。审计不得修改原始数据。 - -#### Scenario: 审计合法遗留树 - -- **WHEN** 遗留 `ThreadTreeState` 满足已知结构和归属不变量 -- **THEN** 审计输出确定的规范实体映射及数量,不写入规范表或原表 - -#### Scenario: 报告污染引用 - -- **WHEN** 遗留树含缺失来源 Message、重复 Fork、跨 Thread active leaf 或辅助表悬空 ID -- **THEN** 审计将该记录标为不可直接迁移,并给出稳定错误代码和涉及的实体 ID - -### Requirement: 迁移阶段保持唯一写入权威 - -在正式切换前,新增规范表和仓储 SHALL 保持禁用或只读验证状态,旧路径仍是唯一写入权威;切换后则规范实体成为唯一权威,系统不得长期双写 JSON 与规范行。 - -#### Scenario: 未启用规范路径 - -- **WHEN** 规范持久化功能开关未启用 -- **THEN** 当前用户写入行为不变,新表只能用于显式测试、审计或隔离环境 - -#### Scenario: 防止无声明双写 - -- **WHEN** 代码尝试在同一运行模式下同时把一次领域变更写入整树 JSON 与规范实体 -- **THEN** 系统拒绝启动该模式或使验证失败 diff --git a/openspec/changes/normalize-conversation-persistence/tasks.md b/openspec/changes/normalize-conversation-persistence/tasks.md deleted file mode 100644 index 19501931..00000000 --- a/openspec/changes/normalize-conversation-persistence/tasks.md +++ /dev/null @@ -1,28 +0,0 @@ -## 1. 数据结构与迁移 - -- [ ] 1.1 定义 `workspaces`、`workspace_members`、`projects`、`conversations`、`conversation_threads`、`thread_forks`、`conversation_turns`、`conversation_messages` 的 Drizzle 结构和稳定命名。 -- [ ] 1.2 生成迁移并补充 Drizzle 无法表达的可延迟复合外键、唯一约束与约束触发器。 -- [ ] 1.3 为所有者查询、Conversation 列表、Thread/Turn 顺序、来源 Message 和当前有效变体建立必要索引。 -- [ ] 1.4 更新本地 schema reset 脚本,使新表只在明确目标 schema 中创建或删除,不影响其他数据库。 - -## 2. 规范仓储 - -- [ ] 2.1 建立 Workspace/Project 与 Conversation/Thread 的纯应用仓储端口和 Drizzle 适配器。 -- [ ] 2.2 实现 ThreadFork 事务写入与根、唯一来源、同 Conversation、来源 Message 归属和无环校验。 -- [ ] 2.3 实现 Turn/Message 追加、角色校验和当前有效变体的预期版本更新。 -- [ ] 2.4 实现确定性 `ConversationSnapshot` 组装、稳定排序和可重建派生索引。 -- [ ] 2.5 增加服务端配置边界,默认禁止规范仓储承载生产写入,并拒绝未声明双写模式。 - -## 3. 遗留审计 - -- [ ] 3.1 实现只读 `ThreadTreeState` 审计器和 legacy → canonical ID/实体映射报告。 -- [ ] 3.2 检查根、Fork、Message 图、active leaf、Artifact、反馈和 `branch_generations` 悬空引用,并为每类错误定义稳定代码。 -- [ ] 3.3 增加 dry-run 命令,仅输出汇总与逐树报告,不修改旧表或规范表。 -- [ ] 3.4 在目标环境执行审计并记录“可重置或必须迁移”的数据证据,供最终切换 change 决策。 - -## 4. 约束验证 - -- [ ] 4.1 覆盖最小根 Conversation、A → B → C Fork、同 Turn 回复变体和有效选择的成功用例。 -- [ ] 4.2 覆盖跨 Conversation Fork、来源 Message 错配、第二入向 Fork、Fork 环、跨 Thread Message/Turn 和错误角色 active Message 的拒绝用例。 -- [ ] 4.3 验证两个相同读取产生等价 `ConversationSnapshot`,且仓储没有整包快照写入端口。 -- [ ] 4.4 运行数据库迁移、`pnpm typecheck`、`pnpm openspec:validate` 和相关验证脚本并记录结果。 diff --git a/openspec/changes/retire-thread-tree-authority/.openspec.yaml b/openspec/changes/retire-thread-tree-authority/.openspec.yaml deleted file mode 100644 index d160e09c..00000000 --- a/openspec/changes/retire-thread-tree-authority/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-08-21 diff --git a/openspec/changes/retire-thread-tree-authority/design.md b/openspec/changes/retire-thread-tree-authority/design.md deleted file mode 100644 index 7070b2c6..00000000 --- a/openspec/changes/retire-thread-tree-authority/design.md +++ /dev/null @@ -1,174 +0,0 @@ -## Context - -新领域链完成后,代码库会暂时同时包含可运行的 legacy ThreadTree 栈和默认关闭的 canonical Conversation 栈。并存代码本身可以支持验证,但生产事实不能并存:任何双写、读失败回退或异步同步都会重新创造 Issue #34 的根因。 - -Cutover 是一次数据、应用、客户端和文档共同变更,不是简单切 feature flag。本设计把决定门禁、冻结、导入、互斥切换、观察、回滚和最终删除分成可验证阶段。 - -## Goals / Non-Goals - -**目标:** - -- 基于真实数据选择重置或导入,并保留审计证据。 -- 在没有未确认旧写入时把 canonical 栈切为唯一权威。 -- 保持新 specs 定义的全部用户行为、授权和计费正确性。 -- 提供切换前后不同的安全回滚策略。 -- 删除旧协议和重复领域事实,不留下永久兼容层。 - -**非目标:** - -- 用双写实现无停机迁移。 -- 在 cutover 中实现分享、Project assets、CLI/MCP 或 Issue #39 收敛。 -- 保留旧客户端的长期写兼容。 -- 删除未经依赖审计的其他聊天表或已归档迁移历史。 - -## Decisions - -### D1. Authority 是单值部署状态 - -应用组合根只接受: - -```text -CONVERSATION_AUTHORITY = legacy | canonical -``` - -服务端健康信息发布 authority、schemaVersion 和 cutover epoch;客户端 boot 必须匹配。`legacy` 组合根不能构造 canonical 写 handler,`canonical` 组合根不能构造 branch-tree 写仓储。配置缺失、未知或两套依赖同时可写时健康检查失败。 - -备选方案是按请求或 Conversation 灰度双 authority;跨记录的列表、billing job 和客户端路由会产生不可证明的混合边界,因此不用于首次 cutover。 - -### D2. 数据处置先产出 ADR,再写执行参数 - -使用 persistence change 的审计器在目标环境输出: - -- legacy Conversation/Thread/Message/Generation 数量与所有者分布; -- 合法、需修复、拒绝记录; -- 反馈、Artifact、标题、usage 和其他 sidecar 引用; -- 数据保留义务与环境用途。 - -单独 ADR 选择: - -1. **确定性导入**:存在任何需保留数据时的默认; -2. **受批准重置**:只限明确无保留义务的隔离环境。 - -任务不会预先写死选择。ADR 包含批准者、时间、范围、备份和不可迁移项处置。 - -### D3. Legacy 局部 ID 通过持久映射迁移 - -许多 JSON 内 ID(如 `main`、`m1`、`b2`)只在单棵树内唯一,不能直接成为公开规范 ID。导入器以 `(legacyTreeId, entityType, localId)` 生成或记录稳定 canonical ID,并输出映射清单。所有 ThreadFork、Generation、反馈、Artifact、标题和 usage 引用通过同一映射解析。 - -导入器使用 upsert/幂等 marker,可在冻结数据上重复 dry-run 和正式运行。单条 Conversation 在事务中导入;全局报告校验源/目标计数、唯一约束、哈希摘要和悬空引用。 - -### D4. 用短维护窗口代替双写 - -执行顺序: - -```text -拒绝新的 legacy mutations/generations - → drain/stop/reconcile 非终态 Generation - → 验证 usage/计费终结 - → 备份 legacy 与空/预导入 canonical 数据 - → 正式 import/reset - → 全量 integrity verification - → 切 canonical authority + canonical client -``` - -维护窗口虽然有短暂不可写,但把最后写入边界变成可证明时刻。双写看似减少停机,却需要再次实现两套语义的协调,风险更高。 - -### D5. 行为矩阵是 release gate,不是人工印象 - -矩阵至少覆盖: - -```text -Auth/owner/project isolation -Conversation list/create/title/archive/restore/delete -Thread title/archive/restore -Column/tree-list/canvas navigation -Selection anchor + A→B→C Fork -Send/edit/regenerate/select variant -Markdown/Artifact/research activities -Copy/feedback/message actions -Streaming/disconnect/Stop/stale recovery/refresh -Usage completeness/exactly-once billing -遗留路由拒绝 -``` - -每项关联自动测试、手工 smoke(仅视觉交互)和新 spec Requirement。已确认旧错误不作为“兼容行为”保留。 - -### D6. 切换采用一个不可分割的 release checkpoint - -在数据验证完成且仍处于维护模式时: - -1. 部署/启用 canonical 服务端 authority; -2. 验证规范 API 健康、schema 和 epoch; -3. 启用 canonical 客户端 build; -4. 对内部 canary actor 执行读写 smoke; -5. 开放流量并关闭维护模式。 - -如果平台不能原子切配置与客户端,则先部署能识别服务端 epoch 的客户端壳,只有 epoch 匹配才加载 canonical 页面;不允许新客户端向 legacy 写入或旧客户端向 canonical 写入。 - -### D7. 旧路由立即拒绝,代码和表延后删除 - -切换时旧 mutation routes 返回 `410` + `legacy_protocol_retired`(认证/不可见检查仍按安全策略执行),旧读取不作为 canonical fallback。保留最小拒绝/遥测 handler 一段声明观察期,用于识别陈旧客户端。 - -观察门禁通过后分两步清理: - -1. 删除 Tree Store、`persistNow`、reconcile、legacy repositories/routes 和所有新代码对 ThreadTree 类型的导入; -2. 在备份保留与依赖扫描通过后,用后续不可逆迁移删除确认只服务旧能力的表/列。 - -历史 SQL migration 文件不重写。名称相似的 assistant-ui 线性表先审计用途,不能因为名字相似顺带删除。 - -### D8. 可观测性围绕正确性门禁 - -Dashboard/日志至少包含: - -- authority/schema/epoch mismatch; -- snapshot/command 成功率与稳定错误码; -- revision/idempotency 冲突; -- Generation 非终态年龄、checkpoint age、Stop latency、stale 收敛; -- `usage_unavailable`、唯一 usage 冲突和对账差异; -- legacy route 调用; -- 导入/运行时外键和不变量失败。 - -阈值在 release runbook 中依据当前基线填写,不在设计中伪造通用数字。日志只记录 ID、分类和计数,不记录私密 Message 正文。 - -### D9. 回滚线以首个 canonical 生产写入为界 - -**切换前:** legacy 数据未改变,可退出维护并继续 legacy。 - -**切换后但尚无 canonical 写入:** 可以关闭流量并回到 legacy,但必须用审计证明零 canonical mutation。 - -**已有 canonical 写入:** legacy 已成为落后备份,不得恢复为权威。选择: - -- 回滚客户端/应用版本,但保留兼容 canonical schema; -- 进入 canonical read-only 并前滚修复; -- 从 cutover 后的规范备份/事务日志恢复。 - -绝不实现 canonical → ThreadTree 反向同步,因为它会把删除掉的重复关系语义重新引入。 - -### D10. OpenSpec 按“历史实现 → 目标替代”排序 - -对依赖旧模型的 changes 建立 supersession 清单。完成的旧 changes 保留真实历史;归档时确保最终生效 specs 由 `define-conversation-domain-model` 和四个后续能力覆盖。未完成 `persist-thread-chat-generations` 不补勾任务,记录被 `migrate-generation-lifecycle` 及 cutover 替代的原因和未完成范围。 - -若归档工具会把旧 requirement 重新合并为目标权威,则使用明确的 superseded/skip-specs 历史处置流程,而不是通过修改旧任务伪造一致性。 - -## Risks / Trade-offs - -- **[风险] 维护窗口暂时阻止聊天写入。** → 提前测量导入时间、预生成 dry-run 映射并保留只读访问;用短冻结换取唯一写入边界。 -- **[风险] 遗留污染数据阻塞整批切换。** → 按 Conversation 事务导入并提前修复;所有排除都需 ADR 批准和备份。 -- **[风险] 新客户端与 authority epoch 不匹配。** → boot 强制检查服务端 epoch/schema,不匹配时显示维护/刷新提示而不写入。 -- **[风险] 切换后无法简单回到旧 JSON。** → 这是避免数据丢失的必要限制;通过 canonical 备份、read-only 和前滚演练降低风险。 -- **[风险] 过早删表破坏隐蔽 job。** → 先拒绝路由和删除代码引用,观察零调用,再以 DB dependency/query audit 门禁物理删除。 - -## Migration Plan - -1. 完成并严格验收全部前置 changes,建立行为矩阵和基线。 -2. 在目标环境运行只读审计,批准 import/reset ADR,完成 dry-run 与映射报告。 -3. 演练维护、drain、备份、导入、验证、切换和 canonical 恢复。 -4. 正式进入维护窗口,冻结旧写入并收敛所有 Generation/计费。 -5. 生成验证备份,执行 import/reset 和全量完整性检查。 -6. 切换 authority/client epoch,执行 canary 和 smoke 后开放流量。 -7. 观察门禁通过后删除旧运行时代码;保留备份至声明期限。 -8. 依赖扫描和恢复验证通过后删除遗留表/列,并如实处置旧 OpenSpec changes。 - -## Open Questions - -仅有一个必须由真实审计回答的部署决策:目标环境采用确定性导入还是受批准重置。该问题不改变架构,但必须在执行 cutover 前以 ADR 关闭。 diff --git a/openspec/changes/retire-thread-tree-authority/proposal.md b/openspec/changes/retire-thread-tree-authority/proposal.md deleted file mode 100644 index e94dbddb..00000000 --- a/openspec/changes/retire-thread-tree-authority/proposal.md +++ /dev/null @@ -1,33 +0,0 @@ -## Why - -前四个 changes 可以把新模型完整实现出来,但只要 `branch_trees.state`、整树 PUT 或旧客户端仍能写入,系统就有两个事实源,任何新关系和修复最终都会再次漂移。最后必须通过有证据、可门禁、不可双写的 cutover,把规范 Conversation 栈切为唯一生产权威,并让旧 ThreadTree 协议和代码安全退场。 - -## What Changes - -- 汇总遗留审计、真实数据量、悬空引用和功能覆盖证据,在 cutover 前明确选择“一次性导入”或“有批准的数据重置”,不得在任务中预设答案。 -- 建立旧/新行为对照与发布门禁,覆盖列表/标题、列/画布、嵌套 Fork、Message 变体/actions、Artifact/反馈、Generation Stop/恢复和计费。 -- 在维护窗口停止旧写入、排空或收敛非终态 Generation、生成可验证备份,并一次性导入/初始化规范实体及所有辅助引用。 -- 以互斥 authority 配置原子切换规范服务端、命令 API 和 normalized 客户端;系统不存在长期双写、双读择优或后台协调模式。 -- **BREAKING**:关闭整树 GET/PUT/DELETE 写协议、遗留 Generation 协调路由和浏览器 save/reconcile 路径;旧客户端收到稳定 retired 响应,不得继续覆盖规范实体。 -- 在观察期后删除 `ThreadTreeState` 新代码依赖、魔法 `main`、重复 parent/children/forks/depth/activeLeaf 字段,以及确认无其他用途的 `branch_trees`/`branch_generations` 结构。 -- 制定不会把规范写入回灌旧 JSON 的回滚方案;切换后问题通过 canonical read-only、前滚修复或规范备份恢复处理。 -- 处理依赖旧权威模型的活跃 OpenSpec changes:完整能力保留为历史,未完成的 `persist-thread-chat-generations` 明确标记被新链替代,不把未完成任务伪装为完成。 - -## Capabilities - -### New Capabilities - -- `conversation-cutover`:定义从 ThreadTree 权威到规范 Conversation 权威的数据决策、发布门禁、互斥切换、旧协议退场、观察与回滚要求。 - -### Modified Capabilities - -- 无。遗留 capabilities 的历史归档顺序由本 change 的迁移任务处理;目标行为由新链的独立 capabilities 定义。 - -## Impact - -- 数据:`branch_trees`、`branch_generations`、Message feedback、Artifact/标题/usage 外键、规范 Conversation 全表和备份。 -- 服务端:旧 branch-tree 路由、Generation sidecar/reconcile、领域 types/selectors/repositories、authority 配置和运维脚本。 -- 客户端:legacy boot/Store/persistence/chat controller 与 canonical 页面开关。 -- 发布:维护窗口、数据审计、canary/smoke、指标、回滚和旧表保留期。 -- OpenSpec:旧 active changes 的历史处置与新 changes 的依赖顺序。 -- 前置依赖:`define-conversation-domain-model`、`normalize-conversation-persistence`、`migrate-generation-lifecycle`、`add-conversation-command-api`、`normalize-conversation-client-state` 全部实现并验收。 diff --git a/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md b/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md deleted file mode 100644 index 4425d92c..00000000 --- a/openspec/changes/retire-thread-tree-authority/specs/conversation-cutover/spec.md +++ /dev/null @@ -1,155 +0,0 @@ -## Purpose - -以可审计、互斥、可回滚且不产生双重事实源的方式,将生产 Conversation 从 ThreadTree JSON 权威切换到规范实体,并安全退役旧协议、代码和数据结构。 - -## ADDED Requirements - -### Requirement: Cutover 必须满足显式前置门禁 - -切换生产权威前,系统 SHALL 证明规范持久化、Generation 生命周期、命令 API 和 normalized 客户端的必需任务及行为矩阵已经完成。发布记录 MUST 包含严格测试结果、数据审计、容量/性能基线、安全检查、备份验证、回滚演练和明确 go/no-go 决策;任一阻塞门禁失败 MUST 取消切换。 - -#### Scenario: Generation partial 回归未通过 - -- **WHEN** 门禁测试仍会在 stale recovery 中覆盖已保存 partial -- **THEN** 发布保持 legacy authority,不进入数据冻结或 cutover - -#### Scenario: 所有门禁满足 - -- **WHEN** 实现、数据、行为、安全、备份和回滚证据均满足记录的验收标准 -- **THEN** 负责人可以批准进入受控维护窗口 - -### Requirement: 用真实审计决定导入或重置 - -系统 SHALL 根据 `normalize-conversation-persistence` 产生的只读审计和目标环境用途,记录选择“一次性导入”或“受批准重置”的决定、影响范围、记录数量、不可迁移项及处理人。存在需保留的用户数据时 MUST 导入或逐项解决,不能为简化开发而静默清空。 - -#### Scenario: 目标环境有需保留用户数据 - -- **WHEN** 审计发现属于真实用户且在保留范围内的 Conversation -- **THEN** cutover 计划选择确定性导入,并为全部不可直接映射记录给出修复或明确排除批准 - -#### Scenario: 隔离开发环境获准重置 - -- **WHEN** 审计证明数据无保留义务且负责人书面批准重置 -- **THEN** 发布记录保存范围、备份与批准证据后,才可初始化空规范数据 - -### Requirement: 一次性导入必须确定且引用完整 - -导入器 SHALL 在旧写入冻结后,把每个 legacy `(tree, entityType, localId)` 确定映射为规范稳定 ID,并迁移 Conversation、Thread、ThreadFork、Turn、Message、Generation、标题、反馈、Artifact 和 usage 关联。导入 MUST 可重复运行、事务安全并输出映射/拒绝报告;任何跨所有者、悬空或计数不一致 MUST 阻止该记录切换。 - -#### Scenario: 导入含嵌套 Fork 的树 - -- **WHEN** 合法遗留树包含 A → B → C 及 Message 反馈、Artifact 和已终结 Generation -- **THEN** 导入产生一个 Conversation、三个 Thread、两条 ThreadFork 和完整辅助外键,重跑不会产生重复实体 - -#### Scenario: 导入发现悬空 Generation Message - -- **WHEN** `branch_generations` 引用无法映射的 JSON Message ID -- **THEN** 导入将该 Conversation 标为阻塞并保持旧备份,不伪造 Message 或忽略计费引用 - -### Requirement: 在无旧写入窗口排空运行任务 - -Cutover SHALL 先进入受控只读/维护模式,拒绝新的 legacy Conversation 写入和 Generation 开始,再等待非终态 Generation 正常完成、停止或由既有规则收敛。直到旧系统没有未确认的模型执行、checkpoint 或计费事务,系统 MUST NOT 执行最终数据导入和 authority 切换。 - -#### Scenario: 维护开始时仍有运行任务 - -- **WHEN** legacy Generation 仍为 `running` 或 `stop_requested` -- **THEN** cutover 等待或按声明策略停止/收敛,并在终态与计费可审计后继续 - -#### Scenario: 维护期间旧客户端发送写入 - -- **WHEN** 旧客户端尝试 PUT 整树或开始 Generation -- **THEN** 系统返回稳定维护响应且不接受新事实 - -### Requirement: 规范 Conversation 必须成为唯一权威 - -Cutover 后,所有读取、命令、Generation、计费和客户端页面 SHALL 只使用规范 Conversation 实体。authority 模式 MUST 在同一部署中互斥;系统不得双写、双读择优、从 legacy 回填缺失规范记录,或运行两个 Store 相互同步。 - -#### Scenario: 规范记录缺失 - -- **WHEN** cutover 后请求的 Conversation 无规范记录但旧 JSON 仍有备份 -- **THEN** 系统返回规范未找到/数据错误并告警,不在请求路径回退读取旧 JSON - -#### Scenario: 配置同时启用两套写入 - -- **WHEN** 部署配置尝试同时启用 legacy 与 canonical authority -- **THEN** 应用启动或健康门禁失败,部署不得接收流量 - -### Requirement: Cutover 后保持目标行为矩阵 - -规范路径 SHALL 跑通 Conversation 列表/标题/归档、Thread 列/画布/归档、A → B → C Fork、锚点、Turn/Message 变体、Message actions、Markdown/Artifact/研究活动、Generation 流式/Stop/刷新恢复、反馈、所有者隔离和计费幂等。行为以新 specs 为准,不要求保留旧数据结构或已确认的错误语义。 - -#### Scenario: 切换后停止部分回答并刷新 - -- **WHEN** 用户在规范路径停止已有 partial 的 Generation 并刷新 -- **THEN** 相同 Thread/Turn/Message、`incomplete` 内容、研究活动、usage 完整度和 `stopped` 状态被恢复 - -#### Scenario: 切换后嵌套 Fork - -- **WHEN** 用户从根 Thread Fork B,再从 B Fork C -- **THEN** 列和画布显示两条 ThreadFork 的正确来源,服务端不存在重复 parent/children/Fork 写入 - -### Requirement: 旧 ThreadTree 协议必须明确退役 - -Authority 切换后,整树写入、旧 Generation 协调、active-leaf 切换和 legacy reconcile 路由 SHALL 被关闭,并对旧客户端返回稳定 `legacy_protocol_retired` 结果或不存在结果。旧协议不得在错误、缺失记录或回滚时重新启用为生产写入入口。 - -#### Scenario: 旧客户端提交整树 PUT - -- **WHEN** cutover 后旧客户端调用 legacy branch-tree 写路由 -- **THEN** 系统拒绝请求并返回稳定 retired 响应,不修改规范或备份数据 - -#### Scenario: 新客户端正常加载 - -- **WHEN** canonical 客户端加载 Conversation -- **THEN** 它只调用规范 snapshot/command/Generation API,不探测或调用旧路由 - -### Requirement: 切换期必须可观测和可审计 - -系统 SHALL 记录 authority epoch/schema、请求路径、命令错误分类、revision 冲突、Generation 终态/checkpoint age、stale 收敛、usage 对账、导入计数和完整性失败。发布 SHALL 使用预先记录的阈值和观察窗口判断继续、只读或前滚修复,日志不得泄漏 Message 内容或凭据。 - -#### Scenario: 切换后 usage_unavailable 异常上升 - -- **WHEN** 计费完整度指标超过批准阈值 -- **THEN** 发布进入声明的保护动作并保留规范记录用于诊断,不切回旧 JSON 结算 - -#### Scenario: 审计查询单次 Fork - -- **WHEN** 运维按 requestId/commandId 调查 Fork -- **THEN** 可以关联 actor、Conversation revision、ThreadFork/outbox 事件和结果,而无需读取整树 mutation diff - -### Requirement: 回滚不得重新引入双重事实源 - -切换前的回滚可以继续使用未修改的 legacy authority;一旦 canonical 接受生产写入,回滚 SHALL 保持规范数据库为事实源,通过关闭写入、回滚应用代码兼容层、恢复规范备份或前滚修复处理。系统 MUST NOT 把新规范写入反向合并回旧 JSON 后恢复旧权威。 - -#### Scenario: 切换后客户端渲染故障 - -- **WHEN** canonical 数据正确但新客户端出现严重回归 -- **THEN** 发布回滚/修复客户端或进入只读,服务端规范实体继续保持权威 - -#### Scenario: 切换后发现规范数据损坏 - -- **WHEN** 完整性检查发现不可接受的规范写入损坏 -- **THEN** 系统停止相关写入并从已验证规范备份恢复或执行审计前滚,不从落后的 legacy JSON 覆盖 - -### Requirement: 遗留代码和表分阶段删除 - -系统 SHALL 在 authority 切换成功后先证明旧路由零调用、无代码写引用且备份可恢复,再按声明保留期删除 ThreadTree 领域类型、浏览器 persistence/reconcile、旧仓储和重复关系字段。物理表删除 MUST 经过依赖清单确认;名称相似但仍服务其他能力的表不得被顺带删除。 - -#### Scenario: 观察期仍有旧路由调用 - -- **WHEN** 指标显示受支持客户端仍调用 branch-tree route -- **THEN** 系统保持路由拒绝状态并追踪调用方,不提前删除诊断信息或备份 - -#### Scenario: 删除旧表前发现外部依赖 - -- **WHEN** 依赖扫描发现某个 billing/feedback job 仍读取 `branch_generations` -- **THEN** 表删除门禁失败,先迁移该 job 并重新验证 - -### Requirement: OpenSpec 历史必须如实标记替代关系 - -依赖 ThreadTree 权威的完成 changes SHALL 作为历史实现记录保留,并在目标 specs 生效顺序中由新链覆盖。未完成的 `persist-thread-chat-generations` MUST 明确标记为被 `migrate-generation-lifecycle` 与本 cutover 替代;系统不得仅为归档而勾选未实现任务或把旧机制继续发布为当前契约。 - -#### Scenario: 处理未完成旧 Generation change - -- **WHEN** cutover 准备归档 OpenSpec 记录 -- **THEN** 文档保留其真实未完成项和 superseded 原因,并使新的 lifecycle spec 成为目标权威 - diff --git a/openspec/changes/retire-thread-tree-authority/tasks.md b/openspec/changes/retire-thread-tree-authority/tasks.md deleted file mode 100644 index 3a91d18c..00000000 --- a/openspec/changes/retire-thread-tree-authority/tasks.md +++ /dev/null @@ -1,60 +0,0 @@ -## 1. 前置实现与发布门禁 - -- [ ] 1.1 验证 `define-conversation-domain-model`、规范持久化、Generation lifecycle、命令 API 和 normalized client 的必需任务及 strict specs 全部完成。 -- [ ] 1.2 建立目标行为矩阵,关联认证、生命周期、列/画布、A → B → C Fork、变体、actions、Artifact/研究、Generation/计费的自动测试和 smoke。 -- [ ] 1.3 记录当前容量/性能/错误/计费基线、cutover 阈值、负责人、观察窗口和 go/no-go checklist。 -- [ ] 1.4 演练 canonical 备份恢复、维护模式、authority epoch mismatch 和切换后只读/前滚回滚流程。 - -## 2. 数据审计与处置 ADR - -- [ ] 2.1 在目标环境运行只读 legacy 审计,输出所有者、Conversation/Thread/Message/Generation、标题、反馈、Artifact 和 usage 数量。 -- [ ] 2.2 逐项处理或明确阻塞悬空、跨所有者、重复 ID、错误 Fork/active path 和计费引用。 -- [ ] 2.3 基于保留义务形成“确定性导入或受批准重置”ADR,记录范围、理由、批准人、备份和排除项。 -- [ ] 2.4 将 ADR 结果转成版本化 cutover 配置,不在代码中隐式猜测环境或数据处置方式。 - -## 3. 确定性导入或重置工具 - -- [ ] 3.1 实现 `(legacyTreeId, entityType, localId) → canonicalId` 的持久映射与幂等 dry-run/report。 -- [ ] 3.2 实现按 Conversation 事务导入 Project/Conversation/Thread/ThreadFork/Turn/Message 和当前有效变体。 -- [ ] 3.3 迁移 Generation、标题、Message feedback、Artifact、usage/billing 和其他审计确认的 sidecar 外键。 -- [ ] 3.4 实现受批准重置路径,仅接受 ADR scope 并在执行前验证目标环境与备份标识。 -- [ ] 3.5 增加 A → B → C、Message 变体、partial Generation、辅助引用、重复运行和故障回滚测试。 -- [ ] 3.6 实现源/目标计数、映射、约束、悬空引用和摘要的全量 post-import verifier。 - -## 4. Authority、维护与旧协议拒绝 - -- [ ] 4.1 实现单值 `legacy | canonical` authority 组合根、schemaVersion/cutover epoch 健康信息和非法双 authority 启动失败。 -- [ ] 4.2 实现维护模式,拒绝新 legacy mutations/Generation,同时保留必要只读和安全认证语义。 -- [ ] 4.3 实现非终态 Generation drain/Stop/stale 收敛与 usage/计费完成检查,未清零时阻止 cutover。 -- [ ] 4.4 为旧整树、active-leaf、Generation sidecar 和 reconcile routes 实现安全 `legacy_protocol_retired` 拒绝与遥测。 -- [ ] 4.5 让 canonical 客户端 boot 校验服务端 authority/schema/epoch,不匹配时不得发送写命令。 - -## 5. Cutover 演练与正式执行 - -- [ ] 5.1 在与生产等价的隔离环境完整演练冻结、drain、备份、import/reset、验证、authority/client 切换和 smoke,并记录耗时。 -- [ ] 5.2 正式进入维护窗口,冻结旧写入并确认所有 Generation、checkpoint、outbox 和计费事务已收敛。 -- [ ] 5.3 创建并验证 legacy/canonical 备份,执行 ADR 选定的数据动作和全量 post-import verifier。 -- [ ] 5.4 原子启用 canonical server/client epoch,对内部 canary 跑行为矩阵关键 smoke 后开放流量。 -- [ ] 5.5 验证 cutover 后所有读取、命令、Generation 和 billing jobs 只访问 canonical repositories。 - -## 6. 观察与回滚保护 - -- [ ] 6.1 建立 authority mismatch、命令错误、revision/idempotency 冲突、Generation age/checkpoint、Stop/stale 和 usage 对账 dashboard/告警。 -- [ ] 6.2 监控旧 route 调用、数据库查询和代码路径,记录陈旧客户端/任务调用方并完成迁移。 -- [ ] 6.3 验证切换后回滚工具不会执行 canonical → ThreadTree 反向同步,也不会把落后 legacy 数据恢复为权威。 -- [ ] 6.4 在声明观察窗口内完成逐日完整性/计费审计,并由负责人批准进入遗留删除阶段。 - -## 7. 删除遗留运行时代码 - -- [ ] 7.1 删除 canonical build 中的 `ThreadTreeState`、魔法 `main`、parent/children/Message forks/depth/activeLeaf 等重复领域事实和 selector。 -- [ ] 7.2 删除 Tree Store mutation、`persistNow`、save debounce、卸载 flush、startup reconcile 和 Generation merge 协调代码。 -- [ ] 7.3 删除旧 branch-tree/Generation 写路由、仓储和新代码对 legacy types 的导入,保留历史迁移文件不重写。 -- [ ] 7.4 运行 `rg`/依赖检查证明运行时代码无旧 authority 读写,并通过完整行为矩阵、typecheck、build 和测试。 - -## 8. 物理清理与 OpenSpec 收口 - -- [ ] 8.1 审计 `branch_trees`、`branch_generations` 及名称相似表的全部 FK、job、查询和备份依赖,形成精确删除清单。 -- [ ] 8.2 在备份保留/恢复验证和零依赖门禁通过后,执行分阶段不可逆迁移删除确认遗留表、列、索引和约束。 -- [ ] 8.3 建立旧 OpenSpec supersession map;保留已完成历史,明确记录 `persist-thread-chat-generations` 未完成项被新 lifecycle/cutover 替代,不补勾任务。 -- [ ] 8.4 按依赖顺序归档新 changes,验证最终 specs 以 Project → Conversation → Thread 模型和 canonical capabilities 为权威。 -- [ ] 8.5 运行最终 `pnpm openspec:validate`、schema 漂移检查、生产 smoke 和数据完整性/计费审计并记录结果。 From 87c7356e1e5f60d577dad53a01d032fcfecd673a Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 15:40:47 +0800 Subject: [PATCH 04/23] =?UTF-8?q?feat:=20=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.openspec.yaml | 2 + .../define-thread-chat-domain-model/design.md | 455 ++++++ .../proposal.md | 43 + .../specs/domain/spec.md | 220 +++ .../.openspec.yaml | 2 + .../design-thread-chat-client-api/design.md | 1363 +++++++++++++++++ .../design/api-contracts.md | 877 +++++++++++ .../design-thread-chat-client-api/proposal.md | 37 + .../specs/thread-chat-client-state/spec.md | 162 ++ .../specs/thread-chat-command-api/spec.md | 174 +++ 10 files changed, 3335 insertions(+) create mode 100644 openspec/changes/define-thread-chat-domain-model/.openspec.yaml create mode 100644 openspec/changes/define-thread-chat-domain-model/design.md create mode 100644 openspec/changes/define-thread-chat-domain-model/proposal.md create mode 100644 openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md create mode 100644 openspec/changes/design-thread-chat-client-api/.openspec.yaml create mode 100644 openspec/changes/design-thread-chat-client-api/design.md create mode 100644 openspec/changes/design-thread-chat-client-api/design/api-contracts.md create mode 100644 openspec/changes/design-thread-chat-client-api/proposal.md create mode 100644 openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md create mode 100644 openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md diff --git a/openspec/changes/define-thread-chat-domain-model/.openspec.yaml b/openspec/changes/define-thread-chat-domain-model/.openspec.yaml new file mode 100644 index 00000000..e685d45e --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-25 diff --git a/openspec/changes/define-thread-chat-domain-model/design.md b/openspec/changes/define-thread-chat-domain-model/design.md new file mode 100644 index 00000000..74259346 --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/design.md @@ -0,0 +1,455 @@ +## Context + +本设计实现 [proposal.md](./proposal.md) 与 [domain 增量规范](./specs/domain/spec.md) 的目标模型。它从当前已运行的 ThreadChat 模型出发,而不是从讨论阶段的草案出发。 + +当前基线由代码和数据库共同确定: + +```text +登录 User +└── branch_trees row(treeId 由客户端生成) + └── state: ThreadTreeState JSONB + ├── threads: Record + │ └── messages: Message[](parentMessageId + activeLeafMessageId) + └── artifacts: Record + +branch_generations ── 以 treeId/threadId/messageId 关联生成 attempt +branch_message_feedback ── 以 treeId/threadId/messageId 关联反馈 +``` + +当前不存在 Project 表,也不存在独立的 Thread、Message 表。`branch_trees.state` 是整棵树的持久化权威;`branch_generations` 是生成 attempt 的服务端 sidecar。新设计必须明确这些现有事实如何被 Project、Thread、Message 与 MessageRun 接替。 + +## Goals / Non-Goals + +**Goals:** + +- 将核心内容模型收敛为 `Project → Thread → Message`。 +- 让 Project 同时成为列表项、Thread 拓扑、标题、共享资源和永久删除边界。 +- 让 Project 在 MVP 中直接归属于当前登录用户,不预建尚不存在的团队/分组层。 +- 保留已确认的 sequence、Message replacement、BaseContext.messageIds、Fork 与 MessageRun 机制。 +- 给出从 `branch_trees`、嵌入实体和 `branch_generations` 迁移到规范化表的 PostgreSQL 目标 Schema 与模块骨架。 + +**Non-Goals:** + +- 本 change 不修改代码、数据库、API、前端或 `tasks.md`。 +- 不设计团队、成员、组织切换或平台级共享;出现真实需求后再独立建模。 +- 不完整设计 Project Memory、Instruction、Target 和 File 的内容协议、检索、版本或同步机制;本设计只固定它们的 Project 归属边界。 +- 不引入 Turn、Message Variant、Generation Chat 实体、通用 revision、V1 幂等系统或 ArtifactVersion。 + +## Decisions + +### D1. 从当前 User → Thread Tree 基线迁移到 User → Project + +目标所有权与聊天内容: + +```text +User +└── Project + ├── Project Resources + │ ├── Memory + │ ├── Instruction + │ ├── Target + │ ├── Files + │ └── Artifacts + └── Thread + ├── Message[] + └── Child Thread + └── Message[] +``` + +服务端从 Session 获得 `actorId`,并校验 Project 的 `ownerUserId` 或未来独立引入的访问授权;客户端只以 `projectId` 作为 ThreadChat 内容入口。这个设计与当前 `branch_trees.user_id` 的直接用户所有权一致,不凭空增加新的中间实体。 + +若未来出现团队协作,应该新增明确的 Project 成员或平台权限模型,并迁移 `ownerUserId`;不在当前领域模型里提前建立没有生命周期和 UI 的空壳层。 + +### D2. Project 是一整簇 Thread 的唯一聚合边界 + +```mermaid +erDiagram + USER ||--o{ PROJECT : owns + PROJECT ||--|{ THREAD : contains + THREAD o|--o{ THREAD : parent_of + THREAD ||--o{ MESSAGE : contains + MESSAGE o|--o| MESSAGE : replaces + MESSAGE ||--o| MESSAGE_RUN : executes + PROJECT ||--o{ PROJECT_MEMORY : shares + PROJECT ||--o{ PROJECT_FILE : shares + PROJECT ||--o{ ARTIFACT : shares + MESSAGE ||--o{ ARTIFACT : produces + + PROJECT { + uuid id PK + text owner_user_id FK + string title + text target + text instruction + } + THREAD { + uuid id PK + uuid project_id FK + uuid parent_thread_id FK + uuid source_message_id FK + jsonb base_context + } + MESSAGE { + uuid id PK + uuid thread_id FK + bigint sequence + string role + jsonb parts + uuid replaces_message_id FK + timestamptz superseded_at + } + MESSAGE_RUN { + uuid id PK + uuid assistant_message_id FK + string status + } + ARTIFACT { + uuid id PK + uuid project_id FK + uuid source_message_id FK + } +``` + +Project 的确定边界: + +- Project 有且仅有一个 Root Thread。 +- 其他 Thread 都通过 Parent/Fork 关系属于同一 Project。 +- “新对话”创建 Project + Root Thread。 +- “对话列表”列出 Projects。 +- `/thread-chat/{projectId}` 是 canonical 内容 URL。 +- Project 永久删除会清理整个 Thread 族群与共享资源。 + +Project 在目标模型中接替当前一条 `branch_trees` 记录所表达的产品边界,但不继承整树 JSON 的物理存储方式。迁移时,旧 `treeId` 只作为旧数据定位键或映射来源,不继续充当由客户端创建的新 Project ID。 + +### D3. Project Resource 共享范围与来源分离 + +Project Resource 的共同规则: + +```text +同一 Project 的全部 Thread 可按权限使用 +不同 Project 默认隔离 +同一用户拥有多个 Project 也不代表内容自动共享 +``` + +Target 与 Instruction 是 Project 当前配置;Memory 和 Files 是 Project 下的集合。Artifact 同时具有两种关系: + +```text +projectId 决定可用范围和生命周期 +sourceMessageId 记录产生来源 +``` + +因此 Artifact 不再“只属于 Message”。Message 提供 provenance,Project 提供 ownership。BaseContext 仍只保存 Message ID,不复制 Artifact 正文。 + +### D4. Thread 角色由关系推导 + +统一 Thread 实体通过关系决定角色: + +```text +parentThreadId = null → Root Thread +parentThreadId != null → Branch/Child Thread +``` + +Fork 不建立独立 `thread_forks` 表;Child Thread 直接保存: + +```ts +type ForkFacts = { + parentThreadId: string + sourceMessageId: string + forkSourceSnapshot: { + schemaVersion: 1 + quote?: string + sourceRole: "user" | "assistant" + sourceSequence: number + } + baseContext: BaseContextV1 +} +``` + +Project、Parent Thread、来源 Message 与 Child Thread 必须属于同一 Project。 + +### D5. sequence 表示 Thread 内服务端写入顺序 + +每个 Thread 的 Message 拥有唯一、单调递增的 `sequence`: + +```text +seq=1 user +seq=2 user +seq=3 assistant +seq=4 user +``` + +系统不要求角色交替,不保存 `prevMessageId/nextMessageId`,也不使用客户端时间排序。默认有效时间线为: + +```sql +SELECT * +FROM messages +WHERE thread_id = :thread_id + AND superseded_at IS NULL +ORDER BY sequence ASC; +``` + +replacement 获得新 sequence 并追加到尾部;旧 Message 保留原 sequence,通过 `supersededAt` 退出默认时间线。编辑最后一条有效 user Message 时,依赖旧内容的有效后缀一起 superseded,再追加 replacement 与新回复。这是上下文后缀失效,不是 user/assistant 配对。 + +完整流程见:[按 sequence 拉取 Thread 当前有效消息](./design/load-thread-messages-by-sequence.md)。 + +### D6. finalized Message 不可变,修改使用 replacement + +不可变边界: + +- user Message 的 `parts` 写入后立即不可改。 +- assistant 生成增量写入 `MessageRun.checkpointParts`。 +- assistant completed 时,最终 `parts` 与 `finalizedAt` 只写一次。 +- finalized Message 的 role、parts、sequence 与来源关系不可更新。 +- `supersededAt` 是时间线状态,不是内容修改。 + +Edit/Regenerate 必须创建新 Message;Regenerate 还必须创建该 assistant Message 唯一的新 MessageRun。旧 Message 不是孤儿:它仍属于原 Thread、保留 sequence,可被 BaseContext 与 Artifact provenance 引用。 + +未来实现必须在 Message Repository、Edit/Regenerate Command 与 Project 永久删除入口加入明确代码注释:finalized 内容不可覆盖,单 Message 不允许 hard delete。 + +完整 Regenerate 事务见:[创建 replacement assistant Message](./design/regenerate-replacement-assistant-message.md)。 + +### D7. BaseContext 只保存有序 messageIds + +```ts +type BaseContextV1 = { + schemaVersion: 1 + messageIds: string[] +} +``` + +Root Thread 没有 BaseContext。Branch Thread 的 BaseContext 由服务端在 Fork 时计算并永久冻结: + +```text +Child BaseContext += Parent.baseContext.messageIds ++ Parent 从开头到 sourceMessage 的有效且具备 Prompt 资格的 Message IDs +``` + +ID 顺序是 Prompt 顺序,不要求跨 Thread sequence 可直接比较。使用 ID 而不是 Parts 可以避免复制长文本、Tool Result 与 Artifact,并让 Parent replacement 后 Child 历史保持不变。 + +该方案依赖一个明确前提:单 Message 不 hard delete;只有 Project 永久删除时,引用它的全部 Child Thread 一并清理。 + +### D8. Fork 资格由前后端共同约束 + +| Message 状态 | 进入 BaseContext | 可作为 Fork source | +|---|---:|---:| +| 有效 user,finalized | 是 | 是 | +| 有效 assistant,completed | 是 | 是 | +| assistant,queued/running | 否 | 否 | +| assistant,failed/stopped | 否 | 否 | +| superseded,但已被旧 BaseContext 引用 | 保留既有引用 | 新 Fork 默认否 | + +最后一条 assistant 仍 queued/running 时,前端禁用 Fork,服务端仍做最终拒绝。Fork 事务校验来源、计算 BaseContext、生成 Child Thread ID,并原子保存全部 ForkFacts。Parent 后续变化不删除 Child,也不重算 BaseContext。 + +### D9. 每条 assistant Message 恰有一条 MessageRun + +```text +user Message → 0 MessageRun +assistant Message → 1 MessageRun +``` + +Regenerate 创建新的 assistant Message,因此也创建新的 MessageRun;不会为旧 Message 增加 attempt 2。 + +```mermaid +stateDiagram-v2 + [*] --> queued + queued --> running + queued --> failed + queued --> stopped + running --> completed + running --> failed + running --> stopped + completed --> [*] + failed --> [*] + stopped --> [*] +``` + +浏览器断开只停止订阅。刷新后通过 assistantMessageId、status、checkpointParts 与 eventSequence 恢复,不创建第二个 Run。完整流程见:[刷新后恢复正在生成的 assistant Message](./design/resume-running-message-after-refresh.md)。 + +### D10. PostgreSQL 目标 Schema + +字段使用 snake_case;所有新实体 ID 由服务端生成 UUID。目标表继续引用项目现有认证 `user.id`,不在本 change 新增其他所有权层。 + +#### 当前持久化到目标实体的对应关系 + +| 当前真实来源 | 目标 | 本 change 固定的边界 | +|---|---|---| +| `branch_trees` 一行 | 一个 Project | `user_id` 成为 `owner_user_id`;`title/custom_title` 迁移到 Project 标题;旧 `treeId` 只作迁移定位,不再由客户端生成新 ID | +| `branch_trees.state.threads` | `threads` 多行 | 每个嵌入 Thread 成为同一 Project 下的独立 row;`parentId/forkFromMsgId/anchorText` 转换为 Parent、source 与 Fork snapshot | +| `Thread.messages` 消息图 | `messages` 多行 | 当前有效路径迁移为默认时间线;所有保留节点都获得服务端 sequence,非默认节点如何标记 superseded 由持久化迁移 change 给出可验证算法 | +| `state.artifacts` | `artifacts` 多行 | 保留内容、来源 Thread/Message,并补 Project ownership | +| `branch_generations` | `message_runs` | 当前 generation 与目标 1:1 MessageRun 的转换必须显式处理历史 attempt;不得把多条 attempt 直接塞给同一 assistant Message | +| `branch_message_feedback` | 后续 Message feedback 表 | 使用迁移后的 Project/Thread/Message 映射重建关联 | + +历史 `branch_trees.user_id IS NULL` 是现有兼容数据,不自动归属于任意用户。迁移实施前必须先按当前“精确 URL 认领”规则完成归属,或把未认领记录隔离;不得猜测 owner。 + +#### `projects` + +| 字段 | 类型 | 语义 | +|---|---|---| +| `id` | uuid | PK | +| `owner_user_id` | text | FK → 现有 user.id,NOT NULL | +| `auto_title` | text | 机器派生标题 | +| `custom_title` | text | 用户标题,展示优先 | +| `target` | text/jsonb | Project 目标;协议后续细化 | +| `instruction` | text/jsonb | Project 指令;协议后续细化 | +| `archived_at` | timestamptz | 归档时间 | +| `created_at`, `updated_at` | timestamptz | NOT NULL | + +展示标题为 `coalesce(custom_title, auto_title)`。索引 `(owner_user_id, updated_at DESC)` 支持当前用户的对话列表和最近 Project。 + +#### `threads` + +| 字段 | 类型 | 语义 | +|---|---|---| +| `id` | uuid | PK | +| `project_id` | uuid | FK → projects,NOT NULL | +| `parent_thread_id` | uuid | 同 Project 自引用;Root 为 NULL | +| `source_message_id` | uuid | Branch 来源 Message;Root 为 NULL | +| `fork_source_snapshot` | jsonb | Branch 必填 | +| `base_context` | jsonb | Branch 必填,BaseContextV1 | +| `auto_title`, `custom_title` | text | Branch 局部标题 | +| `archived_at` | timestamptz | NULLABLE | +| `created_at`, `updated_at` | timestamptz | NOT NULL | + +关键约束:每个 `project_id` 只能有一个 `parent_thread_id IS NULL`;Root 的 Fork 字段全部为 NULL,Branch 全部非 NULL;Parent 与来源必须属于同一 Project。 + +#### `messages` + +| 字段 | 类型 | 语义 | +|---|---|---| +| `id` | uuid | PK | +| `thread_id` | uuid | FK → threads,NOT NULL | +| `sequence` | bigint | Thread 内服务端顺序 | +| `role` | text | user/assistant;不要求交替 | +| `parts` | jsonb | AI SDK v7 UIMessage.parts;运行中 assistant 可为 NULL | +| `replaces_message_id` | uuid | 同 Thread 自引用 | +| `superseded_at` | timestamptz | NULL 表示当前有效 | +| `finalized_at` | timestamptz | 内容封存时间 | +| `created_at` | timestamptz | NOT NULL | + +约束:`UNIQUE(thread_id, sequence)`;一条 Message 最多一个直接 replacement;replacement 必须同 Thread。 + +#### `message_runs` + +| 字段 | 类型 | 语义 | +|---|---|---| +| `id` | uuid | PK | +| `assistant_message_id` | uuid | UNIQUE FK → messages | +| `status` | text | queued/running/completed/failed/stopped | +| `model_id` | text | 实际执行模型 | +| `event_sequence` | bigint | 流恢复游标 | +| `checkpoint_parts` | jsonb | 运行中持久化内容 | +| `error_code`, `error_message` | text | 失败信息 | +| `heartbeat_at`, `stop_requested_at`, `finished_at` | timestamptz | 生命周期时间 | +| `created_at`, `updated_at` | timestamptz | NOT NULL | + +运行状态通过条件更新保护,不引入通用 revision。 + +#### Project 共享资源骨架 + +```text +project_memory_items + id PK + project_id FK + content JSONB + source_message_id FK NULLABLE + created_at / updated_at + +project_files + id PK + project_id FK + storage_key + filename / mime_type / size / status + created_at / updated_at + +artifacts + id PK + project_id FK + source_message_id FK NOT NULL + kind / title / content + created_at +``` + +这些表只固定 Project ownership。Memory 提取、File 存储协议、Artifact 编辑与版本化必须由后续 change 定义。 + +#### 删除策略 + +普通 Repository 不暴露单 Message hard delete。永久删除 Project 使用明确授权命令,在事务中清理 Project Resources、MessageRun、Message、Thread 和 Project。删除用户时由现有认证外键策略清理或触发其全部 Project 的删除流程。 + +### D11. 模块骨架 + +```text +thread-chat/ +├── domain/ +│ ├── project.ts +│ ├── project-resource.ts +│ ├── thread.ts +│ ├── message.ts +│ ├── message-run.ts +│ ├── base-context.ts +│ └── artifact.ts +├── application/ +│ ├── commands/ +│ │ ├── create-project.ts +│ │ ├── append-user-message.ts +│ │ ├── edit-last-user-message.ts +│ │ ├── regenerate-assistant-message.ts +│ │ ├── fork-thread.ts +│ │ └── delete-project-permanently.ts +│ ├── queries/ +│ │ ├── list-projects.ts +│ │ ├── load-project-bootstrap.ts +│ │ └── load-thread-messages.ts +│ └── prompt/ +│ └── resolve-prompt-history.ts +├── infrastructure/ +│ ├── repositories/ +│ ├── message-runner/ +│ └── events/ +└── transport/ + ├── http/ + └── stream/ +``` + +领域层不依赖 React、HTTP、Drizzle 或具体 AI Runtime。AI SDK v7 UIMessage.parts 是 Message 内容兼容契约;Runtime 更换只影响 adapter。 + +### D12. ProjectBootstrap 与伪代码索引 + +进入 `/thread-chat/{projectId}` 时,首屏加载: + +```text +全量 Project 的轻量 Thread topology + + +首屏可见 Thread 的有效 Messages 与 MessageRun 状态 +``` + +不全量加载所有 Branch Message、BaseContext 或大型 Project Resource。具体流程: + +1. [从首页进入 ThreadChat](./design/enter-thread-chat-from-home.md):最近 Project、新建 Project、ProjectBootstrap 与前端恢复。 +2. [按 sequence 拉取 Thread 当前有效消息](./design/load-thread-messages-by-sequence.md):`sequence + supersededAt` 构造默认时间线。 +3. [Regenerate:创建 replacement assistant Message](./design/regenerate-replacement-assistant-message.md):旧 Message 不可变,新 Message 与 Run 原子创建。 +4. [刷新后恢复正在生成的 assistant Message](./design/resume-running-message-after-refresh.md):checkpoint、eventSequence 与重新订阅。 + +## Risks / Trade-offs + +- **[未来需要团队共享 Project]** → 等成员、邀请、角色和所有权转移的真实生命周期明确后,再新增 Project 访问模型;MVP 保持与当前 `branch_trees.user_id` 一致的直接用户所有权。 +- **[Project 既是“对话”又是长期工作项,用户文案可能混淆]** → 代码与规范统一使用 Project;UI 可暂时显示“对话”,但交互测试明确其创建/列表对象是 Project。 +- **[BaseContext.messageIds 位于 JSONB,数组元素没有普通 FK]** → 禁止单 Message hard delete;Fork 时验证 ID,Project 删除时统一清理。 +- **[superseded Message 增加存储]** → MVP 接受追加历史,不预建 GC;Project 删除时统一清理。 +- **[Project Resource 协议尚未完整设计]** → 当前只固定 ownership 与隔离边界,Memory、Files 和 Artifact 演进分别走后续 change。 +- **[MessageRun checkpoint 与最终 Message 竞态]** → 完成事务条件更新 Run,并原子写入最终 parts、finalizedAt 与 completed。 + +## Migration Plan + +本 change 只更新设计。后续实施顺序: + +1. 归档新的 `domain`,确立 Project → Thread → Message 术语。 +2. 建立规范化持久化 change,定义 `branch_trees.state`、嵌入 Thread/Message/Artifact 与 `branch_generations` 到新表的迁移和校验。 +3. 建立 Project/Thread Command API 与 ProjectBootstrap change。 +4. 建立 MessageRun 与恢复流 change。 +5. 建立客户端 Project entities、Thread topology、generationStreams 与 UI selectors。 +6. 完成服务端集成、API 合同、Store/Hook 与端到端验收。 +7. 切换 `/thread-chat/{projectId}` 为唯一入口,再退役旧 ThreadTree/branch generation 权威路径。 + +切换前可以撤销新入口并保留旧读写路径;切换后的数据禁止回写旧整树 JSON。`tasks.md` 将在领域和配套设计全部确认后统一重写,本轮不更新。 diff --git a/openspec/changes/define-thread-chat-domain-model/proposal.md b/openspec/changes/define-thread-chat-domain-model/proposal.md new file mode 100644 index 00000000..20fd378f --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/proposal.md @@ -0,0 +1,43 @@ +## Why + +当前 ThreadChat 的真实权威模型是 `ThreadTreeState`:浏览器生成 `treeId`,`branch_trees` 以一行 `state JSONB` 保存整棵树,Thread、Message 与 Artifact 都嵌在该 JSON 中;生成执行则由 `branch_generations` 作为 sidecar 持久化。代码和数据库中目前没有 Project 实体,也没有独立的 Thread、Message 表。 + +整树读写把内容身份、拓扑、消息、运行状态和界面协作绑在同一个大对象里,已经成为后续分享、独立 API、服务端权限校验和前端增量数据流的共同阻碍。本 change 要从这个真实基线出发,建立规范化的 `Project → Thread → Message` 目标模型:Project 接替当前一棵 Thread Tree 的产品边界,Thread 与 Message 成为可独立寻址的服务端实体。 + +## What Changes + +- **BREAKING**:以 Project 取代当前 Thread Tree 作为一整簇分叉对话的聚合根、列表项、URL 身份与永久删除边界。 +- **BREAKING**:退出 `branch_trees.state` 整树 JSON 权威写入;将 Project、Thread、Message 与 MessageRun 规范化持久化。 +- **BREAKING**:新实体 ID 全部由服务端生成;客户端不再生成 `treeId` 或待创建的 Thread、Message、MessageRun ID。 +- MVP 沿用当前直接用户所有权:Project 通过 `ownerUserId` 归属于用户;本 change 不新建团队、成员或其他平台分组实体。 +- Project 成为一整簇 Thread 的唯一聚合边界,拥有且仅拥有一个 Root Thread,并拥有全部后代 Thread。 +- “新对话”创建新的 `Project + Root Thread`;当前 UI 的“对话列表”列出当前用户拥有或可访问的 Project。 +- Project 负责整簇 Thread 共享的 Memory、Instruction、Target、Files 与 Artifacts;精确资源协议由后续独立 change 定义。 +- Artifact 归属于 Project,同时保留来源 Message 身份,用于解释它由哪个 Thread 中的哪条消息产生。 +- Root Thread 与 Branch Thread 是统一 Thread 的关系角色;目标模型不保留当前 `MainThread`、`ForkedThread` 或整棵 `ThreadTreeState` 作为持久化实体。 +- Thread 内 Message 使用服务端分配的 `sequence` 形成线性追加历史,不要求 user/assistant 角色交替,也不沿用当前 `parentMessageId + activeLeafMessageId` 消息图作为目标时间线模型。 +- finalized Message 不允许原地改写;Edit 与 Regenerate 创建 replacement Message,并使用 `supersededAt` 与 `replacesMessageId` 让旧 Message 退出默认时间线。 +- Fork 仍由服务端创建 Child Thread,并保存 `parentThreadId`、`sourceMessageId`、`forkSourceSnapshot` 和只包含有序 `messageIds` 的 BaseContext。 +- Fork 只能使用 finalized 且具备 Prompt 资格的 Message;最后一条 assistant queued/running 时,前端禁用且后端拒绝 Fork。 +- 单条 Message 不执行 hard delete;只有永久删除整个 Project 时,统一清理其 Thread、Message、MessageRun 与 Project 附属资源。 +- 每条 assistant Message 对应一条持久化 MessageRun;它取代当前 `branch_generations` 的 attempt/current sidecar 语义。user Message 不具有 MessageRun,浏览器断开不停止后台运行。 +- 本 change 只更新领域规范与目标设计,不修改应用代码、数据库、API 或前端;`tasks.md` 按已确认约定暂不更新。 + +## Capabilities + +### New Capabilities + +- 无。 + +### Modified Capabilities + +- `domain`:从当前 `Thread Tree → embedded Thread → embedded Message` 基线迁移到规范化的 `Project → Thread → Message`,并明确 Fork、replacement、BaseContext 与 MessageRun 的目标边界。 + +## Impact + +- OpenSpec:重写 `domain` 增量规范和设计;后续持久化、API、MessageRun、客户端 Store 与退役 change 必须以当前 `ThreadTreeState / branch_trees / branch_generations` 为迁移起点。 +- 后端目标:新增 `projects`、`threads`、`messages`、`message_runs` 等规范化表;逐步退役 `branch_trees.state` 与 `branch_generations` 权威路径。 +- 前端目标:`/thread-chat/{projectId}` 加载 `ProjectBootstrap`;“新对话”创建 Project;“对话列表”列出 Projects;客户端 Store 从整树快照改为规范化实体与衍生拓扑。 +- 身份与权限:Project 先直接引用当前登录用户;所有读取和命令都从服务端 Session 校验用户对 Project 的访问权。 +- 共享资源:Project Memory、Instruction、Target、Files 和 Artifacts 对该 Project 的全部 Thread 可用,不跨 Project 自动共享。 +- 现有数据:迁移必须显式映射 `branch_trees.id/state/title/custom_title/user_id`、嵌入 Thread/Message/Artifact 以及对应 `branch_generations`;不得假设存在其他中间实体或表。 diff --git a/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md b/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md new file mode 100644 index 00000000..4fd957de --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md @@ -0,0 +1,220 @@ +## MODIFIED Requirements + +### Requirement: 使用统一的核心术语 +系统 MUST 使用以下术语表达目标模型: + +- **Project**:一整项可持续工作的内容聚合边界,直接归属于当前用户,拥有一个 Root Thread、全部后代 Thread,以及这些 Thread 共享的 Memory、Instruction、Target、Files 和 Artifacts。当前 UI 的一个“对话列表项”对应一个 Project。 +- **Thread**:Project 中一列可独立继续的线性对话。Root Thread 与 Branch Thread 只是关系角色,不是不同实体类型。 +- **Fork**:从已有 Thread 的确定 Message 创建 Child Thread 的原子操作。 +- **Message**:Thread 中具有稳定 ID、角色、内容和服务端 sequence 的消息实体。 +- **MessageRun**:仅为 assistant Message 持久化的后台生成记录;它属于运行基础设施,不是可独立导航的聊天内容实体。 +- **BaseContext**:Fork 时由服务端冻结的有序 Message ID 列表,表示 Child Thread 继承的有效 Prompt 历史。 +- **ForkSourceSnapshot**:Fork 时冻结的来源定位与引用展示信息。 +- **Project Resource**:在同一 Project 的全部 Thread 间共享、但不跨 Project 自动共享的 Memory、Instruction、Target、File 或 Artifact。 +- **Target**:Project 希望实现的目标集合,用于表达该 Project 的终极目标,以及为实现终极目标而设定的短期目标和中期目标。Target 属于 Project 级共享信息,不表示某个 Thread 的临时任务,也不等同于单条用户消息中的请求。 +- **Artifact**:归属于 Project,并保留来源 Message 身份的持久化产物。 +- **Title**:Project 或 Branch Thread 的短标题。 + +系统 MUST 将当前 Thread Tree 的聚合职责迁移到 Project,并将当前嵌在 `ThreadTreeState` 中的 Thread、Message 和 Artifact 迁移为可独立寻址的实体。目标规范、数据库、API 和客户端实体模型 MUST 使用 Project 表达整簇 Thread 的聚合边界,且 MUST NOT 继续把 Thread Tree、MainThread、ForkedThread、独立 ThreadFork、Turn、Generation 或 Message Variant 作为目标领域实体。 + +#### Scenario: 将一簇分叉对话称为 Project +- **WHEN** 用户查看由一个 Root Thread 和多列 Branch Thread 组成的工作项 +- **THEN** 系统 MUST 将整体领域实体称为 Project,并将每一列称为 Thread + +#### Scenario: 从关系推导 Thread 角色 +- **WHEN** 一个 Thread 没有 Parent Thread +- **THEN** 系统 MUST 将其视为 Root Thread +- **AND** 当一个 Thread 具有 Parent Thread 时,系统 MUST 将其视为 Branch Thread + +#### Scenario: 描述非根线程 +- **WHEN** 产品或代码需要描述由 Fork 创建的非根对话列 +- **THEN** 系统 MUST 将该节点称为 Child Thread 或按相对角色称为 Branch Thread +- **AND** 必须使用 Fork 描述创建动作,不得把 ForkedThread 作为独立实体类型 + +#### Scenario: 从当前 Thread Tree 建立 Project +- **WHEN** 系统迁移一条现有 `branch_trees` 记录 +- **THEN** 它 MUST 建立一个对应 Project,并把该记录中的 Thread、Message 和 Artifact 映射到该 Project 下的规范化实体 +- **AND** 不得把现有整树 JSON 误写成已经规范化的 Project 数据 + +### Requirement: 维护 Project 的 Thread 拓扑不变量 +系统 MUST 使用 Project 与 Thread 关系维护分叉拓扑: + +- 每个 Project MUST 归属于且仅归属于一个用户;其他访问关系不属于本 change。 +- 每个 Project MUST 有且仅有一个没有 Parent Thread 的 Root Thread。 +- 每个 Thread MUST 归属于且仅归属于一个 Project。 +- 每个非 Root Thread MUST 具有同一 Project 内的 Parent Thread、确定的来源 Message、ForkSourceSnapshot 和 BaseContext。 +- Child Thread MUST 可以继续产生自己的 Child Thread,形成任意深度的有向无环层级。 +- 系统 MUST 阻止跨 Project 的 Parent Thread、来源 Message 或 Fork 关系,并阻止形成环。 +- Fork MUST 由服务端原子创建 Child Thread 及全部来源事实;客户端不得构造 Child Thread 新 ID 或 BaseContext。 + +#### Scenario: 创建 Project 根 Thread +- **WHEN** 系统创建一个新 Project +- **THEN** 系统 MUST 在同一事务建立该 Project 唯一的 Root Thread +- **AND** Root Thread MUST 不包含 Parent Thread 或 Fork 来源 + +#### Scenario: 创建嵌套分叉 +- **WHEN** 用户从 Branch Thread 的一条合格 Message 再次 Fork +- **THEN** 系统 MUST 在同一 Project 中创建新的 Child Thread +- **AND** 新 Thread MUST 指向该 Branch Thread 与确定来源 Message + +#### Scenario: 拒绝跨 Project Fork +- **WHEN** Fork 来源 Thread 或来源 Message 不属于目标 Project +- **THEN** 系统 MUST 拒绝请求且不创建任何部分数据 + +#### Scenario: 拒绝 Fork 与拓扑矛盾的状态 +- **WHEN** 非 Root Thread 缺少 Parent Thread、来源 Message、ForkSourceSnapshot 或 BaseContext,或者关系会形成环 +- **THEN** 系统 MUST 拒绝该 Project 状态 + +#### Scenario: Fork 原子失败 +- **WHEN** ForkSourceSnapshot、BaseContext 或 Child Thread 中任一项无法持久化 +- **THEN** 系统 MUST 回滚整个 Fork 操作 + +### Requirement: 明确标题的归属与优先级 +系统 MUST 区分 Project 标题与 Branch Thread 标题: + +1. Project 标题描述整项工作,同时作为 Project 列表项和 Root Thread 列头的展示标题。 +2. Branch Thread 标题描述局部主题,仅覆盖对应列的标题展示。 +3. 用户账户展示名称不得替代 Project 或 Thread 标题。 + +#### Scenario: 展示 Root Thread 标题 +- **WHEN** 客户端展示 Project 的 Root Thread +- **THEN** 客户端 MUST 使用 Project 标题作为该列标题 + +#### Scenario: 主线自动标题与用户重命名并存 +- **WHEN** Project 已生成自动标题,且用户随后设置自定义 Project 标题 +- **THEN** Project 列表和 Root Thread 列头 MUST 展示自定义标题 +- **AND** 系统 MUST 保留自动标题作为机器派生信息 + +#### Scenario: 展示 Branch Thread 标题 +- **WHEN** 客户端展示具有局部标题的 Branch Thread +- **THEN** 客户端 MUST 使用该 Thread 标题 +- **AND** 不得因此修改 Project 标题 + +## ADDED Requirements + +### Requirement: 使用 Project 作为对话列表与共享资源边界 +“新对话”操作 MUST 创建新的 Project 与唯一 Root Thread;“对话列表” MUST 列出当前用户拥有或可访问的 Project。 + +Project 的 Memory、Instruction、Target、Files 和 Artifacts MUST 可供该 Project 的全部 Thread 使用,并 MUST NOT 仅因多个 Project 属于同一用户而自动跨 Project 共享。 + +#### Scenario: 创建新对话 +- **WHEN** 用户点击“新对话” +- **THEN** 系统 MUST 创建新的 Project 与唯一 Root Thread +- **AND** 导航到以服务端 Project ID 标识的 ThreadChat 页面 + +#### Scenario: 展示对话列表 +- **WHEN** 用户打开“对话列表” +- **THEN** 系统 MUST 展示当前用户拥有或可访问的 Projects + +#### Scenario: Thread 使用 Project 资源 +- **WHEN** Project 中任一 Thread 构建允许使用 Project 上下文的请求 +- **THEN** 系统 MUST 允许它引用该 Project 的 Memory、Instruction、Target、Files 和 Artifacts + +#### Scenario: Project 资源隔离 +- **WHEN** 另一个 Project 的 Thread 未获得显式授权 +- **THEN** 系统 MUST NOT 自动向它提供当前 Project 的共享资源 + +### Requirement: 维护 Thread 的线性消息顺序 +系统 MUST 将每个 Thread 内的 Message 保存为严格线性的追加序列。服务端 MUST 为新 Message 分配在该 Thread 内单调递增且唯一的 `sequence`;客户端 MUST 使用 `sequence` 而不是客户端时间、角色交替或前后消息指针恢复稳定顺序。 + +系统 MUST 允许连续多条 user Message,且 MUST NOT 将 `user → assistant` 一一配对作为数据库不变量。“只允许编辑当前最后一条有效 user Message”是 MVP 应用策略,不代表 user Message 必须拥有配对的 assistant Message。 + +#### Scenario: 连续发送多条 user Message +- **WHEN** 用户在 assistant 尚未产生最终回复前又发送一条 user Message +- **THEN** 系统 MUST 将两条 user Message 保存为同一 Thread 内具有不同 sequence 的独立 Message +- **AND** 不得仅因角色没有交替而拒绝或重排它们 + +#### Scenario: 按服务端顺序读取消息 +- **WHEN** 客户端读取一个 Thread 的消息 +- **THEN** 系统 MUST 按 sequence 升序返回有效时间线 +- **AND** 相同 Thread 内不得存在重复 sequence + +### Requirement: 使用不可变 Message replacement +Message 内容在创建完成或 finalized 后 MUST NOT 原地改写。Edit 和 Regenerate MUST 创建具有新 ID 与新 sequence 的 replacement Message,通过 `replacesMessageId` 指向旧 Message,并将旧 Message 标记为 `superseded`。 + +默认时间线 MUST 隐藏 superseded Message,但持久化层 MUST 保留其 ID、sequence、内容和来源关系。MVP MUST NOT 提供 Message Variant,也 MUST NOT hard delete 单条 Message;只有永久删除整个 Project 时,系统才 MUST 统一清理 Project 下的 Thread、Message、MessageRun 和附属资源。 + +#### Scenario: Regenerate assistant 回复 +- **WHEN** 用户对当前可重新生成的 assistant Message 执行 Regenerate +- **THEN** 系统 MUST 创建新的 assistant Message 与新的 MessageRun +- **AND** 旧 assistant Message MUST 被标记为 superseded,但其 ID、sequence 和内容 MUST 保持不变 + +#### Scenario: 编辑最后一条 user Message +- **WHEN** 用户编辑当前 Thread 最后一条有效 user Message +- **THEN** 系统 MUST 创建 replacement user Message +- **AND** 原 Message 及所有 sequence 更大、依赖原内容的有效 Message MUST 退出默认时间线但继续保留 +- **AND** replacement user Message 及后续新回复 MUST 以新 sequence 追加到 Thread 尾部 + +#### Scenario: 拒绝编辑历史 user Message +- **WHEN** 用户尝试编辑并非当前 Thread 最后一条有效 user Message 的消息 +- **THEN** 系统 MUST 按 MVP 策略拒绝操作,并提示使用 Fork 保留另一条历史 + +#### Scenario: 永久删除 Project +- **WHEN** 已获授权的用户确认永久删除整个 Project +- **THEN** 系统 MUST 将该 Project 的共享资源、Thread、Message 和 MessageRun 作为完整边界清理 + +### Requirement: 冻结 Fork 的消息身份上下文 +Fork 时,服务端 MUST 根据来源 Thread 在 Fork 点之前的有效 Prompt 历史生成不可变 BaseContext。BaseContext MUST 包含 schema 版本和按 Prompt 顺序排列的 `messageIds`,不得复制 Message Parts,也不得由客户端提交或重建。 + +进入 BaseContext 或作为 Fork source 的 Message MUST 已 finalized 且具备 Prompt 资格。有效 user Message 可以进入;仅 completed assistant Message 可以进入并作为来源;queued、running、failed 或 stopped assistant Message 不得进入,也不得作为 Fork source。Parent 后续发生 replacement、追加或归档时,既有 Child Thread 的 BaseContext MUST 保持不变。 + +#### Scenario: 从 completed assistant Message Fork +- **WHEN** 用户从 finalized 且 completed 的有效 assistant Message 发起 Fork +- **THEN** 服务端 MUST 在同一 Project 创建 Child Thread +- **AND** BaseContext MUST 冻结到来源 Message 为止的有序有效 Message ID + +#### Scenario: 生成期间不允许 Fork +- **WHEN** 当前 Thread 最后一条 assistant Message 的 MessageRun 处于 queued 或 running +- **THEN** 客户端 MUST 隐藏或禁用 Fork +- **AND** 服务端 MUST 拒绝绕过客户端发起的 Fork 请求 + +#### Scenario: Parent Message 后续被 replacement +- **WHEN** Child Thread 的 BaseContext 引用了之后被 superseded 的 Parent Message +- **THEN** Child Thread MUST 继续按原 Message ID 解析冻结历史 +- **AND** replacement Message MUST NOT 自动替换 BaseContext 中的 ID + +#### Scenario: 客户端试图提交 BaseContext +- **WHEN** Fork 请求包含客户端构造的 BaseContext 或待创建 Child Thread ID +- **THEN** 服务端 MUST 忽略或拒绝这些字段,并只使用服务端计算和生成的值 + +### Requirement: 持久化 assistant MessageRun +每条 assistant Message MUST 具有且仅具有一条持久化 MessageRun,user Message MUST NOT 具有 MessageRun。MessageRun MUST 至少表达 queued、running、completed、failed 和 stopped 状态,并保存恢复流式展示所需的运行进度。 + +浏览器刷新、关闭或流连接断开 MUST 只终止该客户端订阅,不得自动停止后台 MessageRun。客户端重新加载 Thread 时,系统 MUST 通过 assistant Message 及其 MessageRun 恢复最终内容、生成中、失败或停止状态。 + +#### Scenario: 创建 assistant Message +- **WHEN** 服务端接受一次需要 AI 回复的生成命令 +- **THEN** 服务端 MUST 在同一原子边界创建 assistant Message 与唯一 MessageRun + +#### Scenario: 刷新后恢复运行状态 +- **WHEN** assistant Message 的 MessageRun 仍为 queued 或 running 且用户刷新页面 +- **THEN** 客户端 MUST 加载该状态并恢复生成展示与事件订阅 +- **AND** 刷新不得创建第二条 MessageRun + +#### Scenario: 队列启动失败 +- **WHEN** MessageRun 在进入 running 前无法启动 +- **THEN** 系统 MUST 允许它从 queued 转为 failed + +### Requirement: 维护 Project 共享的 Artifact +Artifact MUST 归属于且仅归属于一个 Project,并 MUST 保留产生它的来源 Message 身份。该 Project 的全部 Thread MUST 能按权限引用 Artifact;其他 Project MUST NOT 仅因归属于同一用户而自动获得访问权。 + +BaseContext MUST 通过 Message ID 间接保留 Artifact 的来源语义,不得复制大型 Artifact 内容。MVP MUST NOT 因本重构引入 ArtifactVersion、通用资源图或内容寻址存储。 + +#### Scenario: 同 Project 的 Thread 使用 Artifact +- **WHEN** Project 中一个 Thread 的 Message 产生 Markdown Artifact +- **THEN** 同 Project 的其他 Thread MUST 能按权限引用该 Artifact +- **AND** 系统 MUST 保留 sourceMessageId 作为来源 + +#### Scenario: Fork 历史包含 Artifact +- **WHEN** BaseContext 引用的 Message 产生过 Artifact +- **THEN** 系统 MUST 能通过 Message ID 解析其 Project Artifact 关联 +- **AND** BaseContext 不得复制 Artifact 正文 + +#### Scenario: 隔离其他 Project +- **WHEN** 另一个 Project 未获得显式授权 +- **THEN** 系统 MUST NOT 向它暴露当前 Project 的 Artifact + +## RENAMED Requirements + +- FROM: `### Requirement: 维护线程树的层级不变量` +- TO: `### Requirement: 维护 Project 的 Thread 拓扑不变量` diff --git a/openspec/changes/design-thread-chat-client-api/.openspec.yaml b/openspec/changes/design-thread-chat-client-api/.openspec.yaml new file mode 100644 index 00000000..e685d45e --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-25 diff --git a/openspec/changes/design-thread-chat-client-api/design.md b/openspec/changes/design-thread-chat-client-api/design.md new file mode 100644 index 00000000..adb533bc --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/design.md @@ -0,0 +1,1363 @@ +## Context + +本设计依赖 `define-thread-chat-domain-model` 已确认的服务端领域事实:Project 是一整簇 Thread 的聚合边界,Thread 内 Message 以服务端 sequence 构成线性时间线,Edit/Regenerate 使用 replacement,Fork 的 BaseContext 由服务端冻结,每条 assistant Message 恰有一条 MessageRun。 + +当前前端的真实基线是: + +```text +ThreadTreeState 整棵树 + ↓ +createThreadStore(原地修改) + ↓ +全局 version++ + ↓ +所有订阅者重新读取整树 + +chat-controller 同时承担: +客户端实体 ID + 整树持久化屏障 + Prompt 拼装 ++ Command 协调 + SSE 消费 + Store 修改 +``` + +当前实现中有四个应保留的方向:`useThreadChatRuntime` 作为页面组合根、headless 纯 selector、细分 `net/commands`、流式 delta 合帧。需要退出的是整树权威、客户端实体 ID、全局 version 粗粒度订阅和 controller 职责混合。 + +## Goals / Non-Goals + +**Goals:** + +- 定义前端实体、运行态、UI 态和 ViewModel 的唯一边界。 +- 定义 ThreadChatAppStore 与 Project-scoped ThreadChatProjectStore 的 Zustand 结构。 +- 定义 selector、Store Action、Application Command、Hook 与 UI 的依赖方向。 +- 定义 `/thread-chat/new` 和 `/thread-chat/{projectId}` 的完整生命周期。 +- 给出后端 `/api/v1` 所需的全部 MVP Query、Command、参数关系、响应 DTO 和可手动测试案例。 +- 让 API 契约既服务 Web UI,也能作为未来 CLI/MCP/Token API 的基础。 + +**Non-Goals:** + +- 不实现 Store、Hook、Route Handler、数据库或迁移。 +- 不确定具体 Transport 类、HTTP 库、SSE parser、重试器和认证包装代码。 +- 不实现离线命令队列、通用 Idempotency-Key、跨设备草稿、协同编辑或 Message Variant。 +- 不重新设计服务端数据库 Schema、BaseContext 或 PromptBuilder。 +- 不在本 change 编写 tasks;等本设计和 API 经过多轮确认后统一拆分。 + +## Decisions + +### D1. 客户端只有一份服务端事实 + +数据依赖方向固定为: + +```mermaid +flowchart LR + UI[UI Components] --> Hooks[Selector / Command Hooks] + Hooks --> Commands[Application Commands] + Hooks --> LocalActions[Local UI Store Actions] + Commands --> API[API Capabilities] + API --> Server[Server] + Server --> DTO[Validated DTO] + DTO --> Commands + Commands --> StoreActions[Store Actions] + LocalActions --> StoreActions + StoreActions --> Store[ThreadChatAppStore / ThreadChatProjectStore] + Store --> Selectors[Pure Selectors] + Selectors --> Hooks +``` + +entities slice 只保存服务端确认 DTO。生成流和 UI 可以暂时领先于最终实体,但不得伪造实体身份。Application Command 是唯一允许编排 API 结果的边界;它必须调用 Store Action 写入 Store。Transport、Command 和 React 组件都不直接调用 Zustand `set`/`setState`。 + +弃选:保存服务端实体后再构造一份可修改 `ThreadTreeState`。它会重新产生双向同步、整树覆盖和身份漂移问题。 + +### D2. 前端实体模型 + +```ts +/** 所有服务端实体 ID 对客户端都是不透明字符串;客户端不得解析或自行构造。 */ +type ProjectId = string +type ThreadId = string +type MessageId = string +type ArtifactId = string + +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue } + +type ProjectTarget = { + /** Project 最终希望达到的结果;不是当前任务标题,也不是完成状态。 */ + ultimate: string | null + + /** 当前阶段应优先完成的目标集合;MVP 不为单项分配 Goal ID。 */ + shortTerm: string[] + + /** 连接短期工作与终极目标的阶段性目标集合。 */ + midTerm: string[] +} + +type ProjectEntity = { + id: ProjectId + + /** 服务端授权归属;客户端只读取,绝不能通过请求修改。 */ + ownerUserId: string + + /** 服务端根据内容生成的回退标题。 */ + autoTitle: string | null + + /** 用户显式设置的标题;非空时展示优先级高于 autoTitle。 */ + customTitle: string | null + + /** 整个 Project 及其全部 Thread 共享的目标。 */ + target: ProjectTarget | null + + /** 构造该 Project 内模型请求时统一应用的 Project 指令。 */ + instruction: string | null + + /** 非空表示从默认 Project 列表隐藏,但内容和 Thread 仍然保留。 */ + archivedAt: string | null + createdAt: string + updatedAt: string +} + +type ForkSourceSnapshot = { + /** 允许未来升级快照形状;不是 Project/Thread revision。 */ + schemaVersion: 1 + + /** Fork 时用于来源展示的冻结引用文本;来源以后变化也不回写。 */ + quote?: string + + /** Fork 当时来源 Message 的角色,用于稳定展示和审计。 */ + sourceRole: "user" | "assistant" + + /** Fork 当时来源 Message 在 Parent Thread 内的 sequence。 */ + sourceSequence: number +} + +type ThreadEntity = { + id: ThreadId + + /** Thread 所属聚合边界;Parent、来源 Message 和 Child 必须同 Project。 */ + projectId: ProjectId + + /** null 表示 Project 唯一 Root;非 null 表示 Branch/Child Thread。 */ + parentThreadId: ThreadId | null + + /** Branch 的 Fork 来源 Message;Root 必须为 null。 */ + sourceMessageId: MessageId | null + + /** Branch 创建时冻结的来源展示事实;Root 必须为 null。 */ + forkSourceSnapshot: ForkSourceSnapshot | null + + /** 服务端生成的 Branch 回退标题;Root 的展示标题来自 Project。 */ + autoTitle: string | null + + /** 用户设置的 Branch 标题;不得为 Root 建立第二套标题权威。 */ + customTitle: string | null + + /** 非空只改变 Branch 的默认导航可见性,不删除 Message 或 Child。 */ + archivedAt: string | null + createdAt: string + updatedAt: string +} + +type MessageEntity = { + id: MessageId + + /** Message 只属于一个 Thread;跨 Thread 上下文由服务端 BaseContext 解析。 */ + threadId: ThreadId + + /** 服务端分配的 Thread 内单调顺序;可有间隙,replacement 后不得重排。 */ + sequence: number + + role: "user" | "assistant" + + /** AI SDK v7 UIMessage.parts;运行中 assistant 可为 null,最终内容只封存一次。 */ + parts: UIMessage["parts"] | null + + /** 当前 Message 替代的旧 Message;replacement 仍然是追加的新实体。 */ + replacesMessageId: MessageId | null + + /** 非空表示该 Message 已退出默认有效时间线,但仍保留内容、sequence 和引用。 */ + supersededAt: string | null + + /** 非空表示内容已经封存;封存后的 parts 不允许原地覆盖。 */ + finalizedAt: string | null + createdAt: string +} + +type ArtifactEntity = { + id: ArtifactId + + /** Artifact 对整个 Project 可用,不只属于产生它的 Thread。 */ + projectId: ProjectId + + /** 产生该 Artifact 的 Message,用于 provenance 和 BaseContext 间接解析。 */ + sourceMessageId: MessageId + + /** Artifact 语义类型;具体 kind 协议由后续 Artifact change 固定。 */ + kind: string + title: string + + /** false 表示本响应只返回元数据/引用,不能据 content=null 推断资源为空。 */ + contentIncluded: boolean + + /** 仅在 contentIncluded=true 时有值;大型正文允许按需加载。 */ + content: JsonValue | null + createdAt: string +} + +type AssistantRunState = { + /** 客户端关联键;不暴露服务端内部 MessageRun ID。 */ + assistantMessageId: MessageId + + /** 生成生命周期;它不是 Message 自身的 status 字段。 */ + status: "queued" | "running" | "completed" | "failed" | "stopped" + + /** 服务端实际接受并执行的模型,可能不同于客户端 requestedModelId。 */ + modelId: string + + /** 已持久化的运行中内容;刷新后先用它恢复画面,再续接事件。 */ + checkpointParts: UIMessage["parts"] + + /** 该 assistant Run 内严格递增的恢复游标,不是 Thread revision。 */ + eventSequence: number + + /** 仅保存可展示/可判断的结构化失败,不保存服务端堆栈。 */ + error: { code: string; message: string } | null + + /** 非空表示 Stop 已被服务端接受;不代表 Run 已进入 stopped 终态。 */ + stopRequestedAt: string | null + + /** completed/failed/stopped 的终态时间;queued/running 为 null。 */ + finishedAt: string | null +} +``` + +关键约束: + +- `ProjectTarget` 先只表达终极、短期和中期目标,不引入 Goal ID、进度或截止时间。 +- `Message.parts` 严格使用项目 AI SDK v7 UIMessage parts 协议。 +- Message 不存 `status`;展示状态由 `finalizedAt/supersededAt + AssistantRunState` 派生。 +- Thread 不存 `children/depth/isRoot/breadcrumb`;全部从 Parent 关系派生。 +- AssistantRunState 是前端运行视图,不是可导航 Chat Entity。 +- BaseContext、Prompt History 和服务端 MessageRun ID 不进入普通客户端实体模型。 + +### D3. 两种 Store 生命周期,而不是 Store 树 + +“App 级”和“Project 级”表示生命周期,不表示父 Store 内嵌子 Store。两者都是独立 vanilla Zustand Store 实例,通过 Provider/Runtime 组合。 + +#### 共同请求状态 + +```ts +type LoadState = + | { status: "idle" } + | { status: "loading" } + | { status: "ready" } + | { status: "error"; error: ClientError } + +type CommandState = + | { status: "submitting" } + | { status: "error"; error: ClientError } + +type ThreadMessageWindowState = { + /** 该 Thread 的 Message bundle 是否已经加载。 */ + loadState: LoadState + + /** 服务端是否还有更早有效 Message;MVP 只保留能力,不自动翻页。 */ + hasOlderMessages: boolean + + /** 当前已合并窗口的 sequence 边界;空 Thread 时均为 null。 */ + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} +``` + +`commandByScope` 中不存在 key 表示当前没有提交或错误;成功后删除 key,失败时保留 error 供 UI 展示。`threadMessagesById` 中不存在 Thread key 表示从未请求,不能与“已成功加载但返回空 Message”混淆。 + +#### App 级 ThreadChatAppStore + +```ts +type ProjectSummary = { + id: ProjectId + + /** 已应用 customTitle > autoTitle 回退规则,可直接用于列表。 */ + displayTitle: string + + /** 用于 active/archived 列表过滤。 */ + archivedAt: string | null + + /** 用于 `updatedAt DESC, id DESC` 的稳定最近访问排序。 */ + updatedAt: string + + /** 列表统计,不携带 Thread topology 或 Message 正文。 */ + threadCount: number + messageCount: number +} + +type ProjectCatalogState = { + /** 服务端确认的轻量 Project 摘要;不保存 Project 完整实体。 */ + projectsById: Record + + /** 当前列表查询顺序;与 projectsById 分离以避免复制摘要内容。 */ + orderedProjectIds: ProjectId[] + + /** 首次加载、刷新或加载下一页的整体状态。 */ + loadState: LoadState + + /** 当前列表查询条件;不是当前选中的 Project。 */ + activeFilter: "active" | "archived" + + /** 服务端签发的下一页不透明游标;null 表示没有下一页。 */ + nextCursor: string | null +} + +type AppShellUiState = { + /** 左侧 Project 导航是否展开;只影响当前设备布局。 */ + sidebarOpen: boolean + + /** 用户调整后的 Sidebar 像素宽度;与服务端 Project 内容无关。 */ + sidebarWidth: number + + /** 仅用于客户端过滤已加载 ProjectSummary,不写回服务端。 */ + projectSearchQuery: string + + /** + * 用户已经点击并开始路由跳转、但目标 ProjectProvider 尚未就绪的 Project。 + * 它只驱动列表 loading/防重复点击;当前 Project 的唯一权威仍是 URL。 + * 路由完成、失败或被取消时必须清为 null。 + */ + pendingProjectId: ProjectId | null +} + +type ThreadChatAppState = { + /** Project 列表的服务端确认摘要和查询窗口。 */ + catalog: ProjectCatalogState + + /** 跨 Project 页面持续存在、但不属于任何 Project 的 App 外壳 UI。 */ + shellUi: AppShellUiState +} + +type ThreadChatAppActions = { + mergeProjectPage: (result: ListProjectsResult) => void + upsertProjectSummary: (summary: ProjectSummary) => void + removeProjectSummary: (projectId: ProjectId) => void + setCatalogFilter: (filter: ProjectCatalogState["activeFilter"]) => void + setProjectRoutePending: (projectId: ProjectId | null) => void + setSidebarOpen: (open: boolean) => void + setSidebarWidth: (width: number) => void + setProjectSearchQuery: (query: string) => void +} + +type ThreadChatAppStore = ThreadChatAppState & ThreadChatAppActions +``` + +`ThreadChatAppStore` 取代原先只描述一半的 `ProjectCatalogStore`:Catalog 是服务端列表摘要 slice,AppShellUi 是跨 Project 的本地外壳 slice。这里仍然没有 `selectedProjectId`;Sidebar 选中态必须由 `routeProjectId` 派生。 + +“App 级”不等于模块级服务器单例。Next.js 下必须由 `ThreadChatAppProvider` 创建 vanilla Store,避免跨请求共享状态。 + +无 `initialCatalog` 时,App Store 从空 `projectsById/orderedProjectIds`、`loadState=idle`、`nextCursor=null` 初始化;AppShellUi 使用产品默认 Sidebar 设置,且 `pendingProjectId=null`。 + +#### Project 级 ThreadChatProjectStore + +```ts +type ThreadChatEntitiesState = { + /** Bootstrap 前为 null;合并后必须与 Provider 的 projectId 一致。 */ + project: ProjectEntity | null + + /** 当前 Project 的全量轻量 topology,以及已按命令新增的 Thread。 */ + threadsById: Record + + /** 已加载 Message 的唯一内容表;可以包含已 superseded 的历史实体。 */ + messagesById: Record + + /** + * Thread → 已加载 Message ID 的查询索引,不复制 Message 内容。 + * normalizer 负责去重并按 sequence 排序;有效时间线再由 selector 过滤 supersededAt。 + */ + messageIdsByThreadId: Record + + /** 仅保存 Bootstrap/Message bundle 已包含或用户按需加载的 Artifact。 */ + artifactsById: Record +} + +type StreamBuffer = { + /** 尚未在下一次 UI flush 中合入 checkpoint 的 AI SDK v7 增量事件。 */ + pendingChunks: UIMessageChunk[] + + /** 已接收的最新事件游标,用于丢弃重复或倒序 delta。 */ + lastReceivedEventSequence: number + + /** 是否已经安排 requestAnimationFrame/批量 flush,防止每 token setState。 */ + flushScheduled: boolean +} + +type ThreadChatRunsState = { + /** 每条 assistant Message 的权威运行视图;内部 MessageRun ID 不进入客户端。 */ + byAssistantMessageId: Record + + /** 只在生成期间存在的高频缓冲;不得持久化到 localStorage。 */ + streamBuffersByAssistantMessageId: Record +} + +type ThreadChatRequestsState = { + /** 当前 ProjectBootstrap 的请求状态。 */ + bootstrap: LoadState + + /** 每个 Thread 的加载状态和已合并窗口边界,避免重复请求。 */ + threadMessagesById: Record + + /** + * 当前页面的命令 busy/error 状态,key 由命令名和既有资源 ID 构成, + * 例如 `send:${threadId}`、`regenerate:${messageId}`;它不是网络级幂等记录。 + */ + commandByScope: Record +} + +type ThreadColumnSlot = { + /** 槽位当前展示的 Branch Thread;Root 固定在主列,不进入 slots。 */ + threadId: ThreadId + + /** true 表示折叠为窄条,保留位置但不计入展开列上限。 */ + folded: boolean +} + +type TextSelectionState = { + /** 发生选区的既有 Message。 */ + messageId: MessageId + + /** 用户实际选择的规范文本,用作 Fork anchor 输入而不是服务端历史事实。 */ + exactQuote: string + + /** 在规范文本投影中的 UTF-16 [start,end);无法稳定定位时允许省略。 */ + textPosition?: { start: number; end: number } +} + +type OverlayState = { + /** 当前文本选区/Fork 气泡的语义输入;DOM Rect 和动画状态留在组件局部。 */ + selection: TextSelectionState | null + + /** Thread 切换器当前作用域;DOM 坐标和退场动画仍留在组件局部状态。 */ + threadSwitcherScope: + | { kind: "global" } + | { kind: "column"; threadId: ThreadId } + | { kind: "subtree"; rootThreadId: ThreadId } + | null + + treeListOpen: boolean + helpPanelOpen: boolean + artifactDrawerOpen: boolean +} + +type ThreadChatUiState = { + /** + * Root 右侧的有序 Branch 槽位及折叠态,是分栏布局的唯一权威。 + * `visibleThreadIds` 必须由它派生,不再作为第二份 State 保存。 + */ + columnSlots: ThreadColumnSlot[] + + /** + * 当前接收全局快捷键/工具栏命令的 Thread。 + * 它必须是 Root 或一个未折叠 column slot;ThreadColumn 自身命令仍使用自己的 threadId。 + */ + focusedThreadId: ThreadId | null + + /** 用户显式调整的展开列宽;无条目表示自动均分,折叠细条不读取此值。 */ + columnWidths: Record + + /** null 表示根据视口自适应;非 null 是用户强制的展开列数量上限。 */ + forceColumnCount: number | null + + /** replace 在列满时替换一个槽;fold 保留槽位并把一列折叠为细条。 */ + placementMode: "replace" | "fold" + + /** columns 用于并排深读;canvas 用于查看整个 Thread topology。 */ + viewMode: "columns" | "canvas" + + /** Canvas 中 Thread 的设备本地坐标;不存在条目的 Thread 使用自动布局。 */ + canvasPins: Record + + /** 每个 Thread 独立的未发送输入;切换列时不丢失草稿。 */ + composerDraftByThreadId: Record + + /** 当前 Artifact Drawer 的目标;Drawer 关闭后可以保留,便于重新打开。 */ + selectedArtifactId: ArtifactId | null + + /** 支撑列满 LRU 策略的本地逻辑时钟;不是服务端活动时间。 */ + activationClock: number + + /** Thread 最近一次获得焦点/被打开时的逻辑顺序,用于选择替换或折叠候选。 */ + lastActivatedOrderByThreadId: Record + + /** 只保存跨组件需要共享的语义开关;DOM 引用、坐标和动画计数不进 Store。 */ + overlays: OverlayState +} + +type ThreadChatProjectState = { + /** 服务端确认的 Project/Thread/Message/Artifact 事实。 */ + entities: ThreadChatEntitiesState + + /** assistant Message 的运行视图和未 flush 流缓冲。 */ + runs: ThreadChatRunsState + + /** Query/Command 的客户端请求状态;不是服务端领域状态。 */ + requests: ThreadChatRequestsState + + /** 当前设备上的 Project 工作台布局和交互焦点。 */ + ui: ThreadChatUiState +} + +type ThreadChatProjectActions = { + mergeCreationBundle: (bundle: CreationBundle) => void + mergeBootstrap: (bootstrap: ProjectBootstrap) => void + applyMessageBundle: (bundle: ThreadMessageBundle) => void + applyMessageCreationBundle: (bundle: MessageCreationBundle) => void + applyThreadCreated: (thread: ThreadEntity) => void + applyReplacementBundle: (bundle: ReplacementBundle) => void + applyRunEvent: (event: AssistantMessageEvent) => void + setCommandState: (scope: string, state: CommandState | null) => void + openThread: (threadId: ThreadId, sourceThreadId: ThreadId | null) => void + closeThread: (threadId: ThreadId) => void + focusThread: (threadId: ThreadId) => void + setColumnWidth: (threadId: ThreadId, width: number | null) => void + setPlacementMode: (mode: ThreadChatUiState["placementMode"]) => void + setViewMode: (mode: ThreadChatUiState["viewMode"]) => void +} + +type ThreadChatProjectStore = + & ThreadChatProjectState + & ThreadChatProjectActions +``` + +同一个 Project Store 内使用 slices,可以由一个 Store Action 通过一次 `set` 同时更新 Message、Run 和 request state;这不是把 slices 物理拆成多个 Store。组件通过细粒度 selector 订阅。 + +`columnSlots` 取代原先不充分的 `visibleThreadIds` State:fold 模式必须保存槽位折叠态。`selectVisibleThreadIds` 和 `selectVisibleColumns` 从 Root + `columnSlots` 派生,避免两份分栏权威。 + +Project Store 初始化和分栏变更必须保持: + +- 空 Runtime 从 `project=null`、空实体表、`bootstrap=idle`、空 `columnSlots` 和 `focusedThreadId=null` 初始化。 +- Bootstrap 后 `focusedThreadId` 默认是 Root;Root 固定渲染,不进入 `columnSlots`。 +- `columnSlots.threadId` 必须唯一、属于当前 Project 且不是 Root;恢复 localStorage 时过滤失效 ID。 +- `focusedThreadId` 非 Root 时必须指向未折叠槽位;打开/展开 Thread 同时使其获得焦点。 +- 关闭或折叠当前焦点时,按相邻展开槽位、Root 的顺序选择新焦点,不允许留下悬空 ID。 +- App Store 和 Project Store 的 UI slice 可以写入设备 localStorage;entities、runs 和 requests 不得以它作为持久化权威。 + +### D4. Provider、Runtime 与当前 Project 选择 + +Provider 解决的是“组件树应该使用哪个 Store 实例”,不是第三套业务 State。当前 Project 身份只来自 `/thread-chat/{projectId}` 的路由参数;Catalog 和 AppShellUi 都不保存 `selectedProjectId`。 + +#### Runtime 类型 + +```ts +type ThreadChatAppCommands = { + /** 加载或继续加载轻量 Project Catalog。 */ + loadProjectCatalog: (input?: { reset?: boolean }) => Promise + + /** 设置 pendingProjectId 后执行路由跳转;不直接选择 Project Store。 */ + navigateToProject: (projectId: ProjectId) => void + + /** Project 删除成功后更新 Catalog,并在当前路由命中时导航离开。 */ + deleteProject: (projectId: ProjectId) => Promise +} + +type ThreadChatProjectCommands = { + /** 只在未 seed、未 ready 时加载当前 Provider Project 的 Bootstrap。 */ + loadProjectBootstrap: () => Promise + ensureThreadMessages: (threadId: ThreadId) => Promise + updateProject: (patch: PatchProjectRequest) => Promise + updateThread: (threadId: ThreadId, patch: PatchThreadRequest) => Promise + archiveThread: (threadId: ThreadId) => Promise + sendMessage: (threadId: ThreadId, parts: UIMessage["parts"]) => Promise + forkThread: (input: ForkThreadRequest) => Promise + editLastUserMessage: (input: EditMessageRequest) => Promise + regenerateAssistant: (input: RegenerateMessageRequest) => Promise + submitFeedback: (input: PutMessageFeedbackRequest) => Promise + + /** 唯一会请求服务端停止 Run 的入口;关闭连接不得调用它。 */ + stopAssistant: (assistantMessageId: MessageId) => Promise +} + +type GenerationCoordinator = { + /** 扫描已加载 queued/running Run,并按 assistantMessageId 去重订阅。 */ + resumeLoadedRuns: () => void + + /** 从 Store 中现有 eventSequence 建立或复用一条事件连接。 */ + subscribeAssistant: (assistantMessageId: MessageId) => void + + /** 只断开浏览器订阅;不调用 Stop API。 */ + unsubscribeAssistant: (assistantMessageId: MessageId) => void + + /** Runtime 销毁时关闭全部客户端连接与 flush 调度。 */ + destroy: () => void +} + +type ThreadChatAppRuntime = { + /** Provider-scoped App Store;不是模块级服务器单例。 */ + appStore: StoreApi + + /** 管理 ProjectRuntime 身份和从 /new 到 canonical URL 的一次性交接。 */ + projectRuntimeRegistry: ProjectRuntimeRegistry + + /** 命令工厂内部共享的已校验 API capability;React 组件不得直接调用。 */ + api: ThreadChatApiCapabilities + + /** 只操作 Catalog、AppShell 和跨 Project 路由的 App 级命令。 */ + commands: ThreadChatAppCommands + + /** AppProvider 卸载时销毁所有未释放 Runtime 和客户端连接。 */ + destroy: () => void +} + +type ThreadChatProjectRuntime = { + /** Runtime 与路由、Store.entities.project 必须始终一致的身份。 */ + projectId: ProjectId + + /** 当前 Project 唯一的 Zustand Store 实例。 */ + store: StoreApi + + /** 已绑定 api、store 和 coordinator 的 Project 级业务命令集合。 */ + commands: ThreadChatProjectCommands + + /** + * 管理该 Project 内 assistantMessageId → 事件连接的客户端协调器。 + * 连接对象不进入 Zustand State;断开只取消订阅,不发送 Stop。 + */ + generationCoordinator: GenerationCoordinator +} + +type ProjectRuntimeRegistry = { + /** + * `/new` 创建成功后用 CreationBundle 建立已初始化 Runtime,等待目标路由 Provider 接管。 + * 这是一次性 navigation handoff,不是无边界 Project cache。 + */ + seedFromCreation: (bundle: CreationBundle) => ThreadChatProjectRuntime + + /** + * 为路由 projectId 取得唯一 Runtime:优先消费 seed;否则创建空 Store 等待 Bootstrap。 + * 同一 Provider 生命周期内重复 acquire 必须返回同一实例。 + */ + acquire: (projectId: ProjectId) => ThreadChatProjectRuntime + + /** + * ProjectProvider 卸载时释放租约、关闭浏览器事件连接并销毁无租约 Runtime。 + * 它不得调用服务端 Stop;后台 MessageRun 继续运行。 + */ + release: (projectId: ProjectId) => void + + /** 只用于诊断和测试是否已有实例,不得成为 UI 选择当前 Project 的方式。 */ + peek: (projectId: ProjectId) => ThreadChatProjectRuntime | null +} +``` + +Registry 不是 Zustand Store,也不参与 selector。它只管理包含 Store、Commands 和连接协调器的非序列化 Runtime 对象。P0 在最后一个 Provider release 后立即销毁 ProjectRuntime,不建立 LRU 多 Project 缓存。 + +#### Provider 类型与职责 + +```ts +type ThreadChatRoute = + /** 无持久化实体 ID 的首次创建入口。 */ + | { kind: "new" } + + /** projectId 直接来自 `/thread-chat/{projectId}` 路由参数。 */ + | { kind: "project"; projectId: ProjectId } + +type ThreadChatAppProviderProps = { + children: ReactNode + + /** 可选的首屏 Catalog 数据,用于服务端渲染与客户端 hydration 一致。 */ + initialCatalog?: ListProjectsResult +} + +type ThreadChatProjectProviderProps = { + children: ReactNode + + /** 直接来自路由 params;不是 Catalog Store 的 selected 状态。 */ + projectId: ProjectId +} + +type NewProjectDraftProviderProps = { + children: ReactNode + + /** `/new` 可选初始模型偏好;这里不存在 Project/Thread 实体 ID。 */ + initialRequestedModelId?: string +} +``` + +`ThreadChatRoute` 是 Router 的解析结果,不存入 Zustand。它决定挂载 DraftProvider 还是 ProjectProvider,也因此是当前 Project 身份的唯一来源。 + +`ThreadChatAppProvider`: + +- 在 ThreadChat App 外壳边界用 `createStore` 创建一次 `ThreadChatAppStore`、Registry 和 API capabilities。 +- 通过 React Context 向 Sidebar 与 Project route 提供 `ThreadChatAppRuntime`。 +- “App 级”只表示在 ThreadChat 路由之间持续存在;不得在 Next.js 服务端用模块变量跨请求共享。 + +`ThreadChatProjectProvider`: + +- 使用 route `projectId` 调用 `registry.acquire(projectId)`,并向下提供唯一 `ThreadChatProjectRuntime`。 +- Provider 必须以 `key={projectId}` 挂载;路由身份变化时旧 Provider release,新 Provider acquire。 +- 若 Runtime 尚未由 `/new` seed,则发起 Bootstrap;若已 seed,则直接使用 CreationBundle 内容并恢复 Run 订阅。 +- Bootstrap DTO 的 `project.id` 与任何 Thread `projectId` 不匹配 Provider 身份时,必须拒绝合并。 +- 当前 Provider 就绪或失败时,如果 `pendingProjectId` 等于自己的 projectId,必须将它清为 null。 + +`NewProjectDraftProvider`: + +- 只存在于 `/thread-chat/new`,保存无实体 ID 的本地草稿与提交状态。 +- 创建成功后调用 `registry.seedFromCreation(bundle)`,再 `router.replace(bundle.canonicalUrl)`。 +- 目标 ProjectProvider acquire 同一 seeded Runtime,因此无需为了路由切换重新请求 Bootstrap。 + +Provider 必须只创建一次 vanilla Store/Runtime,不能在 React 重渲染时重新执行 factory: + +```tsx +const ThreadChatAppRuntimeContext = + createContext(null) + +const ThreadChatProjectRuntimeContext = + createContext(null) + +function ThreadChatAppProvider({ + children, + initialCatalog, +}: ThreadChatAppProviderProps) { + /** useState initializer 保证同一次 Provider 生命周期只有一个 AppRuntime。 */ + const [runtime] = useState(() => + createThreadChatAppRuntime({ initialCatalog }), + ) + + useEffect(() => { + return () => runtime.destroy() + }, [runtime]) + + return ( + + {children} + + ) +} + +function ThreadChatProjectProvider({ + children, + projectId, +}: ThreadChatProjectProviderProps) { + const appRuntime = useThreadChatAppRuntime() + + /** 外层 key={projectId} 保证 initializer 捕获的身份不会在实例内漂移。 */ + const [runtime] = useState(() => + appRuntime.projectRuntimeRegistry.acquire(projectId), + ) + + useProjectRuntimeLifecycle(runtime) + + useEffect(() => { + return () => { + appRuntime.projectRuntimeRegistry.release(projectId) + } + }, [appRuntime, projectId]) + + return ( + + {children} + + ) +} +``` + +#### Provider 组合 + +```tsx + + + + {route.kind === "new" ? ( + + + + ) : ( + + + + )} + +``` + +#### Runtime 与 Store Hooks + +```ts +function useThreadChatAppRuntime(): ThreadChatAppRuntime +function useThreadChatProjectRuntime(): ThreadChatProjectRuntime + +function useThreadChatAppStore( + selector: (state: ThreadChatAppStore) => T, +): T { + return useStore(useThreadChatAppRuntime().appStore, selector) +} + +function useThreadChatStore( + selector: (state: ThreadChatProjectStore) => T, +): T { + return useStore(useThreadChatProjectRuntime().store, selector) +} + +function useThreadChatCommands(): ThreadChatProjectCommands { + return useThreadChatProjectRuntime().commands +} +``` + +两个 Runtime Hook 是基础设施 Hook,供 Store/Command/Lifecycle Hook 组合使用;普通 UI 组件应优先使用细粒度 Selector Hook 和 Command Hook,不直接取得 `api`、Registry 或 Coordinator。 + +三个“当前”必须分开: + +```text +current Project = URL routeProjectId +current Project Store = ProjectProvider 当前提供的 Runtime.store +focused Thread = Runtime.store.ui.focusedThreadId +``` + +一个 Project 可以同时展示多个 `columnSlots`,所以没有全局唯一的“selected Thread 内容”。ThreadColumn 的命令始终使用自己的 `threadId`;`focusedThreadId` 只服务全局快捷键、工具栏和无列级参数的 UI 操作。 + +#### Provider 生命周期 + +```mermaid +sequenceDiagram + participant R as Router + participant AP as ThreadChatAppProvider + participant PP as ThreadChatProjectProvider + participant Reg as ProjectRuntimeRegistry + participant RT as ProjectRuntime + participant API as Bootstrap API + + R->>PP: mount(projectId from URL) + PP->>Reg: acquire(projectId) + alt 已由 /new seed + Reg-->>PP: initialized Runtime + else 首次打开已有 Project + Reg-->>PP: empty Runtime + PP->>API: GET /projects/{projectId}/bootstrap + API-->>PP: validated ProjectBootstrap + PP->>RT: mergeBootstrap Store Action + end + PP->>RT: resume queued/running subscriptions + R->>PP: unmount / projectId changed + PP->>Reg: release(projectId) + Reg->>RT: close client subscriptions + destroy Store + Note over Reg,RT: 不发送服务端 Stop +``` + +### D5. 明确什么不能存进 Store + +以下数据必须用 selector 计算: + +```text +rootThreadId +childThreadIds +threadDepth +breadcrumbs +activeMessages +visibleThreadIds +threadTreeRows +branchCount +canFork +canEdit +canRegenerate +threadColumnView +projectHeaderView +``` + +核心 selectors: + +```ts +selectRootThreadId(state): ThreadId +selectChildThreadIds(state, parentThreadId): ThreadId[] +selectThreadLineage(state, threadId): ThreadEntity[] +selectActiveMessages(state, threadId): MessageEntity[] +selectMessageView(state, messageId): MessageView +selectForkAvailability(state, messageId): ForkAvailability +selectThreadColumnView(state, threadId): ThreadColumnView +selectProjectTreeRows(state): ProjectTreeRow[] +selectVisibleThreadIds(state): ThreadId[] +selectVisibleColumns(state): ThreadColumnView[] +``` + +`selectActiveMessages` 的唯一规则: + +```text +message.threadId === threadId +AND supersededAt === null +ORDER BY sequence ASC +``` + +ViewModel 可以组合实体、Run、Artifact 和 UI,但永远不是 API 请求体或持久化对象。 + +### D6. Store Actions 与 Application Commands 取代 monolithic chat-controller + +#### Store Actions + +Store Action 与对应 Zustand Store/slice 共置,只执行同步、可测试的 State Transition,并且是客户端唯一允许调用 `set`/`setState` 的位置。MVP 至少包含: + +```ts +mergeBootstrap → 跨 entities/runs/requests 原子初始化 Project +applyMessageBundle → 合并一个 Thread 的有效 Message、Run 与 Artifact +applyMessageCreationBundle → 合并发送命令返回的 user、assistant 与 Run +applyThreadCreated → 合并服务端创建的 Child Thread +applyReplacementBundle → 原子标记 superseded 并合并 replacement 与新 Run +applyRunEvent → 只更新对应 assistantMessageId 的流状态或终态 +openThread/closeThread → 根据 placementMode 更新 columnSlots +focusThread → 更新全局快捷键与工具栏的 Thread 焦点 +``` + +合并类 Store Action 是内部写入口,不直接暴露给 UI;`openThread`、`closeThread`、`focusThread` 这类纯本地 UI Action 可以由 Command Hook 直接调用。Store Action 不调用 API、不导航、不建立 SSE,并且不得原地改写已确认实体内容。 + +这遵循 Zustand 将 state 与 actions 共置、使用 `set`/`setState` 提交更新以及用 slices 组织大型 Store 的推荐方式。我们额外规定 Store Action 保持同步、IO 放入 Application Command;这是本项目为了固定副作用边界采用的更严格约束,不是 Zustand 强制要求。 + +#### Application Commands + +目标结构: + +```text +client/application/ +├── project-commands +│ ├── loadProjectCatalog +│ ├── createProjectWithFirstMessage +│ ├── updateProject +│ ├── archiveProject +│ └── deleteProject +├── thread-commands +│ ├── loadProjectBootstrap +│ ├── ensureThreadMessages +│ ├── forkThread +│ ├── updateThread +│ └── archiveThread +├── message-commands +│ ├── sendMessage +│ ├── editLastUserMessage +│ ├── regenerateAssistant +│ └── submitFeedback +└── generation-coordinator + ├── resumeLoadedRuns + ├── subscribeAssistant + ├── handleEvent + ├── unsubscribeAssistant + └── destroy +``` + +Application Command 的共同算法: + +```text +读取最小本地前置条件 +→ 调用 Store Action 设置 scope busy(只防当前页面重复提交) +→ 调用一个 API capability +→ 校验响应 DTO +→ 调用一次语义化 Store Action 原子合并服务端事实 +→ 启动后续生命周期动作(订阅、导航、打开列) +→ 调用 Store Action 清理 busy / 写结构化错误 +``` + +Application Command 不生成实体 ID、不构造 Prompt、不提交 BaseContext、不序列化整棵 Project,也不直接调用 `set`/`setState`。 + +### D7. `/thread-chat/new` 是草稿模式,不是 Project + +```ts +type NewProjectDraftState = { + /** 明确表示本 Store 没有持久化 Project 身份,禁止把 "new" 当作 ProjectId。 */ + kind: "new" + + /** 首次发送前的 AI SDK v7 输入草稿;API 失败时必须保留。 */ + draftParts: UIMessage["parts"] + + /** 用户为首次回答选择的模型偏好;实际模型仍以服务端 Run 为准。 */ + requestedModelId: string + + /** 只描述首次创建命令,不代表任何 Project/Message 运行状态。 */ + status: "idle" | "submitting" | "error" + + /** 创建命令的可展示错误;idle/submitting 时为 null。 */ + error: ClientError | null +} + +type NewProjectDraftActions = { + setDraftParts: (parts: UIMessage["parts"]) => void + setRequestedModelId: (modelId: string) => void + markSubmitting: () => void + markError: (error: ClientError) => void + resetSubmission: () => void +} + +type NewProjectDraftStore = NewProjectDraftState & NewProjectDraftActions +``` + +`NewProjectDraftStore` 由 `NewProjectDraftProvider` 创建并随 `/new` 页面销毁,不放进 ThreadChatAppStore,也不插入 ThreadChatProjectStore.entities。UI 通过 `NewProjectScreen` 复用 Composer 和空白列外观,但传递的是 `onSubmitDraft`,不是假的 threadId。 + +```mermaid +sequenceDiagram + participant U as User + participant UI as /thread-chat/new + participant A as ProjectCommand + participant Reg as ProjectRuntimeRegistry + participant S as POST /api/v1/projects + participant DB as Database + participant W as MessageRun Worker + + U->>UI: 输入并发送第一条 Message + UI->>A: createProjectWithFirstMessage(parts, model) + A->>S: 不携带任何新实体 ID + S->>DB: BEGIN + S->>DB: Project + Root + U1 + A1 + queued Run + S->>DB: COMMIT + S-->>A: 201 CreationBundle + canonicalUrl + S->>W: commit 后唤醒 Run + A->>Reg: seedFromCreation(CreationBundle) + Reg-->>A: initialized ProjectRuntime + A->>UI: router.replace(canonicalUrl) + A->>Reg: Runtime 按 A1 + eventSequence 订阅 +``` + +如果事务提交前失败,保留草稿并停留 `/new`。如果服务端已经提交但响应丢失,P0 的按钮防抖无法证明是否创建成功;这是暂缓通用幂等带来的已知限制。 + +### D8. 已有 Project 的 Bootstrap 生命周期 + +`ProjectBootstrap`: + +```ts +type ProjectBootstrap = { + /** 当前 Provider projectId 对应的完整 Project 实体。 */ + project: ProjectEntity + + /** 当前 Project 的全量轻量 Thread topology;不包含 Branch Message。 */ + threadTopology: ThreadEntity[] + + /** 唯一 Root Thread 的首屏 Message window。 */ + initialThread: ThreadMessageBundle +} + +type ThreadMessageBundle = { + /** Bundle 所属 Thread;其余 Message 必须全部匹配该 ID。 */ + threadId: ThreadId + + /** 当前有效时间线窗口,按 sequence 升序;不返回 superseded Message。 */ + messages: MessageEntity[] + + /** 必须且只包含本窗口 assistant Message 对应的 Run。 */ + assistantRuns: AssistantRunState[] + + /** 渲染本窗口 Message 所需且允许内联的 Artifact。 */ + includedArtifacts: ArtifactEntity[] + + /** true 表示服务端还存在更早有效 Message;MVP 不自动加载。 */ + hasOlderMessages: boolean + + /** 返回窗口首尾 sequence;空窗口时均为 null。 */ + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} +``` + +生命周期: + +```text +解析 URL projectId +→ ThreadChatProjectProvider acquire(projectId) +→ 若 Runtime 已由 /new seed:跳过 Bootstrap +→ 否则 GET ProjectBootstrap +→ 校验唯一 Root、Provider projectId 和所有 Thread projectId 关系 +→ mergeBootstrap 一次合并 entities/runs/request state +→ 读取并过滤 localStorage workbench state +→ 初始化 focusedThreadId=Root;columnSlots 恢复合法 Branch 槽位 +→ 渲染 Root + topology/columnSlots +→ generation coordinator 恢复 queued/running Runs +``` + +打开尚未加载的 Branch 时,`ensureThreadMessages(threadId)` 请求一个 ThreadMessageBundle;加载状态为 ready 时直接复用,不重复请求。 + +MVP 返回最新最多 200 条有效 Message,并按 sequence 升序交给客户端。`hasOlderMessages` 只保留能力边界,当前 UI 不实现树内自动分页替换。 + +### D9. 核心 Application Command 伪代码 + +#### 首次创建 + +```ts +async function createProjectWithFirstMessage(input: { + parts: UIMessage["parts"] + requestedModelId?: string +}) { + const { appStore, projectRuntimeRegistry, api } = appRuntime + newDraftStore.getState().markSubmitting() + + try { + const bundle = await api.createProject({ + initialMessage: { parts: input.parts }, + requestedModelId: input.requestedModelId, + }) + + const runtime = projectRuntimeRegistry.seedFromCreation(bundle) + appStore.getState().upsertProjectSummary(toProjectSummary(bundle.project)) + appStore.getState().setProjectRoutePending(bundle.project.id) + + runtime.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId, + ) + router.replace(bundle.canonicalUrl) + } catch (error) { + newDraftStore.getState().markError(normalizeError(error)) + } +} +``` + +#### 发送后续消息 + +```ts +async function sendMessage(threadId: ThreadId, parts: UIMessage["parts"]) { + const scope = `send:${threadId}` + const state = store.getState() + if (state.requests.commandByScope[scope]?.status === "submitting") return + state.setCommandState(scope, { status: "submitting" }) + + try { + const bundle = await api.sendMessage({ threadId, parts }) + store.getState().applyMessageCreationBundle(bundle) + store.getState().setCommandState(scope, null) + generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId, + ) + } catch (error) { + store.getState().setCommandState(scope, { + status: "error", + error: normalizeError(error), + }) + } +} +``` + +服务端响应前只显示 submitting,不创建 optimistic Message。这样牺牲几十毫秒即时气泡,换取零临时 ID、零 ID 替换和明确的服务端身份。 + +#### Fork + +```ts +async function forkThread(input: { + sourceThreadId: ThreadId + sourceMessageId: MessageId + anchor?: TextAnchor +}) { + const availability = selectForkAvailability(store.getState(), input.sourceMessageId) + if (!availability.allowed) return availability.failure + + const child = await api.forkThread(input) + store.getState().applyThreadCreated(child) + store.getState().openThread(child.id, input.sourceThreadId) +} +``` + +#### Regenerate + +```ts +async function regenerateAssistant( + sourceAssistantMessageId: MessageId, + requestedModelId?: string, +) { + const result = await api.regenerate({ + sourceAssistantMessageId, + requestedModelId, + }) + + store.getState().applyReplacementBundle(result) + generationCoordinator.subscribeAssistant( + result.assistantRun.assistantMessageId, + ) +} +``` + +### D10. Hooks 是窄胶水,不是业务层 + +#### Store 基础绑定 + +```ts +useThreadChatAppStore(selector) +useThreadChatStore(selector) +useThreadChatAppRuntime() +useThreadChatProjectRuntime() +``` + +#### Selector Hooks + +```text +useProjectCatalog() +useAppShellUi() +useProject() +useProjectTarget() +useThread(threadId) +useThreadMessages(threadId) +useThreadColumnView(threadId) +useProjectTreeRows() +useAssistantRun(assistantMessageId) +useForkAvailability(messageId) +useVisibleThreadColumns() +useFocusedThreadId() +``` + +#### Command Hooks + +```text +useAppShellCommands() +useProjectCommands() +useThreadCommands(threadId) +useMessageCommands(messageId) +``` + +Command Hook 只绑定 Application Command、纯本地 Store Action 和作用域 ID,例如: + +```ts +function useThreadCommands(threadId: string) { + const commands = useThreadChatCommands() + return useMemo(() => ({ + send: (parts) => commands.sendMessage(threadId, parts), + fork: (messageId, anchor) => commands.forkThread({ + sourceThreadId: threadId, + sourceMessageId: messageId, + anchor, + }), + stop: (assistantMessageId) => commands.stopAssistant(assistantMessageId), + }), [commands, threadId]) +} +``` + +#### 生命周期 Hooks + +```text +useProjectRuntimeLifecycle() // 仅由 ThreadChatProjectProvider 调用 +useEnsureThreadMessagesLoaded(threadId) +useActiveGenerationSubscriptions() +useWorkbenchPersistence() // projectId 从当前 ProjectRuntime 取得 +``` + +生命周期 Hook 可以使用 Effect;Root/children/messages/canFork 等衍生状态禁止用 Effect 镜像。 + +### D11. API 通用契约 + +Base URL:`/api/v1`。 + +共同规则: + +- Session 决定 actor;body 不接受 ownerUserId 作为授权依据。 +- 客户端只传既有资源 ID;新实体 ID 全由服务端生成。 +- Request/Response 使用共享 Zod schema;Message.parts 兼容 AI SDK v7。 +- 普通 JSON 成功响应返回 `{ data: T }` 中的服务端权威 DTO,不只返回 `{ ok: true }`;204 和事件流除外。 +- 错误统一为 `{ error: { code, message, details? } }`。 +- Request Object 严格拒绝未声明字段,避免客户端注入新实体 ID 或服务端内部事实。 +- append/Fork/generation 不使用 Thread/Project revision。 +- P0 不强制 Idempotency-Key;客户端 busy guard 只防重复点击。 +- `requestedModelId` 是本次运行请求,服务端校验后在 AssistantRunState 返回实际 `modelId`。 + +推荐 HTTP 状态: + +| 状态 | 语义 | +|---:|---| +| 200 | Query、更新或幂等终态操作成功 | +| 201 | 创建 Project、Fork 或新 Message bundle 成功 | +| 400 | DTO 形状或字段非法 | +| 401 | Session 无效 | +| 403/404 | 无权访问或资源不存在 | +| 409 | 当前领域状态不允许该命令 | +| 422 | 资源存在但不满足业务资格 | + +### D12. API 契约索引 + +API 总表、参数之间的决定性关系、完整输入输出 Schema、错误码与接口 Case 统一由 [ThreadChat V1 API 详细合同](./design/api-contracts.md) 管理。 + +### D13. 手动验收案例 + +#### Case 1:首次创建 + +```text +1. 打开 /thread-chat/new。 +2. 不发送,刷新或离开。 + 预期:Project 列表不新增记录。 +3. 返回 /new,发送“帮我设计支付系统”。 + 预期:201;返回 Project/Root/U1/A1/Run;URL replace 为 projectId。 +4. 查询 Bootstrap。 + 预期:能读到相同 ID,Run 为 queued/running/completed 之一。 +``` + +#### Case 2:Fork 资格 + +```text +1. A1 正在 running 时请求 Fork。 + 预期:409/422 fork_source_not_finalized,零 Child Thread。 +2. A1 completed 后再次请求。 + 预期:201,Child Thread 的 projectId 与 Parent 相同。 +3. 请求体伪造 baseContext/newThreadId。 + 预期:validation_error 或字段被严格拒绝。 +``` + +#### Case 3:Regenerate replacement + +```text +1. 记录 A1.id、sequence、parts。 +2. POST A1/regenerations。 +3. 预期返回 A2 + R2,A2.replacesMessageId=A1.id。 +4. 再读 Thread。 +5. 默认时间线只含 A2;审计查询中 A1 内容和 sequence 未变化。 +``` + +#### Case 4:刷新恢复 + +```text +1. 发送消息并等待 Run=running。 +2. 记录 eventSequence,刷新页面。 +3. Bootstrap 返回 checkpoint + sequence。 +4. 事件订阅使用 afterEventSequence。 +5. 预期不创建第二个 Run,终态后 Message.parts 与服务端一致。 +``` + +#### Case 5:Target + +```text +1. PATCH Project,设置 ultimate、shortTerm、midTerm。 +2. 重新请求 Bootstrap。 +3. 预期完整返回 Target,其他 Project 不受影响。 +4. PATCH 只提交 customTitle。 +5. 预期 Target 保持不变。 +``` + +### D14. Transport 后置,但边界已固定 + +后续 Transport 只需实现: + +```text +API capability interface +→ HTTP method/path/body +→ auth/session recovery +→ Zod response validation +→ ClientError normalization +→ JSON 或 event stream 解码 +``` + +Transport 不决定业务原子性、不访问 Zustand、不生成实体 ID、不构造 ViewModel。事件载体最终选 SSE 或 WebSocket,不改变 `assistantMessageId + eventSequence` 的逻辑契约。 + +## Risks / Trade-offs + +- **[服务端响应前不插入 optimistic Message,视觉上略慢]** → 使用 thread-scoped submitting 状态和 Composer loading;换取无临时 ID、无实体替换。 +- **[P0 无 Idempotency-Key,响应丢失后首次创建可能重复]** → 当前只做 busy guard 并明确限制;V2 为有副作用命令增加幂等协议。 +- **[一个 Project 一个 Store,频繁切换会重载]** → ProjectCatalog 保留轻量列表;是否引入有界 LRU Store cache 等真实性能数据出现后决定。 +- **[messageIdsByThreadId 与 messagesById 可能不一致]** → 只允许共享 normalizer/merge 函数更新,测试去重、排序和归属不变量。 +- **[Bootstrap 同时返回 topology 与 Root bundle,DTO 较复合]** → 这是一次请求避免首屏瀑布;仍不下载 Branch Message 和大型资源。 +- **[最新 200 条隐藏更早 UI 历史]** → 返回 hasOlder/boundary 保留升级路径;Prompt 历史始终由服务端构造,不受 UI 是否加载完整影响。 +- **[Target 以后需要进度、依赖和截止时间]** → 当前 ProjectTarget 是最小结构;出现目标管理需求后新增 Goal 实体,不提前过度设计。 + +## Migration Plan + +本 change 只完成设计。后续建议拆分: + +1. `add-thread-chat-v1-contracts`:共享 DTO/error schemas 与 API 集成测试夹具。 +2. `add-thread-chat-project-queries`:Project list、Bootstrap、Thread Message Query。 +3. `add-thread-chat-command-api`:create/send/fork/edit/regenerate/metadata/feedback。 +4. `add-thread-chat-generation-events`:AssistantRun query/event/stop 与刷新恢复。 +5. `normalize-thread-chat-zustand-store`:ProjectCatalog + Project-scoped entities/runs/requests/ui slices。 +6. `add-thread-chat-client-commands`:Store Actions、Application Commands、normalizer 和 generation coordinator。 +7. `integrate-thread-chat-hooks-ui`:Provider、selector/command/lifecycle Hooks 与现有 UI 接入。 +8. Transport 可在第 2~4 步随契约落地,也可先用接口注入的测试 adapter,不改变 Store Action/Application Command 设计。 + +切换期不得让新 Store 回写旧 `branch_trees.state`。回滚以路由/feature flag 切回旧页面为边界,新旧实体不做双向同步。 + +## Open Questions + +- P0 是否需要“只追加 user Message、不立即创建 assistant Run”的独立命令?当前 UI 和本设计只提供常用的 `user + assistant + Run` 原子发送,但数据库不变量仍允许未来扩展连续 user Message。 +- Artifact 内容在 ThreadMessageBundle 中按 kind/size 何时内联,何时只返回引用,需要在 Artifact API change 中固定阈值。 +- 事件 Transport 选择 SSE 还是 WebSocket 留到 Transport 设计;逻辑恢复契约已经固定。 diff --git a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md new file mode 100644 index 00000000..19eb9ce8 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md @@ -0,0 +1,877 @@ +# ThreadChat V1 API 详细合同 + +本文是 `design.md` 引用的 API 可实现合同。接口实现、共享 Zod Schema、服务端集成测试和客户端 Transport 必须以本文为准;总表只用于导航。 + +### D12. API 总表 + +本节只是能力索引。逐接口的 Path、Query、Body、成功响应、输出类型、错误码、参数关系和手动 Case 以 [ThreadChat V1 API 详细合同](#threadchat-v1-api-详细合同) 为准。 + +| Method / Path | 功能 | 关键输入 | 核心输出 | 用户场景 | +|---|---|---|---|---| +| `GET /api/v1/projects` | Project 列表 | `status?`, `limit?`, `cursor?` | `ProjectSummary[]` | 打开对话列表 | +| `POST /api/v1/projects` | 首次发送并建 Project | `initialMessage.parts`, `requestedModelId?` | CreationBundle | `/thread-chat/new` 第一次发送 | +| `GET /api/v1/projects/{id}/bootstrap` | 首屏恢复 | projectId | ProjectBootstrap | 打开已有 Project | +| `PATCH /api/v1/projects/{id}` | 更新标题/Target/Instruction | patch 字段 | ProjectEntity | 修改 Project 设置 | +| `POST /api/v1/projects/{id}/archive` | 归档 Project | projectId | ProjectEntity | 从默认列表隐藏 | +| `POST /api/v1/projects/{id}/unarchive` | 取消归档 | projectId | ProjectEntity | 恢复 Project | +| `DELETE /api/v1/projects/{id}` | 永久删除 | projectId;确认由 UI 在调用前完成 | 204 | 删除整个工作项 | +| `GET /api/v1/threads/{id}/messages` | 加载一个 Thread | `limit<=200`, `beforeSequence?` | ThreadMessageBundle | 首次展开 Branch | +| `PATCH /api/v1/threads/{id}` | 更新 Branch 标题 | `customTitle` | ThreadEntity | 重命名分支列 | +| `POST /api/v1/threads/{id}/archive` | 归档 Thread | threadId | ThreadEntity | 收起历史分支 | +| `POST /api/v1/threads/{id}/unarchive` | 取消归档 Thread | threadId | ThreadEntity | 恢复分支 | +| `POST /api/v1/threads/{id}/messages` | 发送并启动回答 | `parts`, `requestedModelId?` | MessageCreationBundle | 普通继续对话 | +| `POST /api/v1/threads/{id}/forks` | 创建 Child Thread | `sourceMessageId`, `anchor?` | ThreadEntity | 从某条 finalized Message 分叉 | +| `POST /api/v1/messages/{id}/edits` | Edit 最后一条 user | `parts`, `requestedModelId?` | ReplacementBundle | 修改最后问题并重答 | +| `POST /api/v1/messages/{id}/regenerations` | Regenerate assistant | `requestedModelId?` | ReplacementBundle | 重新生成最后回答 | +| `PUT /api/v1/messages/{id}/feedback` | 设置评价 | `positive/negative/null` | FeedbackDTO | 点赞/点踩 | +| `GET /api/v1/assistant-messages/{id}/events` | 恢复事件 | `afterEventSequence` | Event stream | 刷新后续接生成 | +| `POST /api/v1/assistant-messages/{id}/stop` | 停止生成 | assistantMessageId | AssistantRunState | 用户点击 Stop | + +### D13. 参数之间的决定性关系 + +以下内容只保留最核心的关系速查;完整 Schema 与所有边界条件见 [ThreadChat V1 API 详细合同](#threadchat-v1-api-详细合同)。 + +#### 创建 Project + +```ts +type CreateProjectRequest = { + initialMessage: { + parts: UIMessage["parts"] + } + requestedModelId?: string +} +``` + +没有 projectId/threadId/messageId。服务端在同一事务生成全部身份。 + +#### 发送 Message + +```ts +type SendMessageRequest = { + parts: UIMessage["parts"] + requestedModelId?: string +} +``` + +threadId 只来自 URL path;body 不重复声明,避免两个值冲突。服务端根据 Thread 找到 Project 和权限。 + +#### Fork + +```ts +type ForkThreadRequest = { + sourceMessageId: string + anchor?: { + exactQuote: string + textPosition?: { start: number; end: number } + } +} +``` + +sourceThreadId 来自 path。服务端验证 sourceMessage.threadId 等于 path Thread;anchor 是用户选择输入,不是权威 BaseContext。 + +#### Project Patch + +```ts +type PatchProjectRequest = { + customTitle?: string | null + target?: ProjectTarget | null + instruction?: string | null +} +``` + +字段缺省表示保持不变;显式 null 表示清空。Target 中短期、中期、终极目标属于一个整体值,MVP 一次整体替换。 + +#### Replacement + +```ts +type ReplacementBundle = { + supersededMessageIds: string[] + createdMessages: MessageEntity[] + assistantRun: AssistantRunState +} +``` + +来源 Message ID 来自 path;body 不提交 replacement ID、sequence 或 supersededAt。 + +## 1. 合同边界 + +### 1.1 共同规则 + +- Base URL 固定为 `/api/v1`。 +- JSON 字段使用 `camelCase`,时间使用 UTC ISO 8601 字符串。 +- Session 决定 actor;请求不得提交 `ownerUserId` 作为授权依据。 +- Path 只放正在操作的既有资源 ID;Body 不重复同一个 ID。 +- 待创建的 Project、Thread、Message 和 MessageRun ID 全部由服务端生成。 +- JSON Object 默认严格校验;未声明字段返回 `400 validation_error`,不得静默接收 `newThreadId`、`baseContext` 等字段。 +- `Message.parts` 必须通过项目基于 AI SDK v7 `UIMessage["parts"]` 建立的共享 Schema。 +- 普通 JSON 成功响应统一为 `{ data: T }`;`204` 和 SSE 除外。 +- append、Fork 和生成命令不使用 Project/Thread revision 或 `If-Match`。 +- P0 不要求 `Idempotency-Key`;同一次按钮交互由客户端 busy guard 防止重复点击。这不是网络级幂等保证。 + +### 1.2 基础类型 + +```ts +type ProjectId = string // 服务端 UUID;客户端按不透明字符串处理 +type ThreadId = string +type MessageId = string +type DateTime = string // UTC ISO 8601,例如 2026-08-25T03:20:10.000Z +type Cursor = string // 服务端签发的不透明游标,客户端不得解析或构造 + +type JsonPrimitive = string | number | boolean | null +type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue } + +type ApiResponse = { + data: T +} + +type ApiErrorResponse = { + error: { + code: ApiErrorCode + message: string + details?: JsonValue + } +} +``` + +`message` 用于开发者诊断,不作为 UI 稳定文案;客户端分支判断只依赖 `code`。 + +### 1.3 Message Parts + +```ts +import type { UIMessage, UIMessageChunk } from "ai" + +type MessageParts = UIMessage["parts"] +type UserMessageParts = UIMessage["parts"] +``` + +`UserMessageParts` 复用 AI SDK v7 的传输结构,但服务端仍必须做角色侧白名单校验。例如,客户端不得伪造仅应由 assistant 或服务端 Tool 执行产生的 Part。至少包含一个有意义内容的数组才是合法输入;空数组、纯空白文本或未知 Part 必须返回 `validation_error`。 + +### 1.4 权威 DTO + +```ts +type ProjectTargetDTO = { + ultimate: string | null + shortTerm: string[] + midTerm: string[] +} + +type ProjectDTO = { + id: ProjectId + ownerUserId: string + autoTitle: string | null + customTitle: string | null + target: ProjectTargetDTO | null + instruction: string | null + archivedAt: DateTime | null + createdAt: DateTime + updatedAt: DateTime +} + +type ProjectSummaryDTO = { + id: ProjectId + displayTitle: string + archivedAt: DateTime | null + updatedAt: DateTime + threadCount: number + messageCount: number +} + +type ForkSourceSnapshotDTO = { + schemaVersion: 1 + quote?: string + sourceRole: "user" | "assistant" + sourceSequence: number +} + +type ThreadDTO = { + id: ThreadId + projectId: ProjectId + parentThreadId: ThreadId | null + sourceMessageId: MessageId | null + forkSourceSnapshot: ForkSourceSnapshotDTO | null + autoTitle: string | null + customTitle: string | null + archivedAt: DateTime | null + createdAt: DateTime + updatedAt: DateTime +} + +type MessageDTO = { + id: MessageId + threadId: ThreadId + sequence: number + role: "user" | "assistant" + parts: MessageParts | null + replacesMessageId: MessageId | null + supersededAt: DateTime | null + finalizedAt: DateTime | null + createdAt: DateTime +} + +type AssistantRunStatus = + | "queued" + | "running" + | "completed" + | "failed" + | "stopped" + +type AssistantRunStateDTO = { + assistantMessageId: MessageId + status: AssistantRunStatus + modelId: string + checkpointParts: MessageParts + eventSequence: number + error: { code: string; message: string } | null + stopRequestedAt: DateTime | null + finishedAt: DateTime | null +} + +type IncludedArtifactDTO = { + id: string + projectId: ProjectId + sourceMessageId: MessageId + kind: string + title: string + contentIncluded: boolean + content: JsonValue | null + createdAt: DateTime +} + +type FeedbackValue = "positive" | "negative" | null + +type FeedbackDTO = { + messageId: MessageId + value: FeedbackValue + updatedAt: DateTime +} +``` + +决定性不变量: + +- `ThreadDTO.parentThreadId === null` 时,它是 Project 唯一 Root,且三个 Fork 字段必须全部为 `null`。 +- `parentThreadId !== null` 时,它是 Branch,`sourceMessageId` 与 `forkSourceSnapshot` 必须非空。 +- `MessageDTO.sequence` 由服务端在 Thread 内分配且唯一、单调递增;客户端不得提交。 +- user Message 创建时即 finalized;运行中的 assistant Message 可有 `parts=null`、`finalizedAt=null`。 +- `AssistantRunStateDTO.assistantMessageId` 必须指向 role 为 assistant 的 Message;每条 assistant Message 恰有一个 Run。 +- `status=completed` 时,Message 必须已有不可变 `parts` 和 `finalizedAt`;`status=failed|stopped` 时 `finishedAt` 非空。 +- `stopRequestedAt` 非空表示服务端已经接受 Stop;它不等于 Run 已进入 `stopped` 终态。 +- `contentIncluded=false` 时 `IncludedArtifactDTO.content` 必须为 `null`;它只表示本响应未内联正文,不表示 Artifact 不存在。 + +### 1.5 复合响应 + +```ts +type ThreadMessageBundleDTO = { + threadId: ThreadId + messages: MessageDTO[] + assistantRuns: AssistantRunStateDTO[] + includedArtifacts: IncludedArtifactDTO[] + hasOlderMessages: boolean + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} + +type ProjectBootstrapDTO = { + project: ProjectDTO + threadTopology: ThreadDTO[] + initialThread: ThreadMessageBundleDTO +} + +type CreationBundleDTO = { + project: ProjectDTO + rootThread: ThreadDTO + userMessage: MessageDTO + assistantMessage: MessageDTO + assistantRun: AssistantRunStateDTO + canonicalUrl: `/thread-chat/${string}` +} + +type MessageCreationBundleDTO = { + userMessage: MessageDTO + assistantMessage: MessageDTO + assistantRun: AssistantRunStateDTO +} + +type ReplacementBundleDTO = { + supersededMessageIds: MessageId[] + createdMessages: MessageDTO[] + assistantRun: AssistantRunStateDTO +} +``` + +`ThreadMessageBundleDTO.messages` 只返回当前有效时间线,即 `supersededAt === null` 的 Message,并按 `sequence ASC` 排列。旧 replacement Message 仍保存在数据库,但不混入默认 UI 时间线。 + +`assistantRuns` 必须且只包含本 bundle 中 assistant Message 的 Run;不得返回属于其他 Thread 或未返回 Message 的 Run。`includedArtifacts` 必须且只包含渲染本次 Message 所需且允许内联的 Artifact。 + +## 2. Project API + +### 2.1 列出 Project + +```http +GET /api/v1/projects?status=active&limit=50&cursor=... +``` + +功能:为首页“对话列表”加载轻量 Project 列表,不加载 Thread topology 或 Message 正文。 + +Query: + +```ts +type ListProjectsQuery = { + status?: "active" | "archived" | "all" // 默认 active + limit?: number // 整数,1..100,默认 50 + cursor?: Cursor // 首次请求省略 +} +``` + +成功响应:`200 ApiResponse`。 + +```ts +type ListProjectsResult = { + items: ProjectSummaryDTO[] + nextCursor: Cursor | null +} +``` + +决定性关系: + +- 排序固定为 `updatedAt DESC, id DESC`;相同时间使用 ID 保证稳定顺序。 +- `status=active` 只返回 `archivedAt=null`;`archived` 只返回非空;`all` 返回两者。 +- Cursor 绑定 actor、status、排序与上页边界。带 cursor 时更换 status 返回 `400 invalid_cursor`。 +- `items.length < limit` 时 `nextCursor` 必须为 `null`。 + +主要错误:`invalid_query`、`invalid_cursor`、`unauthorized`。 + +手动 Case:先以 `limit=2` 请求,记录 `nextCursor`;第二次带同一 `status` 和 cursor,预期不重复第一页 ID。再把 `status` 改为 `archived` 并复用 cursor,预期 `invalid_cursor`。 + +### 2.2 首次发送并创建 Project + +```http +POST /api/v1/projects +Content-Type: application/json +``` + +```ts +type CreateProjectRequest = { + initialMessage: { + parts: UserMessageParts + } + requestedModelId?: string +} +``` + +成功响应:`201 ApiResponse`。 + +决定性关系: + +- 请求中没有 Project、Thread、Message 或 Run ID。 +- `initialMessage.parts` 决定 U1 内容;服务端不得接受客户端提交的 role,U1 固定为 user。 +- `requestedModelId` 是请求偏好;服务端校验并选择实际模型,结果以 `assistantRun.modelId` 为准。 +- 服务端在同一事务创建 Project、唯一 Root、U1、A1 与 queued Run;U1/A1 属于 Root,且 `U1.sequence < A1.sequence`。 +- `canonicalUrl` 必须由返回的 `project.id` 构造。 +- 模型 Worker 只能在事务提交后唤醒。 + +主要错误:`validation_error`、`model_not_available`、`unauthorized`。 + +请求示例: + +```json +{ + "initialMessage": { + "parts": [{ "type": "text", "text": "帮我设计一个支付系统" }] + }, + "requestedModelId": "provider/model" +} +``` + +手动 Case:提交合法内容,验证响应五个对象的关联 ID;再提交空 `parts`,预期 400 且数据库没有残留 Project。 + +### 2.3 加载 Project 首屏 + +```http +GET /api/v1/projects/{projectId}/bootstrap +``` + +Path: + +```ts +type ProjectPath = { projectId: ProjectId } +``` + +Body:无。Query:无。 + +成功响应:`200 ApiResponse`。 + +决定性关系: + +- `project.id` 必须等于 path `projectId`。 +- `threadTopology` 返回该 Project 全部轻量 Thread,必须恰有一个 `parentThreadId=null` 的 Root。 +- 每个 Branch 的 `parentThreadId` 必须能在同一 `threadTopology` 找到,且不得形成环。 +- `initialThread.threadId` 必须等于 Root ID。 +- Root bundle 默认返回最新最多 200 条有效 Message,再按 sequence 升序输出。 +- Bootstrap 不返回 BaseContext、Branch Message、Prompt History 或全部 Project Resource 正文。 + +主要错误:`project_not_found`、`forbidden`、`unauthorized`。 + +手动 Case:创建 Root + 两个 Branch,仅向 Branch 写入消息。Bootstrap 应返回三个 topology item,但 `initialThread.messages` 只能属于 Root。 + +### 2.4 更新 Project 元数据 + +```http +PATCH /api/v1/projects/{projectId} +Content-Type: application/json +``` + +```ts +type PatchProjectRequest = { + customTitle?: string | null + target?: ProjectTargetDTO | null + instruction?: string | null +} +``` + +校验边界: + +- Body 至少出现一个字段。 +- `customTitle` 去除首尾空白后为 1..120 字符;`null` 清除自定义标题。 +- `instruction` 最大 20,000 字符;`null` 清除。 +- `target.ultimate` 最大 4,000 字符;短期/中期数组各最多 50 项,每项去除首尾空白后为 1..500 字符。 +- `target=null` 清除整个 Target;字段缺省则保持原值。 + +成功响应:`200 ApiResponse`。 + +决定性关系: + +- 缺省字段表示“不修改”,显式 `null` 表示“清空”,二者不得混淆。 +- `target` 是一个整体值;提交后一次整体替换,不按数组项做 merge。 +- 更新 Project metadata 不得修改 Thread、Message 或其他 Project。 +- MVP 使用最后一次成功写入生效,不要求 revision。 + +主要错误:`validation_error`、`project_not_found`、`forbidden`。 + +手动 Case:先设置 Target,再只提交 `customTitle`,重新读取 Bootstrap,预期 Target 完全不变;随后提交 `target:null`,预期 Target 被清空。 + +### 2.5 归档与取消归档 Project + +```http +POST /api/v1/projects/{projectId}/archive +POST /api/v1/projects/{projectId}/unarchive +``` + +Body:无。Query:无。 + +成功响应:`200 ApiResponse`。 + +决定性关系: + +- archive 将 `archivedAt` 设置为服务端当前时间;重复 archive 返回当前实体,不重复制造副作用。 +- unarchive 将 `archivedAt` 清为 `null`;重复 unarchive 返回当前实体。 +- 归档只影响默认导航可见性,不删除 Thread、Message 或资源。 + +主要错误:`project_not_found`、`forbidden`、`unauthorized`。 + +### 2.6 永久删除 Project + +```http +DELETE /api/v1/projects/{projectId} +``` + +Body:无。Query:无。UI 的二次确认是调用前置条件,不作为可伪造的 `confirmed=true` 参数。 + +成功响应:`204 No Content`,响应体必须为空。 + +决定性关系: + +- 这是 Project 聚合的唯一永久删除入口。 +- 服务端在受控事务中删除其资源、Run、Message、Thread 和 Project;不得暴露单 Message hard delete。 +- 成功后 Bootstrap 返回 `project_not_found`。 + +主要错误:`project_not_found`、`forbidden`、`project_delete_conflict`。 + +## 3. Thread API + +### 3.1 按 sequence 加载 Thread Message + +```http +GET /api/v1/threads/{threadId}/messages?limit=200&beforeSequence=... +``` + +```ts +type GetThreadMessagesQuery = { + limit?: number // 整数,1..200,默认 200 + beforeSequence?: number // 正整数;独占边界 +} +``` + +成功响应:`200 ApiResponse`。 + +服务端核心查询语义: + +```sql +WHERE thread_id = :threadId + AND superseded_at IS NULL + AND (:beforeSequence IS NULL OR sequence < :beforeSequence) +ORDER BY sequence DESC +LIMIT :limit + 1 +``` + +服务端取出窗口后丢弃多取的一条,再按 `sequence ASC` 输出。 + +决定性关系: + +- `bundle.threadId` 必须等于 path ID;所有 Message 的 `threadId` 也必须一致。 +- `beforeSequence` 是独占边界;不会再次返回等于该值的 Message。 +- `oldestReturnedSequence`/`newestReturnedSequence` 分别等于返回数组首/尾 sequence;空数组时均为 `null`。 +- 多取到第 `limit+1` 条时 `hasOlderMessages=true`,否则 false。 +- sequence 可有间隙;replacement 退出有效时间线后,不得重新编号。 + +主要错误:`invalid_query`、`thread_not_found`、`forbidden`。 + +手动 Case:构造有效 sequence `[1, 2, 5, 8]`,以 `limit=2` 首次读取应得到 `[5, 8]`;再以 `beforeSequence=5` 读取应得到 `[1, 2]`,不得因缺少 3、4 报错。 + +### 3.2 更新 Branch 标题 + +```http +PATCH /api/v1/threads/{threadId} +Content-Type: application/json +``` + +```ts +type PatchThreadRequest = { + customTitle: string | null +} +``` + +成功响应:`200 ApiResponse`。 + +决定性关系: + +- 只允许 Branch;Root 展示标题由 Project 管理,修改 Root 返回 `root_thread_title_owned_by_project`。 +- 字符串规则与 Project customTitle 相同;`null` 清除。 +- Body 不接受 projectId、parentThreadId 或 ForkFacts,防止用 metadata API 改写拓扑。 + +主要错误:`validation_error`、`thread_not_found`、`root_thread_title_owned_by_project`。 + +### 3.3 归档与取消归档 Branch + +```http +POST /api/v1/threads/{threadId}/archive +POST /api/v1/threads/{threadId}/unarchive +``` + +Body:无。成功响应:`200 ApiResponse`。 + +决定性关系: + +- 只允许 Branch;Root 必须通过 Project archive/unarchive 管理。 +- 归档保留 Message、Child Thread、ForkFacts 和 BaseContext。 +- 重复命令返回当前状态,不创建额外实体。 + +主要错误:`thread_not_found`、`root_thread_archive_owned_by_project`、`forbidden`。 + +### 3.4 在既有 Thread 发送 Message + +```http +POST /api/v1/threads/{threadId}/messages +Content-Type: application/json +``` + +```ts +type SendMessageRequest = { + parts: UserMessageParts + requestedModelId?: string +} +``` + +成功响应:`201 ApiResponse`。 + +决定性关系: + +- threadId 只来自 path;Body 不接受 threadId、role、sequence 或新 ID。 +- 服务端在同一事务创建 finalized user Message、占位 assistant Message 与 queued Run。 +- 两条 Message 获得 Thread 当前最大 sequence 之后的两个新 sequence,但系统不要求整条历史角色交替。 +- `assistantRun.assistantMessageId === assistantMessage.id`。 +- 当前产品策略下,同 Thread 已存在 queued/running Run 时返回 `thread_generation_in_progress`,且不创建任何 Message。 +- Prompt History、Branch BaseContext 和 Project Instruction 由服务端解析;客户端不得提交。 + +主要错误:`validation_error`、`thread_not_found`、`thread_archived`、`thread_generation_in_progress`、`model_not_available`。 + +手动 Case:在已有最大 sequence=8 的 Thread 发送,响应 U/A sequence 应大于 8 且递增;同时发起第二次命令,只有一个事务成功,另一个返回冲突且没有半条 Message。 + +### 3.5 Fork Thread + +```http +POST /api/v1/threads/{sourceThreadId}/forks +Content-Type: application/json +``` + +```ts +type ForkThreadRequest = { + sourceMessageId: MessageId + anchor?: { + exactQuote: string + textPosition?: { + start: number // UTF-16 code unit,含 start + end: number // UTF-16 code unit,不含 end,且 end > start + } + } +} +``` + +成功响应:`201 ApiResponse<{ thread: ThreadDTO }>`。 + +决定性关系: + +- `sourceThreadId` 来自 path;`sourceMessageId` 是既有资源 ID,二者共同唯一确定 Fork 来源。 +- 服务端验证 `sourceMessage.threadId === sourceThreadId`;不满足返回 `fork_source_thread_mismatch`。 +- 来源必须未 superseded、已 finalized 且具备 Prompt 资格。assistant 还必须对应 completed Run。 +- `anchor` 只表达用户选区。若同时提交 position,则服务端验证 source Message 文本在 `[start,end)` 等于 `exactQuote`。 +- position 以来源 Message 的规范文本投影为坐标:按 parts 顺序提取所有 text part,并以单个换行连接;非 text part 不占字符位置。前后端必须复用同一投影函数。 +- 客户端不得提交 Child ID、parentThreadId、Project ID、BaseContext 或 ForkSourceSnapshot。 +- 服务端事务内计算 BaseContext、生成 ForkSourceSnapshot、创建 Child;Child Project 必须与来源 Thread 相同。 + +主要错误:`validation_error`、`thread_not_found`、`source_message_not_found`、`fork_source_thread_mismatch`、`fork_source_not_finalized`、`fork_source_superseded`、`fork_anchor_mismatch`。 + +手动 Case:在 running assistant 上请求,预期 `fork_source_not_finalized` 且没有 Child;完成后再请求,预期返回 Branch,且请求/响应都不暴露 BaseContext。 + +## 4. Message 与 replacement API + +### 4.1 Edit 最后一条有效 user Message + +```http +POST /api/v1/messages/{sourceUserMessageId}/edits +Content-Type: application/json +``` + +```ts +type EditMessageRequest = { + parts: UserMessageParts + requestedModelId?: string +} +``` + +成功响应:`201 ApiResponse`。 + +决定性关系: + +- Path Message 必须 role=user、未 superseded,并且是当前 Thread 最后一条有效 user Message;否则返回 `fork_required`。 +- 该命令不是原地修改:旧 Message 的 parts 和 sequence 永远不变。 +- 服务端 supersede 从 source user 开始、所有依赖旧输入的当前有效后缀;角色不需要一问一答。 +- 随后在 Thread 尾部创建 replacement user、replacement assistant 和新 queued Run,均使用新 ID、新 sequence。 +- `createdMessages` 按 sequence 升序返回,必须恰含一条 user 和一条 assistant;user 的 `replacesMessageId` 指向 source。 +- `supersededMessageIds` 是本事务退出默认时间线的完整 ID 集合,客户端据此原子更新 Store。 + +主要错误:`message_not_found`、`message_not_editable`、`fork_required`、`thread_generation_in_progress`、`model_not_available`。 + +手动 Case:Thread 有 `U1(seq1), U2(seq2), A1(seq3)`,Edit U2。预期 U2/A1 被 supersede,再追加 U2b/A2;不得因为存在连续 user Message 而拒绝。 + +### 4.2 Regenerate 当前 assistant Message + +```http +POST /api/v1/messages/{sourceAssistantMessageId}/regenerations +Content-Type: application/json +``` + +```ts +type RegenerateMessageRequest = { + requestedModelId?: string +} +``` + +成功响应:`201 ApiResponse`。 + +决定性关系: + +- Path Message 必须 role=assistant、未 superseded、已 finalized,且原 Run 为 completed。 +- MVP 只允许重新生成当前有效时间线最后一个可重新生成的 assistant;历史位置返回 `fork_required`。 +- 旧 assistant 的 parts 和 sequence 不更新;事务只设置其 `supersededAt`。 +- 服务端在 Thread 尾部创建一条新的 assistant Message,并为它创建唯一 queued Run。 +- `createdMessages` 必须恰含一条 assistant;其 `replacesMessageId` 等于 path ID。 +- 新 assistant 可获得不同 `modelId`,但来源 Message 内容保持不变。 + +主要错误:`message_not_found`、`message_not_regeneratable`、`fork_required`、`thread_generation_in_progress`、`model_not_available`。 + +手动 Case:记录 A1 的 ID、parts、sequence 后 Regenerate。预期返回 A2/R2;重新读取默认时间线只见 A2,但数据库中 A1 的 parts 和 sequence 原样保留。 + +### 4.3 设置 Message feedback + +```http +PUT /api/v1/messages/{assistantMessageId}/feedback +Content-Type: application/json +``` + +```ts +type PutMessageFeedbackRequest = { + value: FeedbackValue +} +``` + +成功响应:`200 ApiResponse`。 + +决定性关系: + +- 目标必须是已 finalized、completed 且未 superseded 的 assistant Message。 +- `positive`/`negative` 使用 upsert;`null` 删除或清空评价,响应仍返回 `value:null`。 +- 相同 value 重复 PUT 返回当前结果,不修改 Message 或 Run。 + +主要错误:`message_not_found`、`message_not_feedback_eligible`、`validation_error`。 + +## 5. 生成事件与 Stop API + +### 5.1 订阅或恢复生成事件 + +```http +GET /api/v1/assistant-messages/{assistantMessageId}/events?afterEventSequence=42 +Accept: text/event-stream +``` + +```ts +type GetAssistantEventsQuery = { + afterEventSequence?: number // 非负整数,默认 0 +} +``` + +成功响应:`200 text/event-stream`,不是 `ApiResponse`。 + +SSE Event: + +```ts +type RunSnapshotEvent = { + type: "run.snapshot" + cursor: number + run: AssistantRunStateDTO + message: MessageDTO + includedArtifacts: IncludedArtifactDTO[] +} + +type RunDeltaEvent = { + type: "run.delta" + eventSequence: number + chunk: UIMessageChunk +} + +type RunCompletedEvent = { + type: "run.completed" + eventSequence: number + run: AssistantRunStateDTO + message: MessageDTO + includedArtifacts: IncludedArtifactDTO[] +} + +type RunFailedEvent = { + type: "run.failed" + eventSequence: number + run: AssistantRunStateDTO +} + +type RunStoppedEvent = { + type: "run.stopped" + eventSequence: number + run: AssistantRunStateDTO + message: MessageDTO +} + +type AssistantMessageEvent = + | RunSnapshotEvent + | RunDeltaEvent + | RunCompletedEvent + | RunFailedEvent + | RunStoppedEvent +``` + +连接语义: + +1. 服务端先验证 assistant Message、所属 Project 权限和 Run。 +2. 首个业务事件固定为 `run.snapshot`。它是当前持久化 checkpoint,不占用新 event sequence;`cursor === run.eventSequence`。 +3. 客户端用 snapshot 原子替换本地 checkpoint,并丢弃本地 `eventSequence <= cursor` 的旧流片段。 +4. 若 Run 仍 queued/running,后续只发送 `eventSequence > cursor` 的 live event,且严格递增。 +5. 若 snapshot 已是终态,服务端发送 snapshot 后即可关闭;不得创建或重启 Run。 +6. `afterEventSequence` 用于校验客户端游标与服务端当前 Run 的关系;缺失事件已由 snapshot 合并恢复,因此 P0 不要求重放每个旧 token delta。 +7. 如果客户端游标大于服务端游标,返回 `409 invalid_event_cursor`;如果客户端游标小于服务端保留窗口,仍通过 snapshot 恢复。 +8. 网络断开只取消订阅,不停止 Run。客户端用最后接受的 eventSequence 重连。 + +HTTP 连接建立前的主要错误:`assistant_message_not_found`、`message_run_not_found`、`invalid_event_cursor`、`forbidden`。连接建立后的运行失败通过 `run.failed` 表达。 + +手动 Case:A1 running、服务端 cursor=42 时刷新,以 `afterEventSequence=38` 重连。首事件必须是 cursor=42 的 snapshot;后续事件从大于 42 开始,数据库仍只有原 Run。 + +### 5.2 停止生成 + +```http +POST /api/v1/assistant-messages/{assistantMessageId}/stop +``` + +Body:无。成功响应:`200 ApiResponse`。 + +决定性关系: + +- queued/running Run 设置 `stopRequestedAt` 并返回提交后的最新状态;实际终态可稍后通过事件变为 stopped。 +- completed/failed/stopped 已是终态,重复 Stop 直接返回当前状态。 +- Stop 不创建 replacement Message 或第二个 Run。 +- 如果停止时存在可展示 checkpoint,最终 stopped Message 是否 finalized 由领域规则决定;MVP stopped assistant 仍不得进入 BaseContext 或作为 Fork source。 + +主要错误:`assistant_message_not_found`、`message_run_not_found`、`forbidden`。 + +## 6. 错误码与 HTTP 映射 + +```ts +type ApiErrorCode = + | "validation_error" + | "invalid_query" + | "invalid_cursor" + | "invalid_event_cursor" + | "unauthorized" + | "forbidden" + | "project_not_found" + | "thread_not_found" + | "message_not_found" + | "assistant_message_not_found" + | "message_run_not_found" + | "model_not_available" + | "thread_archived" + | "thread_generation_in_progress" + | "root_thread_title_owned_by_project" + | "root_thread_archive_owned_by_project" + | "source_message_not_found" + | "fork_source_thread_mismatch" + | "fork_source_not_finalized" + | "fork_source_superseded" + | "fork_anchor_mismatch" + | "message_not_editable" + | "message_not_regeneratable" + | "message_not_feedback_eligible" + | "fork_required" + | "project_delete_conflict" + | "internal_error" +``` + +| HTTP | 使用条件 | +|---:|---| +| 400 | 请求形状、字段值、Query 或客户端游标非法 | +| 401 | 未登录或 Session 无效 | +| 403 | actor 已识别但无访问权;部署也可统一返回 404 防止枚举 | +| 404 | 目标资源不存在或按防枚举策略不可见 | +| 409 | 当前状态发生冲突,例如已有运行中生成、无效事件游标或删除冲突 | +| 422 | 资源存在,但不满足命令资格,例如 Fork source 未 finalized、历史 Edit 必须 Fork | +| 500 | 未预期服务端错误;不得向客户端泄漏堆栈和数据库细节 | + +错误示例: + +```json +{ + "error": { + "code": "fork_source_not_finalized", + "message": "The source assistant message is still running.", + "details": { + "assistantMessageId": "msg_123", + "status": "running" + } + } +} +``` + +## 7. Transport 与测试的强制落点 + +- 服务端 route 输入、Application Service 命令输入和客户端 Transport 输入必须使用同一合同命名,避免 `id` 在不同层表示不同实体。 +- 数据库 Row 不得直接作为 API Response;必须显式映射为本文 DTO。 +- 客户端收到响应后先做 Schema 校验,再交给 Action 原子 merge;React 组件不得消费未校验 JSON。 +- 每个 Command 的集成测试至少覆盖:成功关联关系、授权失败、Schema 非法、领域资格失败、事务回滚。 +- 每个 Query 的集成测试至少覆盖:所属隔离、稳定顺序、空结果、边界窗口和 DTO 不泄漏内部字段。 +- `MessageRun.id`、BaseContext、Prompt History、数据库错误与内部 Worker 细节不得进入普通客户端合同。 diff --git a/openspec/changes/design-thread-chat-client-api/proposal.md b/openspec/changes/design-thread-chat-client-api/proposal.md new file mode 100644 index 00000000..2a753021 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/proposal.md @@ -0,0 +1,37 @@ +## Why + +`define-thread-chat-domain-model` 已经确立服务端目标模型 `Project → Thread → Message`,但当前前端仍围绕整棵 `ThreadTreeState`、全局 version 和承担多重职责的 `chat-controller` 工作。若不先固定客户端的实体边界、状态职责、Store Action/Application Command 语义和服务端能力,后续 API、Store、Hooks 与 UI 会再次各自发明关系和状态。 + +本 change 以已确认的领域模型为前提,定义前端如何保存服务端事实、如何从 Store 派生 UI ViewModel、用户操作如何通过 Application Command 变成服务端命令,以及后端 `/api/v1` 必须提供哪些 Query、Command 与生成恢复能力。 + +## What Changes + +- 定义规范化的前端实体模型:Project、Thread、Message、Artifact,以及以 `assistantMessageId` 为关联键的 AssistantRunState。 +- 定义轻量全局 ProjectCatalogStore 与按 `projectId` 创建的 ThreadChatStore;同一个 Project Store 以高内聚 slices 管理服务端实体、加载状态、生成流状态和本地工作台状态。 +- 明确服务端确认实体、运行态、本地 UI 态和派生 ViewModel 的边界;禁止恢复整棵 `ThreadTreeState` 或把派生树形数据复制为第二份权威状态。 +- 统一 Store State、Store Action、Application Command、Selector Hook、Command Hook 和 Lifecycle Hook 术语。 +- 定义纯 Selector 与三类 Hook 的职责,避免 UI 组件直接 `fetch`、拼 Prompt 或修改实体。 +- 定义前端 Application Commands:Project 生命周期、Thread 加载与 Fork、消息发送/Edit/Regenerate、生成订阅/Stop、feedback 和 Project 元数据更新。 +- 定义 `/thread-chat/new` 为无实体 ID 的本地草稿入口;第一次发送时服务端原子创建 Project、Root Thread、首条 user Message、assistant Message 与 MessageRun,再替换为 canonical Project URL。 +- 定义后端 `/api/v1` 的资源标识、请求参数、响应 DTO、错误、原子性、权限与适用场景;所有新实体 ID 均由服务端生成。 +- MVP 首次加载返回全量轻量 Thread topology 和 Root Thread 数据;其他 Thread 按需一次加载最多 200 条有效 Message,不实现复杂分页替换。 +- 本 change 只形成设计与可测试契约,不实现 Zustand Store、Hooks、Route Handler、Stream Client 或 Transport 封装,也暂不编写实施 tasks。 + +## Capabilities + +### New Capabilities + +- `thread-chat-client-state`:前端规范化实体、Project-scoped Zustand Store、Store Actions、Application Commands、selectors、Hooks、工作台状态与 `/new` 生命周期。 +- `thread-chat-command-api`:支撑 Project、Thread、Message、Fork 和 MessageRun 的 `/api/v1` Query/Command/恢复契约。 + +### Modified Capabilities + +- 无。 + +## Impact + +- 前端目标:替换当前整树 Store、全局 version 订阅、客户端实体 ID 和 `chat-controller` 混合职责;保留组合根、纯 selector、流式合帧与设备本地工作台偏好等正确方向。 +- 后端目标:为新领域模型提供 ProjectBootstrap、Thread Message Query、原子业务 Command 和以 `assistantMessageId + eventSequence` 为核心的生成恢复接口。 +- 共享契约:请求/响应 DTO 必须由前后端共用 schema 校验;具体 HTTP client、认证包装和 SSE 解析属于后续 Transport 设计。 +- 路由:新增 `/thread-chat/new` 草稿入口;已持久化内容使用 `/thread-chat/{projectId}`。 +- OpenSpec:依赖 `define-thread-chat-domain-model` 中 Project、Thread、Message、MessageRun、BaseContext 和 replacement 不变量,不重新定义后端持久化 Schema。 diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md new file mode 100644 index 00000000..fc977016 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md @@ -0,0 +1,162 @@ +## ADDED Requirements + +### Requirement: 统一客户端状态架构术语 +客户端规范、设计和实现 MUST 使用以下术语,并 MUST NOT 用同一个 `Action` 同时指代本地状态转换和跨边界业务流程: + +- **Store State**:Zustand Store 保存的数据,包括服务端确认实体、运行态、请求状态和本地 UI 状态。 +- **Store Action**:与对应 Store 或 slice 共置、通过 Zustand `set`/`setState` 执行一次受控 State Transition 的函数。Store Action MUST NOT 调用后端 API、管理路由或建立事件连接。 +- **Application Command**:位于 Store 外、代表一个用户或生命周期业务意图的可测试流程。它 MAY 调用 API、协调订阅与路由,但 MUST 通过 Store Action 提交状态变化,不得自行调用 `set`/`setState`。 +- **Selector Hook**:通过细粒度 selector 订阅 Store,并向 UI 暴露 State 或衍生 ViewModel 的读取 Hook。 +- **Command Hook**:把作用域 ID 与 Application Command 或纯本地 Store Action 绑定为 UI 事件接口的 Hook;它不得包含业务规则或直接修改 State。 +- **Lifecycle Hook**:负责 Bootstrap、按需加载、生成订阅和本地偏好持久化等 React 生命周期接入的 Hook;真正流程仍委托给 Application Command 或 coordinator。 + +本文中的“状态变更”表示通过 `set`/`setState` 产生新状态并通知订阅者,不表示对现有 State 对象做任意原地修改。 + +#### Scenario: API 成功后提交状态 +- **WHEN** Application Command 收到并校验合法服务端 DTO +- **THEN** 它 MUST 调用语义明确的 Store Action 原子合并 DTO +- **AND** Application Command、Transport 和 React 组件 MUST NOT 绕过 Store Action 直接调用 `set`/`setState` + +#### Scenario: 纯本地 UI 操作 +- **WHEN** 用户只改变可见列、画布位置或 overlay 等本地 UI State +- **THEN** Command Hook MAY 直接调用对应 UI slice 的 Store Action +- **AND** 不得为了纯本地 State Transition 建立没有跨边界职责的 Application Command + +### Requirement: 使用规范化客户端实体模型 +客户端 MUST 以 Project、Thread、Message 和 Artifact 的服务端 DTO 作为已确认内容事实,并 MUST 以 `assistantMessageId` 为关联键保存 AssistantRunState。客户端 MUST NOT 把整棵 Thread 拓扑、Message 列表和运行态重新组合成可整体写回的 `ThreadTreeState`。 + +Thread 的 Root/Branch、children、depth 和 breadcrumb MUST 由 `parentThreadId` 派生;Message 默认时间线 MUST 由 `supersededAt IS NULL` 和 `sequence ASC` 派生。BaseContext MUST 保持为服务端内部事实,不得进入普通客户端实体状态或由客户端重建。 + +#### Scenario: 合并 ProjectBootstrap +- **WHEN** 客户端收到包含 Project、Thread topology、Message、AssistantRunState 和 Artifact 的合法 ProjectBootstrap +- **THEN** 客户端 MUST 按实体 ID 规范化合并各类 DTO +- **AND** 不得保存第二份整树权威快照 + +#### Scenario: 派生 Branch 关系 +- **WHEN** 一个 Thread 的 `parentThreadId` 指向同 Project 的另一个 Thread +- **THEN** selector MUST 将它作为该 Parent 的 Child/Branch Thread 展示 +- **AND** 客户端不得要求服务端同时返回可独立修改的 `children` 数组 + +### Requirement: 按生命周期划分 Zustand Store +客户端 MUST 使用一个轻量 ProjectCatalogStore 管理 Project 列表,并 MUST 为每个打开的 `projectId` 创建独立 ThreadChatStore。ThreadChatStore MUST 以高内聚 slice 区分已确认 entities、加载/命令状态、生成流状态和本地 workbench UI 状态;它们可以位于同一个 Zustand store,但不得互相复制权威数据。 + +ProjectCatalogStore MUST NOT 保存 Thread 或 Message。Project-scoped Store 在离开对应 Project 页面后 MUST 可被销毁,不得让多个 Project 的 Message 长期堆积在无边界全局 Store 中。 + +#### Scenario: 打开两个 Project +- **WHEN** 用户先后打开 Project A 和 Project B +- **THEN** 两个 Project 的 ThreadChatStore MUST 具有隔离的实体、生成和工作台状态 +- **AND** ProjectCatalogStore MUST 只保留两者的轻量列表信息 + +#### Scenario: 高频流事件只更新相关订阅者 +- **WHEN** 某条 assistant Message 收到生成增量 +- **THEN** Store MUST 只改变该 `assistantMessageId` 对应的生成流状态 +- **AND** 只订阅其他 Thread 或 Project 元数据的组件 MUST NOT 因全局 version 而被强制重算 + +### Requirement: 区分服务端事实、运行态和本地 UI 态 +Project、Thread、Message、Artifact 和 MessageRun 终态 MUST 以服务端响应为权威。运行中的 checkpoint 和 eventSequence MUST 保存于生成 slice;visibleThreadIds、列宽、画布位置、composer 草稿、overlay 和选中 Artifact MUST 保存为本地 UI 态。 + +客户端 MAY 将设备相关 workbench UI 态持久化到 localStorage,但 MUST NOT 将它提交为 Project 内容。客户端 MUST NOT 为乐观展示伪造 Project、Thread、Message 或 MessageRun ID。 + +#### Scenario: 发送期间展示等待态 +- **WHEN** 用户提交命令但服务端尚未返回新实体 +- **THEN** 客户端 MUST 使用本地 submitting/busy 状态展示等待 +- **AND** 不得向 entities slice 插入 `temp-*`、`main` 或客户端 UUID 形式的待创建实体 + +#### Scenario: 恢复工作台偏好 +- **WHEN** ProjectBootstrap 成功且浏览器存在该 Project 的列布局偏好 +- **THEN** 客户端 MUST 过滤其中已经不存在的 Thread ID 后恢复布局 +- **AND** 本地布局不得覆盖服务端返回的 Thread topology + +### Requirement: 通过 selector 生成 UI ViewModel +客户端 MUST 使用纯 selector 从 entities、AssistantRunState 和 UI state 生成 ThreadColumnView、ProjectTreeRows、MessageView、ForkAvailability 和 ProjectHeaderView 等 ViewModel。派生结果 MUST NOT 作为另一份可独立修改的领域状态持久化。 + +Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有效 Message 时间线、Message 运行展示、Artifact 来源、Fork 可用性和可见列组合。 + +#### Scenario: replacement 后重算消息视图 +- **WHEN** Store 合并一条 replacement Message 并将来源 Message 标记为 superseded +- **THEN** Thread Message selector MUST 隐藏来源 Message并按 sequence 展示 replacement +- **AND** 不得通过手动修改 UI Message 数组维持第二份时间线 + +#### Scenario: 组合 ThreadColumnView +- **WHEN** UI 请求一个 Thread 的列视图 +- **THEN** selector MUST 组合 Thread、有效 Messages、相关 AssistantRunState、Artifact、breadcrumb 和操作可用性 +- **AND** 该 ViewModel 不得成为 API 写入对象 + +### Requirement: 使用 Application Command 执行业务意图 +UI 组件和 React Hook MUST NOT 直接调用 `fetch`、构造 Prompt、创建实体 ID或修改服务端实体。跨越 API、事件连接或路由的用户意图 MUST 进入可独立测试的 Application Command,由 Command 调用 API capability、校验结果并通过 Store Action 原子合并 Store。 + +Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、发送首条消息、发送后续消息、Fork、Edit、Regenerate、Stop、feedback、更新 Project 元数据以及生成恢复。 + +#### Scenario: Fork Command 成功 +- **WHEN** UI 调用 Fork Application Command 并传入已有 sourceThreadId、sourceMessageId 和选区信息 +- **THEN** Command MUST 调用服务端 Fork API 并通过 Store Action 合并服务端返回的 Child Thread +- **AND** UI 层 MAY 在成功后打开新 Thread,但不得自行计算 BaseContext 或 Child Thread ID + +#### Scenario: Application Command 失败 +- **WHEN** 服务端返回结构化领域错误 +- **THEN** Command MUST 保持既有已确认 entities 不变并通过 Store Action 更新相应命令错误状态 +- **AND** Hook MUST 向 UI 暴露可展示的失败结果 + +### Requirement: 使用 Hooks 连接 Store、Command 与 UI +客户端 MUST 将 Hook 分为 Selector Hook、Command Hook 和 Lifecycle Hook。Selector Hook MUST 通过细粒度 selector 订阅 Store;Command Hook MUST 调用 Application Command 或纯本地 Store Action;Lifecycle Hook MUST 只负责把 React 生命周期接入 Bootstrap、按需加载、生成订阅和本地偏好持久化流程。 + +普通衍生数据 MUST NOT 通过 `useEffect + setState` 镜像 Store。单个 Message 组件 MUST NOT 自行发起 Message 或 Generation 查询。 + +#### Scenario: Thread 组件读取数据 +- **WHEN** ThreadColumn 渲染指定 `threadId` +- **THEN** 它 MUST 通过 Selector Hook 获得 ThreadColumnView +- **AND** 不得接收整棵 Project 状态后自行遍历并维护副本 + +#### Scenario: 页面恢复生成订阅 +- **WHEN** 生命周期 Hook 发现已加载 Message 中存在 queued/running AssistantRunState +- **THEN** 它 MUST 交给统一 generation coordinator 从 eventSequence 恢复订阅 +- **AND** 每个 Message 组件不得建立重复订阅 + +### Requirement: 支持无实体 ID 的新 Project 入口 +`/thread-chat/new` MUST 表示本地新 Project 草稿入口,不得把 `new` 当作 Project ID。页面 MUST 在没有 Project/Thread 实体的情况下复用空白聊天 UI;用户第一次发送前不得写入 Project 列表或创建假的 Root Thread。 + +第一次发送成功后,客户端 MUST 合并服务端原子返回的 Project、Root Thread、user Message、assistant Message 和 AssistantRunState,并 MUST 使用 `router.replace` 进入 `/thread-chat/{projectId}`。失败时 MUST 留在 `/thread-chat/new` 并保留草稿。 + +#### Scenario: 打开后直接离开 new 页面 +- **WHEN** 用户打开 `/thread-chat/new` 但未发送任何内容便离开 +- **THEN** 系统 MUST NOT 创建 Project、Thread、Message 或 MessageRun + +#### Scenario: 首次发送成功 +- **WHEN** 用户在 `/thread-chat/new` 发送第一条有效 Message +- **THEN** 客户端 MUST 调用创建 Project 的 Application Command,并通过 Store Action 使用服务端返回的实体初始化 Project Store +- **AND** URL MUST 被替换为服务端返回的 canonical Project URL + +#### Scenario: 首次发送失败 +- **WHEN** 创建命令在服务端提交前失败 +- **THEN** 客户端 MUST 保留本地输入并允许重试 +- **AND** 不得向 ProjectCatalogStore 增加未确认 Project + +### Requirement: 采用轻量 Bootstrap 与按 Thread 加载 +进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Root Thread 的有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact,但 MUST NOT 包含所有 Branch Message、BaseContext 或全部大型资源正文。 + +其他 Thread 的 Message MUST 在首次打开时按 Thread 加载。MVP 客户端 MUST 支持一次合并最多 200 条按 sequence 升序排列的有效 Message,并 MUST 保留服务端返回的 `hasOlderMessages` 信息,但不要求实现复杂的树内自动分页替换。 + +#### Scenario: 初次进入已有 Project +- **WHEN** 用户打开 `/thread-chat/{projectId}` +- **THEN** 客户端 MUST 先合并 ProjectBootstrap 并展示 Root Thread +- **AND** 不得为了渲染 topology 下载全部 Branch Message + +#### Scenario: 首次打开 Branch Thread +- **WHEN** 用户打开尚未加载 Message 的 Branch Thread +- **THEN** 客户端 MUST 只请求该 Thread 的 Message bundle 并合并结果 +- **AND** 重复打开已加载 Thread 不得自动重复请求 + +### Requirement: 恢复服务端 MessageRun +客户端 MUST 使用 `assistantMessageId + eventSequence` 识别和恢复生成。queued/running 的展示 MUST 先使用服务端 checkpoint,再从最后 eventSequence 之后订阅;completed MUST 使用 finalized Message.parts;failed/stopped MUST 展示终态且不得自动重启。 + +刷新、路由切换或取消订阅 MUST NOT 发送 Stop。只有明确的用户 Stop Application Command 可以请求服务端停止 MessageRun。 + +#### Scenario: 刷新后恢复 running Message +- **WHEN** ProjectBootstrap 或 Thread bundle 返回 running AssistantRunState +- **THEN** 客户端 MUST 立即展示 checkpoint 并从返回的 eventSequence 之后恢复订阅 +- **AND** 不得创建新的 assistant Message 或 MessageRun + +#### Scenario: 收到 completed 事件 +- **WHEN** generation coordinator 收到合法 completed 终态 +- **THEN** 客户端 MUST 合并服务端给出的 finalized Message 和终态 Run +- **AND** 必须清除对应的临时流缓冲 diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md new file mode 100644 index 00000000..b67cda47 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md @@ -0,0 +1,174 @@ +## ADDED Requirements + +### Requirement: 使用版本化 API 和服务端实体身份 +ThreadChat 后端 MUST 在 `/api/v1` 下提供版本化 Query、Command 和事件接口。客户端 MUST 只提交正在操作的既有资源 ID;Project、Thread、Message 和 MessageRun 的新 ID MUST 由服务端生成并在成功响应中返回。 + +服务端 MUST 从认证 Session 确定 actor,MUST 对每个 Project、Thread 和 Message 操作执行归属或访问授权校验。普通 JSON 成功响应 MUST 返回 `{ data: T }` 中的权威 DTO,错误 MUST 返回 `{ error: { code, message, details? } }`;204 和事件流除外。请求 Object MUST 严格校验未声明字段。MVP 普通追加、Fork 和生成命令 MUST NOT 要求 Project/Thread revision 或 If-Match。 + +#### Scenario: 客户端提交待创建实体 ID +- **WHEN** 创建或 Fork 请求包含 newProjectId、newThreadId、newMessageId 或 newMessageRunId +- **THEN** 服务端 MUST 忽略或拒绝这些字段 +- **AND** 成功结果中的实体 ID MUST 来自服务端 + +#### Scenario: 访问其他用户的 Project +- **WHEN** actor 对目标 Project 没有访问权 +- **THEN** 服务端 MUST 返回 403 或不泄漏存在性的 404 +- **AND** 不得返回 Project topology、Message 或运行状态 + +### Requirement: 提供 Project 列表与元数据命令 +服务端 MUST 提供列出、更新、归档和永久删除 Project 的能力。列表 MUST 为轻量 ProjectSummary,按 `updatedAt DESC, id DESC` 稳定排序,不得包含 Thread topology 或 Message 正文。 + +Project metadata 更新 MUST 允许独立修改 customTitle、Target 和 Instruction;未提供字段 MUST 保持不变,显式 null MUST 按字段契约清空。MVP 对 metadata 使用服务端最后写入生效,不引入通用 revision。 + +#### Scenario: 列出 Project +- **WHEN** 客户端请求 `GET /api/v1/projects` +- **THEN** 服务端 MUST 只返回 actor 可访问的 ProjectSummary +- **AND** 每项 MUST 至少包含 id、展示标题、archivedAt、updatedAt 和轻量统计 + +#### Scenario: 更新 Project Target +- **WHEN** 客户端请求 `PATCH /api/v1/projects/{projectId}` 并提交合法 Target +- **THEN** 服务端 MUST 更新该 Project 的终极、短期和中期目标集合 +- **AND** 不得修改 Thread、Message 或其他 Project + +#### Scenario: 永久删除 Project +- **WHEN** 已授权 actor 明确请求永久删除 Project +- **THEN** 服务端 MUST 按领域规范清理其 Thread、Message、MessageRun 和附属资源 +- **AND** 后续 Bootstrap MUST 返回 not_found + +### Requirement: 原子创建首个 Project 对话 +服务端 MUST 允许客户端用一条命令提交首条 user Message,并在同一数据库事务创建 Project、唯一 Root Thread、user Message、assistant Message 和 queued MessageRun。事务成功前不得唤醒模型执行;任一实体写入失败 MUST 回滚全部创建。 + +请求 MUST 包含符合 AI SDK v7 UIMessage.parts 的 `initialMessage.parts`,并 MAY 包含 `requestedModelId`。请求不得包含新实体 ID。响应 MUST 返回全部创建实体、AssistantRunState 和 `/thread-chat/{projectId}` canonical URL。 + +#### Scenario: 首次发送成功 +- **WHEN** 客户端请求 `POST /api/v1/projects` 并提交合法 initialMessage +- **THEN** 服务端 MUST 返回 201 及 Project、Root Thread、U1、A1 和 queued Run +- **AND** 数据库中不得存在缺少 Root Thread 或缺少 A1 Run 的部分 Project + +#### Scenario: 首次发送校验失败 +- **WHEN** initialMessage.parts 为空、非法或不符合允许的 UIMessage part 协议 +- **THEN** 服务端 MUST 返回 validation_error +- **AND** 不得创建任何 Project 数据 + +### Requirement: 提供 ProjectBootstrap Query +服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState 和关联 Artifact。 + +Bootstrap MUST NOT 返回 BaseContext、所有 Branch Message、全部 Project File/Artifact 正文或服务端 Prompt。Thread topology MUST 足以由客户端派生 Root、Child、depth 和 breadcrumb。 + +#### Scenario: 加载现有 Project +- **WHEN** actor 请求可访问 Project 的 Bootstrap +- **THEN** 服务端 MUST 返回且只返回一个 Root Thread,并返回全部轻量 topology +- **AND** Root MessageBundle MUST 按 sequence 升序排列有效 Message + +#### Scenario: Project 不存在 +- **WHEN** projectId 不存在或不可访问 +- **THEN** 服务端 MUST 返回 not_found +- **AND** 不得创建空 Project 作为降级结果 + +### Requirement: 按 Thread 提供 MessageBundle +服务端 MUST 提供 `GET /api/v1/threads/{threadId}/messages`,并 MUST 通过 Thread 所属 Project 校验访问权。响应 MUST 返回有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact;默认最多返回最新 200 条,再按 sequence 升序输出。 + +响应 MUST 包含 `hasOlderMessages` 和可供未来向前加载的边界 sequence。MVP 客户端可以不请求更早页面,但 API 不得让调用方通过下载整棵 Project 才能读取一个 Thread。 + +#### Scenario: Thread 少于 200 条有效 Message +- **WHEN** 客户端读取包含 80 条有效 Message 的 Thread +- **THEN** 服务端 MUST 返回全部 80 条并设置 `hasOlderMessages=false` + +#### Scenario: Thread 超过 200 条有效 Message +- **WHEN** 客户端未指定边界读取包含超过 200 条有效 Message 的 Thread +- **THEN** 服务端 MUST 返回最新 200 条并按 sequence 升序排列 +- **AND** 必须设置 `hasOlderMessages=true` 和更早页面边界 + +### Requirement: 提供 Thread 元数据命令 +服务端 MUST 允许授权 actor 更新 Branch Thread 的 customTitle,并 MUST 提供归档和取消归档 Thread 的显式命令。Root Thread 的展示标题 MUST 继续来自 Project,客户端不得通过 Thread metadata 命令为 Root 建立第二套标题权威。 + +归档 Thread MUST 保留其 Message、Child Thread、Fork 来源和 BaseContext;它只改变默认导航可见性,不得等同于永久删除。 + +#### Scenario: 重命名 Branch Thread +- **WHEN** 客户端请求 `PATCH /api/v1/threads/{threadId}` 并提交合法 customTitle +- **THEN** 服务端 MUST 更新该 Branch Thread 标题并返回最新 Thread DTO +- **AND** 不得修改 Project 标题 + +#### Scenario: 归档 Thread +- **WHEN** 客户端请求 `POST /api/v1/threads/{threadId}/archive` +- **THEN** 服务端 MUST 设置 archivedAt 并保留完整 Thread 内容和后代关系 + +### Requirement: 原子发送后续消息并启动生成 +服务端 MUST 提供在既有 Thread 中发送 user Message 的常用原子命令。该命令 MUST 在同一事务创建 user Message、assistant Message 和 queued MessageRun,并返回三者;模型执行只能在事务提交后启动。 + +Path MUST 包含已有 `threadId`;Body MUST 只包含合法 user `parts` 和可选 `requestedModelId`。服务端 MUST 根据当前有效历史构造 Prompt,客户端不得提交 Prompt History、BaseContext、待创建 ID 或整棵 Project 状态。 + +#### Scenario: 在 Root Thread 发送消息 +- **WHEN** 客户端请求 `POST /api/v1/threads/{threadId}/messages` 并提交合法 user parts +- **THEN** 服务端 MUST 分配新的 sequence 并返回 user Message、assistant Message 和 queued Run +- **AND** 响应中的所有新 ID MUST 由服务端生成 + +#### Scenario: 同 Thread 已有运行中生成 +- **WHEN** 当前产品策略不允许同一 Thread 并发生成且已有 queued/running Run +- **THEN** 服务端 MUST 返回 `thread_generation_in_progress` +- **AND** 不得依赖客户端 busy 状态作为唯一保护 + +### Requirement: 原子创建 Fork Thread +服务端 MUST 提供从已有 sourceThreadId 和 sourceMessageId 创建 Child Thread 的命令。请求 MAY 包含用户选区 anchor/quote,但 MUST NOT 包含 BaseContext、ForkSourceSnapshot 的权威字段或 Child Thread ID。 + +服务端 MUST 验证来源资格、同 Project 关系和无环约束,在一个事务中计算并持久化 BaseContext、ForkSourceSnapshot 和 Child Thread,然后返回新 Thread DTO。 + +#### Scenario: 从 completed assistant Message Fork +- **WHEN** 客户端请求 `POST /api/v1/threads/{sourceThreadId}/forks` 且 sourceMessage 合格 +- **THEN** 服务端 MUST 返回 201 和服务端创建的 Child Thread +- **AND** Child Thread 必须属于同一 Project并冻结到来源 Message 的 BaseContext + +#### Scenario: 从 running assistant Message Fork +- **WHEN** sourceMessage 的 Run 为 queued 或 running +- **THEN** 服务端 MUST 返回 `fork_source_not_finalized` +- **AND** 不得创建部分 Child Thread + +### Requirement: 使用 replacement 命令实现 Edit 和 Regenerate +服务端 MUST 分别提供 Edit 最后一条有效 user Message 和 Regenerate 当前可重新生成 assistant Message 的命令。两种命令 MUST 创建服务端 ID 的 replacement Message;Regenerate 以及 Edit 后的新回答 MUST 同时创建新的 queued MessageRun,且不得覆盖旧 Message.parts 或 sequence。 + +Edit 请求 MUST 只提交 sourceUserMessageId、新 parts 和可选 requestedModelId。Regenerate 请求 MUST 只提交 sourceAssistantMessageId 和可选 requestedModelId。响应 MUST 返回被 superseded 的 Message ID、全部新增 Message 和新的 AssistantRunState。 + +#### Scenario: Regenerate 当前 assistant Message +- **WHEN** 客户端请求 `POST /api/v1/messages/{assistantMessageId}/regenerations` 且来源可重新生成 +- **THEN** 服务端 MUST 返回 replacement assistant Message 和新的 queued Run +- **AND** 来源 Message 的内容与 sequence 必须保持不变 + +#### Scenario: Edit 最后一条 user Message +- **WHEN** 客户端请求 `POST /api/v1/messages/{userMessageId}/edits` 并提交新 parts +- **THEN** 服务端 MUST 返回 replacement user Message、replacement assistant Message、queued Run 和被 superseded 的后缀 ID + +#### Scenario: Edit 历史 user Message +- **WHEN** sourceUserMessageId 不是当前 Thread 最后一条有效 user Message +- **THEN** 服务端 MUST 返回 `fork_required` +- **AND** 不得修改任何既有 Message + +### Requirement: 通过 assistantMessageId 管理生成生命周期 +服务端 MUST 允许客户端使用 assistantMessageId 查询、订阅和停止对应 MessageRun,而不要求客户端理解内部 MessageRun ID。事件流 MUST 使用严格递增的 eventSequence,并 MUST 支持 `afterEventSequence` 恢复。每次连接 MUST 先返回当前持久化 checkpoint snapshot 及其 cursor;若仍在运行,后续 live event MUST 严格大于该 cursor。旧 token delta MAY 不逐条重放,但恢复结果不得丢失已经持久化的生成内容。 + +Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得自动停止 Run。终态事件 MUST 携带或允许随后取得 finalized Message 和终态 AssistantRunState。 + +#### Scenario: 恢复 running 事件流 +- **WHEN** 客户端请求 assistant Message 事件且提交 `afterEventSequence=42` +- **THEN** 服务端 MUST 先发送不旧于该游标的当前 checkpoint snapshot +- **AND** 后续 live event MUST 大于 snapshot cursor +- **AND** 不得重新启动第二个 MessageRun + +#### Scenario: 显式停止生成 +- **WHEN** 客户端请求 `POST /api/v1/assistant-messages/{assistantMessageId}/stop` +- **THEN** 服务端 MUST 对 queued/running Run 记录 stop 请求并返回最新 AssistantRunState +- **AND** 重复 Stop MUST 返回相同终态或当前状态而不创建新 Run + +#### Scenario: 仅断开订阅 +- **WHEN** 浏览器关闭事件连接但未发送 Stop +- **THEN** MessageRun MUST 继续由服务端执行 + +### Requirement: 提供 Message feedback 命令 +服务端 MUST 允许客户端按 assistantMessageId 设置 positive、negative 或 null feedback。只有 finalized 且允许评价的 assistant Message 可以接受 feedback;命令不得修改 Message 内容或 MessageRun。 + +#### Scenario: 设置正向反馈 +- **WHEN** 客户端请求 `PUT /api/v1/messages/{assistantMessageId}/feedback` 并提交 `positive` +- **THEN** 服务端 MUST 返回该 Message 最新 feedback + +#### Scenario: 评价 user Message +- **WHEN** 目标 Message 不是 assistant Message +- **THEN** 服务端 MUST 返回 `message_not_feedback_eligible` From b95e3146187a189b7cb2c3132460e6f80fa90fe0 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 16:14:45 +0800 Subject: [PATCH 05/23] docs(openspec): refine thread chat lifecycle design --- .../define-thread-chat-domain-model/design.md | 10 +- .../design/enter-thread-chat-from-home.md | 102 ++++++ .../load-thread-messages-by-sequence.md | 132 ++++++++ ...egenerate-replacement-assistant-message.md | 203 ++++++++++++ .../resume-running-message-after-refresh.md | 236 ++++++++++++++ .../design-thread-chat-client-api/design.md | 290 ++++++++++++++--- .../design/api-contracts.md | 31 +- .../new-project-first-message-lifecycle.md | 295 ++++++++++++++++++ .../design/open-existing-project-lifecycle.md | 293 +++++++++++++++++ .../design/thread-message-loading.md | 245 +++++++++++++++ .../design-thread-chat-client-api/proposal.md | 2 +- .../specs/thread-chat-client-state/spec.md | 136 +++++++- .../specs/thread-chat-command-api/spec.md | 21 +- 13 files changed, 1940 insertions(+), 56 deletions(-) create mode 100644 openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md create mode 100644 openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md create mode 100644 openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md create mode 100644 openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md create mode 100644 openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md create mode 100644 openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md create mode 100644 openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md diff --git a/openspec/changes/define-thread-chat-domain-model/design.md b/openspec/changes/define-thread-chat-domain-model/design.md index 74259346..32d04bba 100644 --- a/openspec/changes/define-thread-chat-domain-model/design.md +++ b/openspec/changes/define-thread-chat-domain-model/design.md @@ -421,16 +421,22 @@ thread-chat/ ```text 全量 Project 的轻量 Thread topology + -首屏可见 Thread 的有效 Messages 与 MessageRun 状态 +Root Thread 的有效 Messages 与 AssistantRunState ``` 不全量加载所有 Branch Message、BaseContext 或大型 Project Resource。具体流程: -1. [从首页进入 ThreadChat](./design/enter-thread-chat-from-home.md):最近 Project、新建 Project、ProjectBootstrap 与前端恢复。 +1. [从首页进入 ThreadChat](./design/enter-thread-chat-from-home.md):首页导航、`/new` 无实体草稿边界与已有 Project 入口。 2. [按 sequence 拉取 Thread 当前有效消息](./design/load-thread-messages-by-sequence.md):`sequence + supersededAt` 构造默认时间线。 3. [Regenerate:创建 replacement assistant Message](./design/regenerate-replacement-assistant-message.md):旧 Message 不可变,新 Message 与 Run 原子创建。 4. [刷新后恢复正在生成的 assistant Message](./design/resume-running-message-after-refresh.md):checkpoint、eventSequence 与重新订阅。 +Provider、Zustand、路由交接和多栏异步加载属于客户端/API 设计,详见: + +- [`/thread-chat/new` 首条消息与 AI 回复生命周期](../design-thread-chat-client-api/design/new-project-first-message-lifecycle.md) +- [打开已有 Project 生命周期](../design-thread-chat-client-api/design/open-existing-project-lifecycle.md) +- [Thread Message 异步加载设计](../design-thread-chat-client-api/design/thread-message-loading.md) + ## Risks / Trade-offs - **[未来需要团队共享 Project]** → 等成员、邀请、角色和所有权转移的真实生命周期明确后,再新增 Project 访问模型;MVP 保持与当前 `branch_trees.user_id` 一致的直接用户所有权。 diff --git a/openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md b/openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md new file mode 100644 index 00000000..77dfbb9a --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md @@ -0,0 +1,102 @@ +# 从首页进入 ThreadChat + +## 决定性结论 + +登录 Session 已经确定 actor。首页只决定导航目标,不提前创建空 Project,也不把当前 Project ID 写入 App Store: + +```text +“新对话” + → /thread-chat/new + → 用户首次发送前没有 Project、Thread、Message 或 MessageRun + +“继续最近对话” + → 有最近可访问 Project:/thread-chat/{projectId} + → 没有 Project:/thread-chat/new + +“对话列表” + → 分页列出当前用户可访问的 ProjectSummary +``` + +Project Catalog 不是进入 Project 页面之前必须完成的前置请求。用户直接打开 `/thread-chat/{projectId}` 时,URL 是当前 Project 的唯一权威;ProjectProvider 可以独立加载 ProjectBootstrap。 + +## 首页导航伪代码 + +```ts +async function onStartChatClick() { + const latest = selectLatestLoadedProjectSummary(appStore.getState()) + + if (latest) { + router.push(threadChatRoutes.project(latest.id)) + return + } + + // Catalog 尚未加载完成时可以查询最近 ProjectSummary;该查询不创建实体。 + const result = await threadChatApi.listProjects({ + status: "active", + limit: 1, + }) + + const project = result.items[0] + router.push( + project + ? threadChatRoutes.project(project.id) + : threadChatRoutes.newProject(), + ) +} + +function onNewProjectClick() { + // 这里只导航,不请求服务端创建空 Project。 + router.push(threadChatRoutes.newProject()) +} +``` + +## `/thread-chat/new` 的领域边界 + +```ts +type NewProjectDraftState = { + parts: UIMessage["parts"] + requestedModelId?: string + submitState: "idle" | "submitting" | "error" +} +``` + +`NewProjectDraftState` 是本地草稿,不是 Chat Entity。只有用户提交第一条有效 Message 后,服务端才在同一事务创建: + +```text +Project +└── Root Thread + ├── U1 finalized + └── A1 pending ── MessageRun queued +``` + +请求不得携带任何待创建实体 ID。服务端返回 `CreationBundle` 后,客户端才能建立 ProjectRuntime,并使用 `threadChatRoutes.project(bundle.project.id)` 构造目标页面路径后执行 `router.replace`。服务端不返回 Web 页面 URL。 + +完整的无抖动 Provider/Store 交接、AI 事件订阅和终态合并见: + +- [新 Project 首条消息与 AI 回复生命周期](../../design-thread-chat-client-api/design/new-project-first-message-lifecycle.md) + +## 已有 Project 的 Bootstrap 边界 + +进入 `/thread-chat/{projectId}` 时,服务端返回: + +```text +Project ++ 全量轻量 Thread topology ++ ProjectArtifactSummary ++ Root ThreadMessageBundle +``` + +不返回所有 Branch Message、BaseContext、Prompt History 或全部大型 Artifact 正文。刷新前恢复出的 Branch Column 由客户端分别异步加载 MessageBundle。 + +完整的有/无工作台 Snapshot、Runtime Message Cache 和多个 Branch 并行加载流程见: + +- [打开已有 Project 生命周期](../../design-thread-chat-client-api/design/open-existing-project-lifecycle.md) + +## 决定性不变量 + +- `/thread-chat/new` 中的 `new` 不是 Project ID。 +- 用户首次发送前不得创建 Project 或 Root Thread。 +- 当前 Project 身份只来自 URL/ProjectProvider,不来自 App Store 的 selected 状态。 +- Project Catalog 只保存分页摘要,不保存 Thread 或 Message。 +- ProjectBootstrap 不创建领域实体,只读取服务端事实。 +- Root Message 随 Bootstrap 加载;Branch Message 按 Thread 异步加载。 diff --git a/openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md b/openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md new file mode 100644 index 00000000..0210a761 --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md @@ -0,0 +1,132 @@ +# 按 sequence 拉取 Thread 当前有效消息 + +## 这个流程解决什么 + +给定已有 `threadId`,服务端返回该 Thread 当前有效时间线的一个窗口: + +- 只返回 `supersededAt IS NULL` 的 Message。 +- 默认读取最新最多 200 条,再按 `sequence ASC` 输出。 +- user Message 没有 MessageRun;assistant Message 必须有唯一 MessageRun。 +- Bundle 同时返回本窗口渲染所需且允许内联的 Artifact。 +- 不假设 user/assistant 交替,也不依赖客户端时间或 `prevMessageId/nextMessageId`。 + +## 核心伪代码 + +```ts +async function loadThreadMessages(query: { + actorId: UserId + threadId: ThreadId + limit?: number + beforeSequence?: number +}): Promise { + const limit = validateIntegerRange(query.limit ?? 200, 1, 200) + const beforeSequence = validateOptionalPositiveInteger( + query.beforeSequence, + ) + + const thread = await threadRepository.findById(query.threadId) + if (!thread) throw new DomainError("thread_not_found") + + await authorization.requireProjectReadAccess({ + actorId: query.actorId, + projectId: thread.projectId, + }) + + /** + * DESC + limit+1 用于高效取得“最新窗口”和 hasOlderMessages; + * 输出前再翻转为 UI 所需的 sequence ASC。 + */ + const rows = await messageRepository.findActiveWindow({ + threadId: thread.id, + sequenceLessThan: beforeSequence, + orderBy: { sequence: "desc" }, + limit: limit + 1, + }) + + const hasOlderMessages = rows.length > limit + const selected = rows.slice(0, limit).reverse() + + assertStrictlyIncreasingAndUnique( + selected.map((message) => message.sequence), + ) + + const assistantMessageIds = selected + .filter((message) => message.role === "assistant") + .map((message) => message.id) + + const runsByAssistantMessageId = + await messageRunRepository.findByAssistantMessageIds( + assistantMessageIds, + ) + + for (const assistantMessageId of assistantMessageIds) { + if (!runsByAssistantMessageId.has(assistantMessageId)) { + throw new DataIntegrityError("assistant_message_run_missing") + } + } + + const includedArtifacts = + await artifactQuery.loadRenderProjectionForMessages({ + projectId: thread.projectId, + sourceMessageIds: selected.map((message) => message.id), + }) + + return { + threadId: thread.id, + messages: selected.map(toMessageDTO), + assistantRuns: assistantMessageIds.map((messageId) => + toAssistantRunStateDTO(runsByAssistantMessageId.get(messageId)!), + ), + includedArtifacts, + hasOlderMessages, + oldestReturnedSequence: selected[0]?.sequence ?? null, + newestReturnedSequence: selected.at(-1)?.sequence ?? null, + } +} +``` + +## 对应查询语义 + +```sql +SELECT messages.* +FROM messages +WHERE messages.thread_id = :threadId + AND messages.superseded_at IS NULL + AND ( + :beforeSequence IS NULL + OR messages.sequence < :beforeSequence + ) +ORDER BY messages.sequence DESC +LIMIT :limit + 1; +``` + +服务端丢弃多取的一条后,将窗口翻转为 `sequence ASC`。`beforeSequence` 是独占边界。 + +## sequence 有空缺是正常结果 + +```text +数据库历史: +seq=1 U1 active +seq=2 A1 superseded +seq=3 A2 active,replaces A1 +seq=4 U2 active + +当前有效时间线: +seq=1 U1 +seq=3 A2 +seq=4 U2 +``` + +系统不得为了消除空缺而重排或重写 sequence。 + +## 决定性不变量 + +- sequence 只由服务端分配,在同一 Thread 内唯一且单调递增。 +- sequence 表示写入顺序,不表示 user/assistant 配对。 +- replacement 获得新 sequence;旧 Message 保留原 sequence。 +- `supersededAt` 决定 Message 是否进入默认有效时间线。 +- `beforeSequence` 是独占边界;返回窗口按 sequence 升序。 +- `assistantRuns` 必须且只覆盖本窗口中的 assistant Message。 +- `includedArtifacts` 只包含渲染本窗口 Message 所需且允许内联的 Artifact。 +- Thread 读取权限必须通过所属 Project 校验。 +- 查询不得创建 Message、MessageRun、Artifact 或 BaseContext。 diff --git a/openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md b/openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md new file mode 100644 index 00000000..f8e552d7 --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md @@ -0,0 +1,203 @@ +# Regenerate:创建 replacement assistant Message + +## 这个流程解决什么 + +Regenerate 的含义不是“修改旧回答”,而是: + +```text +旧 assistant Message 退出当前有效时间线 + + +创建新的 assistant Message 和新的 MessageRun +``` + +```text +Regenerate 前: +seq=1 U1 active +seq=2 A1 active ── R1 completed + +Regenerate 后: +seq=1 U1 active +seq=2 A1 superseded ── R1 completed +seq=3 A2 active ── R2 queued + replaces A1 +``` + +A1 不会被删除、清空或改写。既有 BaseContext 即使引用 A1,也继续解析 A1,不会自动切换到 A2。 + +A1、A2、R1 和 R2 都通过 Thread 归属于同一个 Project;Project 是权限与永久删除边界。 + +## 核心伪代码 + +```ts +async function regenerateAssistantMessage(command: { + actorId: UserId + sourceAssistantMessageId: MessageId + requestedModelId?: ModelId +}): Promise { + const result = await database.transaction(async (tx) => { + /** + * 锁定来源 Message,把“它是否仍然 active”变成事务内事实。 + * 不使用 Project revision 或 Thread revision。 + */ + const sourceMessage = await messageRepository.findByIdForUpdate( + tx, + command.sourceAssistantMessageId, + ) + + if (!sourceMessage) { + throw new DomainError("message_not_found") + } + + await authorization.requireThreadWriteAccess({ + actorId: command.actorId, + threadId: sourceMessage.threadId, + }) + + /** + * MVP 只允许重新生成当前时间线末尾已经完成的 assistant Message。 + * 历史位置需要保留另一条路线时,使用 Fork。 + */ + assert(sourceMessage.role === "assistant") + assert(sourceMessage.supersededAt === null) + assert(sourceMessage.finalizedAt !== null) + assert( + await messageRepository.isLastActiveMessage(tx, { + threadId: sourceMessage.threadId, + messageId: sourceMessage.id, + }), + ) + + const sourceRun = + await messageRunRepository.findByAssistantMessageIdForUpdate( + tx, + sourceMessage.id, + ) + + if (!sourceRun) { + throw new DataIntegrityError("assistant_message_run_missing") + } + + if (sourceRun.status !== "completed") { + throw new DomainError("assistant_message_not_regeneratable") + } + + /** + * sequence allocator 必须在 Thread 范围内串行化, + * 让并发写入也只能得到不同、递增的 sequence。 + */ + const replacementSequence = + await messageRepository.allocateNextSequence(tx, { + threadId: sourceMessage.threadId, + }) + + /** + * 这是 Regenerate 的核心:创建一条全新的 Message。 + * finalized Message 的 role、parts、sequence 绝不能原地更新。 + */ + const replacementAssistantMessage = await messageRepository.insert( + tx, + { + id: idGenerator.newMessageId(), + threadId: sourceMessage.threadId, + sequence: replacementSequence, + role: "assistant", + parts: null, // 最终 parts 只在 completed 时写入一次 + replacesMessageId: sourceMessage.id, + supersededAt: null, + finalizedAt: null, + createdAt: clock.now(), + }, + ) + + /** + * 一条 assistant Message 恰有一条 MessageRun。 + * A2 获得 R2;不会给 A1/R1 增加 attempt=2。 + */ + const replacementMessageRun = await messageRunRepository.insert( + tx, + { + id: idGenerator.newMessageRunId(), + assistantMessageId: replacementAssistantMessage.id, + status: "queued", + modelId: await modelPolicy.resolve({ + actorId: command.actorId, + threadId: sourceMessage.threadId, + requestedModelId: command.requestedModelId, + }), + eventSequence: 0, + checkpointParts: [], + createdAt: clock.now(), + }, + ) + + /** + * A1 只退出默认有效时间线: + * - 不改 A1.parts; + * - 不改 A1.sequence; + * - 不删除 A1/R1; + * - 不修改引用 A1 的既有 BaseContext。 + */ + const superseded = await messageRepository.markSupersededIfActive( + tx, + { + messageId: sourceMessage.id, + supersededAt: clock.now(), + }, + ) + + if (!superseded) { + throw new ConcurrencyError("message_already_superseded") + } + + return { + supersededMessageId: sourceMessage.id, + replacementAssistantMessage, + replacementMessageRun, + } + }) + + /** + * 先提交 durable queued Run,再唤醒后台执行器。 + * 唤醒失败时,queued Run 扫描器仍可恢复执行。 + */ + try { + await messageRunDispatcher.wakeUpAfterCommit( + result.replacementMessageRun.id, + ) + } catch (error) { + logger.error("replacement_run_wakeup_failed", error) + // durable queued Run 由扫描器恢复;不能把已提交 replacement 误报为回滚。 + } + + /** + * API 不向客户端暴露内部 MessageRun ID;客户端使用 assistantMessageId + * 关联运行状态并订阅事件。 + */ + return { + supersededMessageIds: [result.supersededMessageId], + createdMessages: [toMessageDTO(result.replacementAssistantMessage)], + assistantRun: toAssistantRunStateDTO(result.replacementMessageRun), + } +} +``` + +## 为什么顺序是“先创建 replacement,再标记旧消息” + +这些写入处在同一事务,外部不可观察到中间状态。先插入 replacement 可以让数据库的: + +```text +UNIQUE(replaces_message_id) +``` + +尽早阻止两个并发请求同时替换 A1;随后使用条件更新将 A1 标记为 superseded。任一步失败都回滚全部写入。 + +## 决定性不变量 + +- Regenerate 只接受当前末尾、active、finalized 且 Run=completed 的 assistant Message。 +- A1 的 ID、parts、sequence、finalizedAt 和 R1 运行事实保持不变。 +- A2 由服务端生成新 ID,使用 Thread 的下一个 sequence,并通过 `replacesMessageId` 指向 A1。 +- A2 与 R2 必须在同一事务创建;A2 只对应 R2。 +- 对客户端返回 `ReplacementBundle`;MessageRun 内部 ID 不进入普通客户端模型。 +- replacement、来源 Message 与 Thread 必须属于同一个 Project。 +- replacement 事务不需要 Thread/Project revision。 +- 单条 Message 永不 hard delete;A1 只在 Project 永久删除时统一清理。 diff --git a/openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md b/openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md new file mode 100644 index 00000000..97fbfccd --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md @@ -0,0 +1,236 @@ +# 刷新后恢复正在生成的 assistant Message + +## 这个流程解决什么 + +浏览器刷新、关闭页面或事件连接断开只取消本地订阅;后台 MessageRun 继续运行。重新加载包含该 assistant Message 的 ThreadMessageBundle 后,客户端必须: + +1. 合并当前有效 Message 与独立 AssistantRunState。 +2. 立即显示服务端持久化的 `checkpointParts`。 +3. 使用 `assistantMessageId + eventSequence` 恢复订阅。 +4. completed 事件直接携带 finalized Message,不再终态后二次 GET Message。 +5. 不创建新的 Message 或 MessageRun。 + +Project 页如何恢复多个 Branch Column、并行加载各自 MessageBundle,见 [打开已有 Project 生命周期](../../design-thread-chat-client-api/design/open-existing-project-lifecycle.md)。 + +## 后端 Thread Bundle + +```ts +async function loadThreadForRefresh(query: { + actorId: UserId + threadId: ThreadId +}): Promise { + // 复用统一查询:active Message、sequence 窗口、独立 Run 与 Artifact。 + return loadThreadMessages({ + ...query, + limit: 200, + }) +} +``` + +客户端按 `assistantMessageId` 关联 Message 与 Run;公开 DTO 不包含服务端内部 MessageRun ID。 + +## 前端合并与恢复 + +```ts +function applyThreadBundleAndResume(bundle: ThreadMessageBundle) { + // 一次 Store Action 合并 Message、Run、Artifact 与 Thread ready 状态。 + store.getState().applyMessageBundle(bundle) + + for (const run of bundle.assistantRuns) { + switch (run.status) { + case "completed": + // 最终内容已经位于对应 finalized Message.parts。 + generationCoordinator.unsubscribeAssistant( + run.assistantMessageId, + ) + break + + case "failed": + case "stopped": + // checkpoint 可以展示,但终态不自动重启或重连。 + generationCoordinator.unsubscribeAssistant( + run.assistantMessageId, + ) + break + + case "queued": + case "running": + // selector 立即用 checkpointParts 恢复画面,不等待下一 token。 + generationCoordinator.subscribeAssistant( + run.assistantMessageId, + ) + break + } + } +} +``` + +## 事件订阅 + +```ts +function subscribeAssistant(assistantMessageId: MessageId) { + const current = selectAssistantRun( + store.getState(), + assistantMessageId, + ) + + if (!current || !isQueuedOrRunning(current.status)) return + if (connections.has(assistantMessageId)) return + + const connection = api.subscribeAssistantEvents({ + assistantMessageId, + afterEventSequence: current.eventSequence, + + onSnapshot(event) { + /** + * 首个业务事件固定为 run.snapshot: + * - run.checkpointParts 是当前持久化内容; + * - cursor === run.eventSequence; + * - snapshot 可以是终态。 + */ + store.getState().applyRunEvent(event) + streamBuffer.dropThrough({ + assistantMessageId, + eventSequence: event.cursor, + }) + + if (isTerminal(event.run.status)) { + connections.delete(assistantMessageId) + connection.close() + } + }, + + onDelta(event) { + const latest = selectAssistantRun( + store.getState(), + assistantMessageId, + ) + + const lastReceived = selectLastReceivedEventSequence( + store.getState(), + assistantMessageId, + ) + + if ( + !latest || + event.eventSequence <= Math.max( + latest.eventSequence, + lastReceived, + ) + ) return + + // 高频 chunk 先进入 buffer,按 UI frame 合并,不逐 token set Zustand。 + streamBuffer.enqueue(assistantMessageId, event) + }, + + onCompleted(event) { + /** + * completed 事件原子携带: + * finalized Message、completed Run、includedArtifacts、Artifact Summary。 + * 不需要再 GET Message,也不能以前端累计 chunk 作为最终权威。 + */ + store.getState().applyRunEvent(event) + streamBuffer.clear(assistantMessageId) + connections.delete(assistantMessageId) + connection.close() + }, + + onFailedOrStopped(event) { + store.getState().applyRunEvent(event) + streamBuffer.clear(assistantMessageId) + connections.delete(assistantMessageId) + connection.close() + }, + }) + + connections.set(assistantMessageId, connection) +} +``` + +`run.snapshot` 与 `run.completed` 携带的 Artifact Summary 必须按 `changeSequence` 合并;较小的乱序 Summary 不得覆盖较新的页面统计。 + +## 后台完成 + +```ts +async function completeMessageRun(input: { + messageRunId: MessageRunId + finalParts: UIMessagePart[] +}) { + const completed = await database.transaction(async (tx) => { + const run = await messageRunRepository.findByIdForUpdate( + tx, + input.messageRunId, + ) + + // 迟到结果不得覆盖 failed/stopped/completed 终态。 + if (run.status !== "running") return null + + const assistantMessage = await messageRepository.findByIdForUpdate( + tx, + run.assistantMessageId, + ) + + assert(assistantMessage.role === "assistant") + assert(assistantMessage.finalizedAt === null) + + const thread = await threadRepository.findByIdTx( + tx, + assistantMessage.threadId, + ) + if (!thread) throw new DataIntegrityError("message_thread_missing") + + const finalizedMessage = + await messageRepository.finalizeAssistantMessageOnce(tx, { + messageId: assistantMessage.id, + parts: input.finalParts, + finalizedAt: clock.now(), + }) + + const completedRun = + await messageRunRepository.transitionIfCurrent(tx, { + messageRunId: run.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: clock.now(), + }) + + if (!completedRun) { + throw new ConcurrencyError("message_run_status_changed") + } + + const includedArtifacts = + await artifactQuery.loadRenderProjectionForMessagesTx(tx, { + projectId: thread.projectId, + sourceMessageIds: [assistantMessage.id], + }) + + const artifactSummary = + await artifactQuery.loadProjectSummaryTx(tx, { + projectId: thread.projectId, + }) + + return { + message: finalizedMessage, + run: completedRun, + includedArtifacts, + artifactSummary, + } + }) + + if (!completed) return + + // 事务提交后发布;订阅者收到自包含的终态事件。 + await runEventPublisher.publishCompleted(completed) +} +``` + +## 决定性不变量 + +- 刷新和取消订阅不得改变 MessageRun 状态。 +- queued/running 恢复来源是 `checkpointParts + eventSequence`。 +- 客户端只按 `assistantMessageId` 关联和订阅 Run。 +- 同一 assistantMessageId 的订阅必须去重。 +- completed 最终内容来自事件携带的 finalized Message,不来自前端累计 chunk。 +- completed/failed/stopped 终态不自动重启。 +- 页面刷新不得创建第二条 assistant Message 或 MessageRun。 +- MessageRun 终态转换使用条件更新,不需要通用 revision。 diff --git a/openspec/changes/design-thread-chat-client-api/design.md b/openspec/changes/design-thread-chat-client-api/design.md index adb533bc..f0e77cc0 100644 --- a/openspec/changes/design-thread-chat-client-api/design.md +++ b/openspec/changes/design-thread-chat-client-api/design.md @@ -74,6 +74,9 @@ type ThreadId = string type MessageId = string type ArtifactId = string +/** 纯客户端工作台中的物理列身份;不是服务端实体 ID,绝不能传给领域 API。 */ +type ColumnSlotId = string + type JsonValue = | string | number @@ -204,6 +207,20 @@ type ArtifactEntity = { createdAt: string } +type ProjectArtifactSummary = { + /** + * 服务端在 Project Artifact 集合发生变化时单调递增的读模型游标。 + * 客户端只用于拒绝乱序旧快照;它不是 Project revision,也不进入请求。 + */ + changeSequence: number + + /** 当前 Project 下全部 Artifact 数量,不受客户端已加载窗口影响。 */ + total: number + + /** 按服务端稳定 kind 统计;Markdown 数量读取 `byKind.markdown ?? 0`。 */ + byKind: Record +} + type AssistantRunState = { /** 客户端关联键;不暴露服务端内部 MessageRun ID。 */ assistantMessageId: MessageId @@ -411,12 +428,32 @@ type ThreadChatRequestsState = { commandByScope: Record } +type ThreadChatReadModelsState = { + /** + * 服务端确认的 Project 级 Artifact 统计投影。 + * 它不是 Artifact 实体集合;不能从局部加载的 artifactsById 反推或覆盖。 + */ + artifactSummary: ProjectArtifactSummary | null +} + type ThreadColumnSlot = { + /** + * 物理列的稳定本地身份。切换本列 Thread 时保持不变;它可以由客户端生成, + * 因为它不是 Project、Thread、Message 或 Artifact 等服务端实体 ID。 + */ + slotId: ColumnSlotId + /** 槽位当前展示的 Branch Thread;Root 固定在主列,不进入 slots。 */ threadId: ThreadId /** true 表示折叠为窄条,保留位置但不计入展开列上限。 */ folded: boolean + + /** + * 用户为该物理列提交的像素宽度;null 表示自动均分。 + * 它属于 Slot 而不是 Thread,因此切换 Thread 后宽度不变。 + */ + widthPx: number | null } type TextSelectionState = { @@ -437,7 +474,7 @@ type OverlayState = { /** Thread 切换器当前作用域;DOM 坐标和退场动画仍留在组件局部状态。 */ threadSwitcherScope: | { kind: "global" } - | { kind: "column"; threadId: ThreadId } + | { kind: "column"; slotId: ColumnSlotId } | { kind: "subtree"; rootThreadId: ThreadId } | null @@ -454,13 +491,13 @@ type ThreadChatUiState = { columnSlots: ThreadColumnSlot[] /** - * 当前接收全局快捷键/工具栏命令的 Thread。 - * 它必须是 Root 或一个未折叠 column slot;ThreadColumn 自身命令仍使用自己的 threadId。 + * 当前接收全局快捷键/工具栏命令的物理列。`"root"` 表示固定 Root 列; + * 非 null 的 ColumnSlotId 必须指向一个未折叠 Slot。当前 Thread 由该 Slot 派生。 */ - focusedThreadId: ThreadId | null + focusedSlotId: "root" | ColumnSlotId | null - /** 用户显式调整的展开列宽;无条目表示自动均分,折叠细条不读取此值。 */ - columnWidths: Record + /** Root 物理列的显式像素宽度;null 表示自动均分。 */ + rootColumnWidthPx: number | null /** null 表示根据视口自适应;非 null 是用户强制的展开列数量上限。 */ forceColumnCount: number | null @@ -483,8 +520,8 @@ type ThreadChatUiState = { /** 支撑列满 LRU 策略的本地逻辑时钟;不是服务端活动时间。 */ activationClock: number - /** Thread 最近一次获得焦点/被打开时的逻辑顺序,用于选择替换或折叠候选。 */ - lastActivatedOrderByThreadId: Record + /** 物理列最近一次获得焦点/被打开时的逻辑顺序,用于选择替换或折叠候选。 */ + lastActivatedOrderBySlotId: Record<"root" | ColumnSlotId, number> /** 只保存跨组件需要共享的语义开关;DOM 引用、坐标和动画计数不进 Store。 */ overlays: OverlayState @@ -500,6 +537,9 @@ type ThreadChatProjectState = { /** Query/Command 的客户端请求状态;不是服务端领域状态。 */ requests: ThreadChatRequestsState + /** Project 级服务端统计等非实体读模型。 */ + readModels: ThreadChatReadModelsState + /** 当前设备上的 Project 工作台布局和交互焦点。 */ ui: ThreadChatUiState } @@ -512,11 +552,18 @@ type ThreadChatProjectActions = { applyThreadCreated: (thread: ThreadEntity) => void applyReplacementBundle: (bundle: ReplacementBundle) => void applyRunEvent: (event: AssistantMessageEvent) => void + setBootstrapLoadState: (state: LoadState) => void setCommandState: (scope: string, state: CommandState | null) => void - openThread: (threadId: ThreadId, sourceThreadId: ThreadId | null) => void - closeThread: (threadId: ThreadId) => void - focusThread: (threadId: ThreadId) => void - setColumnWidth: (threadId: ThreadId, width: number | null) => void + setThreadMessageLoadState: (threadId: ThreadId, state: LoadState) => void + restoreWorkbenchSnapshot: (snapshot: ThreadWorkbenchSnapshotV1) => void + resetWorkbenchToDefault: () => void + openThread: (threadId: ThreadId, sourceSlotId: "root" | ColumnSlotId) => void + switchColumnThread: (slotId: ColumnSlotId, threadId: ThreadId) => void + closeColumn: (slotId: ColumnSlotId) => void + focusColumn: (slotId: "root" | ColumnSlotId) => void + commitColumnWidths: ( + widths: Partial>, + ) => void setPlacementMode: (mode: ThreadChatUiState["placementMode"]) => void setViewMode: (mode: ThreadChatUiState["viewMode"]) => void } @@ -528,21 +575,84 @@ type ThreadChatProjectStore = 同一个 Project Store 内使用 slices,可以由一个 Store Action 通过一次 `set` 同时更新 Message、Run 和 request state;这不是把 slices 物理拆成多个 Store。组件通过细粒度 selector 订阅。 -`columnSlots` 取代原先不充分的 `visibleThreadIds` State:fold 模式必须保存槽位折叠态。`selectVisibleThreadIds` 和 `selectVisibleColumns` 从 Root + `columnSlots` 派生,避免两份分栏权威。 +`columnSlots` 取代原先不充分的 `visibleThreadIds` State:fold 模式必须保存槽位折叠态。每个 Slot 用稳定 `slotId` 表示物理列,`threadId` 只是该列当前展示的内容;切换 Thread 不得重建 Slot 或转移列宽。`selectVisibleThreadIds` 和 `selectVisibleColumns` 从 Root + `columnSlots` 派生,避免两份分栏权威。 + +现有分栏分割线拖拽能力必须保留。分割线位于相邻两个展开列之间;拖拽时同时调整左右两列,并继续遵守当前 UI/CSS 的最小宽度约束。拖拽过程中的 Pointer 坐标、临时宽度和捕获状态保留在 Resizer Hook/组件局部,不得按每个 Pointer Move 写入 Zustand。Pointer Up、键盘步进或双击复位时,通过一次 `commitColumnWidths` 原子提交受影响物理列的最终宽度;双击复位写入 `null`,恢复当前自动均分行为。 + +Root 宽度写入 `rootColumnWidthPx`,Branch 宽度写入对应 `ThreadColumnSlot.widthPx`。折叠细条不参与相邻列拖拽;切换 Slot 的 Thread 必须保留宽度。提交后的宽度进入 `ThreadWorkbenchSnapshotV1`,刷新后恢复;拖拽瞬时状态和 Pointer 信息不得持久化。该交互完全是本地工作台行为,不调用后端 API。 Project Store 初始化和分栏变更必须保持: -- 空 Runtime 从 `project=null`、空实体表、`bootstrap=idle`、空 `columnSlots` 和 `focusedThreadId=null` 初始化。 -- Bootstrap 后 `focusedThreadId` 默认是 Root;Root 固定渲染,不进入 `columnSlots`。 -- `columnSlots.threadId` 必须唯一、属于当前 Project 且不是 Root;恢复 localStorage 时过滤失效 ID。 -- `focusedThreadId` 非 Root 时必须指向未折叠槽位;打开/展开 Thread 同时使其获得焦点。 +- 空 Runtime 从 `project=null`、空实体表、`bootstrap=idle`、`artifactSummary=null`、空 `columnSlots` 和 `focusedSlotId=null` 初始化。 +- Bootstrap 后 `focusedSlotId` 默认是 `"root"`;Root 固定渲染,不进入 `columnSlots`。 +- `columnSlots.slotId` 和 `columnSlots.threadId` 都必须唯一;Thread 必须属于当前 Project 且不是 Root。恢复 localStorage 时过滤失效 Thread ID、重复 Slot ID 和重复 Thread ID。 +- `focusedSlotId` 非 Root 时必须指向未折叠槽位;打开/展开 Thread 同时使对应 Slot 获得焦点。 - 关闭或折叠当前焦点时,按相邻展开槽位、Root 的顺序选择新焦点,不允许留下悬空 ID。 - App Store 和 Project Store 的 UI slice 可以写入设备 localStorage;entities、runs 和 requests 不得以它作为持久化权威。 +#### Project 工作台视图刷新恢复 + +刷新页面后必须恢复刷新前的 Project 工作台视图,但不恢复任何列的滚动位置。持久化对象使用独立、带版本的投影,不能直接序列化整个 `ThreadChatUiState`: + +```ts +type ThreadWorkbenchSnapshotV1 = { + schemaVersion: 1 + + /** Root 右侧的物理列、当前 Thread、折叠态和物理列宽。 */ + columnSlots: ThreadColumnSlot[] + + /** 刷新前接收全局快捷键和工具栏操作的列。 */ + focusedSlotId: "root" | ColumnSlotId + + /** 固定 Root 列的显式宽度;null 表示自动均分。 */ + rootColumnWidthPx: number | null + + forceColumnCount: number | null + placementMode: "replace" | "fold" + viewMode: "columns" | "canvas" + + /** Canvas 节点的设备本地摆放;不存在的 Thread 使用自动布局。 */ + canvasPins: Record +} +``` + +该投影按 `projectId + schemaVersion` 写入设备 localStorage,并采用防抖保存。Bootstrap 成功后才能恢复,恢复时必须: + +1. 只保留 topology 中仍然存在、属于当前 Project 且不是 Root 的 Slot Thread。 +2. 去除重复或非法的 `slotId`、重复 `threadId`、非法宽度和无效 Canvas pin。 +3. 如果 `focusedSlotId` 已失效或指向折叠列,则回退到相邻未折叠列;仍无候选时回退 `"root"`。 +4. 恢复 Slot、宽度、折叠状态、焦点、列数偏好、放置模式、Columns/Canvas 模式与 Canvas pin。 +5. 不恢复滚动条位置、DOM 引用、弹层坐标、动画状态、文本选区、Switcher/Help 临时打开态、请求错误、命令 busy 状态、流缓冲或 Generation 连接。 +6. Composer 草稿不是“视图恢复”的一部分;如果以后要求跨刷新草稿恢复,必须使用独立草稿投影,不得混入本 Snapshot。 + +localStorage 不可用、内容损坏或版本未知时,必须安全回退到默认视图:只显示 Root、`focusedSlotId="root"`,其余使用当前 UI 默认值。恢复工作台视图不代表恢复服务端内容;Message 与 Run 仍然只能来自 Bootstrap、MessageBundle 和生成事件。 + +`artifactSummary` 的合并必须比较 `changeSequence`:只接受大于当前值的快照;相同 sequence 的内容必须完全一致;更小的乱序快照必须忽略。这个游标仅解决多个生成事件乱序覆盖页面统计的问题,不是 Project/Thread revision,客户端不得在 Command 中提交它。 + ### D4. Provider、Runtime 与当前 Project 选择 Provider 解决的是“组件树应该使用哪个 Store 实例”,不是第三套业务 State。当前 Project 身份只来自 `/thread-chat/{projectId}` 的路由参数;Catalog 和 AppShellUi 都不保存 `selectedProjectId`。 +页面路径属于客户端 Router,不属于后端领域契约。所有 ThreadChat 页面导航必须通过同一个集中式路由构造器完成,Application Command、Hook 和组件不得自行拼接 URL: + +```ts +type ThreadChatRoutes = { + /** 无持久化 Project 身份的本地草稿入口。 */ + newProject: () => string + + /** 根据服务端返回的资源 ID 构造客户端页面路径。 */ + project: (projectId: ProjectId) => string +} + +const threadChatRoutes: ThreadChatRoutes = { + newProject: () => "/thread-chat/new", + project: (projectId) => + `/thread-chat/${encodeURIComponent(projectId)}`, +} +``` + +`ThreadChatRoutes` 是客户端 navigation/router 模块的 capability,不进入 Zustand State,也不进入 API DTO。服务端只返回 `project.id`;Web、CLI、MCP 或未来其他客户端各自决定如何呈现或导航该资源。路由结构变化时,只修改客户端路由模块及 Router 配置,不修改后端创建命令。 + #### Runtime 类型 ```ts @@ -588,11 +698,22 @@ type GenerationCoordinator = { destroy: () => void } +type ThreadMessageLoader = { + /** + * 确保指定 Thread 的 MessageBundle 已加载;ready 时直接返回, + * 同一 threadId 已在飞时复用同一个 Promise。 + */ + ensure: (threadId: ThreadId) => Promise + + /** Runtime 销毁时中止当前 Project 的全部 Message Query。 */ + destroy: () => void +} + type ThreadChatAppRuntime = { /** Provider-scoped App Store;不是模块级服务器单例。 */ appStore: StoreApi - /** 管理 ProjectRuntime 身份和从 /new 到 canonical URL 的一次性交接。 */ + /** 管理 ProjectRuntime 身份和从 /new 到已创建 Project 路由的一次性交接。 */ projectRuntimeRegistry: ProjectRuntimeRegistry /** 命令工厂内部共享的已校验 API capability;React 组件不得直接调用。 */ @@ -615,6 +736,12 @@ type ThreadChatProjectRuntime = { /** 已绑定 api、store 和 coordinator 的 Project 级业务命令集合。 */ commands: ThreadChatProjectCommands + /** + * 管理 threadId → in-flight Promise/AbortController 的非序列化加载器。 + * Promise 与 AbortController 不进入 Zustand;UI 只订阅 requests slice。 + */ + threadMessageLoader: ThreadMessageLoader + /** * 管理该 Project 内 assistantMessageId → 事件连接的客户端协调器。 * 连接对象不进入 Zustand State;断开只取消订阅,不发送 Stop。 @@ -699,7 +826,7 @@ type NewProjectDraftProviderProps = { `NewProjectDraftProvider`: - 只存在于 `/thread-chat/new`,保存无实体 ID 的本地草稿与提交状态。 -- 创建成功后调用 `registry.seedFromCreation(bundle)`,再 `router.replace(bundle.canonicalUrl)`。 +- 创建成功后调用 `registry.seedFromCreation(bundle)`,再执行 `router.replace(threadChatRoutes.project(bundle.project.id))`。 - 目标 ProjectProvider acquire 同一 seeded Runtime,因此无需为了路由切换重新请求 Bootstrap。 Provider 必须只创建一次 vanilla Store/Runtime,不能在 React 重渲染时重新执行 factory: @@ -809,10 +936,11 @@ function useThreadChatCommands(): ThreadChatProjectCommands { ```text current Project = URL routeProjectId current Project Store = ProjectProvider 当前提供的 Runtime.store -focused Thread = Runtime.store.ui.focusedThreadId +focused Column = Runtime.store.ui.focusedSlotId +focused Thread = 由 focusedSlotId 对应的 Root/Slot 派生 ``` -一个 Project 可以同时展示多个 `columnSlots`,所以没有全局唯一的“selected Thread 内容”。ThreadColumn 的命令始终使用自己的 `threadId`;`focusedThreadId` 只服务全局快捷键、工具栏和无列级参数的 UI 操作。 +一个 Project 可以同时展示多个 `columnSlots`,所以没有全局唯一的“selected Thread 内容”。ThreadColumn 的命令始终使用自己 Slot 当前的 `threadId`;`focusedSlotId` 只服务全局快捷键、工具栏和无列级参数的 UI 操作。切换本列 Thread 时 Slot 身份与焦点保持不变。 #### Provider 生命周期 @@ -855,13 +983,65 @@ activeMessages visibleThreadIds threadTreeRows branchCount +artifactCount +markdownArtifactCount canFork canEdit canRegenerate threadColumnView +threadColumnHeaderView projectHeaderView ``` +页面统计的来源必须明确区分:`threadCount` 与 `branchCount` 从完整 `threadsById` topology 派生;`artifactCount` 与 `markdownArtifactCount` 从服务端 `artifactSummary` 派生。客户端不得用局部加载的 `artifactsById` 长度冒充 Project 总数。 + +```ts +type ProjectHeaderView = { + displayTitle: string + threadCount: number + branchCount: number + artifactCount: number + markdownArtifactCount: number +} + +type ThreadColumnHeaderView = { + /** `"root"` 表示固定 Root 列;其余值标识稳定物理 Slot。 */ + slotId: "root" | ColumnSlotId + threadId: ThreadId + title: string + isRoot: boolean + depth: number + + /** 从 Root 到当前 Thread 的 lineage;不是 Thread Entity 内的持久化数组。 */ + breadcrumbs: Array<{ + threadId: ThreadId + title: string + isCurrent: boolean + }> + + /** 只包含直接 Child;完整 Project 搜索仍由全局 Thread 切换器承担。 */ + directChildCount: number + directChildren: Array<{ + threadId: ThreadId + title: string + isOpen: boolean + }> + + /** + * Root 为 null。needs_load 表示仅因 Parent Message 尚未加载而无法判断, + * 不得把“本地不存在”误判成来源失效。 + */ + forkSource: { + parentThreadId: ThreadId + sourceMessageId: MessageId + availability: "available" | "needs_load" | "unavailable" + } | null + + canSwitch: boolean + canCollapse: boolean +} +``` + 核心 selectors: ```ts @@ -871,10 +1051,13 @@ selectThreadLineage(state, threadId): ThreadEntity[] selectActiveMessages(state, threadId): MessageEntity[] selectMessageView(state, messageId): MessageView selectForkAvailability(state, messageId): ForkAvailability -selectThreadColumnView(state, threadId): ThreadColumnView +selectThreadColumnView(state, slotId): ThreadColumnView +selectThreadColumnHeaderView(state, slotId): ThreadColumnHeaderView selectProjectTreeRows(state): ProjectTreeRow[] selectVisibleThreadIds(state): ThreadId[] selectVisibleColumns(state): ThreadColumnView[] +selectFocusedThreadId(state): ThreadId | null +selectProjectHeaderView(state): ProjectHeaderView ``` `selectActiveMessages` 的唯一规则: @@ -895,16 +1078,21 @@ Store Action 与对应 Zustand Store/slice 共置,只执行同步、可测试 ```ts mergeBootstrap → 跨 entities/runs/requests 原子初始化 Project +setBootstrapLoadState → 只设置 Bootstrap loading/error;ready 由 mergeBootstrap 提交 applyMessageBundle → 合并一个 Thread 的有效 Message、Run 与 Artifact +setThreadMessageLoadState → 独立设置某个 Thread 的 loading/error;ready 由 Bundle 原子提交 applyMessageCreationBundle → 合并发送命令返回的 user、assistant 与 Run applyThreadCreated → 合并服务端创建的 Child Thread applyReplacementBundle → 原子标记 superseded 并合并 replacement 与新 Run -applyRunEvent → 只更新对应 assistantMessageId 的流状态或终态 -openThread/closeThread → 根据 placementMode 更新 columnSlots -focusThread → 更新全局快捷键与工具栏的 Thread 焦点 +applyRunEvent → 更新对应 assistantMessageId,并在事件携带时合并 Artifact Summary +openThread/closeColumn → 根据现有 placementMode 更新稳定 columnSlots +switchColumnThread → 只替换 Slot 的 threadId,保留 slotId、宽度和物理位置 +focusColumn → 更新全局快捷键与工具栏的物理列焦点 +commitColumnWidths → 原子提交分割线左右物理列的最终宽度或复位为自动均分 +restoreWorkbenchSnapshot/resetWorkbenchToDefault → Bootstrap 后恢复合法视图或使用当前默认值 ``` -合并类 Store Action 是内部写入口,不直接暴露给 UI;`openThread`、`closeThread`、`focusThread` 这类纯本地 UI Action 可以由 Command Hook 直接调用。Store Action 不调用 API、不导航、不建立 SSE,并且不得原地改写已确认实体内容。 +合并类 Store Action 是内部写入口,不直接暴露给 UI;`openThread`、`switchColumnThread`、`closeColumn`、`focusColumn` 这类纯本地 UI Action 可以由 Command Hook 直接调用。Store Action 不调用 API、不导航、不建立 SSE,并且不得原地改写已确认实体内容。 这遵循 Zustand 将 state 与 actions 共置、使用 `set`/`setState` 提交更新以及用 slices 组织大型 Store 的推荐方式。我们额外规定 Store Action 保持同步、IO 放入 Application Command;这是本项目为了固定副作用边界采用的更严格约束,不是 Zustand 强制要求。 @@ -986,6 +1174,8 @@ type NewProjectDraftStore = NewProjectDraftState & NewProjectDraftActions `NewProjectDraftStore` 由 `NewProjectDraftProvider` 创建并随 `/new` 页面销毁,不放进 ThreadChatAppStore,也不插入 ThreadChatProjectStore.entities。UI 通过 `NewProjectScreen` 复用 Composer 和空白列外观,但传递的是 `onSubmitDraft`,不是假的 threadId。 +从空白 Draft 提交、服务端原子创建、Registry seed、目标 ProjectProvider 接管、无空白帧路由交接和完整 AI 回复事件流程见 [`/thread-chat/new` 首条消息与 AI 回复生命周期](./design/new-project-first-message-lifecycle.md)。 + ```mermaid sequenceDiagram participant U as User @@ -1002,11 +1192,12 @@ sequenceDiagram S->>DB: BEGIN S->>DB: Project + Root + U1 + A1 + queued Run S->>DB: COMMIT - S-->>A: 201 CreationBundle + canonicalUrl + S-->>A: 201 CreationBundle S->>W: commit 后唤醒 Run + A->>A: threadChatRoutes.project(project.id) A->>Reg: seedFromCreation(CreationBundle) Reg-->>A: initialized ProjectRuntime - A->>UI: router.replace(canonicalUrl) + A->>UI: router.replace(projectUrl) A->>Reg: Runtime 按 A1 + eventSequence 订阅 ``` @@ -1024,6 +1215,9 @@ type ProjectBootstrap = { /** 当前 Project 的全量轻量 Thread topology;不包含 Branch Message。 */ threadTopology: ThreadEntity[] + /** Project 全量 Artifact 的服务端统计投影,不受本次内联 Artifact 窗口影响。 */ + artifactSummary: ProjectArtifactSummary + /** 唯一 Root Thread 的首屏 Message window。 */ initialThread: ThreadMessageBundle } @@ -1057,16 +1251,26 @@ type ThreadMessageBundle = { → ThreadChatProjectProvider acquire(projectId) → 若 Runtime 已由 /new seed:跳过 Bootstrap → 否则 GET ProjectBootstrap -→ 校验唯一 Root、Provider projectId 和所有 Thread projectId 关系 -→ mergeBootstrap 一次合并 entities/runs/request state -→ 读取并过滤 localStorage workbench state -→ 初始化 focusedThreadId=Root;columnSlots 恢复合法 Branch 槽位 -→ 渲染 Root + topology/columnSlots -→ generation coordinator 恢复 queued/running Runs +→ 校验唯一 Root、Provider projectId、所有 Thread projectId 和 Artifact Summary +→ mergeBootstrap 一次合并 entities/runs/requests/readModels state +→ 读取、迁移并过滤该 Project 的 localStorage ThreadWorkbenchSnapshot +→ 默认 focusedSlotId="root";Snapshot 合法时恢复列槽、列宽、折叠态、焦点和视图模式 +→ 立即渲染已 ready 的 Root 和各 Branch Column Loading Shell +→ 对恢复出的每个 Branch 非阻塞、并行调用 ensureThreadMessages(threadId) +→ 每个 Branch 独立进入 ready 或 error;一个失败不得阻塞其他列 +→ 每个 MessageBundle 合并后恢复其中 queued/running Runs ``` 打开尚未加载的 Branch 时,`ensureThreadMessages(threadId)` 请求一个 ThreadMessageBundle;加载状态为 ready 时直接复用,不重复请求。 +同一 `threadId` 的并发 ensure 必须复用 `ThreadMessageLoader` 中同一个 in-flight Promise;不同 Thread 可以并行加载。关闭或切换 Column 不取消已经开始的 Thread Query,成功结果继续按 `threadId` 合并并成为当前 Project Runtime 的缓存;只有 ProjectRuntime 销毁才统一 Abort。Abort 不得写入可重试 error。 + +Message Query 与 Assistant Run 是两个正交状态:Column 只有在 Message Query ready 后才有完整 Message 时间线;若 Bundle 中某个 Run 为 queued/running,则先展示其 checkpoint,再由 GenerationCoordinator 从 eventSequence 恢复。Loader 的 Promise、AbortController 和 in-flight Map 不进入 Zustand。 + +完整状态机、刷新并行恢复时序、去重与迟到响应规则见 [Thread Message 异步加载设计](./design/thread-message-loading.md)。 + +已有 Project 的完整冷启动、无 Workbench Snapshot、有 Snapshot、多 Branch 并行加载和 Runtime Message Cache 分支见 [打开已有 Project 生命周期](./design/open-existing-project-lifecycle.md)。 + MVP 返回最新最多 200 条有效 Message,并按 sequence 升序交给客户端。`hasOlderMessages` 只保留能力边界,当前 UI 不实现树内自动分页替换。 ### D9. 核心 Application Command 伪代码 @@ -1086,6 +1290,7 @@ async function createProjectWithFirstMessage(input: { initialMessage: { parts: input.parts }, requestedModelId: input.requestedModelId, }) + const projectUrl = threadChatRoutes.project(bundle.project.id) const runtime = projectRuntimeRegistry.seedFromCreation(bundle) appStore.getState().upsertProjectSummary(toProjectSummary(bundle.project)) @@ -1094,7 +1299,7 @@ async function createProjectWithFirstMessage(input: { runtime.generationCoordinator.subscribeAssistant( bundle.assistantRun.assistantMessageId, ) - router.replace(bundle.canonicalUrl) + router.replace(projectUrl) } catch (error) { newDraftStore.getState().markError(normalizeError(error)) } @@ -1132,6 +1337,8 @@ async function sendMessage(threadId: ThreadId, parts: UIMessage["parts"]) { ```ts async function forkThread(input: { + /** 纯客户端来源物理列,只用于成功后的工作台放置,不进入 API Body。 */ + sourceSlotId: "root" | ColumnSlotId sourceThreadId: ThreadId sourceMessageId: MessageId anchor?: TextAnchor @@ -1139,9 +1346,13 @@ async function forkThread(input: { const availability = selectForkAvailability(store.getState(), input.sourceMessageId) if (!availability.allowed) return availability.failure - const child = await api.forkThread(input) + const child = await api.forkThread({ + sourceThreadId: input.sourceThreadId, + sourceMessageId: input.sourceMessageId, + anchor: input.anchor, + }) store.getState().applyThreadCreated(child) - store.getState().openThread(child.id, input.sourceThreadId) + store.getState().openThread(child.id, input.sourceSlotId) } ``` @@ -1184,11 +1395,14 @@ useProject() useProjectTarget() useThread(threadId) useThreadMessages(threadId) -useThreadColumnView(threadId) +useThreadColumnView(slotId) +useThreadColumnHeaderView(slotId) useProjectTreeRows() useAssistantRun(assistantMessageId) useForkAvailability(messageId) useVisibleThreadColumns() +useProjectHeaderView() +useFocusedColumnId() useFocusedThreadId() ``` diff --git a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md index 19eb9ce8..3943186e 100644 --- a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md +++ b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md @@ -2,7 +2,7 @@ 本文是 `design.md` 引用的 API 可实现合同。接口实现、共享 Zod Schema、服务端集成测试和客户端 Transport 必须以本文为准;总表只用于导航。 -### D12. API 总表 +### D1. API 总表 本节只是能力索引。逐接口的 Path、Query、Body、成功响应、输出类型、错误码、参数关系和手动 Case 以 [ThreadChat V1 API 详细合同](#threadchat-v1-api-详细合同) 为准。 @@ -27,7 +27,7 @@ | `GET /api/v1/assistant-messages/{id}/events` | 恢复事件 | `afterEventSequence` | Event stream | 刷新后续接生成 | | `POST /api/v1/assistant-messages/{id}/stop` | 停止生成 | assistantMessageId | AssistantRunState | 用户点击 Stop | -### D13. 参数之间的决定性关系 +### D2. 参数之间的决定性关系 以下内容只保留最核心的关系速查;完整 Schema 与所有边界条件见 [ThreadChat V1 API 详细合同](#threadchat-v1-api-详细合同)。 @@ -237,6 +237,17 @@ type IncludedArtifactDTO = { createdAt: DateTime } +type ProjectArtifactSummaryDTO = { + /** Project Artifact 集合变化时由服务端单调递增;只用于客户端拒绝旧统计。 */ + changeSequence: number + + /** 当前 Project 下全部 Artifact 数量,不受本次响应内联窗口影响。 */ + total: number + + /** 按稳定 Artifact kind 聚合;Markdown 数量读取 `byKind.markdown ?? 0`。 */ + byKind: Record +} + type FeedbackValue = "positive" | "negative" | null type FeedbackDTO = { @@ -256,6 +267,8 @@ type FeedbackDTO = { - `status=completed` 时,Message 必须已有不可变 `parts` 和 `finalizedAt`;`status=failed|stopped` 时 `finishedAt` 非空。 - `stopRequestedAt` 非空表示服务端已经接受 Stop;它不等于 Run 已进入 `stopped` 终态。 - `contentIncluded=false` 时 `IncludedArtifactDTO.content` 必须为 `null`;它只表示本响应未内联正文,不表示 Artifact 不存在。 +- `ProjectArtifactSummaryDTO.changeSequence` 必须是非负整数,并在该 Project 的 Artifact 集合发生变化时单调递增;它只排序统计快照,不是 revision、ETag 或客户端写入前置条件。 +- `ProjectArtifactSummaryDTO.total` 必须等于 `byKind` 所有非负整数值之和;它统计 Project 全量 Artifact,不能退化为当前响应的 `includedArtifacts.length`。 ### 1.5 复合响应 @@ -273,16 +286,17 @@ type ThreadMessageBundleDTO = { type ProjectBootstrapDTO = { project: ProjectDTO threadTopology: ThreadDTO[] + artifactSummary: ProjectArtifactSummaryDTO initialThread: ThreadMessageBundleDTO } type CreationBundleDTO = { project: ProjectDTO rootThread: ThreadDTO + artifactSummary: ProjectArtifactSummaryDTO userMessage: MessageDTO assistantMessage: MessageDTO assistantRun: AssistantRunStateDTO - canonicalUrl: `/thread-chat/${string}` } type MessageCreationBundleDTO = { @@ -302,6 +316,8 @@ type ReplacementBundleDTO = { `assistantRuns` 必须且只包含本 bundle 中 assistant Message 的 Run;不得返回属于其他 Thread 或未返回 Message 的 Run。`includedArtifacts` 必须且只包含渲染本次 Message 所需且允许内联的 Artifact。 +`ProjectBootstrapDTO.artifactSummary` 与 `CreationBundleDTO.artifactSummary` 是 Project 级统计读模型。它不枚举 Artifact,也不能由客户端根据局部 `includedArtifacts` 重建;首次创建且尚未产生 Artifact 时必须返回 `{ changeSequence: 0, total: 0, byKind: {} }`。 + ## 2. Project API ### 2.1 列出 Project @@ -366,7 +382,8 @@ type CreateProjectRequest = { - `initialMessage.parts` 决定 U1 内容;服务端不得接受客户端提交的 role,U1 固定为 user。 - `requestedModelId` 是请求偏好;服务端校验并选择实际模型,结果以 `assistantRun.modelId` 为准。 - 服务端在同一事务创建 Project、唯一 Root、U1、A1 与 queued Run;U1/A1 属于 Root,且 `U1.sequence < A1.sequence`。 -- `canonicalUrl` 必须由返回的 `project.id` 构造。 +- 初始事务尚未生成 Artifact,因此 `artifactSummary` 必须是 `{ changeSequence: 0, total: 0, byKind: {} }`;后续由生成事件携带最新统计。 +- 创建响应只返回服务端资源身份与领域数据,不返回或拼接 Web 页面 URL。客户端必须使用 `project.id` 和集中式路由构造器决定导航目标。 - 模型 Worker 只能在事务提交后唤醒。 主要错误:`validation_error`、`model_not_available`、`unauthorized`。 @@ -405,6 +422,7 @@ Body:无。Query:无。 - `project.id` 必须等于 path `projectId`。 - `threadTopology` 返回该 Project 全部轻量 Thread,必须恰有一个 `parentThreadId=null` 的 Root。 - 每个 Branch 的 `parentThreadId` 必须能在同一 `threadTopology` 找到,且不得形成环。 +- `artifactSummary` 必须统计该 Project 的全部 Artifact,不能只统计 `initialThread.includedArtifacts`。 - `initialThread.threadId` 必须等于 Root ID。 - Root bundle 默认返回最新最多 200 条有效 Message,再按 sequence 升序输出。 - Bootstrap 不返回 BaseContext、Branch Message、Prompt History 或全部 Project Resource 正文。 @@ -740,6 +758,8 @@ type RunSnapshotEvent = { run: AssistantRunStateDTO message: MessageDTO includedArtifacts: IncludedArtifactDTO[] + /** 当前 Project 的最新 Artifact 总量,用于刷新或重连后校正页面统计。 */ + artifactSummary: ProjectArtifactSummaryDTO } type RunDeltaEvent = { @@ -754,6 +774,8 @@ type RunCompletedEvent = { run: AssistantRunStateDTO message: MessageDTO includedArtifacts: IncludedArtifactDTO[] + /** 本次完成事务提交后的 Project Artifact 总量。 */ + artifactSummary: ProjectArtifactSummaryDTO } type RunFailedEvent = { @@ -787,6 +809,7 @@ type AssistantMessageEvent = 6. `afterEventSequence` 用于校验客户端游标与服务端当前 Run 的关系;缺失事件已由 snapshot 合并恢复,因此 P0 不要求重放每个旧 token delta。 7. 如果客户端游标大于服务端游标,返回 `409 invalid_event_cursor`;如果客户端游标小于服务端保留窗口,仍通过 snapshot 恢复。 8. 网络断开只取消订阅,不停止 Run。客户端用最后接受的 eventSequence 重连。 +9. 客户端必须按 `changeSequence` 合并 `run.snapshot` 或 `run.completed` 携带的 `artifactSummary`:更大值原子替换,相同值必须一致,更小值忽略;不得按重复事件自行累加。 HTTP 连接建立前的主要错误:`assistant_message_not_found`、`message_run_not_found`、`invalid_event_cursor`、`forbidden`。连接建立后的运行失败通过 `run.failed` 表达。 diff --git a/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md b/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md new file mode 100644 index 00000000..9c098d5f --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md @@ -0,0 +1,295 @@ +# `/thread-chat/new` 首条消息与 AI 回复生命周期 + +## 1. 目标 + +用户从空白 `/thread-chat/new` 发送首条 Message 后,系统必须原子创建 Project 和首个聊天链路,并无空白帧地交接到 `/thread-chat/{projectId}`: + +```text +本地 Draft UI + → 服务端 CreationBundle + → 先建立 ready ProjectRuntime + → 客户端根据 project.id 构造目标 Project URL + → 再替换当前 URL + → 同一 Runtime 继续接收 AI 事件 +``` + +不能先导航到 Project URL、显示 Bootstrap Loading,再等待 Project 创建结果;也不能创建 `tempProjectId/tempThreadId/tempMessageId` 后二次替换身份。 + +## 2. 页面状态机 + +```mermaid +stateDiagram-v2 + [*] --> DraftIdle: 打开 /thread-chat/new + DraftIdle --> DraftSubmitting: 提交首条 Message + DraftSubmitting --> DraftError: 创建事务前失败 + DraftError --> DraftSubmitting: Retry + DraftSubmitting --> RuntimeSeeded: 收到 CreationBundle + RuntimeSeeded --> ProjectRoute: 客户端构造路由且 registry 已有 ready Runtime 后 router.replace + ProjectRoute --> Generating: A1 queued/running + Generating --> Completed: run.completed + Generating --> Failed: run.failed/stopped +``` + +`DraftSubmitting` 期间保持当前页面和 Composer 几何位置,不清空草稿、不挂载空 Project Store。按钮只进入 submitting/disabled 状态。旧 `/new` 页面必须持续渲染到目标 Project Route 已能从 Registry 取得 seeded Runtime 为止。 + +## 3. 服务端原子创建 + +```ts +async function createProjectWithFirstMessage(command: { + actorId: UserId + initialMessageParts: UIMessage["parts"] + requestedModelId?: ModelId +}): Promise { + const internal = await database.transaction(async (tx) => { + validateUserMessageParts(command.initialMessageParts) + + const project = await projectRepository.insert(tx, { + id: idGenerator.newProjectId(), + ownerUserId: command.actorId, + autoTitle: null, + customTitle: null, + target: null, + instruction: null, + }) + + const rootThread = await threadRepository.insert(tx, { + id: idGenerator.newThreadId(), + projectId: project.id, + parentThreadId: null, + sourceMessageId: null, + forkSourceSnapshot: null, + }) + + const userMessage = await messageRepository.insert(tx, { + id: idGenerator.newMessageId(), + threadId: rootThread.id, + sequence: 1, + role: "user", + parts: command.initialMessageParts, + replacesMessageId: null, + supersededAt: null, + finalizedAt: clock.now(), + }) + + const assistantMessage = await messageRepository.insert(tx, { + id: idGenerator.newMessageId(), + threadId: rootThread.id, + sequence: 2, + role: "assistant", + parts: null, + replacesMessageId: null, + supersededAt: null, + finalizedAt: null, + }) + + const messageRun = await messageRunRepository.insert(tx, { + id: idGenerator.newMessageRunId(), + assistantMessageId: assistantMessage.id, + status: "queued", + modelId: await modelPolicy.resolve({ + actorId: command.actorId, + projectId: project.id, + requestedModelId: command.requestedModelId, + }), + checkpointParts: [], + eventSequence: 0, + }) + + await messageRunOutbox.enqueue(tx, { + messageRunId: messageRun.id, + }) + + return { + project, + rootThread, + userMessage, + assistantMessage, + messageRun, + } + }) + + // 事务提交后才能唤醒 Worker;唤醒失败不得把已提交创建误报成整体失败。 + try { + await messageRunDispatcher.wakeUpAfterCommit( + internal.messageRun.id, + ) + } catch (error) { + logger.error("message_run_wakeup_failed", error) + // durable Outbox / queued scanner 后续恢复执行。 + } + + return { + project: toProjectDTO(internal.project), + rootThread: toThreadDTO(internal.rootThread), + artifactSummary: { + changeSequence: 0, + total: 0, + byKind: {}, + }, + userMessage: toMessageDTO(internal.userMessage), + assistantMessage: toMessageDTO(internal.assistantMessage), + assistantRun: toAssistantRunStateDTO(internal.messageRun), + } +} +``` + +CreationBundle 返回前 Project、Root、U1、A1 和 Run 必须已经 durable。模型 Worker 可以已经从 queued 进入 running;事件订阅的首个 `run.snapshot` 会用服务端当前 checkpoint 校正 CreationBundle 中较早的 queued 视图。 + +服务端不得返回或拼接 ThreadChat 页面 URL。URL 是 Web 客户端的展示与导航约定,不是 Project 领域数据;客户端只使用 CreationBundle 中的 `project.id` 通过集中式 `threadChatRoutes.project(projectId)` 构造目标路径。 + +## 4. 客户端创建命令与无抖动交接 + +```ts +async function submitNewProjectDraft() { + const draft = newDraftStore.getState() + if (draft.status === "submitting") return + + const frozenParts = cloneAndValidateUserParts(draft.draftParts) + const requestedModelId = draft.requestedModelId + + // 不清空 Composer;保持 /new 当前画面直到目标 Project Route ready。 + newDraftStore.getState().markSubmitting() + + try { + const bundle = await api.createProject({ + initialMessage: { parts: frozenParts }, + requestedModelId, + // 不提交任何新实体 ID。 + }) + + validateCreationBundle(bundle) + const projectUrl = threadChatRoutes.project(bundle.project.id) + + /** + * 决定性顺序:先 seed,后导航。 + * seedFromCreation 创建并完整初始化 ProjectRuntime: + * - entities.project/root/U1/A1 + * - runs[A1] + * - readModels.artifactSummary + * - bootstrap=ready + * - Root Message window=ready + * - focusedSlotId="root",columnSlots=[] + */ + const runtime = appRuntime.projectRuntimeRegistry.seedFromCreation( + bundle, + ) + + // Catalog 只能合并服务端确认的 ProjectSummary。 + appRuntime.appStore.getState().upsertProjectSummary( + toProjectSummary(bundle.project), + ) + appRuntime.appStore.getState().setProjectRoutePending( + bundle.project.id, + ) + + /** + * 订阅在导航前挂到 AppProvider 持有的 Runtime: + * 即使 AI 很快完成,终态也先进入同一个 Store,不会丢失。 + */ + runtime.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId, + ) + + /** + * 使用客户端软导航替换 /new;当前页面保留到新 Route 提交。 + * 目标 ProjectProvider acquire 的是同一个 seeded Runtime, + * 因此首帧直接是 Root + U1 + A1,不再请求 Bootstrap 或闪空白 Loading。 + */ + router.replace(projectUrl) + } catch (error) { + /** + * 明确 validation/authorization/事务回滚:仍停留 /new,保留草稿并可 Retry。 + * 网络中断或响应丢失可能发生在服务端提交之后;P0 没有 Idempotency-Key, + * 这种 unknown outcome 不得自动重试,必须显示“创建结果未知”。 + */ + newDraftStore.getState().markError( + normalizeCreateProjectError(error), + ) + } +} +``` + +`seedFromCreation` 必须持有一次性 navigation handoff lease:`NewProjectDraftProvider` 卸载不能销毁该 Runtime;目标 `ThreadChatProjectProvider` 用相同 `projectId` acquire 后消费 seed 并接管租约。 + +## 5. 目标 ProjectProvider 接管 + +```ts +function mountProjectProvider(projectId: ProjectId) { + const runtime = registry.acquire(projectId) + + assert(runtime.projectId === projectId) + + if (runtime.store.getState().requests.bootstrap.status === "ready") { + // /new seed handoff:跳过 GET ProjectBootstrap。 + clearMatchingPendingProjectId(projectId) + return runtime + } + + // 只有直接打开/刷新 URL、内存中没有 seed 时才走正常 Bootstrap。 + void runtime.commands.loadProjectBootstrap() + return runtime +} +``` + +为了避免 UI 抖动,页面组件边界必须满足: + +```text +ThreadChatAppProvider / App Shell + 在 /new 与 /{projectId} 之间保持挂载 + +NewProjectScreen 与 Root Thread Screen + 使用相同 Column/Composer 外形尺寸 + +router.replace 之前 + seeded Runtime 已经 ready + +目标 Project Route 首帧 + 不出现第二次 Bootstrap Loading +``` + +不要求 Draft 与 U1 使用相同 React key;身份从本地草稿切换为服务端 Message 时允许组件重建,但不得插入空白 Project 帧、重置整个 App Shell 或先清空 Composer 再等待路由。 + +## 6. AI 回复事件 + +```ts +function onAssistantEvent(event: AssistantMessageEvent) { + switch (event.type) { + case "run.snapshot": + // 用持久化 checkpoint 校正 queued/running/terminal 状态。 + runtime.store.getState().applyRunEvent(event) + break + + case "run.delta": + // Store Action 只入 frame buffer 并调度合帧,不逐 token 刷新最终实体。 + runtime.store.getState().applyRunEvent(event) + break + + case "run.completed": + /** + * 一次合并 finalized A1、completed Run、Artifact 和最新 Summary。 + * UI 从 checkpoint/running 直接过渡到 Message.parts/finalized。 + */ + runtime.store.getState().applyRunEvent(event) + break + + case "run.failed": + case "run.stopped": + runtime.store.getState().applyRunEvent(event) + break + } +} +``` + +如果浏览器在生成中刷新,内存 seed 消失,页面按“打开已有 Project”冷启动:Bootstrap 返回 Root A1 和当前 Run checkpoint,再从 eventSequence 恢复,不创建第二个 Run。 + +## 7. 决定性不变量 + +- 首次发送前只有 NewProjectDraftStore,没有 Project Store 或假实体。 +- 客户端不生成 Project、Thread、Message 或 MessageRun ID。 +- CreationBundle 完整初始化 Runtime 后才能导航。 +- 目标 ProjectProvider 必须接管同一 seeded Runtime,并跳过 Bootstrap。 +- `/new` 当前画面保留到目标 Project Route 可提交,不显示中间空白 Project。 +- AI 订阅可以在导航前开始;事件始终写入 seeded Runtime。 +- completed 事件携带 finalized Message;不以前端累计 token 作为最终权威。 +- 浏览器刷新只改变客户端恢复路径,不改变后台 MessageRun。 +- P0 对响应丢失后的创建结果无法安全判定;在幂等命令落地前不得自动重试 unknown outcome。 diff --git a/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md b/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md new file mode 100644 index 00000000..470e15d4 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md @@ -0,0 +1,293 @@ +# 打开已有 Project 生命周期 + +## 1. 先区分三种“缓存” + +打开 `/thread-chat/{projectId}` 时可能存在三种完全不同的缓存,不能混为一谈: + +```text +Project Catalog Cache + 只含 ProjectSummary;不影响 Project 页面能否加载 + +ThreadWorkbenchSnapshot(localStorage) + 只含 Column Slot、Thread ID、宽度、折叠态、焦点和视图模式 + 不含 Project/Thread/Message/Run 实体 + +ProjectRuntime Message Cache(内存) + 只在当前 ProjectRuntime 生命周期内存在 + threadMessagesById[threadId]=ready 时可避免重复请求 +``` + +浏览器 HTTP Cache 不作为领域正确性或 Zustand 恢复机制。P0 在 ProjectRuntime 最后一个 Provider lease 释放后销毁 Runtime,不承诺跨 Project 路由保存 Message 内存缓存;页面刷新后一定重新 Bootstrap。 + +## 2. 路由与 Provider + +```ts +function enterExistingProjectRoute(projectId: ProjectId) { + // projectId 直接来自 URL;不写入 App Store selectedProjectId。 + const runtime = appRuntime.projectRuntimeRegistry.acquire(projectId) + + if (runtime.store.getState().requests.bootstrap.status === "ready") { + // /new seed handoff 或同一 Runtime 已经完成 Bootstrap。 + runtime.generationCoordinator.resumeLoadedRuns() + return runtime + } + + // Catalog 是否已经加载不构成前置条件。 + void runtime.commands.loadProjectBootstrap() + return runtime +} +``` + +在 Bootstrap 完成前,Project 页面只有一个 Project-level loading shell。不能根据 Catalog Summary 构造假 Project Entity 或 Root Thread。 + +## 3. 冷启动 Bootstrap + +```ts +async function loadProjectBootstrap(): Promise { + const state = store.getState() + + if (state.requests.bootstrap.status === "ready") return + if (bootstrapPromise) return bootstrapPromise + + state.setBootstrapLoadState({ status: "loading" }) + + bootstrapPromise = api + .getProjectBootstrap(runtime.projectId) + .then((bootstrap) => { + validateProjectBootstrap(bootstrap, { + expectedProjectId: runtime.projectId, + }) + + /** + * 一次 State Transition 合并: + * - Project Entity + * - 全量轻量 Thread topology + * - Artifact Summary + * - Root Message/Run/Artifact + * - Root ThreadMessageWindow ready + * - Bootstrap ready + */ + store.getState().mergeBootstrap(bootstrap) + + restoreWorkbenchAndLoadBranches(bootstrap.threadTopology) + generationCoordinator.resumeLoadedRuns() + }) + .catch((error) => { + if (runtimeDisposed || isAbortError(error)) return + store.getState().setBootstrapLoadState({ + status: "error", + error: normalizeError(error), + }) + }) + .finally(() => { + bootstrapPromise = null + }) + + return bootstrapPromise +} +``` + +## 4. 没有 Workbench Snapshot + +```ts +function restoreWithoutSnapshot(rootThreadId: ThreadId) { + // Store Action 使用当前 UI 默认值:只显示并聚焦 Root。 + store.getState().resetWorkbenchToDefault() + + // Root MessageBundle 已在 Bootstrap 中 ready,不再请求。 + assertThreadMessagesReady(rootThreadId) +} +``` + +结果: + +```text +Root Column ready +Branch Columns 没有打开,因此不请求 Branch Message +``` + +完整 topology 已在 Store 中,Tree Switcher、Child Count 和 Breadcrumb selector 可以工作,但不代表 Branch Message 已加载。 + +## 5. 有 Workbench Snapshot + +Workbench Snapshot 必须等 Bootstrap topology 到达后才能校验和恢复: + +```ts +function restoreWorkbenchAndLoadBranches( + threadTopology: ThreadEntity[], +) { + const raw = workbenchStorage.read(runtime.projectId) + const snapshot = sanitizeWorkbenchSnapshot(raw, { + projectId: runtime.projectId, + threadTopology, + }) + + if (!snapshot) { + restoreWithoutSnapshot(selectRootThreadId(store.getState())) + return + } + + // 只恢复 UI 事实;不得覆盖 Bootstrap entities/topology。 + store.getState().restoreWorkbenchSnapshot(snapshot) + + for (const slot of snapshot.columnSlots) { + /** + * 非阻塞 fan-out:不 await 全部 Branch 后再渲染。 + * Slot 已经恢复,因此 Column 可以立刻按自己的 Query State + * 显示 loading / ready / error。 + */ + void runtime.commands.ensureThreadMessages(slot.threadId) + } +} +``` + +Snapshot 只恢复“之前打开了哪些 Thread”,不缓存 Message。冷启动/刷新时,多个恢复 Branch 通常都需要分别请求 `ThreadMessageBundle`。 + +```mermaid +sequenceDiagram + participant R as Router + participant P as ProjectProvider + participant B as Bootstrap API + participant S as Project Store + participant LS as Workbench Snapshot + participant L as ThreadMessageLoader + participant UI as Columns + + R->>P: /thread-chat/{projectId} + P->>B: GET ProjectBootstrap + B-->>P: Project + topology + Root bundle + stats + P->>S: mergeBootstrap + S-->>UI: Root ready + P->>LS: read + sanitize after topology + LS-->>P: Slots A / B / C + P->>S: restoreWorkbenchSnapshot + S-->>UI: A/B/C Column loading shells + + par Thread A + P->>L: ensure(A) + L-->>S: A ready/error + and Thread B + P->>L: ensure(B) + L-->>S: B ready/error + and Thread C + P->>L: ensure(C) + L-->>S: C ready/error + end + + Note over UI,L: Root 与各 Branch 独立展示,不存在全页面 Promise.all 等待 +``` + +## 6. 有 Runtime Message Cache + +在同一个 ProjectRuntime 仍存活时,用户关闭后重新打开某个 Thread,或多个入口同时请求同一 Thread: + +```ts +async function ensureThreadMessagesInsideLoader(threadId: ThreadId) { + const window = store.getState().requests.threadMessagesById[threadId] + + if (window?.loadState.status === "ready") { + // 已有 Message Runtime Cache;零请求。 + return + } + + const inFlight = inFlightByThreadId.get(threadId) + if (inFlight) { + // 同一 Thread 请求去重。 + return inFlight + } + + return loadValidateAndMergeThreadMessages(threadId) +} +``` + +不同 Thread 的请求互不复用,可以并行;同一 Thread 只能有一个 in-flight Promise。 + +## 7. 每列独立状态 + +```ts +function selectThreadColumnView( + state: ThreadChatProjectStore, + slotId: "root" | ColumnSlotId, +): ThreadColumnView { + const threadId = selectSlotThreadId(state, slotId) + const window = state.requests.threadMessagesById[threadId] + + if (!window || window.loadState.status === "loading") { + return createLoadingColumnView(slotId, threadId) + } + + if (window.loadState.status === "error") { + return createErrorColumnView({ + slotId, + threadId, + error: window.loadState.error, + canRetry: true, + }) + } + + return createReadyColumnView({ + slotId, + thread: state.entities.threadsById[threadId], + messages: selectActiveMessages(state, threadId), + assistantRuns: selectRunsForThread(state, threadId), + artifacts: selectIncludedArtifactsForThread(state, threadId), + hasOlderMessages: window.hasOlderMessages, + }) +} +``` + +可能同时存在: + +```text +Root ready +Branch A running assistant +Branch B loading messages +Branch C error,可 Retry +Branch D ready +``` + +这些状态互不覆盖。一个 Branch Query 失败不得把 Project Bootstrap 或其他 Column 改为 error。 + +## 8. Slot 切换与迟到响应 + +```text +Slot S 显示 Thread A,A 正在加载 +→ 用户把 S 切换成 Thread B +→ A 的响应后来到达 +→ Bundle 只合并到 messages/requests[A] +→ S 继续显示 B +→ A 留在 Runtime Message Cache +``` + +`applyMessageBundle` 使用 `bundle.threadId` 合并;绝不能使用“发起请求时所在 slotId”决定写入目标。关闭或切换 Column 不取消 Query;ProjectRuntime 销毁时才统一 Abort,且 Abort 不写 error。 + +## 9. Message 与 Generation 恢复 + +每个 Bundle 合并成功后: + +```ts +function afterMessageBundleMerged(bundle: ThreadMessageBundle) { + for (const run of bundle.assistantRuns) { + if (run.status === "queued" || run.status === "running") { + // UI 已能先显示 checkpointParts。 + generationCoordinator.subscribeAssistant( + run.assistantMessageId, + ) + } + } +} +``` + +Message Query ready 与 Assistant Run running 可以同时成立。事件订阅只更新对应 `assistantMessageId`,不触发其他 Thread 重新加载。 + +## 10. 决定性不变量 + +- Project Catalog 加载不是 ProjectBootstrap 的前置条件。 +- URL/Provider 是当前 Project 身份权威,App Store 不保存 selectedProjectId。 +- Workbench Snapshot 只缓存视图,不缓存服务端实体或 Message。 +- 冷启动无 Snapshot 时只打开 Root,不请求未打开 Branch。 +- 有 Snapshot 时先恢复 Column Shell,再非阻塞并行 ensure 各 Branch。 +- 同 Thread 请求去重,不同 Thread 独立并行和独立 error。 +- Runtime Message Cache 只在当前 ProjectRuntime 生命周期内有效。 +- Bundle 始终按 threadId 合并,迟到响应不得改变 Slot 当前 Thread。 +- ProjectRuntime 销毁才 Abort Thread Query;Abort 不停止服务端 MessageRun。 diff --git a/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md b/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md new file mode 100644 index 00000000..d60c0130 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md @@ -0,0 +1,245 @@ +# Thread Message 异步加载设计 + +## 1. 边界与权威 + +Project Catalog 只分页加载 `ProjectSummary`;它不保存当前 Project 的 Thread 或 Message。当前 Project 身份由 `/thread-chat/{projectId}` URL 决定,`ThreadChatProjectProvider` 据此取得独立 `ThreadChatProjectRuntime`。 + +```mermaid +flowchart TD + App[ThreadChatAppStore] -->|分页 ProjectSummary| Catalog[Project Catalog] + URL["URL /thread-chat/{projectId}"] --> Provider[ThreadChatProjectProvider] + Provider --> Runtime[ThreadChatProjectRuntime] + Runtime --> Store[ThreadChatProjectStore] + Runtime --> Loader[ThreadMessageLoader] + Runtime --> Runs[GenerationCoordinator] + Loader --> API[Thread Message API] + API --> Action[applyMessageBundle] + Action --> Store + Store --> Selectors[Pure Selectors] + Selectors --> Columns[Root / Branch Columns] +``` + +数据职责: + +- App Store:Project 列表摘要与跨 Project 外壳 UI,不保存 `selectedProjectId`。 +- URL/Provider:决定当前 ProjectRuntime。 +- Project Store:规范化保存当前 Project 的 Thread、已加载 Message、Run、Artifact、请求状态和本地工作台状态。 +- ThreadMessageLoader:保存非序列化 `threadId → Promise/AbortController`,负责请求去重与 Runtime 级取消。 +- Column Slot:只决定某个物理列当前展示哪个 Thread,不拥有 Message 数据。 + +## 2. Bootstrap 与按需加载 + +`ProjectBootstrap` 一次返回 Project、完整轻量 Thread topology、Project Artifact Summary 和 Root Thread MessageBundle。Root 在 Bootstrap 合并后视为 ready;Bootstrap 不下载所有 Branch Message。 + +Branch Message 在以下时机调用 `ensureThreadMessages(threadId)`: + +1. 用户首次打开尚未加载的 Branch。 +2. 刷新恢复 `ThreadWorkbenchSnapshot` 后,恢复出的 Branch Column 尚未 ready。 +3. 用户对 error Column 明确 Retry。 + +```mermaid +sequenceDiagram + participant UI as Page + participant P as ProjectProvider + participant API as Bootstrap API + participant S as Project Store + participant LS as localStorage + participant L as ThreadMessageLoader + + UI->>P: route projectId + P->>API: GET /projects/{projectId}/bootstrap + API-->>P: Project + topology + Root bundle + stats + P->>S: mergeBootstrap + S-->>UI: Root ready,立即渲染 + P->>LS: 读取并校验 Workbench Snapshot + LS-->>P: Branch Slots A / B / C + P->>S: 恢复 Slot、宽度、折叠态与焦点 + S-->>UI: Branch Column Loading Shell + + par Branch A + P->>L: ensure(A) + and Branch B + P->>L: ensure(B) + and Branch C + P->>L: ensure(C) + end + + Note over UI,L: 不等待全部 Branch;每列独立 ready/error +``` + +Provider 不得 `await Promise.all(...)` 后才展示页面。Root 与 topology 合并后立即渲染;恢复出的 Branch 使用各自 `ThreadMessageWindowState` 展示 loading、ready 或 error。 + +## 3. Store 中的加载状态 + +`requests.threadMessagesById` 按 `threadId` 隔离: + +```ts +type ThreadMessageWindowState = { + loadState: LoadState + hasOlderMessages: boolean + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} +``` + +决定性语义: + +```text +不存在 threadMessagesById[threadId] + = 从未请求 + +loadState.loading + = 已有请求在飞 + +loadState.ready + 空 messageIds + = 请求成功,但 Thread 当前有效时间线为空 + +loadState.error + = 请求失败,可以独立 Retry +``` + +不得用 `messageIds.length === 0` 判断是否需要请求。 + +```mermaid +stateDiagram-v2 + [*] --> NotRequested: 不存在 Thread key + NotRequested --> Loading: ensure + Loading --> Ready: Bundle 校验并合并成功 + Loading --> Error: 网络/权限/DTO 校验失败 + Error --> Loading: Retry + Ready --> Ready: 再次打开直接复用 +``` + +## 4. ThreadMessageLoader + +Loader 是 ProjectRuntime 的非序列化基础设施,不是 Zustand slice: + +```ts +type ThreadMessageLoader = { + ensure(threadId: ThreadId): Promise + destroy(): void +} +``` + +内部至少维护: + +```ts +inFlightByThreadId: Map> +abortByThreadId: Map +disposed: boolean +``` + +核心伪代码: + +```ts +async function ensureThreadMessages(threadId: ThreadId): Promise { + assertThreadBelongsToRuntimeProject(threadId) + + const window = store.getState().requests.threadMessagesById[threadId] + if (window?.loadState.status === "ready") return + + const existing = inFlightByThreadId.get(threadId) + if (existing) return existing + + store.getState().setThreadMessageLoadState(threadId, { + status: "loading", + }) + + const abortController = new AbortController() + abortByThreadId.set(threadId, abortController) + + const request = api + .getThreadMessages(threadId, { signal: abortController.signal }) + .then((bundle) => { + if (disposed) return + + validateMessageBundle(bundle, { + expectedProjectId: runtime.projectId, + expectedThreadId: threadId, + }) + + // 一次同步 State Transition 合并 Message、Run、Artifact 和 ready 窗口。 + store.getState().applyMessageBundle(bundle) + + // 只恢复本 Bundle 中 queued/running 的 assistant Run。 + generationCoordinator.resumeLoadedRuns() + }) + .catch((error) => { + if (disposed || isAbortError(error)) return + store.getState().setThreadMessageLoadState(threadId, { + status: "error", + error: normalizeError(error), + }) + }) + .finally(() => { + inFlightByThreadId.delete(threadId) + abortByThreadId.delete(threadId) + }) + + inFlightByThreadId.set(threadId, request) + return request +} +``` + +同一 Thread 的多个调用复用同一个 Promise;不同 Thread 的请求互不等待。`applyMessageBundle` 必须验证 Bundle 的 Thread/Project 归属、Message 的 `threadId`、sequence 窗口和 Run/Artifact 引用后再原子合并。 + +## 5. Column 与迟到响应 + +请求按 `threadId` 写入实体和请求索引,不按 `slotId` 写入。因此切换物理列不会产生“旧响应覆盖新列”的竞态: + +```text +Slot S 展示 Thread A +→ ensure(A) +→ 用户把 Slot S 切换为 Thread B +→ A 的响应稍后到达 +→ 响应只合并到 messageIdsByThreadId[A] 和 requests[A] +→ Slot S 继续展示 B +→ A 成为 ProjectRuntime 内可复用缓存 +``` + +关闭或切换 Column 不取消已经开始的 Thread Query。这样可以避免 UI 快速切换导致重复网络请求;只有 ProjectProvider 释放并销毁整个 ProjectRuntime 时,Loader 才 Abort 全部请求。Abort 只是生命周期结束,不得写入 `loadState.error`。 + +## 6. Message Query 与 Generation 状态 + +一个 Column 的展示由两个正交状态组合: + +```text +Thread Message Query + not_requested / loading / ready / error + +Assistant Run + queued / running / completed / failed / stopped +``` + +Message Query ready 后,如果 Bundle 中存在 queued/running Run: + +1. 立即使用 `checkpointParts` 恢复已经持久化的生成内容。 +2. 使用 `assistantMessageId + eventSequence` 建立或复用事件订阅。 +3. Column 继续显示 ready 的 Message 时间线,同时对应 Assistant Message 显示 running。 + +单个 Branch Message Query 失败只影响该列;不得使 Bootstrap、Root、其他 Branch 或 GenerationCoordinator 进入全局 error。 + +## 7. Selector 与 UI 输入 + +Column 不自行读取整个 Store 或发请求。它通过 Slot 找到当前 Thread,再由 selector 组合: + +```text +ColumnSlot.threadId + + ThreadEntity + + ThreadMessageWindowState + + active MessageEntity[] + + AssistantRunState + + included ArtifactEntity + → ThreadColumnView +``` + +`ThreadColumnView` 至少表达列级: + +```ts +type ThreadColumnLoadView = + | { status: "loading" } + | { status: "error"; error: ClientError; canRetry: true } + | { status: "ready"; hasOlderMessages: boolean } +``` + +不存在 Thread key 时,已经打开的 Column 也应呈现 loading shell,并由 Lifecycle/Command Hook 触发 ensure;React 组件不得直接 `fetch` 或自行维护第二份 Message loading state。 diff --git a/openspec/changes/design-thread-chat-client-api/proposal.md b/openspec/changes/design-thread-chat-client-api/proposal.md index 2a753021..eb396f21 100644 --- a/openspec/changes/design-thread-chat-client-api/proposal.md +++ b/openspec/changes/design-thread-chat-client-api/proposal.md @@ -12,7 +12,7 @@ - 统一 Store State、Store Action、Application Command、Selector Hook、Command Hook 和 Lifecycle Hook 术语。 - 定义纯 Selector 与三类 Hook 的职责,避免 UI 组件直接 `fetch`、拼 Prompt 或修改实体。 - 定义前端 Application Commands:Project 生命周期、Thread 加载与 Fork、消息发送/Edit/Regenerate、生成订阅/Stop、feedback 和 Project 元数据更新。 -- 定义 `/thread-chat/new` 为无实体 ID 的本地草稿入口;第一次发送时服务端原子创建 Project、Root Thread、首条 user Message、assistant Message 与 MessageRun,再替换为 canonical Project URL。 +- 定义 `/thread-chat/new` 为无实体 ID 的本地草稿入口;第一次发送时服务端原子创建 Project、Root Thread、首条 user Message、assistant Message 与 MessageRun,客户端再根据返回的 `project.id` 通过集中式路由构造器替换为已创建 Project 的页面 URL。 - 定义后端 `/api/v1` 的资源标识、请求参数、响应 DTO、错误、原子性、权限与适用场景;所有新实体 ID 均由服务端生成。 - MVP 首次加载返回全量轻量 Thread topology 和 Root Thread 数据;其他 Thread 按需一次加载最多 200 条有效 Message,不实现复杂分页替换。 - 本 change 只形成设计与可测试契约,不实现 Zustand Store、Hooks、Route Handler、Stream Client 或 Transport 封装,也暂不编写实施 tasks。 diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md index fc977016..2f31f38d 100644 --- a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md @@ -38,7 +38,7 @@ Thread 的 Root/Branch、children、depth 和 breadcrumb MUST 由 `parentThreadI - **AND** 客户端不得要求服务端同时返回可独立修改的 `children` 数组 ### Requirement: 按生命周期划分 Zustand Store -客户端 MUST 使用一个轻量 ProjectCatalogStore 管理 Project 列表,并 MUST 为每个打开的 `projectId` 创建独立 ThreadChatStore。ThreadChatStore MUST 以高内聚 slice 区分已确认 entities、加载/命令状态、生成流状态和本地 workbench UI 状态;它们可以位于同一个 Zustand store,但不得互相复制权威数据。 +客户端 MUST 使用一个轻量 ProjectCatalogStore 管理 Project 列表,并 MUST 为每个打开的 `projectId` 创建独立 ThreadChatStore。ThreadChatStore MUST 以高内聚 slice 区分已确认 entities、服务端统计 read models、加载/命令状态、生成流状态和本地 workbench UI 状态;它们可以位于同一个 Zustand store,但不得互相复制权威数据。 ProjectCatalogStore MUST NOT 保存 Thread 或 Message。Project-scoped Store 在离开对应 Project 页面后 MUST 可被销毁,不得让多个 Project 的 Message 长期堆积在无边界全局 Store 中。 @@ -53,7 +53,7 @@ ProjectCatalogStore MUST NOT 保存 Thread 或 Message。Project-scoped Store - **AND** 只订阅其他 Thread 或 Project 元数据的组件 MUST NOT 因全局 version 而被强制重算 ### Requirement: 区分服务端事实、运行态和本地 UI 态 -Project、Thread、Message、Artifact 和 MessageRun 终态 MUST 以服务端响应为权威。运行中的 checkpoint 和 eventSequence MUST 保存于生成 slice;visibleThreadIds、列宽、画布位置、composer 草稿、overlay 和选中 Artifact MUST 保存为本地 UI 态。 +Project、Thread、Message、Artifact、Project Artifact Summary 和 MessageRun 终态 MUST 以服务端响应为权威。运行中的 checkpoint 和 eventSequence MUST 保存于生成 slice;visibleThreadIds、Column Slot、列宽、画布位置、composer 草稿、overlay 和选中 Artifact MUST 保存为本地 UI 态。 客户端 MAY 将设备相关 workbench UI 态持久化到 localStorage,但 MUST NOT 将它提交为 Project 内容。客户端 MUST NOT 为乐观展示伪造 Project、Thread、Message 或 MessageRun ID。 @@ -64,13 +64,63 @@ Project、Thread、Message、Artifact 和 MessageRun 终态 MUST 以服务端响 #### Scenario: 恢复工作台偏好 - **WHEN** ProjectBootstrap 成功且浏览器存在该 Project 的列布局偏好 -- **THEN** 客户端 MUST 过滤其中已经不存在的 Thread ID 后恢复布局 +- **THEN** 客户端 MUST 过滤其中已经不存在的 Thread ID、重复 Slot/Thread、非法宽度和无效 Canvas pin 后恢复布局 - **AND** 本地布局不得覆盖服务端返回的 Thread topology +### Requirement: 使用稳定 Column Slot 表达物理列 +客户端 MUST 使用纯本地 `ColumnSlotId` 表示 Root 右侧物理列,并 MUST 将 Slot 当前展示的 `threadId` 与 Slot 身份分离。Root MUST 使用独立固定列身份,不得进入 Branch Slot 数组。 + +切换一个物理列所展示的 Thread 时,客户端 MUST 保留该 Slot 的 `slotId`、物理位置、折叠态和显式列宽,只替换 `threadId`。`ColumnSlotId` 不是服务端实体 ID,MUST NOT 进入 Project、Thread、Message、Fork 或 Generation API 请求。 + +#### Scenario: 本列切换 Thread +- **WHEN** 用户通过现有列 Header 切换器把 Slot S 从 Thread A 切换到 Thread B +- **THEN** Store MUST 保持 S 的 `slotId`、宽度、折叠态和位置不变,并将 S 的 `threadId` 更新为 B +- **AND** 不得因为切换内容而重建物理列或把 A 的宽度改绑到 Thread B 实体 + +#### Scenario: 恢复重复 Thread 的非法 Snapshot +- **WHEN** 本地 Snapshot 中两个 Slot 指向同一个 Thread 或复用了同一个 Slot ID +- **THEN** 客户端 MUST 按稳定顺序只保留第一个合法 Slot +- **AND** 不得在同一工作台恢复两列相同 Thread + +### Requirement: 支持分栏分割线拖拽 +客户端 MUST 保留现有相邻展开列之间的分割线拖拽能力。分割线 MUST 同时调整左右两个物理列,并遵守当前 UI 的最小列宽约束;Root 宽度 MUST 归属于固定 Root 列,Branch 宽度 MUST 归属于稳定 Column Slot,而不是 Thread Entity。 + +Pointer Move 期间的坐标、临时宽度和 Pointer Capture MUST 保持为 Resizer Hook 或组件局部瞬时状态,不得逐帧写入 Zustand。Pointer Up、键盘步进或双击复位时,客户端 MUST 通过一次 Store Action 原子提交受影响列的最终宽度;提交后的宽度 MUST 进入工作台 Snapshot,滚动位置和拖拽瞬时状态不得持久化。该操作 MUST NOT 调用服务端 API。 + +#### Scenario: 拖拽相邻列分割线 +- **WHEN** 用户拖动两个展开列之间的分割线并释放 Pointer +- **THEN** Store MUST 在一次 State Transition 中提交左右两列最终宽度 +- **AND** 不得在每次 Pointer Move 时修改 Zustand 或触发服务端请求 + +#### Scenario: 切换 Thread 后保留列宽 +- **WHEN** 用户调整 Slot S 的宽度后,通过现有 Header 切换器把 S 从 Thread A 切换到 Thread B +- **THEN** Slot S MUST 保留原宽度 +- **AND** Thread A 与 Thread B Entity 均不得保存该宽度 + +#### Scenario: 双击分割线恢复自动宽度 +- **WHEN** 用户双击现有分割线复位 +- **THEN** 客户端 MUST 清除受影响物理列的显式宽度并恢复当前自动均分行为 +- **AND** 下一次工作台 Snapshot MUST 保存复位后的状态 + +### Requirement: 刷新后恢复 Project 工作台视图 +客户端 MUST 按 `projectId` 将带 `schemaVersion` 的工作台视图投影防抖保存到设备 localStorage。刷新并完成 ProjectBootstrap 后,客户端 MUST 恢复合法的列槽、每列当前 Thread、折叠态、物理列宽、焦点列、列数偏好、放置模式、Columns/Canvas 模式和 Canvas pin。 + +工作台 Snapshot MUST NOT 直接序列化整个 UI Store,也 MUST NOT 包含服务端实体、Message/Run、滚动位置、DOM 引用、弹层坐标、动画、文本选区、临时 Switcher/Help 打开态、请求状态、命令状态、流缓冲、Generation 连接或 Composer 草稿。 + +#### Scenario: 刷新恢复多栏视图 +- **WHEN** 用户在 Project P 中打开多个 Branch、调整列宽、折叠一列并刷新页面 +- **THEN** Bootstrap 成功后客户端 MUST 恢复刷新前仍然合法的 Slot、Thread、列宽、折叠态和焦点 +- **AND** 每列滚动位置 MUST 使用当前 UI 默认行为,不得从 Snapshot 恢复 + +#### Scenario: Snapshot 损坏或版本未知 +- **WHEN** localStorage 不可用、Snapshot 无法解析或 `schemaVersion` 不受支持 +- **THEN** 客户端 MUST 安全回退为只显示 Root 且聚焦 Root 的当前默认视图 +- **AND** 不得影响 ProjectBootstrap、Message 展示或 Generation 恢复 + ### Requirement: 通过 selector 生成 UI ViewModel -客户端 MUST 使用纯 selector 从 entities、AssistantRunState 和 UI state 生成 ThreadColumnView、ProjectTreeRows、MessageView、ForkAvailability 和 ProjectHeaderView 等 ViewModel。派生结果 MUST NOT 作为另一份可独立修改的领域状态持久化。 +客户端 MUST 使用纯 selector 从 entities、服务端 read models、AssistantRunState 和 UI state 生成 ThreadColumnView、ThreadColumnHeaderView、ProjectTreeRows、MessageView、ForkAvailability 和 ProjectHeaderView 等 ViewModel。派生结果 MUST NOT 作为另一份可独立修改的领域状态持久化。 -Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有效 Message 时间线、Message 运行展示、Artifact 来源、Fork 可用性和可见列组合。 +Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有效 Message 时间线、Message 运行展示、Artifact 来源、Fork 可用性、可见列组合、物理 Slot、页面统计和列 Header 操作信息。 #### Scenario: replacement 后重算消息视图 - **WHEN** Store 合并一条 replacement Message 并将来源 Message 标记为 superseded @@ -82,6 +132,21 @@ Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有 - **THEN** selector MUST 组合 Thread、有效 Messages、相关 AssistantRunState、Artifact、breadcrumb 和操作可用性 - **AND** 该 ViewModel 不得成为 API 写入对象 +#### Scenario: 组合每列 Header ViewModel +- **WHEN** UI 请求 Root 或某个 Branch Slot 的 Header +- **THEN** selector MUST 派生标题、Root/Branch、depth、breadcrumbs、直接 Child 数量与列表、Fork 来源可用性以及 switch/collapse 能力 +- **AND** `directChildren` MUST 只包含直接 Child;不得把它作为可修改 children 数组写回 Thread Entity + +#### Scenario: 组合页面统计 +- **WHEN** UI 请求 ProjectHeaderView +- **THEN** `threadCount` 与 `branchCount` MUST 从完整 Thread topology 派生,Artifact 与 Markdown 总数 MUST 从服务端 Artifact Summary 派生 +- **AND** 不得使用局部 `artifactsById` 数量冒充 Project Artifact 总数 + +#### Scenario: 多个 Run 的 Artifact Summary 乱序到达 +- **WHEN** 当前 Summary 的 `changeSequence=8`,随后收到另一个 Run 携带的 `changeSequence=7` +- **THEN** Store MUST 忽略该旧 Summary 并继续展示 sequence 8 的统计 +- **AND** `changeSequence` MUST NOT 被提交给任何 Project、Thread、Message 或 Generation Command + ### Requirement: 使用 Application Command 执行业务意图 UI 组件和 React Hook MUST NOT 直接调用 `fetch`、构造 Prompt、创建实体 ID或修改服务端实体。跨越 API、事件连接或路由的用户意图 MUST 进入可独立测试的 Application Command,由 Command 调用 API capability、校验结果并通过 Store Action 原子合并 Store。 @@ -115,7 +180,7 @@ Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、 ### Requirement: 支持无实体 ID 的新 Project 入口 `/thread-chat/new` MUST 表示本地新 Project 草稿入口,不得把 `new` 当作 Project ID。页面 MUST 在没有 Project/Thread 实体的情况下复用空白聊天 UI;用户第一次发送前不得写入 Project 列表或创建假的 Root Thread。 -第一次发送成功后,客户端 MUST 合并服务端原子返回的 Project、Root Thread、user Message、assistant Message 和 AssistantRunState,并 MUST 使用 `router.replace` 进入 `/thread-chat/{projectId}`。失败时 MUST 留在 `/thread-chat/new` 并保留草稿。 +第一次发送成功后,客户端 MUST 合并服务端原子返回的 Project、Root Thread、user Message、assistant Message 和 AssistantRunState,并 MUST 使用集中式路由构造器根据 `project.id` 得到目标 Project URL,再通过 `router.replace` 进入该页面。失败时 MUST 留在 `/thread-chat/new` 并保留草稿。 #### Scenario: 打开后直接离开 new 页面 - **WHEN** 用户打开 `/thread-chat/new` 但未发送任何内容便离开 @@ -124,7 +189,19 @@ Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、 #### Scenario: 首次发送成功 - **WHEN** 用户在 `/thread-chat/new` 发送第一条有效 Message - **THEN** 客户端 MUST 调用创建 Project 的 Application Command,并通过 Store Action 使用服务端返回的实体初始化 Project Store -- **AND** URL MUST 被替换为服务端返回的 canonical Project URL +- **AND** URL MUST 被替换为客户端路由构造器根据服务端 `project.id` 生成的目标 Project URL + +#### Scenario: CreationBundle 无空白帧交接 +- **WHEN** `/new` 创建命令返回合法 CreationBundle +- **THEN** 客户端 MUST 先用 CreationBundle 建立 bootstrap-ready 的 seeded ProjectRuntime,再执行 `router.replace(threadChatRoutes.project(bundle.project.id))` +- **AND** 目标 ProjectProvider MUST acquire 同一个 seeded Runtime 并跳过第二次 ProjectBootstrap +- **AND** `/new` 当前页面 MUST 保持渲染到目标 Project Route 可提交,不得插入空白 Project、第二次 Bootstrap Loading 或清空后等待的 Composer 帧 +- **AND** 客户端 MUST NOT 从 CreationBundle 读取页面 URL + +#### Scenario: AI 事件早于目标 ProjectProvider 挂载 +- **WHEN** seeded Runtime 在路由交接完成前收到 A1 的 snapshot、delta 或 terminal 事件 +- **THEN** GenerationCoordinator MUST 把事件合并到该 seeded Runtime +- **AND** 目标 ProjectProvider 接管后 MUST 直接展示同一 Store 的最新状态,不得重新创建 Run 或丢弃已到达事件 #### Scenario: 首次发送失败 - **WHEN** 创建命令在服务端提交前失败 @@ -132,20 +209,63 @@ Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、 - **AND** 不得向 ProjectCatalogStore 增加未确认 Project ### Requirement: 采用轻量 Bootstrap 与按 Thread 加载 -进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Root Thread 的有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact,但 MUST NOT 包含所有 Branch Message、BaseContext 或全部大型资源正文。 +进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Project Artifact Summary、Root Thread 的有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact,但 MUST NOT 包含所有 Branch Message、BaseContext 或全部大型资源正文。 其他 Thread 的 Message MUST 在首次打开时按 Thread 加载。MVP 客户端 MUST 支持一次合并最多 200 条按 sequence 升序排列的有效 Message,并 MUST 保留服务端返回的 `hasOlderMessages` 信息,但不要求实现复杂的树内自动分页替换。 +客户端 MUST 使用 ProjectRuntime 级 `ThreadMessageLoader` 管理 `threadId → in-flight Promise/AbortController`。Promise、AbortController 和 in-flight Map MUST NOT 进入 Zustand;Store 只保存每个 Thread 的 `LoadState`、窗口边界和已合并实体。同一 Thread 的并发 ensure MUST 复用同一个 Promise,不同 Thread MUST 可以并行加载。 + #### Scenario: 初次进入已有 Project - **WHEN** 用户打开 `/thread-chat/{projectId}` - **THEN** 客户端 MUST 先合并 ProjectBootstrap 并展示 Root Thread - **AND** 不得为了渲染 topology 下载全部 Branch Message +#### Scenario: 刷新恢复多个 Branch Column +- **WHEN** Bootstrap 后恢复出的工作台 Snapshot 包含多个尚未 ready 的 Branch Thread +- **THEN** Lifecycle MUST 非阻塞地并行调用每个 Branch 的 `ensureThreadMessages` +- **AND** Root MUST 立即渲染;不得等待所有 Branch 请求完成后才展示页面 +- **AND** 每个 Branch MUST 独立进入 loading、ready 或 error,一个失败不得阻塞其他列 + +#### Scenario: Workbench Snapshot 不是 Message Cache +- **WHEN** 页面刷新后 localStorage 存在多个 Branch Slot,但新的 ProjectRuntime 只有 Root MessageBundle +- **THEN** 客户端 MUST 使用 Snapshot 恢复列布局,并分别 ensure 每个 Branch MessageBundle +- **AND** 不得从 Snapshot 构造 Message、Run 或 ready 状态 + +#### Scenario: 没有 Workbench Snapshot +- **WHEN** ProjectBootstrap 成功但当前设备没有合法 Workbench Snapshot +- **THEN** 客户端 MUST 使用当前默认视图,只展示且聚焦 Root +- **AND** 不得主动加载任何未打开 Branch 的 Message + +#### Scenario: ProjectRuntime 中已有 Message Cache +- **WHEN** 同一 ProjectRuntime 内某个 Branch 的 ThreadMessageWindow 已是 ready 后再次打开该 Thread +- **THEN** 客户端 MUST 直接复用已合并实体和窗口状态 +- **AND** 不得重复请求该 Thread 的 MessageBundle + #### Scenario: 首次打开 Branch Thread - **WHEN** 用户打开尚未加载 Message 的 Branch Thread - **THEN** 客户端 MUST 只请求该 Thread 的 Message bundle 并合并结果 - **AND** 重复打开已加载 Thread 不得自动重复请求 +#### Scenario: 同一 Thread 并发 ensure +- **WHEN** 两个 Lifecycle/Command Hook 在第一个请求完成前同时 ensure 同一 threadId +- **THEN** ThreadMessageLoader MUST 返回同一个 in-flight Promise +- **AND** 服务端只能收到一个对应 Message Query + +#### Scenario: Slot 切换后旧 Thread 响应迟到 +- **WHEN** Slot S 在 Thread A 请求期间切换到 Thread B,随后 A 的 Bundle 到达 +- **THEN** Bundle MUST 只按 threadId 合并到 A 的实体索引和请求状态 +- **AND** S MUST 继续展示 B;不得让迟到响应把 Slot 切回 A + +#### Scenario: ProjectRuntime 销毁 +- **WHEN** ProjectProvider 卸载并销毁 ProjectRuntime +- **THEN** ThreadMessageLoader MUST Abort 该 Runtime 的全部在飞 Message Query +- **AND** Abort MUST NOT 写入可重试 error,也不得影响服务端 MessageRun + +#### Scenario: 关闭 Column 时请求仍在进行 +- **WHEN** 用户关闭或切换正在加载的 Branch Column,但 ProjectRuntime 仍存活 +- **THEN** Loader MUST 允许请求完成并按 threadId 缓存合法结果 +- **AND** Column 操作不得把请求结果合并到当前 Slot 的新 Thread + ### Requirement: 恢复服务端 MessageRun 客户端 MUST 使用 `assistantMessageId + eventSequence` 识别和恢复生成。queued/running 的展示 MUST 先使用服务端 checkpoint,再从最后 eventSequence 之后订阅;completed MUST 使用 finalized Message.parts;failed/stopped MUST 展示终态且不得自动重启。 diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md index b67cda47..b3a8e41c 100644 --- a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md @@ -38,33 +38,48 @@ Project metadata 更新 MUST 允许独立修改 customTitle、Target 和 Instruc ### Requirement: 原子创建首个 Project 对话 服务端 MUST 允许客户端用一条命令提交首条 user Message,并在同一数据库事务创建 Project、唯一 Root Thread、user Message、assistant Message 和 queued MessageRun。事务成功前不得唤醒模型执行;任一实体写入失败 MUST 回滚全部创建。 -请求 MUST 包含符合 AI SDK v7 UIMessage.parts 的 `initialMessage.parts`,并 MAY 包含 `requestedModelId`。请求不得包含新实体 ID。响应 MUST 返回全部创建实体、AssistantRunState 和 `/thread-chat/{projectId}` canonical URL。 +请求 MUST 包含符合 AI SDK v7 UIMessage.parts 的 `initialMessage.parts`,并 MAY 包含 `requestedModelId`。请求不得包含新实体 ID。响应 MUST 返回全部创建实体、初始 ProjectArtifactSummary 和 AssistantRunState。服务端 MUST NOT 返回或构造 Web 页面 URL;页面路由由客户端根据响应中的 `project.id` 决定。 #### Scenario: 首次发送成功 - **WHEN** 客户端请求 `POST /api/v1/projects` 并提交合法 initialMessage - **THEN** 服务端 MUST 返回 201 及 Project、Root Thread、U1、A1 和 queued Run +- **AND** 初始 ProjectArtifactSummary MUST 是 `{ changeSequence: 0, total: 0, byKind: {} }` - **AND** 数据库中不得存在缺少 Root Thread 或缺少 A1 Run 的部分 Project +#### Scenario: 创建响应与 Web 路由解耦 +- **WHEN** 服务端成功创建首个 Project 对话 +- **THEN** CreationBundle MUST 包含可作为资源身份的 `project.id` +- **AND** CreationBundle MUST NOT 包含 `canonicalUrl`、`pageUrl` 或其他客户端页面路径 +- **AND** Web 客户端 MUST 使用集中式路由构造器决定导航目标 + #### Scenario: 首次发送校验失败 - **WHEN** initialMessage.parts 为空、非法或不符合允许的 UIMessage part 协议 - **THEN** 服务端 MUST 返回 validation_error - **AND** 不得创建任何 Project 数据 ### Requirement: 提供 ProjectBootstrap Query -服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState 和关联 Artifact。 +服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、ProjectArtifactSummary、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState 和关联 Artifact。 Bootstrap MUST NOT 返回 BaseContext、所有 Branch Message、全部 Project File/Artifact 正文或服务端 Prompt。Thread topology MUST 足以由客户端派生 Root、Child、depth 和 breadcrumb。 +ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至少返回服务端单调 `changeSequence`、总数和按稳定 `kind` 聚合的数量。它不得退化为 Root MessageBundle 中 `includedArtifacts` 的数量;`total` 必须等于各 kind 计数之和。`changeSequence` 只用于拒绝乱序旧统计,不得成为 Command 请求参数或写入前置条件。 + #### Scenario: 加载现有 Project - **WHEN** actor 请求可访问 Project 的 Bootstrap - **THEN** 服务端 MUST 返回且只返回一个 Root Thread,并返回全部轻量 topology - **AND** Root MessageBundle MUST 按 sequence 升序排列有效 Message +- **AND** ProjectArtifactSummary MUST 覆盖该 Project 全量 Artifact #### Scenario: Project 不存在 - **WHEN** projectId 不存在或不可访问 - **THEN** 服务端 MUST 返回 not_found - **AND** 不得创建空 Project 作为降级结果 +#### Scenario: 未加载 Branch Artifact 仍计入统计 +- **WHEN** Project 的 Branch Thread 中存在 3 个 Markdown Artifact,但 Bootstrap 不返回该 Branch 的 Message +- **THEN** `artifactSummary.byKind.markdown` MUST 仍然等于 3 +- **AND** Root bundle 的 `includedArtifacts` MAY 不包含这 3 个 Artifact + ### Requirement: 按 Thread 提供 MessageBundle 服务端 MUST 提供 `GET /api/v1/threads/{threadId}/messages`,并 MUST 通过 Thread 所属 Project 校验访问权。响应 MUST 返回有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact;默认最多返回最新 200 条,再按 sequence 升序输出。 @@ -145,7 +160,7 @@ Edit 请求 MUST 只提交 sourceUserMessageId、新 parts 和可选 requestedMo ### Requirement: 通过 assistantMessageId 管理生成生命周期 服务端 MUST 允许客户端使用 assistantMessageId 查询、订阅和停止对应 MessageRun,而不要求客户端理解内部 MessageRun ID。事件流 MUST 使用严格递增的 eventSequence,并 MUST 支持 `afterEventSequence` 恢复。每次连接 MUST 先返回当前持久化 checkpoint snapshot 及其 cursor;若仍在运行,后续 live event MUST 严格大于该 cursor。旧 token delta MAY 不逐条重放,但恢复结果不得丢失已经持久化的生成内容。 -Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得自动停止 Run。终态事件 MUST 携带或允许随后取得 finalized Message 和终态 AssistantRunState。 +Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得自动停止 Run。终态事件 MUST 携带或允许随后取得 finalized Message 和终态 AssistantRunState。`run.snapshot` 与 `run.completed` MUST 携带当前 ProjectArtifactSummary,使客户端能够在刷新、重连和 Artifact 生成完成后校正 Project 页面统计;客户端不得依赖事件次数自行累加。 #### Scenario: 恢复 running 事件流 - **WHEN** 客户端请求 assistant Message 事件且提交 `afterEventSequence=42` From 56daeb674482efcd30f4811a871ada61d072f90f Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 17:34:22 +0800 Subject: [PATCH 06/23] docs(openspec): unify thread chat implementation roadmap --- .gitignore | 2 + .../define-thread-chat-domain-model/design.md | 44 +++---- .../proposal.md | 8 +- .../specs/domain/spec.md | 7 +- .../define-thread-chat-domain-model/tasks.md | 80 +++++++++++++ .../design-thread-chat-client-api/design.md | 65 +++++----- .../design/api-contracts.md | 55 +++++++-- .../new-project-first-message-lifecycle.md | 3 +- .../design/open-existing-project-lifecycle.md | 4 +- .../design/thread-message-loading.md | 8 +- .../design-thread-chat-client-api/proposal.md | 2 +- .../specs/thread-chat-client-state/spec.md | 16 ++- .../specs/thread-chat-command-api/spec.md | 28 ++++- .../design-thread-chat-client-api/tasks.md | 113 ++++++++++++++++++ 14 files changed, 345 insertions(+), 90 deletions(-) create mode 100644 openspec/changes/define-thread-chat-domain-model/tasks.md create mode 100644 openspec/changes/design-thread-chat-client-api/tasks.md diff --git a/.gitignore b/.gitignore index 42b6aca8..31173775 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ next-env.d.ts # thread-chat e2e 验收脚本的截图输出 e2e/thread-chat/shots/ .vercel + +.local-backups diff --git a/openspec/changes/define-thread-chat-domain-model/design.md b/openspec/changes/define-thread-chat-domain-model/design.md index 32d04bba..3d11412f 100644 --- a/openspec/changes/define-thread-chat-domain-model/design.md +++ b/openspec/changes/define-thread-chat-domain-model/design.md @@ -26,18 +26,18 @@ branch_message_feedback ── 以 treeId/threadId/messageId 关联反馈 - 让 Project 同时成为列表项、Thread 拓扑、标题、共享资源和永久删除边界。 - 让 Project 在 MVP 中直接归属于当前登录用户,不预建尚不存在的团队/分组层。 - 保留已确认的 sequence、Message replacement、BaseContext.messageIds、Fork 与 MessageRun 机制。 -- 给出从 `branch_trees`、嵌入实体和 `branch_generations` 迁移到规范化表的 PostgreSQL 目标 Schema 与模块骨架。 +- 给出接替 `branch_trees`、嵌入实体和 `branch_generations` 权威职责的 PostgreSQL 目标 Schema 与模块骨架。 **Non-Goals:** -- 本 change 不修改代码、数据库、API、前端或 `tasks.md`。 +- 本 change 的 design/spec 固定整体领域基线;代码、数据库、API 和前端通过 `tasks.md` 中的分拆交付链逐步落地,不在单一步骤整体重写。 - 不设计团队、成员、组织切换或平台级共享;出现真实需求后再独立建模。 - 不完整设计 Project Memory、Instruction、Target 和 File 的内容协议、检索、版本或同步机制;本设计只固定它们的 Project 归属边界。 - 不引入 Turn、Message Variant、Generation Chat 实体、通用 revision、V1 幂等系统或 ArtifactVersion。 ## Decisions -### D1. 从当前 User → Thread Tree 基线迁移到 User → Project +### D1. 由 User → Project 接替当前 User → Thread Tree 基线 目标所有权与聊天内容: @@ -119,7 +119,7 @@ Project 的确定边界: - `/thread-chat/{projectId}` 是 canonical 内容 URL。 - Project 永久删除会清理整个 Thread 族群与共享资源。 -Project 在目标模型中接替当前一条 `branch_trees` 记录所表达的产品边界,但不继承整树 JSON 的物理存储方式。迁移时,旧 `treeId` 只作为旧数据定位键或映射来源,不继续充当由客户端创建的新 Project ID。 +Project 在目标模型中接替当前一条 `branch_trees` 记录所表达的产品边界,但不继承整树 JSON 的物理存储方式。新 Project ID 由服务端生成;旧 `treeId` 不再作为新模型的资源身份,也不建立历史数据映射。 ### D3. Project Resource 共享范围与来源分离 @@ -270,18 +270,18 @@ stateDiagram-v2 字段使用 snake_case;所有新实体 ID 由服务端生成 UUID。目标表继续引用项目现有认证 `user.id`,不在本 change 新增其他所有权层。 -#### 当前持久化到目标实体的对应关系 +#### 当前权威职责到目标实体的接替关系 -| 当前真实来源 | 目标 | 本 change 固定的边界 | +| 当前真实来源 | 目标 | 本 change 固定的职责接替边界 | |---|---|---| -| `branch_trees` 一行 | 一个 Project | `user_id` 成为 `owner_user_id`;`title/custom_title` 迁移到 Project 标题;旧 `treeId` 只作迁移定位,不再由客户端生成新 ID | -| `branch_trees.state.threads` | `threads` 多行 | 每个嵌入 Thread 成为同一 Project 下的独立 row;`parentId/forkFromMsgId/anchorText` 转换为 Parent、source 与 Fork snapshot | -| `Thread.messages` 消息图 | `messages` 多行 | 当前有效路径迁移为默认时间线;所有保留节点都获得服务端 sequence,非默认节点如何标记 superseded 由持久化迁移 change 给出可验证算法 | -| `state.artifacts` | `artifacts` 多行 | 保留内容、来源 Thread/Message,并补 Project ownership | -| `branch_generations` | `message_runs` | 当前 generation 与目标 1:1 MessageRun 的转换必须显式处理历史 attempt;不得把多条 attempt 直接塞给同一 assistant Message | -| `branch_message_feedback` | 后续 Message feedback 表 | 使用迁移后的 Project/Thread/Message 映射重建关联 | +| `branch_trees` 一行 | 一个 Project | 接替整簇对话、标题和 owner 边界;Project ID 改由服务端生成 | +| `branch_trees.state.threads` | `threads` 多行 | 接替嵌入 Thread;Parent、source 与 Fork snapshot 成为规范化关系 | +| `Thread.messages` 消息图 | `messages` 多行 | 接替消息图;默认时间线改由服务端 sequence 与 superseded 状态表达 | +| `state.artifacts` | `artifacts` 多行 | 接替内嵌 registry;内容独立保存,并增加 Project ownership 与 Message provenance | +| `branch_generations` | `message_runs` | 接替 attempt sidecar;目标关系固定为每条 assistant Message 恰有一条 Run | +| `branch_message_feedback` | Message feedback 表 | 接替 tree/thread/message 复合身份,直接关联规范化 assistant Message | -历史 `branch_trees.user_id IS NULL` 是现有兼容数据,不自动归属于任意用户。迁移实施前必须先按当前“精确 URL 认领”规则完成归属,或把未认领记录隔离;不得猜测 owner。 +上表用于防止实现误读当前基线,不表示需要迁移旧记录。本地开发不保留现有 `branch_trees` 数据,不建立 treeId 映射、未认领记录处理或历史 generation attempt 转换逻辑。 #### `projects` @@ -448,14 +448,14 @@ Provider、Zustand、路由交接和多栏异步加载属于客户端/API 设计 ## Migration Plan -本 change 只更新设计。后续实施顺序: +本 change 先固定设计,并通过 `tasks.md` 跟踪后端实施。交付顺序: -1. 归档新的 `domain`,确立 Project → Thread → Message 术语。 -2. 建立规范化持久化 change,定义 `branch_trees.state`、嵌入 Thread/Message/Artifact 与 `branch_generations` 到新表的迁移和校验。 -3. 建立 Project/Thread Command API 与 ProjectBootstrap change。 -4. 建立 MessageRun 与恢复流 change。 -5. 建立客户端 Project entities、Thread topology、generationStreams 与 UI selectors。 -6. 完成服务端集成、API 合同、Store/Hook 与端到端验收。 -7. 切换 `/thread-chat/{projectId}` 为唯一入口,再退役旧 ThreadTree/branch generation 权威路径。 +1. 冻结术语、职责与测试门;建立 Vitest、隔离 PostgreSQL 测试数据库和 Fake AI Runtime。 +2. 使用 `pnpm db:push` 在测试数据库建立规范化 Schema,不迁移本地旧数据。 +3. 实现领域、Repository、Application Command/Query 与 MessageRun 后台执行,并通过后端集成测试。 +4. 由 `design-thread-chat-client-api` 实现 `/api/v1`、SSE、前端状态架构和现有 UI 接入;后端/API 测试全部通过前不得开始 UI 接入。 +5. E2E 验收通过后删除旧 ThreadTree/branch generation 权威代码与表,再次 `pnpm db:push`。 +6. 先归档本 domain change,再归档依赖它的客户端/API change,消除循环等待。 +7. 归档完成后,把稳定目标架构提炼到 `openspec/specs/domain/architecture.md`。 -切换前可以撤销新入口并保留旧读写路径;切换后的数据禁止回写旧整树 JSON。`tasks.md` 将在领域和配套设计全部确认后统一重写,本轮不更新。 +开发期间旧页面可以继续运行,但新旧后端不双写;前端切换前的回退方式是保留旧实现代码,而不是维护两套数据同步。`tasks.md` 记录唯一实施顺序、阶段门、旧权威退役、归档及永久架构文档提炼。 diff --git a/openspec/changes/define-thread-chat-domain-model/proposal.md b/openspec/changes/define-thread-chat-domain-model/proposal.md index 20fd378f..a7600fee 100644 --- a/openspec/changes/define-thread-chat-domain-model/proposal.md +++ b/openspec/changes/define-thread-chat-domain-model/proposal.md @@ -21,7 +21,7 @@ - Fork 只能使用 finalized 且具备 Prompt 资格的 Message;最后一条 assistant queued/running 时,前端禁用且后端拒绝 Fork。 - 单条 Message 不执行 hard delete;只有永久删除整个 Project 时,统一清理其 Thread、Message、MessageRun 与 Project 附属资源。 - 每条 assistant Message 对应一条持久化 MessageRun;它取代当前 `branch_generations` 的 attempt/current sidecar 语义。user Message 不具有 MessageRun,浏览器断开不停止后台运行。 -- 本 change 只更新领域规范与目标设计,不修改应用代码、数据库、API 或前端;`tasks.md` 按已确认约定暂不更新。 +- 本 change 作为 Issue #34 的领域基线与后端交付清单;数据库、领域、Repository、Application 与后台运行按 `tasks.md` 的阶段门实施,不再为同一目标增建一组管理性 change。 ## Capabilities @@ -31,13 +31,13 @@ ### Modified Capabilities -- `domain`:从当前 `Thread Tree → embedded Thread → embedded Message` 基线迁移到规范化的 `Project → Thread → Message`,并明确 Fork、replacement、BaseContext 与 MessageRun 的目标边界。 +- `domain`:将当前 `Thread Tree → embedded Thread → embedded Message` 基线重构为规范化的 `Project → Thread → Message`,并明确 Fork、replacement、BaseContext 与 MessageRun 的目标边界。 ## Impact -- OpenSpec:重写 `domain` 增量规范和设计;后续持久化、API、MessageRun、客户端 Store 与退役 change 必须以当前 `ThreadTreeState / branch_trees / branch_generations` 为迁移起点。 +- OpenSpec:重写 `domain` 增量规范和设计;后续实现必须以当前 `ThreadTreeState / branch_trees / branch_generations` 为被替换的真实基线。 - 后端目标:新增 `projects`、`threads`、`messages`、`message_runs` 等规范化表;逐步退役 `branch_trees.state` 与 `branch_generations` 权威路径。 - 前端目标:`/thread-chat/{projectId}` 加载 `ProjectBootstrap`;“新对话”创建 Project;“对话列表”列出 Projects;客户端 Store 从整树快照改为规范化实体与衍生拓扑。 - 身份与权限:Project 先直接引用当前登录用户;所有读取和命令都从服务端 Session 校验用户对 Project 的访问权。 - 共享资源:Project Memory、Instruction、Target、Files 和 Artifacts 对该 Project 的全部 Thread 可用,不跨 Project 自动共享。 -- 现有数据:迁移必须显式映射 `branch_trees.id/state/title/custom_title/user_id`、嵌入 Thread/Message/Artifact 以及对应 `branch_generations`;不得假设存在其他中间实体或表。 +- 本地数据:当前处于本地开发阶段,不保留旧 `branch_trees` 数据;目标 Schema 通过隔离测试数据库验证后使用 `pnpm db:push` 重建,不实现旧 treeId、嵌入实体或 `branch_generations` 的数据迁移器。 diff --git a/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md b/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md index 4fd957de..3d131bc2 100644 --- a/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md +++ b/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md @@ -15,7 +15,7 @@ - **Artifact**:归属于 Project,并保留来源 Message 身份的持久化产物。 - **Title**:Project 或 Branch Thread 的短标题。 -系统 MUST 将当前 Thread Tree 的聚合职责迁移到 Project,并将当前嵌在 `ThreadTreeState` 中的 Thread、Message 和 Artifact 迁移为可独立寻址的实体。目标规范、数据库、API 和客户端实体模型 MUST 使用 Project 表达整簇 Thread 的聚合边界,且 MUST NOT 继续把 Thread Tree、MainThread、ForkedThread、独立 ThreadFork、Turn、Generation 或 Message Variant 作为目标领域实体。 +目标实现 MUST 由 Project 接替当前 Thread Tree 的聚合职责,并将 Thread、Message 和 Artifact 保存为可独立寻址的实体。目标规范、数据库、API 和客户端实体模型 MUST 使用 Project 表达整簇 Thread 的聚合边界,且 MUST NOT 继续把 Thread Tree、MainThread、ForkedThread、独立 ThreadFork、Turn、Generation 或 Message Variant 作为目标领域实体。 #### Scenario: 将一簇分叉对话称为 Project - **WHEN** 用户查看由一个 Root Thread 和多列 Branch Thread 组成的工作项 @@ -31,11 +31,6 @@ - **THEN** 系统 MUST 将该节点称为 Child Thread 或按相对角色称为 Branch Thread - **AND** 必须使用 Fork 描述创建动作,不得把 ForkedThread 作为独立实体类型 -#### Scenario: 从当前 Thread Tree 建立 Project -- **WHEN** 系统迁移一条现有 `branch_trees` 记录 -- **THEN** 它 MUST 建立一个对应 Project,并把该记录中的 Thread、Message 和 Artifact 映射到该 Project 下的规范化实体 -- **AND** 不得把现有整树 JSON 误写成已经规范化的 Project 数据 - ### Requirement: 维护 Project 的 Thread 拓扑不变量 系统 MUST 使用 Project 与 Thread 关系维护分叉拓扑: diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md new file mode 100644 index 00000000..a7aea5d4 --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -0,0 +1,80 @@ +## 0. 阶段 0:冻结实施边界 + +- [x] 0.1 对本 change 与 `design-thread-chat-client-api` 执行严格校验,确认 Project、Thread、Message、MessageRun、BaseContext、replacement 与 Artifact 术语一致。 +- [x] 0.2 固定职责分界:本 change 只交付数据库、领域、Repository、Application 与 MessageRun 后台执行;API、客户端、UI 和 E2E 由 `design-thread-chat-client-api` 承担。 +- [x] 0.3 固定本地数据库策略:不迁移现有 `branch_trees` 数据,不建立 treeId 映射或双写;使用独立测试数据库验证后,通过 `pnpm db:push` 重建本地目标 Schema。 +- [x] 0.4 固定测试策略:Vitest 负责单元、Repository 和 Application 集成测试;真实 PostgreSQL 不 mock;AI Runtime、邮件和外部存储在自动测试中使用可控 adapter。 +- [x] 0.5 固定阶段门与归档顺序:后端领域通过后才能实现 API,后端/API 全部通过后才能接入前端,E2E 通过后先归档本 change,再归档客户端/API change。 + +## 1. 建立后端测试基础设施 + +- [ ] 1.1 增加 Vitest 及 `test`、`test:unit`、`test:integration`、`test:api`、`test:watch` 脚本;默认命令不得调用真实模型供应商。 +- [ ] 1.2 建立物理隔离的 PostgreSQL 测试数据库配置,例如 `thread-chat-test`,并让测试专用 Drizzle 配置只读取 `TEST_DATABASE_URL`。 +- [ ] 1.3 在测试初始化中校验数据库名称或明确 allowlist;目标不是测试数据库时立即终止,禁止误删开发数据库。 +- [ ] 1.4 建立测试 Schema 重建流程:只删除测试库的 `thread_chat` schema,再执行测试配置对应的 `drizzle-kit push`。 +- [ ] 1.5 建立用户、Project、Thread、Message、Artifact 与 MessageRun fixture factory;测试 ID 仍由服务端/fixture factory 生成,不进入生产客户端逻辑。 +- [ ] 1.6 定义可注入的 AI Runtime capability 与 Fake AI Runtime,支持固定 delta、completed、failed、stopped、Markdown Artifact tool output 和恢复事件。 +- [ ] 1.7 约束测试并行:纯单元测试可以并行;共享 PostgreSQL 的集成/API 测试在证明数据隔离前使用单 worker 或独立事务边界。 + +## 2. 建立规范化数据库 Schema + +- [ ] 2.1 在 `lib/db/schema.ts` 中新增 `projects`、`threads`、`messages`、`message_runs`、`artifacts` 与 Message feedback 表;Project Memory/File 只保留后续扩展边界,不实现完整协议。 +- [ ] 2.2 为 Project owner、唯一 Root Thread、Thread 内唯一 sequence、同 Message 单一 replacement 和 assistant Message 单一 MessageRun 建立数据库可表达的约束与索引。 +- [ ] 2.3 为 Root/Branch ForkFacts 的空值组合、Message role、MessageRun status 和非负 eventSequence 建立数据库可表达的约束。 +- [ ] 2.4 明确无法由普通 CHECK 表达的同 Project Parent/source/replacement 关系,交给事务内 Repository 校验,不伪造不可靠约束。 +- [ ] 2.5 为 Project 永久删除定义级联边界;普通 Message Repository 和 Route 不得暴露单 Message hard delete。 +- [ ] 2.6 对隔离测试数据库执行 `pnpm db:push` 等价的测试配置命令,并验证从空库可以一次建立完整目标 Schema。 + +## 3. 实现纯领域模型与 Repository + +- [ ] 3.1 按 design 的模块骨架建立不依赖 React、HTTP、Drizzle 或具体 AI Runtime 的 Project、Thread、Message、MessageRun、BaseContext 与 Artifact 领域类型。 +- [ ] 3.2 实现 Root/Branch 关系判定、唯一 Root、同 Project Parent/source、无环拓扑与 ForkFacts 完整性验证。 +- [ ] 3.3 实现 Thread 内服务端 sequence 分配,并用数据库唯一约束与并发测试保证无重复 sequence。 +- [ ] 3.4 实现 finalized Message 不可变与 replacement 规则;Repository 的更新入口必须明确禁止原地覆盖 finalized parts 和 sequence。 +- [ ] 3.5 实现 BaseContextV1 的验证、持久化和有序 Message ID 解析;BaseContext 只能由服务端计算。 +- [ ] 3.6 实现 MessageRun 的 queued/running/completed/failed/stopped 条件状态转换、checkpointParts、eventSequence 与 Stop 请求持久化。 +- [ ] 3.7 实现 Project、Thread、Message、MessageRun、Artifact 与 feedback Repository;所有 Query 从 actor 校验 Project owner scope。 +- [ ] 3.8 实现 Artifact 独立持久化及 `sourceMessageId` provenance;Message 的 AI SDK v7 tool output 只保存 `artifactId`,不复制 Markdown 正文。 + +## 4. 实现核心 Application Command 与 Query + +- [ ] 4.1 原子创建 `Project + Root Thread + U1 + A1 + queued MessageRun`,所有新实体 ID 由服务端生成,提交前不启动 AI Runtime。 +- [ ] 4.2 原子追加 `user Message + assistant Message + queued MessageRun`,不把 user/assistant 角色交替建成数据库不变量。 +- [ ] 4.3 实现 Fork Thread:验证 finalized source、冻结 ForkSourceSnapshot 和 BaseContext、创建 Child Thread,任一步失败整体回滚。 +- [ ] 4.4 实现 Regenerate:保留旧 assistant Message 内容和 sequence,标记 superseded,追加 replacement assistant Message 与新 MessageRun。 +- [ ] 4.5 实现 Edit last user Message:追加 replacement user Message,将依赖旧输入的有效后缀 supersede,并创建新 assistant Message 与 MessageRun。 +- [ ] 4.6 实现 Project metadata、Branch metadata、archive/unarchive、feedback 与 Project 永久删除 Application Command。 +- [ ] 4.7 实现 Project 列表、ProjectBootstrap、Thread Message 和 Artifact-by-ID Query;Bootstrap 只返回轻量 topology 与 Root bundle,Thread 默认最多 200 条有效 Message。 +- [ ] 4.8 实现 Prompt History:`BaseContext.messageIds + 当前 Thread 有效 Prompt Message`,排除不合格 assistant 状态,且不依赖客户端已加载窗口。 + +## 5. 实现 MessageRun 后台执行 + +- [ ] 5.1 将 `message_runs.status=queued` 作为持久化待执行事实;事务提交后才尝试唤醒执行器,不建立通用任务平台。 +- [ ] 5.2 实现条件领取 queued Run、heartbeat、checkpoint/eventSequence 持久化和 completed/failed/stopped 原子终态提交,防止同一 Run 重复执行。 +- [ ] 5.3 实现最小 queued scanner,补偿事务已提交但即时唤醒失败的 Run;不得创建第二条 assistant Message 或 MessageRun。 +- [ ] 5.4 接入真实 AI Runtime adapter,并确保领域/Application 只依赖 capability;自动测试使用 Fake AI Runtime,不发起真实计费请求。 +- [ ] 5.5 完成 Markdown Artifact 工具结果投影:创建独立 Artifact,最终 Message tool output 保存稳定 `artifactId`。 +- [ ] 5.6 实现显式 Stop;浏览器刷新、断开连接或客户端 Runtime 销毁不得调用 Stop,也不得终止后台执行。 + +## 6. 后端领域验收门 + +- [ ] 6.1 完成领域单元测试:Root/Branch、拓扑无环、sequence、replacement、Fork 资格、BaseContext、Prompt History 与 MessageRun 状态机。 +- [ ] 6.2 完成 Repository 集成测试:真实 PostgreSQL owner scope、唯一 Root、并发 sequence、finalized 不可变、replacement、Artifact provenance 与单一 MessageRun。 +- [ ] 6.3 完成 Application 集成测试:create/send/Fork/Edit/Regenerate/Stop/delete 的事务提交与整体回滚。 +- [ ] 6.4 使用 Fake AI Runtime 测试 delta、完成、失败、Stop、queued scanner 补偿、刷新时 checkpoint 恢复与 Artifact tool output。 +- [ ] 6.5 执行 `pnpm typecheck`、后端 unit/integration tests 和 `pnpm openspec:validate`,全部通过后记录实现证据;未通过不得进入 API Route 实现。 + +## 7. E2E 通过后的旧后端退役 + +- [ ] 7.1 等待 `design-thread-chat-client-api` 完成 API、前端接入与 Ego Browser E2E;在此前保留旧实现代码,但不建立新旧数据双写。 +- [ ] 7.2 E2E 通过后删除旧 `branch_trees.state`、`branch_generations`、消息图 active leaf 与旧整体保存的后端权威职责。 +- [ ] 7.3 从 Drizzle Schema 删除不再使用的旧表/列,并对本地与隔离测试数据库执行 `pnpm db:push`;无需迁移或保留旧数据。 +- [ ] 7.4 再次执行后端、API、前端集成、build、E2E 与 OpenSpec 严格校验,确认代码中不存在旧写入路径。 +- [ ] 7.5 对照 domain spec 的 Requirement/Scenario 记录自动测试或手动验收证据。 +- [ ] 7.6 在全部门槛满足后先归档 `define-thread-chat-domain-model`,将增量 domain spec 合入正式 `openspec/specs/domain/spec.md`。 + +## 8. 归档后提炼永久领域架构 + +- [ ] 8.1 从已归档 design 提炼稳定目标架构到 `openspec/specs/domain/architecture.md`,只保留实体、关系、ER 图、不变量、最终 DB Schema 与模块边界。 +- [ ] 8.2 在正式 `openspec/specs/domain/spec.md` 中链接 `architecture.md`,明确冲突时可验证 Requirement 优先。 +- [ ] 8.3 校验永久架构文档与正式 spec、实际 Drizzle Schema 和模块结构一致,并把后续同步更新列为验收项。 diff --git a/openspec/changes/design-thread-chat-client-api/design.md b/openspec/changes/design-thread-chat-client-api/design.md index f0e77cc0..9b183728 100644 --- a/openspec/changes/design-thread-chat-client-api/design.md +++ b/openspec/changes/design-thread-chat-client-api/design.md @@ -33,11 +33,11 @@ chat-controller 同时承担: **Non-Goals:** -- 不实现 Store、Hook、Route Handler、数据库或迁移。 -- 不确定具体 Transport 类、HTTP 库、SSE parser、重试器和认证包装代码。 +- 不在单一步骤整体重写 Store、Hook、Route Handler 或旧页面;通过 `tasks.md` 中的小步交付链逐层接入。 +- P0 事件 Transport 固定为 SSE;具体 HTTP client、SSE parser、重试器和认证包装库由实现 change 选择,但不得改变共享契约。 - 不实现离线命令队列、通用 Idempotency-Key、跨设备草稿、协同编辑或 Message Variant。 - 不重新设计服务端数据库 Schema、BaseContext 或 PromptBuilder。 -- 不在本 change 编写 tasks;等本设计和 API 经过多轮确认后统一拆分。 +- 不在本 change 重新设计 Markdown Artifact 的身份关系;Message 的 AI SDK v7 tool result 只保存 `artifactId` 引用,Artifact 内容仍是独立服务端实体并在用户打开时按 ID 加载。 ## Decisions @@ -199,14 +199,16 @@ type ArtifactEntity = { kind: string title: string - /** false 表示本响应只返回元数据/引用,不能据 content=null 推断资源为空。 */ - contentIncluded: boolean - - /** 仅在 contentIncluded=true 时有值;大型正文允许按需加载。 */ - content: JsonValue | null + /** 通过 Artifact Query 按 ID 加载的完整内容,不嵌入 ThreadMessageBundle。 */ + content: JsonValue createdAt: string } +type MarkdownArtifactToolOutput = { + /** tool result 只保存独立 Artifact 的稳定引用,不复制 Markdown 正文。 */ + artifactId: ArtifactId +} + type ProjectArtifactSummary = { /** * 服务端在 Project Artifact 集合发生变化时单调递增的读模型游标。 @@ -391,7 +393,7 @@ type ThreadChatEntitiesState = { */ messageIdsByThreadId: Record - /** 仅保存 Bootstrap/Message bundle 已包含或用户按需加载的 Artifact。 */ + /** 只保存用户实际打开并通过 Artifact Query 加载过的完整 Artifact。 */ artifactsById: Record } @@ -421,6 +423,9 @@ type ThreadChatRequestsState = { /** 每个 Thread 的加载状态和已合并窗口边界,避免重复请求。 */ threadMessagesById: Record + /** 用户打开 Artifact Drawer 后按 ID 加载内容;MessageBundle 只提供 artifactId 引用。 */ + artifactById: Record + /** * 当前页面的命令 busy/error 状态,key 由命令名和既有资源 ID 构成, * 例如 `send:${threadId}`、`regenerate:${messageId}`;它不是网络级幂等记录。 @@ -552,9 +557,11 @@ type ThreadChatProjectActions = { applyThreadCreated: (thread: ThreadEntity) => void applyReplacementBundle: (bundle: ReplacementBundle) => void applyRunEvent: (event: AssistantMessageEvent) => void + applyArtifact: (artifact: ArtifactEntity) => void setBootstrapLoadState: (state: LoadState) => void setCommandState: (scope: string, state: CommandState | null) => void setThreadMessageLoadState: (threadId: ThreadId, state: LoadState) => void + setArtifactLoadState: (artifactId: ArtifactId, state: LoadState) => void restoreWorkbenchSnapshot: (snapshot: ThreadWorkbenchSnapshotV1) => void resetWorkbenchToDefault: () => void openThread: (threadId: ThreadId, sourceSlotId: "root" | ColumnSlotId) => void @@ -671,6 +678,7 @@ type ThreadChatProjectCommands = { /** 只在未 seed、未 ready 时加载当前 Provider Project 的 Bootstrap。 */ loadProjectBootstrap: () => Promise ensureThreadMessages: (threadId: ThreadId) => Promise + ensureArtifact: (artifactId: ArtifactId) => Promise updateProject: (patch: PatchProjectRequest) => Promise updateThread: (threadId: ThreadId, patch: PatchThreadRequest) => Promise archiveThread: (threadId: ThreadId) => Promise @@ -1079,8 +1087,9 @@ Store Action 与对应 Zustand Store/slice 共置,只执行同步、可测试 ```ts mergeBootstrap → 跨 entities/runs/requests 原子初始化 Project setBootstrapLoadState → 只设置 Bootstrap loading/error;ready 由 mergeBootstrap 提交 -applyMessageBundle → 合并一个 Thread 的有效 Message、Run 与 Artifact +applyMessageBundle → 合并一个 Thread 的有效 Message 与 Run;Artifact 只保留在 Message parts 的 ID 引用 setThreadMessageLoadState → 独立设置某个 Thread 的 loading/error;ready 由 Bundle 原子提交 +applyArtifact/setArtifactLoadState → 用户打开 Drawer 时按 ID 合并独立 Artifact 内容与加载状态 applyMessageCreationBundle → 合并发送命令返回的 user、assistant 与 Run applyThreadCreated → 合并服务端创建的 Child Thread applyReplacementBundle → 原子标记 superseded 并合并 replacement 与新 Run @@ -1232,9 +1241,6 @@ type ThreadMessageBundle = { /** 必须且只包含本窗口 assistant Message 对应的 Run。 */ assistantRuns: AssistantRunState[] - /** 渲染本窗口 Message 所需且允许内联的 Artifact。 */ - includedArtifacts: ArtifactEntity[] - /** true 表示服务端还存在更早有效 Message;MVP 不自动加载。 */ hasOlderMessages: boolean @@ -1271,7 +1277,7 @@ Message Query 与 Assistant Run 是两个正交状态:Column 只有在 Message 已有 Project 的完整冷启动、无 Workbench Snapshot、有 Snapshot、多 Branch 并行加载和 Runtime Message Cache 分支见 [打开已有 Project 生命周期](./design/open-existing-project-lifecycle.md)。 -MVP 返回最新最多 200 条有效 Message,并按 sequence 升序交给客户端。`hasOlderMessages` 只保留能力边界,当前 UI 不实现树内自动分页替换。 +MVP 返回最新最多 200 条有效 Message,并按 sequence 升序交给客户端。`hasOlderMessages` 只保留能力边界,当前 UI 不实现树内自动分页替换。Markdown tool result 在 Message parts 中保存 `artifactId`;ThreadMessageBundle 不返回 Markdown 正文。只有用户打开 Artifact 时才按 ID 加载独立 Artifact 内容。 ### D9. 核心 Application Command 伪代码 @@ -1399,6 +1405,7 @@ useThreadColumnView(slotId) useThreadColumnHeaderView(slotId) useProjectTreeRows() useAssistantRun(assistantMessageId) +useArtifact(artifactId) useForkAvailability(messageId) useVisibleThreadColumns() useProjectHeaderView() @@ -1439,6 +1446,7 @@ useProjectRuntimeLifecycle() // 仅由 ThreadChatProjectProvider 调用 useEnsureThreadMessagesLoaded(threadId) useActiveGenerationSubscriptions() useWorkbenchPersistence() // projectId 从当前 ProjectRuntime 取得 +useEnsureArtifactLoaded(artifactId) // 仅在 Artifact Drawer 打开时按 ID 加载 ``` 生命周期 Hook 可以使用 Effect;Root/children/messages/canFork 等衍生状态禁止用 Effect 镜像。 @@ -1543,7 +1551,7 @@ API capability interface → JSON 或 event stream 解码 ``` -Transport 不决定业务原子性、不访问 Zustand、不生成实体 ID、不构造 ViewModel。事件载体最终选 SSE 或 WebSocket,不改变 `assistantMessageId + eventSequence` 的逻辑契约。 +Transport 不决定业务原子性、不访问 Zustand、不生成实体 ID、不构造 ViewModel。P0 使用 SSE 承载生成事件;未来替换 Transport 也不得改变 `assistantMessageId + eventSequence` 的逻辑契约。 ## Risks / Trade-offs @@ -1557,21 +1565,20 @@ Transport 不决定业务原子性、不访问 Zustand、不生成实体 ID、 ## Migration Plan -本 change 只完成设计。后续建议拆分: +本 change 不再为同一目标建立七个额外管理性 change,统一按 `tasks.md` 的阶段门实施: -1. `add-thread-chat-v1-contracts`:共享 DTO/error schemas 与 API 集成测试夹具。 -2. `add-thread-chat-project-queries`:Project list、Bootstrap、Thread Message Query。 -3. `add-thread-chat-command-api`:create/send/fork/edit/regenerate/metadata/feedback。 -4. `add-thread-chat-generation-events`:AssistantRun query/event/stop 与刷新恢复。 -5. `normalize-thread-chat-zustand-store`:ProjectCatalog + Project-scoped entities/runs/requests/ui slices。 -6. `add-thread-chat-client-commands`:Store Actions、Application Commands、normalizer 和 generation coordinator。 -7. `integrate-thread-chat-hooks-ui`:Provider、selector/command/lifecycle Hooks 与现有 UI 接入。 -8. Transport 可在第 2~4 步随契约落地,也可先用接口注入的测试 adapter,不改变 Store Action/Application Command 设计。 +1. 共享 DTO/error schemas、Transport capability 与 API 测试夹具。 +2. Project/Thread Query、Command、Artifact Query、SSE 与 Stop。 +3. 后端集成测试和 API 合同测试;全部通过前不得开始前端重构。 +4. Project-scoped Store、Runtime、Application Commands、Coordinator 与 Hooks。 +5. 使用 Ego Browser 固定现有 UI 参考截图和交互清单后,将新数据链路无损接入现有组件。 +6. 前端集成与 E2E 通过后删除旧客户端权威代码,等待 domain change 清理旧后端并先行归档。 +7. domain change 归档后归档本 change,并提炼稳定客户端架构与 API 合同。 -切换期不得让新 Store 回写旧 `branch_trees.state`。回滚以路由/feature flag 切回旧页面为边界,新旧实体不做双向同步。 +开发期间保留旧页面代码作为切换前回退边界,但不引入 feature flag、双写或新旧实体同步。P0 使用 SSE;Store 和 Application Command 可以先通过接口注入的测试 adapter 验证。 -## Open Questions +## Resolved P0 Boundaries -- P0 是否需要“只追加 user Message、不立即创建 assistant Run”的独立命令?当前 UI 和本设计只提供常用的 `user + assistant + Run` 原子发送,但数据库不变量仍允许未来扩展连续 user Message。 -- Artifact 内容在 ThreadMessageBundle 中按 kind/size 何时内联,何时只返回引用,需要在 Artifact API change 中固定阈值。 -- 事件 Transport 选择 SSE 还是 WebSocket 留到 Transport 设计;逻辑恢复契约已经固定。 +- P0 不提供“只追加 user Message”的独立命令。每次发送原子创建 `user Message + assistant Message + MessageRun`;数据库领域模型不要求角色交替,保留未来增加独立 user append 命令的能力。 +- P0 沿用 Artifact 引用关系:Markdown tool result 保存 `artifactId`,正文由独立 Artifact Query 在用户打开时加载。MessageBundle 不复制 Markdown 正文,因此不需要设计“多大才拆开”的阈值。 +- P0 生成事件使用 SSE。客户端按 `assistantMessageId + eventSequence` 恢复;未来若改用其他 Transport,不改变该逻辑契约。 diff --git a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md index 3943186e..91a4f255 100644 --- a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md +++ b/openspec/changes/design-thread-chat-client-api/design/api-contracts.md @@ -24,6 +24,7 @@ | `POST /api/v1/messages/{id}/edits` | Edit 最后一条 user | `parts`, `requestedModelId?` | ReplacementBundle | 修改最后问题并重答 | | `POST /api/v1/messages/{id}/regenerations` | Regenerate assistant | `requestedModelId?` | ReplacementBundle | 重新生成最后回答 | | `PUT /api/v1/messages/{id}/feedback` | 设置评价 | `positive/negative/null` | FeedbackDTO | 点赞/点踩 | +| `GET /api/v1/artifacts/{id}` | 加载 Artifact 内容 | artifactId | ArtifactDTO | 用户打开 Markdown 文档 | | `GET /api/v1/assistant-messages/{id}/events` | 恢复事件 | `afterEventSequence` | Event stream | 刷新后续接生成 | | `POST /api/v1/assistant-messages/{id}/stop` | 停止生成 | assistantMessageId | AssistantRunState | 用户点击 Stop | @@ -226,17 +227,27 @@ type AssistantRunStateDTO = { finishedAt: DateTime | null } -type IncludedArtifactDTO = { +type ArtifactDTO = { id: string projectId: ProjectId sourceMessageId: MessageId kind: string title: string - contentIncluded: boolean - content: JsonValue | null + content: JsonValue createdAt: DateTime } +/** + * Markdown Artifact 工具完成后的目标 output 协议。 + * + * 当前项目基线由服务端在工具完成后生成 Artifact ID,再写入领域消息的 + * `message.artifactIds`;V1 契约将这层关系收敛到 AI SDK v7 tool output, + * 使 Message 本身携带稳定引用,但仍不复制 Markdown 正文。 + */ +type MarkdownArtifactToolOutput = { + artifactId: string +} + type ProjectArtifactSummaryDTO = { /** Project Artifact 集合变化时由服务端单调递增;只用于客户端拒绝旧统计。 */ changeSequence: number @@ -266,9 +277,10 @@ type FeedbackDTO = { - `AssistantRunStateDTO.assistantMessageId` 必须指向 role 为 assistant 的 Message;每条 assistant Message 恰有一个 Run。 - `status=completed` 时,Message 必须已有不可变 `parts` 和 `finalizedAt`;`status=failed|stopped` 时 `finishedAt` 非空。 - `stopRequestedAt` 非空表示服务端已经接受 Stop;它不等于 Run 已进入 `stopped` 终态。 -- `contentIncluded=false` 时 `IncludedArtifactDTO.content` 必须为 `null`;它只表示本响应未内联正文,不表示 Artifact 不存在。 +- Markdown tool result 必须通过 AI SDK v7 Message part 的 output 保存 `artifactId` 引用;其目标 output 必须符合 `MarkdownArtifactToolOutput`,不得把 Markdown 正文复制进 tool result。 +- `ArtifactDTO` 只由独立 Artifact Query 返回;ThreadMessageBundle、ProjectBootstrap 和生成事件不得内联 Artifact 正文。 - `ProjectArtifactSummaryDTO.changeSequence` 必须是非负整数,并在该 Project 的 Artifact 集合发生变化时单调递增;它只排序统计快照,不是 revision、ETag 或客户端写入前置条件。 -- `ProjectArtifactSummaryDTO.total` 必须等于 `byKind` 所有非负整数值之和;它统计 Project 全量 Artifact,不能退化为当前响应的 `includedArtifacts.length`。 +- `ProjectArtifactSummaryDTO.total` 必须等于 `byKind` 所有非负整数值之和;它统计 Project 全量 Artifact,不能退化为客户端当前 `artifactsById` 缓存的数量。 ### 1.5 复合响应 @@ -277,7 +289,6 @@ type ThreadMessageBundleDTO = { threadId: ThreadId messages: MessageDTO[] assistantRuns: AssistantRunStateDTO[] - includedArtifacts: IncludedArtifactDTO[] hasOlderMessages: boolean oldestReturnedSequence: number | null newestReturnedSequence: number | null @@ -314,9 +325,9 @@ type ReplacementBundleDTO = { `ThreadMessageBundleDTO.messages` 只返回当前有效时间线,即 `supersededAt === null` 的 Message,并按 `sequence ASC` 排列。旧 replacement Message 仍保存在数据库,但不混入默认 UI 时间线。 -`assistantRuns` 必须且只包含本 bundle 中 assistant Message 的 Run;不得返回属于其他 Thread 或未返回 Message 的 Run。`includedArtifacts` 必须且只包含渲染本次 Message 所需且允许内联的 Artifact。 +`assistantRuns` 必须且只包含本 bundle 中 assistant Message 的 Run;不得返回属于其他 Thread 或未返回 Message 的 Run。Message 通过 tool result 中的 `artifactId` 引用 Artifact;本 bundle 不复制 Artifact 正文。 -`ProjectBootstrapDTO.artifactSummary` 与 `CreationBundleDTO.artifactSummary` 是 Project 级统计读模型。它不枚举 Artifact,也不能由客户端根据局部 `includedArtifacts` 重建;首次创建且尚未产生 Artifact 时必须返回 `{ changeSequence: 0, total: 0, byKind: {} }`。 +`ProjectBootstrapDTO.artifactSummary` 与 `CreationBundleDTO.artifactSummary` 是 Project 级统计读模型。它不枚举 Artifact,也不能由客户端根据已加载的 `artifactsById` 重建;首次创建且尚未产生 Artifact 时必须返回 `{ changeSequence: 0, total: 0, byKind: {} }`。 ## 2. Project API @@ -422,7 +433,7 @@ Body:无。Query:无。 - `project.id` 必须等于 path `projectId`。 - `threadTopology` 返回该 Project 全部轻量 Thread,必须恰有一个 `parentThreadId=null` 的 Root。 - 每个 Branch 的 `parentThreadId` 必须能在同一 `threadTopology` 找到,且不得形成环。 -- `artifactSummary` 必须统计该 Project 的全部 Artifact,不能只统计 `initialThread.includedArtifacts`。 +- `artifactSummary` 必须统计该 Project 的全部 Artifact,不能只统计客户端已经按 ID 加载的 Artifact。 - `initialThread.threadId` 必须等于 Root ID。 - Root bundle 默认返回最新最多 200 条有效 Message,再按 sequence 升序输出。 - Bootstrap 不返回 BaseContext、Branch Message、Prompt History 或全部 Project Resource 正文。 @@ -757,7 +768,6 @@ type RunSnapshotEvent = { cursor: number run: AssistantRunStateDTO message: MessageDTO - includedArtifacts: IncludedArtifactDTO[] /** 当前 Project 的最新 Artifact 总量,用于刷新或重连后校正页面统计。 */ artifactSummary: ProjectArtifactSummaryDTO } @@ -773,7 +783,6 @@ type RunCompletedEvent = { eventSequence: number run: AssistantRunStateDTO message: MessageDTO - includedArtifacts: IncludedArtifactDTO[] /** 本次完成事务提交后的 Project Artifact 总量。 */ artifactSummary: ProjectArtifactSummaryDTO } @@ -832,7 +841,28 @@ Body:无。成功响应:`200 ApiResponse`。 主要错误:`assistant_message_not_found`、`message_run_not_found`、`forbidden`。 -## 6. 错误码与 HTTP 映射 +## 6. Artifact API + +### 6.1 按 ID 加载 Artifact + +```http +GET /api/v1/artifacts/{artifactId} +``` + +Query:无。Body:无。成功响应:`200 ApiResponse`。 + +决定性关系: + +- 服务端必须从 Artifact 所属 Project 校验当前 actor 的访问权。 +- 返回的 `artifact.id` 必须等于 path `artifactId`。 +- Message tool result 中的 `artifactId` 只是引用;Artifact 正文只从本接口返回。 +- 客户端可以在 ProjectRuntime 生命周期内按 ID 缓存成功结果;刷新后按需重新加载。 + +主要错误:`artifact_not_found`、`forbidden`、`unauthorized`。 + +手动 Case:打开包含 Markdown tool result 的 Message,读取其中 `artifactId` 后请求本接口;预期返回对应 Markdown。使用其他用户 Project 的 artifactId 请求时,预期 not_found/forbidden。 + +## 7. 错误码与 HTTP 映射 ```ts type ApiErrorCode = @@ -847,6 +877,7 @@ type ApiErrorCode = | "message_not_found" | "assistant_message_not_found" | "message_run_not_found" + | "artifact_not_found" | "model_not_available" | "thread_archived" | "thread_generation_in_progress" diff --git a/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md b/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md index 9c098d5f..b342775c 100644 --- a/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md +++ b/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md @@ -266,7 +266,8 @@ function onAssistantEvent(event: AssistantMessageEvent) { case "run.completed": /** - * 一次合并 finalized A1、completed Run、Artifact 和最新 Summary。 + * 一次合并 finalized A1、completed Run 和最新 Artifact Summary; + * Message tool result 只含 artifactId,正文等用户打开后再按 ID 加载。 * UI 从 checkpoint/running 直接过渡到 Message.parts/finalized。 */ runtime.store.getState().applyRunEvent(event) diff --git a/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md b/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md index 470e15d4..eea90b61 100644 --- a/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md +++ b/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md @@ -63,7 +63,7 @@ async function loadProjectBootstrap(): Promise { * - Project Entity * - 全量轻量 Thread topology * - Artifact Summary - * - Root Message/Run/Artifact + * - Root Message/Run;Artifact 正文仍按 ID 加载 * - Root ThreadMessageWindow ready * - Bootstrap ready */ @@ -230,7 +230,7 @@ function selectThreadColumnView( thread: state.entities.threadsById[threadId], messages: selectActiveMessages(state, threadId), assistantRuns: selectRunsForThread(state, threadId), - artifacts: selectIncludedArtifactsForThread(state, threadId), + artifactRefs: selectArtifactRefsFromMessages(state, threadId), hasOlderMessages: window.hasOlderMessages, }) } diff --git a/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md b/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md index d60c0130..d56b37a2 100644 --- a/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md +++ b/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md @@ -23,7 +23,7 @@ flowchart TD - App Store:Project 列表摘要与跨 Project 外壳 UI,不保存 `selectedProjectId`。 - URL/Provider:决定当前 ProjectRuntime。 -- Project Store:规范化保存当前 Project 的 Thread、已加载 Message、Run、Artifact、请求状态和本地工作台状态。 +- Project Store:规范化保存当前 Project 的 Thread、已加载 Message、Run、按需加载的 Artifact、请求状态和本地工作台状态。 - ThreadMessageLoader:保存非序列化 `threadId → Promise/AbortController`,负责请求去重与 Runtime 级取消。 - Column Slot:只决定某个物理列当前展示哪个 Thread,不拥有 Message 数据。 @@ -158,7 +158,7 @@ async function ensureThreadMessages(threadId: ThreadId): Promise { expectedThreadId: threadId, }) - // 一次同步 State Transition 合并 Message、Run、Artifact 和 ready 窗口。 + // 一次同步 State Transition 合并 Message、Run 和 ready 窗口;Artifact 正文按 ID 另行加载。 store.getState().applyMessageBundle(bundle) // 只恢复本 Bundle 中 queued/running 的 assistant Run。 @@ -181,7 +181,7 @@ async function ensureThreadMessages(threadId: ThreadId): Promise { } ``` -同一 Thread 的多个调用复用同一个 Promise;不同 Thread 的请求互不等待。`applyMessageBundle` 必须验证 Bundle 的 Thread/Project 归属、Message 的 `threadId`、sequence 窗口和 Run/Artifact 引用后再原子合并。 +同一 Thread 的多个调用复用同一个 Promise;不同 Thread 的请求互不等待。`applyMessageBundle` 必须验证 Bundle 的 Thread/Project 归属、Message 的 `threadId`、sequence 窗口和 Run 关联后再原子合并。Message 中的 `artifactId` 只是引用,不在此流程加载正文。 ## 5. Column 与迟到响应 @@ -229,7 +229,7 @@ ColumnSlot.threadId + ThreadMessageWindowState + active MessageEntity[] + AssistantRunState - + included ArtifactEntity + + Message parts 中的 artifactId 引用 → ThreadColumnView ``` diff --git a/openspec/changes/design-thread-chat-client-api/proposal.md b/openspec/changes/design-thread-chat-client-api/proposal.md index eb396f21..dc5ed462 100644 --- a/openspec/changes/design-thread-chat-client-api/proposal.md +++ b/openspec/changes/design-thread-chat-client-api/proposal.md @@ -15,7 +15,7 @@ - 定义 `/thread-chat/new` 为无实体 ID 的本地草稿入口;第一次发送时服务端原子创建 Project、Root Thread、首条 user Message、assistant Message 与 MessageRun,客户端再根据返回的 `project.id` 通过集中式路由构造器替换为已创建 Project 的页面 URL。 - 定义后端 `/api/v1` 的资源标识、请求参数、响应 DTO、错误、原子性、权限与适用场景;所有新实体 ID 均由服务端生成。 - MVP 首次加载返回全量轻量 Thread topology 和 Root Thread 数据;其他 Thread 按需一次加载最多 200 条有效 Message,不实现复杂分页替换。 -- 本 change 只形成设计与可测试契约,不实现 Zustand Store、Hooks、Route Handler、Stream Client 或 Transport 封装,也暂不编写实施 tasks。 +- 本 change 作为客户端状态与 `/api/v1` 契约的唯一交付清单;Zustand Store、Hooks、Route Handler、SSE Client 和 Transport 按 `tasks.md` 的阶段门实施,不再为同一目标增建管理性 change。 ## Capabilities diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md index 2f31f38d..34c5d321 100644 --- a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md @@ -28,9 +28,10 @@ Thread 的 Root/Branch、children、depth 和 breadcrumb MUST 由 `parentThreadId` 派生;Message 默认时间线 MUST 由 `supersededAt IS NULL` 和 `sequence ASC` 派生。BaseContext MUST 保持为服务端内部事实,不得进入普通客户端实体状态或由客户端重建。 #### Scenario: 合并 ProjectBootstrap -- **WHEN** 客户端收到包含 Project、Thread topology、Message、AssistantRunState 和 Artifact 的合法 ProjectBootstrap -- **THEN** 客户端 MUST 按实体 ID 规范化合并各类 DTO +- **WHEN** 客户端收到包含 Project、Thread topology、Root Message、AssistantRunState 和 Project Artifact Summary 的合法 ProjectBootstrap +- **THEN** 客户端 MUST 按实体 ID 规范化合并这些 DTO 与读模型 - **AND** 不得保存第二份整树权威快照 +- **AND** Bootstrap 中的 Message 只通过 tool result 的 `artifactId` 引用 Artifact,不得附带 Artifact 正文 #### Scenario: 派生 Branch 关系 - **WHEN** 一个 Thread 的 `parentThreadId` 指向同 Project 的另一个 Thread @@ -129,9 +130,14 @@ Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有 #### Scenario: 组合 ThreadColumnView - **WHEN** UI 请求一个 Thread 的列视图 -- **THEN** selector MUST 组合 Thread、有效 Messages、相关 AssistantRunState、Artifact、breadcrumb 和操作可用性 +- **THEN** selector MUST 组合 Thread、有效 Messages、相关 AssistantRunState、Message 中的 Artifact 引用、breadcrumb 和操作可用性 - **AND** 该 ViewModel 不得成为 API 写入对象 +#### Scenario: 打开 Artifact Drawer +- **WHEN** 用户打开 Message tool result 中 `artifactId` 指向的 Artifact +- **THEN** Lifecycle/Command Hook MUST 按 ID 调用 Artifact Query,并将成功结果缓存到当前 Project Store +- **AND** Message 组件不得自行请求或复制 Artifact 正文 + #### Scenario: 组合每列 Header ViewModel - **WHEN** UI 请求 Root 或某个 Branch Slot 的 Header - **THEN** selector MUST 派生标题、Root/Branch、depth、breadcrumbs、直接 Child 数量与列表、Fork 来源可用性以及 switch/collapse 能力 @@ -209,10 +215,12 @@ Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、 - **AND** 不得向 ProjectCatalogStore 增加未确认 Project ### Requirement: 采用轻量 Bootstrap 与按 Thread 加载 -进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Project Artifact Summary、Root Thread 的有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact,但 MUST NOT 包含所有 Branch Message、BaseContext 或全部大型资源正文。 +进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Project Artifact Summary、Root Thread 的有效 Message 和相关 AssistantRunState,但 MUST NOT 包含所有 Branch Message、BaseContext 或 Artifact 正文。 其他 Thread 的 Message MUST 在首次打开时按 Thread 加载。MVP 客户端 MUST 支持一次合并最多 200 条按 sequence 升序排列的有效 Message,并 MUST 保留服务端返回的 `hasOlderMessages` 信息,但不要求实现复杂的树内自动分页替换。 +P0 的 Markdown tool result MUST 只向客户端提供 `artifactId` 引用。只有用户打开 Artifact Drawer 时,客户端才 MUST 通过 Artifact Query 按 ID 加载正文;加载 ProjectBootstrap、ThreadMessageBundle 或 topology 不得提前下载 Markdown 正文。 + 客户端 MUST 使用 ProjectRuntime 级 `ThreadMessageLoader` 管理 `threadId → in-flight Promise/AbortController`。Promise、AbortController 和 in-flight Map MUST NOT 进入 Zustand;Store 只保存每个 Thread 的 `LoadState`、窗口边界和已合并实体。同一 Thread 的并发 ensure MUST 复用同一个 Promise,不同 Thread MUST 可以并行加载。 #### Scenario: 初次进入已有 Project diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md index b3a8e41c..608bae9f 100644 --- a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md +++ b/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md @@ -58,11 +58,11 @@ Project metadata 更新 MUST 允许独立修改 customTitle、Target 和 Instruc - **AND** 不得创建任何 Project 数据 ### Requirement: 提供 ProjectBootstrap Query -服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、ProjectArtifactSummary、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState 和关联 Artifact。 +服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、ProjectArtifactSummary、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState。Message 中可以包含 Artifact ID 引用,但 Bootstrap 不得返回 Artifact 正文。 Bootstrap MUST NOT 返回 BaseContext、所有 Branch Message、全部 Project File/Artifact 正文或服务端 Prompt。Thread topology MUST 足以由客户端派生 Root、Child、depth 和 breadcrumb。 -ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至少返回服务端单调 `changeSequence`、总数和按稳定 `kind` 聚合的数量。它不得退化为 Root MessageBundle 中 `includedArtifacts` 的数量;`total` 必须等于各 kind 计数之和。`changeSequence` 只用于拒绝乱序旧统计,不得成为 Command 请求参数或写入前置条件。 +ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至少返回服务端单调 `changeSequence`、总数和按稳定 `kind` 聚合的数量。它不得退化为客户端已经按 ID 加载的 Artifact 数量;`total` 必须等于各 kind 计数之和。`changeSequence` 只用于拒绝乱序旧统计,不得成为 Command 请求参数或写入前置条件。 #### Scenario: 加载现有 Project - **WHEN** actor 请求可访问 Project 的 Bootstrap @@ -78,13 +78,15 @@ ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至 #### Scenario: 未加载 Branch Artifact 仍计入统计 - **WHEN** Project 的 Branch Thread 中存在 3 个 Markdown Artifact,但 Bootstrap 不返回该 Branch 的 Message - **THEN** `artifactSummary.byKind.markdown` MUST 仍然等于 3 -- **AND** Root bundle 的 `includedArtifacts` MAY 不包含这 3 个 Artifact +- **AND** Root bundle MUST NOT 因此返回这 3 个 Artifact 的正文 ### Requirement: 按 Thread 提供 MessageBundle -服务端 MUST 提供 `GET /api/v1/threads/{threadId}/messages`,并 MUST 通过 Thread 所属 Project 校验访问权。响应 MUST 返回有效 Message、相关 AssistantRunState 和渲染这些 Message 所需的 Artifact;默认最多返回最新 200 条,再按 sequence 升序输出。 +服务端 MUST 提供 `GET /api/v1/threads/{threadId}/messages`,并 MUST 通过 Thread 所属 Project 校验访问权。响应 MUST 返回有效 Message 与相关 AssistantRunState;默认最多返回最新 200 条,再按 sequence 升序输出。 响应 MUST 包含 `hasOlderMessages` 和可供未来向前加载的边界 sequence。MVP 客户端可以不请求更早页面,但 API 不得让调用方通过下载整棵 Project 才能读取一个 Thread。 +P0 中,Markdown tool result MUST 在符合 AI SDK v7 的 Message part 中保存 `artifactId`,不得复制 Markdown 正文。客户端需要展示正文时 MUST 通过独立 Artifact Query 按 ID 加载。 + #### Scenario: Thread 少于 200 条有效 Message - **WHEN** 客户端读取包含 80 条有效 Message 的 Thread - **THEN** 服务端 MUST 返回全部 80 条并设置 `hasOlderMessages=false` @@ -113,6 +115,10 @@ ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至 Path MUST 包含已有 `threadId`;Body MUST 只包含合法 user `parts` 和可选 `requestedModelId`。服务端 MUST 根据当前有效历史构造 Prompt,客户端不得提交 Prompt History、BaseContext、待创建 ID 或整棵 Project 状态。 +P0 MUST NOT 额外提供“只创建 user Message、不创建 assistant Message 与 MessageRun”的发送命令。该限制只是当前 API 能力边界,不得被实现为 user/assistant 必须角色交替的数据库约束。 + +P0 MUST NOT 额外提供“只创建 user Message、不创建 assistant Message 与 MessageRun”的发送命令。该限制只是当前 API 能力边界,不得被实现为 user/assistant 必须角色交替的数据库约束。 + #### Scenario: 在 Root Thread 发送消息 - **WHEN** 客户端请求 `POST /api/v1/threads/{threadId}/messages` 并提交合法 user parts - **THEN** 服务端 MUST 分配新的 sequence 并返回 user Message、assistant Message 和 queued Run @@ -158,7 +164,7 @@ Edit 请求 MUST 只提交 sourceUserMessageId、新 parts 和可选 requestedMo - **AND** 不得修改任何既有 Message ### Requirement: 通过 assistantMessageId 管理生成生命周期 -服务端 MUST 允许客户端使用 assistantMessageId 查询、订阅和停止对应 MessageRun,而不要求客户端理解内部 MessageRun ID。事件流 MUST 使用严格递增的 eventSequence,并 MUST 支持 `afterEventSequence` 恢复。每次连接 MUST 先返回当前持久化 checkpoint snapshot 及其 cursor;若仍在运行,后续 live event MUST 严格大于该 cursor。旧 token delta MAY 不逐条重放,但恢复结果不得丢失已经持久化的生成内容。 +服务端 MUST 允许客户端使用 assistantMessageId 查询、通过 SSE 订阅并停止对应 MessageRun,而不要求客户端理解内部 MessageRun ID。SSE 事件流 MUST 使用严格递增的 eventSequence,并 MUST 支持 `afterEventSequence` 恢复。每次连接 MUST 先返回当前持久化 checkpoint snapshot 及其 cursor;若仍在运行,后续 live event MUST 严格大于该 cursor。旧 token delta MAY 不逐条重放,但恢复结果不得丢失已经持久化的生成内容。 Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得自动停止 Run。终态事件 MUST 携带或允许随后取得 finalized Message 和终态 AssistantRunState。`run.snapshot` 与 `run.completed` MUST 携带当前 ProjectArtifactSummary,使客户端能够在刷新、重连和 Artifact 生成完成后校正 Project 页面统计;客户端不得依赖事件次数自行累加。 @@ -177,6 +183,18 @@ Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得 - **WHEN** 浏览器关闭事件连接但未发送 Stop - **THEN** MessageRun MUST 继续由服务端执行 +### Requirement: 按 ID 加载 Artifact +服务端 MUST 提供 `GET /api/v1/artifacts/{artifactId}`,并 MUST 通过 Artifact 所属 Project 校验访问权。Message、ThreadMessageBundle、ProjectBootstrap 和生成事件 MUST 只通过 `artifactId` 引用 Artifact,不得复制 Markdown 正文。 + +#### Scenario: 打开 Markdown Artifact +- **WHEN** 用户打开 Message tool result 引用的 Markdown Artifact +- **THEN** 客户端 MUST 使用 `artifactId` 请求 Artifact Query +- **AND** 服务端 MUST 返回对应 Artifact 的完整内容 + +#### Scenario: 未打开 Artifact +- **WHEN** 客户端只加载 ProjectBootstrap 或 ThreadMessageBundle,但用户没有打开 Artifact +- **THEN** 服务端 MUST NOT 返回 Artifact 正文 + ### Requirement: 提供 Message feedback 命令 服务端 MUST 允许客户端按 assistantMessageId 设置 positive、negative 或 null feedback。只有 finalized 且允许评价的 assistant Message 可以接受 feedback;命令不得修改 Message 内容或 MessageRun。 diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md new file mode 100644 index 00000000..bedc1f60 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -0,0 +1,113 @@ +## 0. 阶段 0:冻结 API、客户端与 UI 实施边界 + +- [x] 0.1 对本 change 与 `define-thread-chat-domain-model` 执行严格校验,确认 API/客户端只消费既定领域关系,不重新定义 Project、Thread、Message、MessageRun、replacement 或 BaseContext。 +- [x] 0.2 固定职责分界:本 change 负责共享契约、`/api/v1`、SSE、API 测试、客户端状态架构、现有 UI 接入与 E2E;数据库、领域、Repository 和 MessageRun 执行由 domain change 负责。 +- [x] 0.3 固定 P0:发送原子创建 `user + assistant + Run`、Markdown tool output 只保存 `artifactId`、生成 Transport 使用 SSE、通用幂等命令留到 V2。 +- [x] 0.4 固定 UI 硬门槛:允许改变 Project loading、拆分组件和替换数据胶水;最终布局、样式和既有交互行为不得改变。若实现必须产生用户可见差异,立即暂停并向用户确认。 +- [x] 0.5 固定实施顺序:后端领域门 → API 门 → 前端 Store/Runtime/Hooks → UI 无损接入 → 前后端集成 → Ego Browser E2E → 清理归档。 +- [x] 0.6 固定归档顺序:E2E 与旧权威清理完成后先归档 domain change,再归档本 change,避免循环依赖。 + +## 1. 建立共享 V1 契约与 Transport 边界 + +- [ ] 1.1 为 `api-contracts.md` 中全部 ID、DTO、Request、Response、Error 和 SSE Event 建立前后端共享严格 Zod Schema。 +- [ ] 1.2 为 AI SDK v7 `UIMessage.parts` 建立共享校验入口,限制客户端提交合法 user parts,并定义 Markdown tool output 的 `artifactId` 结构。 +- [ ] 1.3 定义不依赖 Zustand、React 或具体 HTTP 库的 `ThreadChatApiCapabilities`,覆盖 Project、Thread、Message、Artifact、feedback、SSE 与 Stop。 +- [ ] 1.4 实现 JSON Transport 的 Session/认证处理、请求编码、响应 Schema 校验和统一 `ClientError` 映射。 +- [ ] 1.5 实现集中式 `threadChatRoutes`;页面 URL 只由客户端构造,服务端 DTO 不包含 `canonicalUrl` 或其他页面路径。 +- [ ] 1.6 建立契约 fixture,覆盖成功 DTO、结构化错误、未知字段拒绝、错误实体归属和非法 AI SDK part。 + +## 2. 实现 Project 与 Thread Query API + +- [ ] 2.1 实现 `GET /api/v1/projects` 的 owner scope、active/archived 过滤、稳定排序、limit 和绑定查询条件的不透明 cursor。 +- [ ] 2.2 实现 `GET /api/v1/projects/{projectId}/bootstrap`,返回 Project、全量轻量 topology、ProjectArtifactSummary 与唯一 Root ThreadMessageBundle。 +- [ ] 2.3 实现 `GET /api/v1/threads/{threadId}/messages`,按有效 Message sequence 查询最新最多 200 条并返回窗口边界。 +- [ ] 2.4 确保 Bootstrap 和 MessageBundle 不返回 BaseContext、Prompt History、未打开 Branch Message 或 Artifact 正文。 +- [ ] 2.5 实现 `GET /api/v1/artifacts/{artifactId}`,从 Artifact 所属 Project 校验 actor,并只在按 ID 请求时返回完整内容。 +- [ ] 2.6 实现 ProjectArtifactSummary 的服务端统计和单调 `changeSequence`,不得从客户端已加载 Artifact 数量反推。 + +## 3. 实现 Project、Thread 与 Message Command API + +- [ ] 3.1 实现 `POST /api/v1/projects`,调用后端 Application 原子创建 Project、Root、U1、A1 与 queued Run,并在响应中返回 CreationBundle。 +- [ ] 3.2 实现 Project metadata、archive/unarchive 和永久删除 API,保持缺省字段与显式 null 的不同语义。 +- [ ] 3.3 实现 Branch Thread title、archive/unarchive API,并拒绝通过 Thread metadata 修改 Root 标题或归档 Root。 +- [ ] 3.4 实现 `POST /api/v1/threads/{threadId}/messages`,原子返回 user Message、assistant Message 与 queued Run;P0 不增加 user-only append API。 +- [ ] 3.5 实现 Fork API,只接受既有 sourceThreadId、sourceMessageId 与 anchor,不接受 Child ID、BaseContext 或 ForkSourceSnapshot 权威字段。 +- [ ] 3.6 实现 Edit 与 Regenerate replacement API,返回 ReplacementBundle,保证旧 finalized Message parts 与 sequence 不变。 +- [ ] 3.7 实现 feedback API,只允许对合格 assistant Message 设置 positive、negative 或 null。 +- [ ] 3.8 为全部 Route 加入 Session actor、owner scope、归档状态、业务资格、严格字段校验和统一错误映射。 + +## 4. 实现 SSE 生成恢复与 Stop + +- [ ] 4.1 实现 `GET /api/v1/assistant-messages/{id}/events` SSE;每次连接首个业务事件固定为持久化 `run.snapshot`。 +- [ ] 4.2 实现 `afterEventSequence` 校验、重复/倒序过滤和 snapshot 后严格递增的 live event。 +- [ ] 4.3 实现 `run.delta`、`run.completed`、`run.failed`、`run.stopped` Schema,并在 snapshot/completed 携带最新 Artifact Summary。 +- [ ] 4.4 确保 finalized Message tool output 只包含 `artifactId`,SSE 不复制 Artifact 正文。 +- [ ] 4.5 实现显式 Stop API;关闭页面、刷新、切换 Thread 或取消 SSE 不得触发 Stop。 +- [ ] 4.6 验证 queued/running Run 刷新后复用同一 assistantMessageId 和 MessageRun,不启动第二次执行。 + +## 5. 后端 API 验收门 + +- [ ] 5.1 在 domain change 的 Vitest 与隔离 PostgreSQL 基础上完成 API 合同测试;自动测试统一使用 Fake AI Runtime,不调用真实模型。 +- [ ] 5.2 覆盖 Project list/bootstrap、Thread Message window、Artifact-by-ID、metadata、archive/delete、feedback 和全部错误码。 +- [ ] 5.3 覆盖 create/send/Fork/Edit/Regenerate/Stop 的权限、严格输入、原子响应与事务回滚;确认服务端不返回页面 URL。 +- [ ] 5.4 覆盖 SSE snapshot、eventSequence、delta、terminal、断开重连、重复连接和显式 Stop。 +- [ ] 5.5 覆盖 Markdown Artifact:Message 只保存 `artifactId`、Bootstrap/MessageBundle/SSE 无正文、Artifact Query 按 ID 返回正文。 +- [ ] 5.6 执行 `pnpm typecheck`、domain unit/integration、API tests、`pnpm build` 与 `pnpm openspec:validate`;全部通过并记录证据后才能开始第 6 节前端工作。 + +## 6. 建立前端测试基础与 Zustand Store + +- [ ] 6.1 增加 Testing Library、user-event 与适配 React Hooks 的 Vitest DOM 环境;不得把 Ego Browser E2E 混入单元测试。 +- [ ] 6.2 使用 vanilla Zustand 建立 Provider-scoped `ThreadChatAppStore`,包含 Project Catalog 与 AppShellUi slices,不保存 selectedProjectId。 +- [ ] 6.3 建立按 projectId 创建的 `ThreadChatProjectStore`,包含 entities、runs、requests、readModels 与 workbench ui slices。 +- [ ] 6.4 实现共享 normalizer,维护 `messagesById + messageIdsByThreadId` 的归属、去重与 sequence 排序。 +- [ ] 6.5 实现 Creation、Bootstrap、Message、replacement、Run Event、Artifact 与 Summary 的语义化 Store Actions。 +- [ ] 6.6 实现稳定 Column Slot、Root/Branch 宽度、折叠、焦点、placement、Canvas pin、overlay 与 Workbench Snapshot Store Actions。 +- [ ] 6.7 确保 Store Action 只同步调用 Zustand `set`/`setState`,不请求 API、不导航、不创建连接,也不原地修改已确认实体。 +- [ ] 6.8 完成 Store/normalizer 测试:归属拒绝、sequence、replacement、重复 DTO、乱序 Summary、局部 Run 更新与 Snapshot 校验。 + +## 7. 实现 Runtime、Application Commands 与 Hooks + +- [ ] 7.1 实现 `ThreadChatAppRuntime`、`ThreadChatProjectRuntime` 与 `ProjectRuntimeRegistry`,保证 Provider 生命周期内 projectId 对应唯一 Runtime。 +- [ ] 7.2 实现 `ThreadChatAppProvider`、`ThreadChatProjectProvider` 与 `NewProjectDraftProvider`,避免 Next.js 服务端模块级 Store 跨请求共享。 +- [ ] 7.3 实现 `/thread-chat/new` seeded Runtime handoff:先合并 CreationBundle,再由客户端 route builder replace,目标 Provider 跳过第二次 Bootstrap。 +- [ ] 7.4 实现 ThreadMessageLoader 的 threadId 级 Promise 去重、跨 Thread 并行与 Runtime destroy 统一 Abort。 +- [ ] 7.5 实现 ArtifactLoader 的 artifactId 级按需缓存,不因 Thread 加载自动请求正文。 +- [ ] 7.6 实现 GenerationCoordinator 的 assistantMessageId 级连接去重、snapshot 合并、断线重连、取消订阅和 destroy。 +- [ ] 7.7 实现 Catalog、Bootstrap、send、Fork、Edit、Regenerate、Stop、feedback 和 metadata Application Commands;客户端不生成服务端实体 ID。 +- [ ] 7.8 实现纯 selectors、Selector Hooks、Command Hooks 与 Lifecycle Hooks;组件不得直接 fetch 或维护第二份领域状态。 +- [ ] 7.9 完成 Runtime、Loader、Coordinator、Command、Selector 与 Hook 的接口注入测试。 + +## 8. 建立 UI Parity 基线并无损接入现有 UI + +- [ ] 8.1 在修改 UI 数据接缝前,使用 Ego Browser 和专用本地测试账号记录现有空白页、单列、多栏、Header、Fork、Artifact Drawer、折叠/切换、breadcrumb 与分割线拖拽的参考截图和交互清单。 +- [ ] 8.2 保持现有组件、CSS 类名、布局与交互输出,先只把 Project 列表、`/new` 和已有 Project 页面接到新的 Provider/Runtime。 +- [ ] 8.3 将 Root/Branch Column 改为消费 ThreadColumnView 和 ThreadColumnHeaderView;允许内部拆分复用,但最终呈现不得变化。 +- [ ] 8.4 接入现有 Header 的 Child 选择、Thread 切换、收起和 breadcrumb,保持稳定物理 Slot 与列宽。 +- [ ] 8.5 保留相邻列分割线拖拽;Pointer Move 使用组件瞬时状态,Pointer Up/键盘/双击复位只提交一次 Store Action。 +- [ ] 8.6 实现按 projectId 的 Workbench Snapshot 防抖保存和刷新恢复;恢复多栏、折叠、焦点和列宽,不恢复滚动条或 Composer 草稿。 +- [ ] 8.7 接入 Artifact Drawer 按 `artifactId` 加载及独立 loading/error;生成期间禁用 Fork,服务端仍作最终校验。 +- [ ] 8.8 逐项对照阶段 8.1 的截图和交互清单;任何必须产生用户可见差异的实现立即停止,记录影响并向用户确认后才能继续。 + +## 9. 前后端集成与 Ego Browser E2E + +- [ ] 9.1 完成 `/new` 无实体草稿、首次发送、seeded Runtime 无空白帧切换和 AI 事件早于目标 Provider 挂载的集成测试。 +- [ ] 9.2 完成已有 Project 冷启动、有/无 Workbench Snapshot、多 Branch 并行加载、单列失败和刷新恢复 running Run 的集成测试。 +- [ ] 9.3 使用 Ego Browser 通过邮箱注册专用本地测试账号;不得依赖真实邮箱验证或真实模型随机输出作为断言。 +- [ ] 9.4 E2E 验证 Project 创建/列表、首条与后续消息、生成中刷新、Stop、Edit、Regenerate、Fork 与嵌套 Fork。 +- [ ] 9.5 E2E 验证多栏异步加载、Header Child 选择、Thread 切换、收起、breadcrumb、分割线拖拽与刷新视图恢复。 +- [ ] 9.6 E2E 验证 Markdown Artifact 创建、消息 `artifactId` 引用、Drawer 按 ID 加载和其他 Project 访问隔离。 +- [ ] 9.7 对比 UI parity 基线,确认除已批准的 Project loading 外,最终样式和交互行为没有变化。 + +## 10. 旧客户端退役与归档 + +- [ ] 10.1 E2E 通过后将 `/thread-chat/new`、`/thread-chat/{projectId}` 和 Project 列表切到新权威路径;开发期间不维护 feature flag 或双写。 +- [ ] 10.2 删除客户端 treeId、新实体 UUID、整树 version 订阅、旧 ThreadTree 写回和 monolithic chat-controller 权威职责;保留仍被新组合根复用的纯 UI 部件。 +- [ ] 10.3 通知 domain change 执行旧后端/Schema 退役,并等待其完成全量测试和先行归档。 +- [ ] 10.4 domain change 归档后,执行 `pnpm typecheck`、全部自动测试、`pnpm build`、Ego Browser E2E 和 OpenSpec 严格校验。 +- [ ] 10.5 对照两个增量 spec 的 Requirement/Scenario 汇总实现证据,在全部门槛满足后归档 `design-thread-chat-client-api`。 + +## 11. 归档后沉淀稳定客户端与 API 文档 + +- [ ] 11.1 从已归档 design 提炼客户端架构到 `openspec/specs/thread-chat-client-state/architecture.md`,保留 Store、Runtime、Provider、Command、Hook 与异步加载边界。 +- [ ] 11.2 将落地后的 API 合同提炼到 `openspec/specs/thread-chat-command-api/api-contracts.md`,并校验它与共享 Schema 和实际 Route Handler 一致。 +- [ ] 11.3 在两个正式 `spec.md` 中链接对应说明文档,明确冲突时可验证 Requirement 优先,并把后续同步更新列为验收项。 From c7da8ba9726b22cf2174ca9d2568dee193b817c0 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 17:56:00 +0800 Subject: [PATCH 07/23] test(thread-chat): establish isolated backend test infrastructure --- .env.example | 2 + drizzle.test.config.ts | 18 + lib/db/schema.ts | 4 + .../application/ports/ai-runtime.ts | 34 + .../define-thread-chat-domain-model/tasks.md | 14 +- package.json | 10 +- pnpm-lock.yaml | 611 +++++++++++++++++- scripts/create-test-database.mjs | 32 + scripts/lib/test-database-safety.mjs | 42 ++ scripts/reset-test-schema.mjs | 76 +++ tests/factories/thread-chat-factories.ts | 196 ++++++ tests/fakes/fake-ai-runtime.ts | 62 ++ .../test-database-isolation.test.ts | 31 + tests/setup/require-test-database.ts | 7 + tests/unit/fake-ai-runtime.test.ts | 72 +++ tests/unit/test-database-safety.test.ts | 26 + tests/unit/thread-chat-factories.test.ts | 41 ++ vitest.api.config.ts | 15 + vitest.config.ts | 11 + vitest.integration.config.ts | 15 + 20 files changed, 1309 insertions(+), 10 deletions(-) create mode 100644 drizzle.test.config.ts create mode 100644 lib/thread-chat/application/ports/ai-runtime.ts create mode 100644 scripts/create-test-database.mjs create mode 100644 scripts/lib/test-database-safety.mjs create mode 100644 scripts/reset-test-schema.mjs create mode 100644 tests/factories/thread-chat-factories.ts create mode 100644 tests/fakes/fake-ai-runtime.ts create mode 100644 tests/integration/test-database-isolation.test.ts create mode 100644 tests/setup/require-test-database.ts create mode 100644 tests/unit/fake-ai-runtime.test.ts create mode 100644 tests/unit/test-database-safety.test.ts create mode 100644 tests/unit/thread-chat-factories.test.ts create mode 100644 vitest.api.config.ts create mode 100644 vitest.config.ts create mode 100644 vitest.integration.config.ts diff --git a/.env.example b/.env.example index bfdfee17..255ce700 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,8 @@ ARK_CODING_BASE_URL=https://ark.cn-beijing.volces.com/api/coding/v3 # - DIRECT_URL:迁移(pnpm db:migrate)用「直连」(端口 5432)——池化连接不适合跑 DDL。 DATABASE_URL=postgres://postgres.xxxx:password@aws-0-region.pooler.supabase.com:6543/postgres DIRECT_URL=postgres://postgres.xxxx:password@aws-0-region.pooler.supabase.com:5432/postgres +# 测试只允许连接物理隔离的 thread-chat-test 数据库;测试 Drizzle 配置不会回退到 DATABASE_URL。 +TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/thread-chat-test # 可选:每实例连接数上限(Serverless + 池化下宜小);直连想启用预处理语句设 DB_PREPARE=true。 # DB_POOL_MAX=10 # DB_PREPARE=false diff --git a/drizzle.test.config.ts b/drizzle.test.config.ts new file mode 100644 index 00000000..7016bf62 --- /dev/null +++ b/drizzle.test.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "drizzle-kit" +import { DB_SCHEMA } from "./lib/db/pg-schema" +import { + assertSafeTestDatabaseUrl, + loadTestDatabaseEnvironment, +} from "./scripts/lib/test-database-safety.mjs" + +loadTestDatabaseEnvironment() + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) + +export default defineConfig({ + schema: "./lib/db/schema.ts", + out: "./drizzle-test", + dialect: "postgresql", + dbCredentials: { url: testDatabaseUrl }, + schemaFilter: [DB_SCHEMA], +}) diff --git a/lib/db/schema.ts b/lib/db/schema.ts index bb6fb6a5..67441474 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -21,6 +21,10 @@ import type { } from "@/lib/thread-chat/domain/generation" import type { MessageFeedback } from "@/lib/thread-chat/domain/types" +// drizzle-kit 必须能从 schema 入口发现自定义 namespace;只导出其中的表会把 +// thread_chat 误判为待删除 schema。 +export { dbSchema } from "./pg-schema" + // 认证与计费表在独立文件中定义,这里统一 re-export,使 drizzle 客户端与迁移能感知它们。 export * from "./auth-schema" export * from "./billing-schema" diff --git a/lib/thread-chat/application/ports/ai-runtime.ts b/lib/thread-chat/application/ports/ai-runtime.ts new file mode 100644 index 00000000..cfc0b3df --- /dev/null +++ b/lib/thread-chat/application/ports/ai-runtime.ts @@ -0,0 +1,34 @@ +import type { UIMessage } from "ai" + +/** Markdown Artifact 由运行器持久化;AI Runtime 只返回待投影的工具结果。 */ +export type MarkdownArtifactRuntimeOutput = { + kind: "markdown" + title: string + content: string +} + +/** AI Runtime 的供应商无关输出;持久化游标由 MessageRun runner 分配。 */ +export type AiRuntimeEvent = + | { type: "delta"; partsDelta: UIMessage["parts"] } + | { type: "artifact"; output: MarkdownArtifactRuntimeOutput } + | { type: "completed"; parts: UIMessage["parts"] } + | { type: "failed"; error: { code: string; message: string } } + | { type: "stopped" } + +export type AiRuntimeRequest = { + messageRunId: string + assistantMessageId: string + modelId: string + prompt: UIMessage[] +} + +/** + * MessageRun 执行器只依赖此 capability。真实供应商与测试 Fake 都必须实现它, + * 浏览器订阅生命周期不得直接控制这里的 AbortSignal。 + */ +export interface AiRuntime { + execute( + request: AiRuntimeRequest, + options?: { signal?: AbortSignal } + ): AsyncIterable +} diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index a7aea5d4..514884c9 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -8,13 +8,13 @@ ## 1. 建立后端测试基础设施 -- [ ] 1.1 增加 Vitest 及 `test`、`test:unit`、`test:integration`、`test:api`、`test:watch` 脚本;默认命令不得调用真实模型供应商。 -- [ ] 1.2 建立物理隔离的 PostgreSQL 测试数据库配置,例如 `thread-chat-test`,并让测试专用 Drizzle 配置只读取 `TEST_DATABASE_URL`。 -- [ ] 1.3 在测试初始化中校验数据库名称或明确 allowlist;目标不是测试数据库时立即终止,禁止误删开发数据库。 -- [ ] 1.4 建立测试 Schema 重建流程:只删除测试库的 `thread_chat` schema,再执行测试配置对应的 `drizzle-kit push`。 -- [ ] 1.5 建立用户、Project、Thread、Message、Artifact 与 MessageRun fixture factory;测试 ID 仍由服务端/fixture factory 生成,不进入生产客户端逻辑。 -- [ ] 1.6 定义可注入的 AI Runtime capability 与 Fake AI Runtime,支持固定 delta、completed、failed、stopped、Markdown Artifact tool output 和恢复事件。 -- [ ] 1.7 约束测试并行:纯单元测试可以并行;共享 PostgreSQL 的集成/API 测试在证明数据隔离前使用单 worker 或独立事务边界。 +- [x] 1.1 增加 Vitest 及 `test`、`test:unit`、`test:integration`、`test:api`、`test:watch` 脚本;默认命令不得调用真实模型供应商。 +- [x] 1.2 建立物理隔离的 PostgreSQL 测试数据库配置,例如 `thread-chat-test`,并让测试专用 Drizzle 配置只读取 `TEST_DATABASE_URL`。 +- [x] 1.3 在测试初始化中校验数据库名称或明确 allowlist;目标不是测试数据库时立即终止,禁止误删开发数据库。 +- [x] 1.4 建立测试 Schema 重建流程:只删除测试库的 `thread_chat` schema,再执行测试配置对应的 `drizzle-kit push`。 +- [x] 1.5 建立用户、Project、Thread、Message、Artifact 与 MessageRun fixture factory;测试 ID 仍由服务端/fixture factory 生成,不进入生产客户端逻辑。 +- [x] 1.6 定义可注入的 AI Runtime capability 与 Fake AI Runtime,支持固定 delta、completed、failed、stopped、Markdown Artifact tool output 和恢复事件。 +- [x] 1.7 约束测试并行:纯单元测试可以并行;共享 PostgreSQL 的集成/API 测试在证明数据隔离前使用单 worker 或独立事务边界。 ## 2. 建立规范化数据库 Schema diff --git a/package.json b/package.json index dd4472f5..d59886c2 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,19 @@ "start": "next start", "lint": "eslint", "format": "prettier --write \"**/*.{ts,tsx}\"", + "test": "pnpm test:unit && pnpm test:integration && pnpm test:api", + "test:unit": "vitest run --config vitest.config.ts", + "test:integration": "pnpm test:db:reset && vitest run --config vitest.integration.config.ts", + "test:api": "pnpm test:db:reset && vitest run --config vitest.api.config.ts", + "test:watch": "vitest --config vitest.config.ts", "typecheck": "tsc --noEmit", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:reset-schema": "node scripts/reset-thread-chat-schema.mjs", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", + "test:db:create": "node scripts/create-test-database.mjs", + "test:db:reset": "node scripts/reset-test-schema.mjs", "openspec:validate": "openspec validate --all --strict" }, "dependencies": { @@ -93,7 +100,8 @@ "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.8.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 588ffe0c..ddb38c98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 1.1.3 better-auth: specifier: ^1.6.23 - version: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -231,6 +231,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) packages: @@ -1818,6 +1821,9 @@ packages: resolution: {integrity: sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==} engines: {node: '>=14'} + '@oxc-project/types@0.146.0': + resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@posthog/core@1.39.6': resolution: {integrity: sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw==} @@ -2528,6 +2534,105 @@ packages: react-redux: optional: true + '@rolldown/binding-android-arm-eabi@1.2.5': + resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.5': + resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.5': + resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.5': + resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.5': + resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.5': + resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.5': + resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.5': + resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.5': + resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.5': + resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.5': + resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.5': + resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.5': + resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.5': + resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2718,6 +2823,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -2760,6 +2868,9 @@ packages: '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2993,6 +3104,35 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} @@ -3119,6 +3259,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + assistant-cloud@0.1.34: resolution: {integrity: sha512-kmB9qJmwf1Kb3FoLvVnDV7lsT2vRwaiNX9iDoEs+3Gli8aOQ667DPUIXvEXPyV5zF4gfT0E7GFNEcYWRQTElAA==} @@ -3315,6 +3459,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3813,6 +3961,9 @@ packages: resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -3986,6 +4137,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -4013,6 +4167,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} @@ -4632,30 +4790,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -4663,6 +4851,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -4670,6 +4865,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -4677,6 +4879,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -4684,22 +4893,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5038,6 +5270,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -5146,6 +5382,9 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5189,6 +5428,10 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + postgres@3.4.9: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} @@ -5495,6 +5738,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.2.5: + resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} @@ -5610,6 +5858,9 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5643,6 +5894,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} @@ -5650,6 +5904,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -5778,10 +6035,21 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6002,6 +6270,90 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6028,6 +6380,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7746,6 +8103,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.42.0': {} + '@oxc-project/types@0.146.0': {} + '@posthog/core@1.39.6': dependencies: '@posthog/types': 1.392.1 @@ -8510,6 +8869,53 @@ snapshots: react: 19.2.8 react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) + '@rolldown/binding-android-arm-eabi@1.2.5': + optional: true + + '@rolldown/binding-android-arm64@1.2.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.5': + optional: true + + '@rolldown/binding-darwin-x64@1.2.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.5': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -8689,6 +9095,11 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -8732,6 +9143,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.9 @@ -8939,6 +9352,47 @@ snapshots: '@vercel/oidc@3.2.0': {} + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + '@workflow/serde@4.1.0': {} '@xyflow/react@12.11.2(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(immer@11.1.9)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': @@ -9099,6 +9553,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + assistant-cloud@0.1.34: dependencies: assistant-stream: 0.3.25 @@ -9149,7 +9605,7 @@ snapshots: elkjs: 0.11.1 entities: 7.0.1 - better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))): dependencies: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.3)(nanostores@1.4.0) '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.3)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9)) @@ -9174,6 +9630,7 @@ snapshots: next: 16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -9255,6 +9712,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9680,6 +10139,8 @@ snapshots: iterator.prototype: 1.1.5 math-intrinsics: 1.1.0 + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -10004,6 +10465,10 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} etag@1.8.1: {} @@ -10043,6 +10508,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.4.0: {} + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 @@ -10656,36 +11123,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -10702,6 +11202,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + lines-and-columns@1.2.4: {} locate-path@3.0.0: @@ -11232,6 +11748,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.2 + obug@2.1.4: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -11357,6 +11875,8 @@ snapshots: path-to-regexp@8.4.2: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -11392,6 +11912,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres@3.4.9: {} posthog-node@5.39.4: @@ -11729,6 +12255,27 @@ snapshots: reusify@1.1.0: {} + rolldown@1.2.5: + dependencies: + '@oxc-project/types': 0.146.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.5 + '@rolldown/binding-android-arm64': 1.2.5 + '@rolldown/binding-darwin-arm64': 1.2.5 + '@rolldown/binding-darwin-x64': 1.2.5 + '@rolldown/binding-freebsd-x64': 1.2.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 + '@rolldown/binding-linux-arm64-gnu': 1.2.5 + '@rolldown/binding-linux-arm64-musl': 1.2.5 + '@rolldown/binding-linux-ppc64-gnu': 1.2.5 + '@rolldown/binding-linux-s390x-gnu': 1.2.5 + '@rolldown/binding-linux-x64-gnu': 1.2.5 + '@rolldown/binding-linux-x64-musl': 1.2.5 + '@rolldown/binding-openharmony-arm64': 1.2.5 + '@rolldown/binding-win32-arm64-msvc': 1.2.5 + '@rolldown/binding-win32-x64-msvc': 1.2.5 + rou3@0.7.12: {} router@2.2.0: @@ -11948,6 +12495,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -11972,6 +12521,8 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + standardwebhooks@1.0.0: dependencies: '@stablelib/base64': 1.0.1 @@ -11979,6 +12530,8 @@ snapshots: statuses@2.0.2: {} + std-env@4.2.0: {} + stdin-discarder@0.2.2: {} stop-iteration-iterator@1.1.0: @@ -12117,11 +12670,17 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyrainbow@3.1.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -12388,6 +12947,49 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.26 + rolldown: 1.2.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.43 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.0 + yaml: 2.9.0 + + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 20.19.43 + transitivePeerDependencies: + - msw + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -12437,6 +13039,11 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: diff --git a/scripts/create-test-database.mjs b/scripts/create-test-database.mjs new file mode 100644 index 00000000..35f9e257 --- /dev/null +++ b/scripts/create-test-database.mjs @@ -0,0 +1,32 @@ +import postgres from "postgres" +import { + assertSafeTestDatabaseUrl, + loadTestDatabaseEnvironment, + TEST_DATABASE_NAME, +} from "./lib/test-database-safety.mjs" + +loadTestDatabaseEnvironment() +const testDatabaseUrl = new URL( + assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +) +const adminUrl = new URL(testDatabaseUrl) +adminUrl.pathname = "/postgres" + +const sql = postgres(adminUrl.toString(), { max: 1 }) + +try { + const existing = await sql` + select 1 + from pg_database + where datname = ${TEST_DATABASE_NAME} + ` + + if (existing.length === 0) { + await sql.unsafe(`CREATE DATABASE "${TEST_DATABASE_NAME}"`) + console.log(`[test:db:create] 已创建 ${TEST_DATABASE_NAME}。`) + } else { + console.log(`[test:db:create] ${TEST_DATABASE_NAME} 已存在。`) + } +} finally { + await sql.end() +} diff --git a/scripts/lib/test-database-safety.mjs b/scripts/lib/test-database-safety.mjs new file mode 100644 index 00000000..c077d554 --- /dev/null +++ b/scripts/lib/test-database-safety.mjs @@ -0,0 +1,42 @@ +import { config } from "dotenv" + +export const TEST_DATABASE_NAME = "thread-chat-test" + +/** 只加载测试连接变量;数据库配置禁止回退到开发库连接。 */ +export function loadTestDatabaseEnvironment() { + config({ path: [".env.test.local", ".env.local"], quiet: true }) +} + +/** + * 返回经过 allowlist 校验的测试数据库 URL。任何其他数据库名都会立即终止测试操作。 + * + * @param {string | undefined} rawUrl + */ +export function assertSafeTestDatabaseUrl(rawUrl) { + if (!rawUrl) { + throw new Error( + "未配置 TEST_DATABASE_URL;测试数据库操作已停止,且不会回退到 DATABASE_URL。" + ) + } + + const normalized = rawUrl.trim().replace(/^(['"])(.*)\1$/, "$2") + let url + try { + url = new URL(normalized) + } catch { + throw new Error("TEST_DATABASE_URL 不是合法的 PostgreSQL URL。") + } + + if (!url.protocol.startsWith("postgres")) { + throw new Error("TEST_DATABASE_URL 必须使用 postgres 协议。") + } + + const databaseName = decodeURIComponent(url.pathname.slice(1)) + if (databaseName !== TEST_DATABASE_NAME) { + throw new Error( + `拒绝操作数据库 ${JSON.stringify(databaseName)};测试 allowlist 仅包含 ${TEST_DATABASE_NAME}。` + ) + } + + return url.toString() +} diff --git a/scripts/reset-test-schema.mjs b/scripts/reset-test-schema.mjs new file mode 100644 index 00000000..88bd6b2f --- /dev/null +++ b/scripts/reset-test-schema.mjs @@ -0,0 +1,76 @@ +import { spawnSync } from "node:child_process" +import postgres from "postgres" +import { + assertSafeTestDatabaseUrl, + loadTestDatabaseEnvironment, +} from "./lib/test-database-safety.mjs" + +const TEST_SCHEMA = "thread_chat" + +loadTestDatabaseEnvironment() +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 1 }) + +try { + // vector 是当前 Schema 的数据库级扩展;只确保存在,不删除或修改其他 schema。 + await sql`CREATE EXTENSION IF NOT EXISTS vector` + await sql`DROP SCHEMA IF EXISTS ${sql(TEST_SCHEMA)} CASCADE` + // drizzle-kit 在 schemaFilter 指向不存在的 schema 时会在 introspection 阶段失败; + // 先建立空 namespace,再由 push 从零创建其中的全部表与约束。 + await sql`CREATE SCHEMA ${sql(TEST_SCHEMA)}` + console.log(`[test:db:reset] 已将测试库中的 ${TEST_SCHEMA} 重置为空 schema。`) +} finally { + await sql.end() +} + +const push = spawnSync( + "pnpm", + [ + "exec", + "drizzle-kit", + "push", + "--config", + "drizzle.test.config.ts", + "--force", + ], + { + env: { ...process.env, TEST_DATABASE_URL: testDatabaseUrl }, + encoding: "utf8", + } +) + +process.stdout.write(push.stdout ?? "") +process.stderr.write(push.stderr ?? "") +if (push.error) throw push.error +if (push.status !== 0) process.exit(push.status ?? 1) +if ( + /PostgresError|\bError:/.test(`${push.stdout ?? ""}\n${push.stderr ?? ""}`) +) { + throw new Error("drizzle-kit push 输出了数据库错误。") +} + +const verificationSql = postgres(testDatabaseUrl, { max: 1 }) +try { + const [schema] = await verificationSql` + select + exists( + select 1 + from information_schema.schemata + where schema_name = ${TEST_SCHEMA} + ) as exists, + ( + select count(*)::integer + from information_schema.tables + where table_schema = ${TEST_SCHEMA} + ) as table_count + ` + if (!schema?.exists || schema.table_count === 0) { + throw new Error( + `drizzle-kit push 返回成功,但 ${TEST_SCHEMA} schema 未完整建立。` + ) + } +} finally { + await verificationSql.end() +} + +console.log("[test:db:reset] 测试 Schema 已从空状态重建。") diff --git a/tests/factories/thread-chat-factories.ts b/tests/factories/thread-chat-factories.ts new file mode 100644 index 00000000..33a9a0f8 --- /dev/null +++ b/tests/factories/thread-chat-factories.ts @@ -0,0 +1,196 @@ +import { randomUUID } from "node:crypto" +import type { UIMessage } from "ai" + +type JsonValue = + string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } + +const FIXTURE_TIME = new Date("2026-01-01T00:00:00.000Z") + +export type UserFixture = { + id: string + name: string + email: string + emailVerified: boolean + createdAt: Date + updatedAt: Date +} + +export type ProjectFixture = { + id: string + ownerUserId: string + autoTitle: string | null + customTitle: string | null + target: JsonValue | null + instruction: JsonValue | null + archivedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export type ThreadFixture = { + id: string + projectId: string + parentThreadId: string | null + sourceMessageId: string | null + forkSourceSnapshot: JsonValue | null + baseContext: { schemaVersion: 1; messageIds: string[] } | null + autoTitle: string | null + customTitle: string | null + archivedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export type MessageFixture = { + id: string + threadId: string + sequence: number + role: "user" | "assistant" + parts: UIMessage["parts"] | null + replacesMessageId: string | null + supersededAt: Date | null + finalizedAt: Date | null + createdAt: Date +} + +export type ArtifactFixture = { + id: string + projectId: string + sourceMessageId: string + kind: string + title: string + content: JsonValue + createdAt: Date +} + +export type MessageRunFixture = { + id: string + assistantMessageId: string + status: "queued" | "running" | "completed" | "failed" | "stopped" + modelId: string + eventSequence: number + checkpointParts: UIMessage["parts"] + errorCode: string | null + errorMessage: string | null + heartbeatAt: Date | null + stopRequestedAt: Date | null + finishedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export function createUserFixture( + overrides: Partial = {} +): UserFixture { + const id = overrides.id ?? randomUUID() + return { + id, + name: "测试用户", + email: `${id}@thread-chat.test`, + emailVerified: true, + createdAt: FIXTURE_TIME, + updatedAt: FIXTURE_TIME, + ...overrides, + } +} + +export function createProjectFixture( + overrides: Partial = {} +): ProjectFixture { + return { + id: randomUUID(), + ownerUserId: randomUUID(), + autoTitle: "测试 Project", + customTitle: null, + target: null, + instruction: null, + archivedAt: null, + createdAt: FIXTURE_TIME, + updatedAt: FIXTURE_TIME, + ...overrides, + } +} + +export function createThreadFixture( + overrides: Partial = {} +): ThreadFixture { + const isBranch = overrides.parentThreadId != null + return { + id: randomUUID(), + projectId: randomUUID(), + parentThreadId: null, + sourceMessageId: null, + forkSourceSnapshot: null, + baseContext: null, + autoTitle: null, + customTitle: null, + archivedAt: null, + createdAt: FIXTURE_TIME, + updatedAt: FIXTURE_TIME, + ...(isBranch + ? { + sourceMessageId: randomUUID(), + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "assistant", + sourceSequence: 2, + }, + baseContext: { schemaVersion: 1, messageIds: [] }, + } + : {}), + ...overrides, + } +} + +export function createMessageFixture( + overrides: Partial = {} +): MessageFixture { + return { + id: randomUUID(), + threadId: randomUUID(), + sequence: 1, + role: "user", + parts: [{ type: "text", text: "测试消息" }], + replacesMessageId: null, + supersededAt: null, + finalizedAt: FIXTURE_TIME, + createdAt: FIXTURE_TIME, + ...overrides, + } +} + +export function createArtifactFixture( + overrides: Partial = {} +): ArtifactFixture { + return { + id: randomUUID(), + projectId: randomUUID(), + sourceMessageId: randomUUID(), + kind: "markdown", + title: "测试 Artifact", + content: { markdown: "# 测试" }, + createdAt: FIXTURE_TIME, + ...overrides, + } +} + +export function createMessageRunFixture( + overrides: Partial = {} +): MessageRunFixture { + return { + id: randomUUID(), + assistantMessageId: randomUUID(), + status: "queued", + modelId: "fake/test-model", + eventSequence: 0, + checkpointParts: [], + errorCode: null, + errorMessage: null, + heartbeatAt: null, + stopRequestedAt: null, + finishedAt: null, + createdAt: FIXTURE_TIME, + updatedAt: FIXTURE_TIME, + ...overrides, + } +} diff --git a/tests/fakes/fake-ai-runtime.ts b/tests/fakes/fake-ai-runtime.ts new file mode 100644 index 00000000..767900a4 --- /dev/null +++ b/tests/fakes/fake-ai-runtime.ts @@ -0,0 +1,62 @@ +import type { + AiRuntime, + AiRuntimeEvent, + AiRuntimeRequest, +} from "@/lib/thread-chat/application/ports/ai-runtime" + +export type FakeAiRuntimeScenario = { + events: AiRuntimeEvent[] +} + +export type FakeAiRuntimeRecoveryEvent = { + eventSequence: number + event: AiRuntimeEvent +} + +/** 可控且不访问网络的 AI Runtime;测试脚本决定全部 delta、工具与终态。 */ +export class FakeAiRuntime implements AiRuntime { + readonly invocations: AiRuntimeRequest[] = [] + private readonly scenarios = new Map() + + setScenario(messageRunId: string, scenario: FakeAiRuntimeScenario): void { + this.scenarios.set(messageRunId, structuredClone(scenario)) + } + + async *execute( + request: AiRuntimeRequest, + options: { signal?: AbortSignal } = {} + ): AsyncIterable { + this.invocations.push(structuredClone(request)) + const scenario = this.scenarios.get(request.messageRunId) + if (!scenario) { + yield { + type: "failed", + error: { + code: "fake_scenario_missing", + message: `未配置 MessageRun ${request.messageRunId} 的 Fake 场景。`, + }, + } + return + } + + for (const event of scenario.events) { + if (options.signal?.aborted) { + yield { type: "stopped" } + return + } + yield structuredClone(event) + } + } + + /** 为恢复流测试生成稳定游标,并只返回指定游标之后的事件。 */ + recoveryEventsAfter( + messageRunId: string, + afterEventSequence: number + ): FakeAiRuntimeRecoveryEvent[] { + const events = this.scenarios.get(messageRunId)?.events ?? [] + return events + .map((event, index) => ({ eventSequence: index + 1, event })) + .filter((entry) => entry.eventSequence > afterEventSequence) + .map((entry) => structuredClone(entry)) + } +} diff --git a/tests/integration/test-database-isolation.test.ts b/tests/integration/test-database-isolation.test.ts new file mode 100644 index 00000000..4f636fa6 --- /dev/null +++ b/tests/integration/test-database-isolation.test.ts @@ -0,0 +1,31 @@ +import { afterAll, describe, expect, it } from "vitest" +import postgres from "postgres" +import { + assertSafeTestDatabaseUrl, + TEST_DATABASE_NAME, +} from "../../scripts/lib/test-database-safety.mjs" + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 1 }) + +afterAll(async () => { + await sql.end() +}) + +describe("PostgreSQL 测试库隔离", () => { + it("实际连接 thread-chat-test,且 thread_chat schema 已由 db:push 建立", async () => { + const [database] = await sql<{ name: string }[]>` + select current_database() as name + ` + const [schema] = await sql<{ exists: boolean }[]>` + select exists( + select 1 + from information_schema.schemata + where schema_name = 'thread_chat' + ) as exists + ` + + expect(database.name).toBe(TEST_DATABASE_NAME) + expect(schema.exists).toBe(true) + }) +}) diff --git a/tests/setup/require-test-database.ts b/tests/setup/require-test-database.ts new file mode 100644 index 00000000..3016b8db --- /dev/null +++ b/tests/setup/require-test-database.ts @@ -0,0 +1,7 @@ +import { + assertSafeTestDatabaseUrl, + loadTestDatabaseEnvironment, +} from "../../scripts/lib/test-database-safety.mjs" + +loadTestDatabaseEnvironment() +assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) diff --git a/tests/unit/fake-ai-runtime.test.ts b/tests/unit/fake-ai-runtime.test.ts new file mode 100644 index 00000000..54669cc1 --- /dev/null +++ b/tests/unit/fake-ai-runtime.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest" +import { FakeAiRuntime } from "../fakes/fake-ai-runtime" + +const request = { + messageRunId: "run-1", + assistantMessageId: "assistant-1", + modelId: "fake/test-model", + prompt: [], +} + +async function collect(source: AsyncIterable): Promise { + const values: T[] = [] + for await (const value of source) values.push(value) + return values +} + +describe("FakeAiRuntime", () => { + it("按脚本输出 delta、Artifact 与 completed", async () => { + const runtime = new FakeAiRuntime() + runtime.setScenario(request.messageRunId, { + events: [ + { + type: "delta", + partsDelta: [{ type: "text", text: "第一段" }], + }, + { + type: "artifact", + output: { kind: "markdown", title: "文档", content: "# 文档" }, + }, + { + type: "completed", + parts: [{ type: "text", text: "完成" }], + }, + ], + }) + + const events = await collect(runtime.execute(request)) + + expect(events.map((event) => event.type)).toEqual([ + "delta", + "artifact", + "completed", + ]) + expect(runtime.invocations).toEqual([request]) + }) + + it("支持 failed、stopped 和按 eventSequence 恢复事件", async () => { + const runtime = new FakeAiRuntime() + runtime.setScenario(request.messageRunId, { + events: [ + { type: "delta", partsDelta: [] }, + { type: "failed", error: { code: "provider_error", message: "失败" } }, + ], + }) + + expect(runtime.recoveryEventsAfter(request.messageRunId, 1)).toEqual([ + { + eventSequence: 2, + event: { + type: "failed", + error: { code: "provider_error", message: "失败" }, + }, + }, + ]) + + const controller = new AbortController() + controller.abort() + expect( + await collect(runtime.execute(request, { signal: controller.signal })) + ).toEqual([{ type: "stopped" }]) + }) +}) diff --git a/tests/unit/test-database-safety.test.ts b/tests/unit/test-database-safety.test.ts new file mode 100644 index 00000000..b421fabe --- /dev/null +++ b/tests/unit/test-database-safety.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest" +import { + assertSafeTestDatabaseUrl, + TEST_DATABASE_NAME, +} from "../../scripts/lib/test-database-safety.mjs" + +describe("测试数据库安全检查", () => { + it("只接受 allowlist 中的物理测试数据库", () => { + const result = assertSafeTestDatabaseUrl( + `postgres://postgres:postgres@localhost:5432/${TEST_DATABASE_NAME}` + ) + + expect(new URL(result).pathname).toBe(`/${TEST_DATABASE_NAME}`) + }) + + it("拒绝开发数据库并且不回退", () => { + expect(() => + assertSafeTestDatabaseUrl( + "postgres://postgres:postgres@localhost:5432/thread-chat" + ) + ).toThrow(/拒绝操作数据库/) + expect(() => assertSafeTestDatabaseUrl(undefined)).toThrow( + /不会回退到 DATABASE_URL/ + ) + }) +}) diff --git a/tests/unit/thread-chat-factories.test.ts b/tests/unit/thread-chat-factories.test.ts new file mode 100644 index 00000000..5aa0db1c --- /dev/null +++ b/tests/unit/thread-chat-factories.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest" +import { + createArtifactFixture, + createMessageFixture, + createMessageRunFixture, + createProjectFixture, + createThreadFixture, + createUserFixture, +} from "../factories/thread-chat-factories" + +describe("ThreadChat fixture factories", () => { + it("由 factory 生成服务端侧测试 ID 并允许显式关联", () => { + const user = createUserFixture() + const project = createProjectFixture({ ownerUserId: user.id }) + const root = createThreadFixture({ projectId: project.id }) + const message = createMessageFixture({ + threadId: root.id, + role: "assistant", + }) + const artifact = createArtifactFixture({ + projectId: project.id, + sourceMessageId: message.id, + }) + const run = createMessageRunFixture({ assistantMessageId: message.id }) + + expect( + new Set([user.id, project.id, root.id, message.id, artifact.id]).size + ).toBe(5) + expect(project.ownerUserId).toBe(user.id) + expect(artifact.sourceMessageId).toBe(message.id) + expect(run.assistantMessageId).toBe(message.id) + }) + + it("Branch fixture 自动补齐 ForkFacts", () => { + const branch = createThreadFixture({ parentThreadId: "parent-thread" }) + + expect(branch.sourceMessageId).not.toBeNull() + expect(branch.forkSourceSnapshot).not.toBeNull() + expect(branch.baseContext).toEqual({ schemaVersion: 1, messageIds: [] }) + }) +}) diff --git a/vitest.api.config.ts b/vitest.api.config.ts new file mode 100644 index 00000000..414d3364 --- /dev/null +++ b/vitest.api.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + test: { + environment: "node", + include: ["tests/api/**/*.test.ts"], + setupFiles: ["./tests/setup/require-test-database.ts"], + fileParallelism: false, + maxWorkers: 1, + passWithNoTests: true, + clearMocks: true, + restoreMocks: true, + }, +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..e52170d9 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + test: { + environment: "node", + include: ["tests/unit/**/*.test.ts"], + clearMocks: true, + restoreMocks: true, + }, +}) diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 00000000..549daae0 --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + test: { + environment: "node", + include: ["tests/integration/**/*.test.ts"], + setupFiles: ["./tests/setup/require-test-database.ts"], + fileParallelism: false, + maxWorkers: 1, + passWithNoTests: true, + clearMocks: true, + restoreMocks: true, + }, +}) From 8554ef21666c935d8be5c41941dba231a4051159 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:01:24 +0800 Subject: [PATCH 08/23] feat(thread-chat): add normalized domain schema --- lib/db/schema.ts | 258 ++++++++++++++ .../define-thread-chat-domain-model/tasks.md | 12 +- tests/integration/normalized-schema.test.ts | 328 ++++++++++++++++++ 3 files changed, 592 insertions(+), 6 deletions(-) create mode 100644 tests/integration/normalized-schema.test.ts diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 67441474..615ec982 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -8,8 +8,13 @@ import { boolean, vector, primaryKey, + bigint, + check, + uuid, + type AnyPgColumn, } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" +import type { UIMessage } from "ai" import { dbSchema } from "./pg-schema" import { EMBEDDING_DIMENSIONS } from "@/constants/rag" import { user } from "./auth-schema" @@ -175,6 +180,259 @@ export const branchMessageFeedback = dbSchema.table( ] ) +/** Project 是一整簇 Root/Branch Thread 的 owner scope 与永久删除边界。 */ +export const projects = dbSchema.table( + "projects", + { + id: uuid("id").primaryKey().defaultRandom(), + ownerUserId: text("owner_user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + autoTitle: text("auto_title"), + customTitle: text("custom_title"), + target: jsonb("target").$type<{ + ultimate: string | null + shortTerm: string[] + midTerm: string[] + }>(), + instruction: text("instruction"), + archivedAt: timestamp("archived_at", { withTimezone: true }), + artifactChangeSequence: bigint("artifact_change_sequence", { + mode: "number", + }) + .notNull() + .default(0), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + index("projects_owner_updated_idx").on( + table.ownerUserId, + table.updatedAt.desc(), + table.id.desc() + ), + check( + "projects_artifact_change_sequence_nonnegative_ck", + sql`${table.artifactChangeSequence} >= 0` + ), + ] +) + +/** Root/Branch 角色由 parent_thread_id 是否为空推导,不建立第二套实体类型。 */ +export const threads = dbSchema.table( + "threads", + { + id: uuid("id").primaryKey().defaultRandom(), + projectId: uuid("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + parentThreadId: uuid("parent_thread_id").references( + (): AnyPgColumn => threads.id, + { onDelete: "cascade" } + ), + // 同 Project、Parent/source 归属与无环关系无法由普通 CHECK 可靠表达, + // 必须由后续 Repository 在事务内锁定并校验。 + sourceMessageId: uuid("source_message_id").references( + (): AnyPgColumn => messages.id + ), + forkSourceSnapshot: jsonb("fork_source_snapshot").$type<{ + schemaVersion: 1 + quote?: string + sourceRole: "user" | "assistant" + sourceSequence: number + }>(), + baseContext: jsonb("base_context").$type<{ + schemaVersion: 1 + messageIds: string[] + }>(), + autoTitle: text("auto_title"), + customTitle: text("custom_title"), + archivedAt: timestamp("archived_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex("threads_one_root_per_project_uq") + .on(table.projectId) + .where(sql`${table.parentThreadId} is null`), + index("threads_project_parent_idx").on( + table.projectId, + table.parentThreadId + ), + index("threads_source_message_idx").on(table.sourceMessageId), + check( + "threads_fork_facts_complete_ck", + sql`( + ${table.parentThreadId} is null + and ${table.sourceMessageId} is null + and ${table.forkSourceSnapshot} is null + and ${table.baseContext} is null + ) or ( + ${table.parentThreadId} is not null + and ${table.sourceMessageId} is not null + and ${table.forkSourceSnapshot} is not null + and ${table.baseContext} is not null + )` + ), + ] +) + +/** Thread 内的稳定线性时间线;finalized 内容只能通过 replacement 演进。 */ +export const messages = dbSchema.table( + "messages", + { + id: uuid("id").primaryKey().defaultRandom(), + threadId: uuid("thread_id") + .notNull() + .references(() => threads.id, { onDelete: "cascade" }), + sequence: bigint("sequence", { mode: "number" }).notNull(), + role: text("role").$type<"user" | "assistant">().notNull(), + parts: jsonb("parts").$type(), + // 同 Thread、同角色与来源状态属于跨行事务校验;数据库只保证来源存在且 + // 每条旧 Message 最多有一个直接 replacement。 + replacesMessageId: uuid("replaces_message_id").references( + (): AnyPgColumn => messages.id + ), + supersededAt: timestamp("superseded_at", { withTimezone: true }), + finalizedAt: timestamp("finalized_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex("messages_thread_sequence_uq").on( + table.threadId, + table.sequence + ), + uniqueIndex("messages_single_replacement_uq") + .on(table.replacesMessageId) + .where(sql`${table.replacesMessageId} is not null`), + index("messages_thread_active_sequence_idx") + .on(table.threadId, table.sequence) + .where(sql`${table.supersededAt} is null`), + check("messages_sequence_positive_ck", sql`${table.sequence} > 0`), + check("messages_role_ck", sql`${table.role} in ('user', 'assistant')`), + check( + "messages_not_self_replacement_ck", + sql`${table.replacesMessageId} is null or ${table.replacesMessageId} <> ${table.id}` + ), + ] +) + +/** 每条 assistant Message 恰有一条运行记录;user Message 的排除由 Repository 校验。 */ +export const messageRuns = dbSchema.table( + "message_runs", + { + id: uuid("id").primaryKey().defaultRandom(), + assistantMessageId: uuid("assistant_message_id") + .notNull() + .unique() + .references(() => messages.id, { onDelete: "cascade" }), + status: text("status") + .$type<"queued" | "running" | "completed" | "failed" | "stopped">() + .notNull() + .default("queued"), + modelId: text("model_id").notNull(), + eventSequence: bigint("event_sequence", { mode: "number" }) + .notNull() + .default(0), + checkpointParts: jsonb("checkpoint_parts") + .$type() + .notNull() + .default(sql`'[]'::jsonb`), + errorCode: text("error_code"), + errorMessage: text("error_message"), + heartbeatAt: timestamp("heartbeat_at", { withTimezone: true }), + stopRequestedAt: timestamp("stop_requested_at", { withTimezone: true }), + finishedAt: timestamp("finished_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + index("message_runs_status_heartbeat_idx").on( + table.status, + table.heartbeatAt + ), + check( + "message_runs_status_ck", + sql`${table.status} in ('queued', 'running', 'completed', 'failed', 'stopped')` + ), + check( + "message_runs_event_sequence_nonnegative_ck", + sql`${table.eventSequence} >= 0` + ), + ] +) + +/** Artifact 内容独立持久化,Project 决定 owner scope,Message 保留 provenance。 */ +export const artifacts = dbSchema.table( + "artifacts", + { + id: uuid("id").primaryKey().defaultRandom(), + projectId: uuid("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + // source Message 与 Artifact 的 Project 归属由 Repository 在同一事务复验。 + sourceMessageId: uuid("source_message_id") + .notNull() + .references(() => messages.id), + changeSequence: bigint("change_sequence", { mode: "number" }).notNull(), + kind: text("kind").notNull(), + title: text("title").notNull(), + content: jsonb("content").$type().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex("artifacts_project_change_sequence_uq").on( + table.projectId, + table.changeSequence + ), + index("artifacts_project_kind_idx").on(table.projectId, table.kind), + index("artifacts_source_message_idx").on(table.sourceMessageId), + check( + "artifacts_change_sequence_positive_ck", + sql`${table.changeSequence} > 0` + ), + ] +) + +/** assistant Message 的当前互斥反馈;null 由删除该行表达。 */ +export const messageFeedback = dbSchema.table( + "message_feedback", + { + assistantMessageId: uuid("assistant_message_id") + .primaryKey() + .references(() => messages.id, { onDelete: "cascade" }), + feedback: text("feedback").$type<"positive" | "negative">().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + check( + "message_feedback_value_ck", + sql`${table.feedback} in ('positive', 'negative')` + ), + ] +) + // RAG 向量索引:超大文档改走检索而非全文注入时,存分块及其 embedding。 export const attachmentChunks = dbSchema.table( "attachment_chunks", diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index 514884c9..1b8294f3 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -18,12 +18,12 @@ ## 2. 建立规范化数据库 Schema -- [ ] 2.1 在 `lib/db/schema.ts` 中新增 `projects`、`threads`、`messages`、`message_runs`、`artifacts` 与 Message feedback 表;Project Memory/File 只保留后续扩展边界,不实现完整协议。 -- [ ] 2.2 为 Project owner、唯一 Root Thread、Thread 内唯一 sequence、同 Message 单一 replacement 和 assistant Message 单一 MessageRun 建立数据库可表达的约束与索引。 -- [ ] 2.3 为 Root/Branch ForkFacts 的空值组合、Message role、MessageRun status 和非负 eventSequence 建立数据库可表达的约束。 -- [ ] 2.4 明确无法由普通 CHECK 表达的同 Project Parent/source/replacement 关系,交给事务内 Repository 校验,不伪造不可靠约束。 -- [ ] 2.5 为 Project 永久删除定义级联边界;普通 Message Repository 和 Route 不得暴露单 Message hard delete。 -- [ ] 2.6 对隔离测试数据库执行 `pnpm db:push` 等价的测试配置命令,并验证从空库可以一次建立完整目标 Schema。 +- [x] 2.1 在 `lib/db/schema.ts` 中新增 `projects`、`threads`、`messages`、`message_runs`、`artifacts` 与 Message feedback 表;Project Memory/File 只保留后续扩展边界,不实现完整协议。 +- [x] 2.2 为 Project owner、唯一 Root Thread、Thread 内唯一 sequence、同 Message 单一 replacement 和 assistant Message 单一 MessageRun 建立数据库可表达的约束与索引。 +- [x] 2.3 为 Root/Branch ForkFacts 的空值组合、Message role、MessageRun status 和非负 eventSequence 建立数据库可表达的约束。 +- [x] 2.4 明确无法由普通 CHECK 表达的同 Project Parent/source/replacement 关系,交给事务内 Repository 校验,不伪造不可靠约束。 +- [x] 2.5 为 Project 永久删除定义级联边界;普通 Message Repository 和 Route 不得暴露单 Message hard delete。 +- [x] 2.6 对隔离测试数据库执行 `pnpm db:push` 等价的测试配置命令,并验证从空库可以一次建立完整目标 Schema。 ## 3. 实现纯领域模型与 Repository diff --git a/tests/integration/normalized-schema.test.ts b/tests/integration/normalized-schema.test.ts new file mode 100644 index 00000000..eb49a28a --- /dev/null +++ b/tests/integration/normalized-schema.test.ts @@ -0,0 +1,328 @@ +import { randomUUID } from "node:crypto" +import { afterAll, describe, expect, it } from "vitest" +import postgres from "postgres" +import { assertSafeTestDatabaseUrl } from "../../scripts/lib/test-database-safety.mjs" + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 1 }) + +type OwnedProject = { + userId: string + projectId: string + rootThreadId: string +} + +async function createOwnedProject(): Promise { + const userId = randomUUID() + const projectId = randomUUID() + const rootThreadId = randomUUID() + + await sql` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${userId}, + 'Schema Test', + ${`${userId}@thread-chat.test`}, + true, + now(), + now() + ) + ` + await sql` + insert into thread_chat.projects (id, owner_user_id) + values (${projectId}, ${userId}) + ` + await sql` + insert into thread_chat.threads (id, project_id) + values (${rootThreadId}, ${projectId}) + ` + + return { userId, projectId, rootThreadId } +} + +async function deleteTestUser(userId: string): Promise { + await sql`delete from thread_chat."user" where id = ${userId}` +} + +afterAll(async () => { + await sql.end() +}) + +describe("规范化 ThreadChat Schema", () => { + it("从空 schema 一次建立六张目标表", async () => { + const rows = await sql<{ tableName: string }[]>` + select table_name as "tableName" + from information_schema.tables + where table_schema = 'thread_chat' + and table_name in ( + 'projects', + 'threads', + 'messages', + 'message_runs', + 'artifacts', + 'message_feedback' + ) + order by table_name + ` + + expect(rows.map((row) => row.tableName)).toEqual([ + "artifacts", + "message_feedback", + "message_runs", + "messages", + "projects", + "threads", + ]) + }) + + it("约束唯一 Root 与完整 Root/Branch ForkFacts", async () => { + const owned = await createOwnedProject() + try { + await expect( + sql` + insert into thread_chat.threads (id, project_id) + values (${randomUUID()}, ${owned.projectId}) + ` + ).rejects.toMatchObject({ code: "23505" }) + + await expect( + sql` + insert into thread_chat.threads ( + id, project_id, parent_thread_id + ) values ( + ${randomUUID()}, ${owned.projectId}, ${owned.rootThreadId} + ) + ` + ).rejects.toMatchObject({ code: "23514" }) + + const sourceMessageId = randomUUID() + await sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values ( + ${sourceMessageId}, + ${owned.rootThreadId}, + 1, + 'user', + '[]'::jsonb, + now() + ) + ` + await sql` + insert into thread_chat.threads ( + id, + project_id, + parent_thread_id, + source_message_id, + fork_source_snapshot, + base_context + ) values ( + ${randomUUID()}, + ${owned.projectId}, + ${owned.rootThreadId}, + ${sourceMessageId}, + ${sql.json({ + schemaVersion: 1, + sourceRole: "user", + sourceSequence: 1, + })}, + ${sql.json({ schemaVersion: 1, messageIds: [sourceMessageId] })} + ) + ` + } finally { + await deleteTestUser(owned.userId) + } + }) + + it("约束 sequence、replacement、角色、Run 状态与非负游标", async () => { + const owned = await createOwnedProject() + try { + const userMessageId = randomUUID() + const assistantMessageId = randomUUID() + await sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values + (${userMessageId}, ${owned.rootThreadId}, 1, 'user', '[]'::jsonb, now()), + (${assistantMessageId}, ${owned.rootThreadId}, 2, 'assistant', '[]'::jsonb, now()) + ` + + await expect( + sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values ( + ${randomUUID()}, ${owned.rootThreadId}, 1, 'user', '[]'::jsonb, now() + ) + ` + ).rejects.toMatchObject({ code: "23505" }) + await expect( + sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values ( + ${randomUUID()}, ${owned.rootThreadId}, 3, 'system', '[]'::jsonb, now() + ) + ` + ).rejects.toMatchObject({ code: "23514" }) + await expect( + sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values ( + ${randomUUID()}, ${owned.rootThreadId}, 0, 'user', '[]'::jsonb, now() + ) + ` + ).rejects.toMatchObject({ code: "23514" }) + + const replacementId = randomUUID() + await sql` + insert into thread_chat.messages ( + id, + thread_id, + sequence, + role, + parts, + replaces_message_id, + finalized_at + ) values ( + ${replacementId}, + ${owned.rootThreadId}, + 3, + 'assistant', + '[]'::jsonb, + ${assistantMessageId}, + now() + ) + ` + await expect( + sql` + insert into thread_chat.messages ( + id, + thread_id, + sequence, + role, + parts, + replaces_message_id, + finalized_at + ) values ( + ${randomUUID()}, + ${owned.rootThreadId}, + 4, + 'assistant', + '[]'::jsonb, + ${assistantMessageId}, + now() + ) + ` + ).rejects.toMatchObject({ code: "23505" }) + + await sql` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id + ) values ( + ${randomUUID()}, ${assistantMessageId}, 'queued', 'fake/test-model' + ) + ` + await expect( + sql` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id + ) values ( + ${randomUUID()}, ${assistantMessageId}, 'queued', 'fake/test-model' + ) + ` + ).rejects.toMatchObject({ code: "23505" }) + await expect( + sql` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id + ) values ( + ${randomUUID()}, ${replacementId}, 'unknown', 'fake/test-model' + ) + ` + ).rejects.toMatchObject({ code: "23514" }) + await expect( + sql` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id, event_sequence + ) values ( + ${randomUUID()}, ${replacementId}, 'queued', 'fake/test-model', -1 + ) + ` + ).rejects.toMatchObject({ code: "23514" }) + } finally { + await deleteTestUser(owned.userId) + } + }) + + it("永久删除 Project 级联清理 Thread、Message、Run、Artifact 与 feedback", async () => { + const owned = await createOwnedProject() + try { + const assistantMessageId = randomUUID() + const artifactId = randomUUID() + await sql` + insert into thread_chat.messages ( + id, thread_id, sequence, role, parts, finalized_at + ) values ( + ${assistantMessageId}, + ${owned.rootThreadId}, + 1, + 'assistant', + '[]'::jsonb, + now() + ) + ` + await sql` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id + ) values ( + ${randomUUID()}, ${assistantMessageId}, 'queued', 'fake/test-model' + ) + ` + await sql` + insert into thread_chat.artifacts ( + id, + project_id, + source_message_id, + change_sequence, + kind, + title, + content + ) values ( + ${artifactId}, + ${owned.projectId}, + ${assistantMessageId}, + 1, + 'markdown', + '测试 Artifact', + '{}'::jsonb + ) + ` + await sql` + insert into thread_chat.message_feedback ( + assistant_message_id, feedback + ) values (${assistantMessageId}, 'positive') + ` + + await sql` + delete from thread_chat.projects + where id = ${owned.projectId} + ` + + const [remaining] = await sql<{ count: number }[]>` + select ( + (select count(*) from thread_chat.threads where project_id = ${owned.projectId}) + + (select count(*) from thread_chat.messages where id = ${assistantMessageId}) + + (select count(*) from thread_chat.message_runs where assistant_message_id = ${assistantMessageId}) + + (select count(*) from thread_chat.artifacts where id = ${artifactId}) + + (select count(*) from thread_chat.message_feedback where assistant_message_id = ${assistantMessageId}) + )::integer as count + ` + expect(remaining.count).toBe(0) + } finally { + await deleteTestUser(owned.userId) + } + }) +}) From a01acc7ab87dda1c8de621c466a1ea953813119b Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:12:53 +0800 Subject: [PATCH 09/23] feat(thread-chat): implement domain model and repositories --- lib/db/index.ts | 1 + lib/thread-chat/domain/artifact.ts | 37 ++ lib/thread-chat/domain/base-context.ts | 51 +++ lib/thread-chat/domain/domain-error.ts | 35 ++ lib/thread-chat/domain/ids.ts | 6 + lib/thread-chat/domain/message-run.ts | 56 +++ lib/thread-chat/domain/message.ts | 67 +++ lib/thread-chat/domain/project.ts | 24 ++ lib/thread-chat/domain/thread.ts | 95 +++++ .../repositories/artifact-repository.ts | 136 ++++++ .../infrastructure/repositories/database.ts | 89 ++++ .../repositories/feedback-repository.ts | 55 +++ .../infrastructure/repositories/index.ts | 42 ++ .../repositories/message-repository.ts | 189 +++++++++ .../repositories/message-run-repository.ts | 207 ++++++++++ .../repositories/project-repository.ts | 89 ++++ .../repositories/thread-repository.ts | 158 +++++++ .../define-thread-chat-domain-model/tasks.md | 16 +- tests/factories/thread-chat-factories.ts | 79 +--- tests/integration/repositories.test.ts | 386 ++++++++++++++++++ tests/unit/domain-model.test.ts | 251 ++++++++++++ 21 files changed, 1994 insertions(+), 75 deletions(-) create mode 100644 lib/thread-chat/domain/artifact.ts create mode 100644 lib/thread-chat/domain/base-context.ts create mode 100644 lib/thread-chat/domain/domain-error.ts create mode 100644 lib/thread-chat/domain/ids.ts create mode 100644 lib/thread-chat/domain/message-run.ts create mode 100644 lib/thread-chat/domain/message.ts create mode 100644 lib/thread-chat/domain/project.ts create mode 100644 lib/thread-chat/domain/thread.ts create mode 100644 lib/thread-chat/infrastructure/repositories/artifact-repository.ts create mode 100644 lib/thread-chat/infrastructure/repositories/database.ts create mode 100644 lib/thread-chat/infrastructure/repositories/feedback-repository.ts create mode 100644 lib/thread-chat/infrastructure/repositories/index.ts create mode 100644 lib/thread-chat/infrastructure/repositories/message-repository.ts create mode 100644 lib/thread-chat/infrastructure/repositories/message-run-repository.ts create mode 100644 lib/thread-chat/infrastructure/repositories/project-repository.ts create mode 100644 lib/thread-chat/infrastructure/repositories/thread-repository.ts create mode 100644 tests/integration/repositories.test.ts create mode 100644 tests/unit/domain-model.test.ts diff --git a/lib/db/index.ts b/lib/db/index.ts index 3547f0d6..78c7cfd1 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -22,4 +22,5 @@ const client = }) if (process.env.NODE_ENV !== "production") globalThis.__dbClient = client +export const dbClient = client export const db = drizzle(client, { schema }) diff --git a/lib/thread-chat/domain/artifact.ts b/lib/thread-chat/domain/artifact.ts new file mode 100644 index 00000000..20a1d89a --- /dev/null +++ b/lib/thread-chat/domain/artifact.ts @@ -0,0 +1,37 @@ +import { invariant } from "./domain-error" +import type { ArtifactId, MessageId, ProjectId } from "./ids" +import type { Message } from "./message" +import type { Thread } from "./thread" + +export type Artifact = { + id: ArtifactId + projectId: ProjectId + sourceMessageId: MessageId + changeSequence: number + kind: string + title: string + content: unknown + createdAt: Date +} + +export type MarkdownArtifactToolOutput = { artifactId: ArtifactId } + +export function assertArtifactProvenance( + artifact: Pick, + sourceMessage: Message, + sourceThread: Thread +): void { + invariant( + sourceMessage.id === artifact.sourceMessageId && + sourceMessage.threadId === sourceThread.id && + sourceThread.projectId === artifact.projectId, + "artifact_provenance_invalid", + "Artifact source Message 必须属于同一 Project。" + ) +} + +export function toMarkdownArtifactToolOutput( + artifact: Pick +): MarkdownArtifactToolOutput { + return { artifactId: artifact.id } +} diff --git a/lib/thread-chat/domain/base-context.ts b/lib/thread-chat/domain/base-context.ts new file mode 100644 index 00000000..e1b5d54e --- /dev/null +++ b/lib/thread-chat/domain/base-context.ts @@ -0,0 +1,51 @@ +import { invariant } from "./domain-error" +import type { MessageId } from "./ids" +import type { Message } from "./message" + +export type BaseContextV1 = { + schemaVersion: 1 + messageIds: MessageId[] +} + +export function validateBaseContext(value: unknown): BaseContextV1 { + invariant( + typeof value === "object" && value !== null, + "base_context_invalid", + "BaseContext 必须是对象。" + ) + const candidate = value as { schemaVersion?: unknown; messageIds?: unknown } + invariant( + candidate.schemaVersion === 1 && Array.isArray(candidate.messageIds), + "base_context_invalid", + "BaseContext 必须使用 schemaVersion=1 与 messageIds。" + ) + invariant( + candidate.messageIds.every( + (messageId): messageId is string => + typeof messageId === "string" && messageId.length > 0 + ), + "base_context_invalid", + "BaseContext.messageIds 必须是非空字符串数组。" + ) + invariant( + new Set(candidate.messageIds).size === candidate.messageIds.length, + "base_context_invalid", + "BaseContext.messageIds 不得重复。" + ) + return { schemaVersion: 1, messageIds: [...candidate.messageIds] } +} + +export function resolveBaseContextMessages( + context: BaseContextV1, + messagesById: ReadonlyMap +): Message[] { + return context.messageIds.map((messageId) => { + const message = messagesById.get(messageId) + invariant( + message, + "base_context_message_missing", + `BaseContext 引用的 Message ${messageId} 不存在。` + ) + return message + }) +} diff --git a/lib/thread-chat/domain/domain-error.ts b/lib/thread-chat/domain/domain-error.ts new file mode 100644 index 00000000..4d943ab0 --- /dev/null +++ b/lib/thread-chat/domain/domain-error.ts @@ -0,0 +1,35 @@ +export type ThreadChatDomainErrorCode = + | "project_owner_mismatch" + | "project_root_invalid" + | "thread_fork_facts_invalid" + | "thread_parent_invalid" + | "thread_source_invalid" + | "thread_cycle" + | "message_not_finalized" + | "message_superseded" + | "message_replacement_invalid" + | "message_not_fork_eligible" + | "base_context_invalid" + | "base_context_message_missing" + | "message_run_transition_invalid" + | "artifact_provenance_invalid" + | "feedback_not_eligible" + | "entity_not_found" + +export class ThreadChatDomainError extends Error { + constructor( + readonly code: ThreadChatDomainErrorCode, + message: string + ) { + super(message) + this.name = "ThreadChatDomainError" + } +} + +export function invariant( + condition: unknown, + code: ThreadChatDomainErrorCode, + message: string +): asserts condition { + if (!condition) throw new ThreadChatDomainError(code, message) +} diff --git a/lib/thread-chat/domain/ids.ts b/lib/thread-chat/domain/ids.ts new file mode 100644 index 00000000..847c7b5c --- /dev/null +++ b/lib/thread-chat/domain/ids.ts @@ -0,0 +1,6 @@ +export type UserId = string +export type ProjectId = string +export type ThreadId = string +export type MessageId = string +export type MessageRunId = string +export type ArtifactId = string diff --git a/lib/thread-chat/domain/message-run.ts b/lib/thread-chat/domain/message-run.ts new file mode 100644 index 00000000..c1245773 --- /dev/null +++ b/lib/thread-chat/domain/message-run.ts @@ -0,0 +1,56 @@ +import type { UIMessage } from "ai" +import { invariant } from "./domain-error" +import type { MessageId, MessageRunId } from "./ids" + +export type MessageRunStatus = + "queued" | "running" | "completed" | "failed" | "stopped" + +export type MessageRun = { + id: MessageRunId + assistantMessageId: MessageId + status: MessageRunStatus + modelId: string + eventSequence: number + checkpointParts: UIMessage["parts"] + errorCode: string | null + errorMessage: string | null + heartbeatAt: Date | null + stopRequestedAt: Date | null + finishedAt: Date | null + createdAt: Date + updatedAt: Date +} + +const ALLOWED_TRANSITIONS: Readonly< + Record +> = { + queued: ["running", "failed", "stopped"], + running: ["completed", "failed", "stopped"], + completed: [], + failed: [], + stopped: [], +} + +export function isTerminalMessageRunStatus(status: MessageRunStatus): boolean { + return status === "completed" || status === "failed" || status === "stopped" +} + +export function assertMessageRunTransition( + current: MessageRunStatus, + next: MessageRunStatus +): void { + invariant( + ALLOWED_TRANSITIONS[current].includes(next), + "message_run_transition_invalid", + `MessageRun 不允许从 ${current} 转为 ${next}。` + ) +} + +export function nextEventSequence(current: number): number { + invariant( + Number.isSafeInteger(current) && current >= 0, + "message_run_transition_invalid", + "eventSequence 必须是非负安全整数。" + ) + return current + 1 +} diff --git a/lib/thread-chat/domain/message.ts b/lib/thread-chat/domain/message.ts new file mode 100644 index 00000000..a587b0ba --- /dev/null +++ b/lib/thread-chat/domain/message.ts @@ -0,0 +1,67 @@ +import type { UIMessage } from "ai" +import { invariant } from "./domain-error" +import type { MessageId, ThreadId } from "./ids" +import type { MessageRun } from "./message-run" + +export type MessageRole = "user" | "assistant" + +export type Message = { + id: MessageId + threadId: ThreadId + sequence: number + role: MessageRole + parts: UIMessage["parts"] | null + replacesMessageId: MessageId | null + supersededAt: Date | null + finalizedAt: Date | null + createdAt: Date +} + +export function selectEffectiveMessages( + messages: readonly Message[] +): Message[] { + return messages + .filter((message) => message.supersededAt === null) + .toSorted((left, right) => left.sequence - right.sequence) +} + +export function assertMessageCanBeReplaced( + source: Message, + replacement: Pick +): void { + invariant( + source.finalizedAt !== null, + "message_not_finalized", + "只有 finalized Message 可以被 replacement。" + ) + invariant( + source.supersededAt === null, + "message_superseded", + "已 superseded Message 不能再次创建 replacement。" + ) + invariant( + replacement.replacesMessageId === source.id && + replacement.threadId === source.threadId && + replacement.role === source.role, + "message_replacement_invalid", + "replacement 必须指向同 Thread、同角色的来源 Message。" + ) +} + +export function assertMessageForkEligible( + message: Message, + run: MessageRun | null +): void { + invariant( + message.finalizedAt !== null && message.supersededAt === null, + "message_not_fork_eligible", + "Fork source 必须 finalized 且仍在有效时间线。" + ) + if (message.role === "assistant") { + invariant( + run?.status === "completed", + "message_not_fork_eligible", + "只有 completed assistant Message 可以作为 Fork source。" + ) + } +} diff --git a/lib/thread-chat/domain/project.ts b/lib/thread-chat/domain/project.ts new file mode 100644 index 00000000..e52658ce --- /dev/null +++ b/lib/thread-chat/domain/project.ts @@ -0,0 +1,24 @@ +import type { ProjectId, UserId } from "./ids" + +export type ProjectTarget = { + ultimate: string | null + shortTerm: string[] + midTerm: string[] +} + +export type Project = { + id: ProjectId + ownerUserId: UserId + autoTitle: string | null + customTitle: string | null + target: ProjectTarget | null + instruction: string | null + archivedAt: Date | null + artifactChangeSequence: number + createdAt: Date + updatedAt: Date +} + +export function getProjectDisplayTitle(project: Project): string { + return project.customTitle ?? project.autoTitle ?? "新对话" +} diff --git a/lib/thread-chat/domain/thread.ts b/lib/thread-chat/domain/thread.ts new file mode 100644 index 00000000..86754133 --- /dev/null +++ b/lib/thread-chat/domain/thread.ts @@ -0,0 +1,95 @@ +import type { BaseContextV1 } from "./base-context" +import { validateBaseContext } from "./base-context" +import { invariant } from "./domain-error" +import type { MessageId, ProjectId, ThreadId } from "./ids" +import type { Message } from "./message" + +export type ForkSourceSnapshot = { + schemaVersion: 1 + quote?: string + sourceRole: "user" | "assistant" + sourceSequence: number +} + +export type Thread = { + id: ThreadId + projectId: ProjectId + parentThreadId: ThreadId | null + sourceMessageId: MessageId | null + forkSourceSnapshot: ForkSourceSnapshot | null + baseContext: BaseContextV1 | null + autoTitle: string | null + customTitle: string | null + archivedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export function isRootThread(thread: Thread): boolean { + return thread.parentThreadId === null +} + +export function validateThreadTopology( + projectId: ProjectId, + projectThreads: readonly Thread[], + messagesById: ReadonlyMap +): void { + const byId = new Map(projectThreads.map((thread) => [thread.id, thread])) + const roots = projectThreads.filter(isRootThread) + invariant( + roots.length === 1, + "project_root_invalid", + "Project 必须恰有一个 Root Thread。" + ) + + for (const thread of projectThreads) { + invariant( + thread.projectId === projectId, + "thread_parent_invalid", + "Thread 必须属于当前 Project。" + ) + if (isRootThread(thread)) { + invariant( + thread.sourceMessageId === null && + thread.forkSourceSnapshot === null && + thread.baseContext === null, + "thread_fork_facts_invalid", + "Root Thread 不得包含 ForkFacts。" + ) + continue + } + + invariant( + thread.sourceMessageId && thread.forkSourceSnapshot && thread.baseContext, + "thread_fork_facts_invalid", + "Branch Thread 必须包含完整 ForkFacts。" + ) + const parent = byId.get(thread.parentThreadId!) + invariant( + parent?.projectId === projectId, + "thread_parent_invalid", + "Branch Parent 必须属于同一 Project。" + ) + const source = messagesById.get(thread.sourceMessageId) + invariant( + source?.threadId === parent.id, + "thread_source_invalid", + "Fork source Message 必须属于 Parent Thread。" + ) + validateBaseContext(thread.baseContext) + } + + for (const thread of projectThreads) { + const visited = new Set() + let cursor: Thread | undefined = thread + while (cursor?.parentThreadId) { + invariant( + !visited.has(cursor.id), + "thread_cycle", + "Thread topology 不得形成环。" + ) + visited.add(cursor.id) + cursor = byId.get(cursor.parentThreadId) + } + } +} diff --git a/lib/thread-chat/infrastructure/repositories/artifact-repository.ts b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts new file mode 100644 index 00000000..407e5047 --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts @@ -0,0 +1,136 @@ +import type { Artifact } from "../../domain/artifact" +import { assertArtifactProvenance } from "../../domain/artifact" +import { invariant } from "../../domain/domain-error" +import type { ArtifactId, MessageId, ProjectId, UserId } from "../../domain/ids" +import { + mapArtifact, + mapMessage, + mapThread, + toSqlJson, + type ThreadChatSql, +} from "./database" + +export class ArtifactRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async insert(input: { + actorId: UserId + id: ArtifactId + projectId: ProjectId + sourceMessageId: MessageId + kind: string + title: string + content: unknown + }): Promise { + const [sourceRow] = await this.sql[]>` + select + m.id, + m.thread_id as "threadId", + m.sequence, + m.role, + m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", + m.finalized_at as "finalizedAt", + m.created_at as "createdAt", + t.id as "sourceThreadId", + t.project_id as "sourceProjectId", + t.parent_thread_id as "parentThreadId", + t.source_message_id as "threadSourceMessageId", + t.fork_source_snapshot as "forkSourceSnapshot", + t.base_context as "baseContext", + t.auto_title as "threadAutoTitle", + t.custom_title as "threadCustomTitle", + t.archived_at as "threadArchivedAt", + t.created_at as "threadCreatedAt", + t.updated_at as "threadUpdatedAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id = ${input.sourceMessageId} + and t.project_id = ${input.projectId} + and p.owner_user_id = ${input.actorId} + for update of p, m + ` + invariant( + sourceRow, + "artifact_provenance_invalid", + "Artifact source 不属于 actor 的目标 Project。" + ) + const sourceMessage = mapMessage(sourceRow) + const sourceThread = mapThread({ + id: sourceRow.sourceThreadId, + projectId: sourceRow.sourceProjectId, + parentThreadId: sourceRow.parentThreadId, + sourceMessageId: sourceRow.threadSourceMessageId, + forkSourceSnapshot: sourceRow.forkSourceSnapshot, + baseContext: sourceRow.baseContext, + autoTitle: sourceRow.threadAutoTitle, + customTitle: sourceRow.threadCustomTitle, + archivedAt: sourceRow.threadArchivedAt, + createdAt: sourceRow.threadCreatedAt, + updatedAt: sourceRow.threadUpdatedAt, + }) + assertArtifactProvenance(input, sourceMessage, sourceThread) + + const [counter] = await this.sql<{ changeSequence: number }[]>` + update thread_chat.projects + set artifact_change_sequence = artifact_change_sequence + 1, + updated_at = now() + where id = ${input.projectId} and owner_user_id = ${input.actorId} + returning artifact_change_sequence as "changeSequence" + ` + invariant(counter, "project_owner_mismatch", "Project 不属于当前 actor。") + + const [row] = await this.sql[]>` + insert into thread_chat.artifacts ( + id, + project_id, + source_message_id, + change_sequence, + kind, + title, + content + ) values ( + ${input.id}, + ${input.projectId}, + ${input.sourceMessageId}, + ${counter.changeSequence}, + ${input.kind}, + ${input.title}, + ${this.sql.json(toSqlJson(input.content))} + ) + returning + id, + project_id as "projectId", + source_message_id as "sourceMessageId", + change_sequence as "changeSequence", + kind, + title, + content, + created_at as "createdAt" + ` + return mapArtifact(row) + } + + async findOwnedById( + actorId: UserId, + artifactId: ArtifactId + ): Promise { + const [row] = await this.sql[]>` + select + a.id, + a.project_id as "projectId", + a.source_message_id as "sourceMessageId", + a.change_sequence as "changeSequence", + a.kind, + a.title, + a.content, + a.created_at as "createdAt" + from thread_chat.artifacts a + join thread_chat.projects p on p.id = a.project_id + where a.id = ${artifactId} and p.owner_user_id = ${actorId} + ` + return row ? mapArtifact(row) : null + } +} diff --git a/lib/thread-chat/infrastructure/repositories/database.ts b/lib/thread-chat/infrastructure/repositories/database.ts new file mode 100644 index 00000000..bb679849 --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/database.ts @@ -0,0 +1,89 @@ +import type postgres from "postgres" +import type { Artifact } from "../../domain/artifact" +import type { Message } from "../../domain/message" +import type { MessageRun } from "../../domain/message-run" +import type { Project } from "../../domain/project" +import type { Thread } from "../../domain/thread" + +export type ThreadChatSql = postgres.Sql | postgres.TransactionSql + +export function toSqlJson(value: unknown): postgres.JSONValue { + return value as postgres.JSONValue +} + +export function mapProject(row: Record): Project { + return { + id: String(row.id), + ownerUserId: String(row.ownerUserId), + autoTitle: (row.autoTitle as string | null) ?? null, + customTitle: (row.customTitle as string | null) ?? null, + target: (row.target as Project["target"]) ?? null, + instruction: (row.instruction as string | null) ?? null, + archivedAt: (row.archivedAt as Date | null) ?? null, + artifactChangeSequence: Number(row.artifactChangeSequence), + createdAt: row.createdAt as Date, + updatedAt: row.updatedAt as Date, + } +} + +export function mapThread(row: Record): Thread { + return { + id: String(row.id), + projectId: String(row.projectId), + parentThreadId: (row.parentThreadId as string | null) ?? null, + sourceMessageId: (row.sourceMessageId as string | null) ?? null, + forkSourceSnapshot: + (row.forkSourceSnapshot as Thread["forkSourceSnapshot"]) ?? null, + baseContext: (row.baseContext as Thread["baseContext"]) ?? null, + autoTitle: (row.autoTitle as string | null) ?? null, + customTitle: (row.customTitle as string | null) ?? null, + archivedAt: (row.archivedAt as Date | null) ?? null, + createdAt: row.createdAt as Date, + updatedAt: row.updatedAt as Date, + } +} + +export function mapMessage(row: Record): Message { + return { + id: String(row.id), + threadId: String(row.threadId), + sequence: Number(row.sequence), + role: row.role as Message["role"], + parts: (row.parts as Message["parts"]) ?? null, + replacesMessageId: (row.replacesMessageId as string | null) ?? null, + supersededAt: (row.supersededAt as Date | null) ?? null, + finalizedAt: (row.finalizedAt as Date | null) ?? null, + createdAt: row.createdAt as Date, + } +} + +export function mapMessageRun(row: Record): MessageRun { + return { + id: String(row.id), + assistantMessageId: String(row.assistantMessageId), + status: row.status as MessageRun["status"], + modelId: String(row.modelId), + eventSequence: Number(row.eventSequence), + checkpointParts: row.checkpointParts as MessageRun["checkpointParts"], + errorCode: (row.errorCode as string | null) ?? null, + errorMessage: (row.errorMessage as string | null) ?? null, + heartbeatAt: (row.heartbeatAt as Date | null) ?? null, + stopRequestedAt: (row.stopRequestedAt as Date | null) ?? null, + finishedAt: (row.finishedAt as Date | null) ?? null, + createdAt: row.createdAt as Date, + updatedAt: row.updatedAt as Date, + } +} + +export function mapArtifact(row: Record): Artifact { + return { + id: String(row.id), + projectId: String(row.projectId), + sourceMessageId: String(row.sourceMessageId), + changeSequence: Number(row.changeSequence), + kind: String(row.kind), + title: String(row.title), + content: row.content, + createdAt: row.createdAt as Date, + } +} diff --git a/lib/thread-chat/infrastructure/repositories/feedback-repository.ts b/lib/thread-chat/infrastructure/repositories/feedback-repository.ts new file mode 100644 index 00000000..0054d684 --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/feedback-repository.ts @@ -0,0 +1,55 @@ +import { invariant } from "../../domain/domain-error" +import type { MessageId, UserId } from "../../domain/ids" +import type { ThreadChatSql } from "./database" + +export type MessageFeedbackValue = "positive" | "negative" + +export class FeedbackRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async set(input: { + actorId: UserId + assistantMessageId: MessageId + feedback: MessageFeedbackValue | null + }): Promise { + const [eligible] = await this.sql` + select m.id + from thread_chat.messages m + join thread_chat.message_runs r on r.assistant_message_id = m.id + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id = ${input.assistantMessageId} + and m.role = 'assistant' + and m.finalized_at is not null + and m.superseded_at is null + and r.status = 'completed' + and p.owner_user_id = ${input.actorId} + for update of m + ` + invariant( + eligible, + "feedback_not_eligible", + "Message 不属于 actor 或不满足 feedback 资格。" + ) + + if (input.feedback === null) { + await this.sql` + delete from thread_chat.message_feedback + where assistant_message_id = ${input.assistantMessageId} + ` + return null + } + + const [row] = await this.sql<{ feedback: MessageFeedbackValue }[]>` + insert into thread_chat.message_feedback ( + assistant_message_id, feedback + ) values ( + ${input.assistantMessageId}, ${input.feedback} + ) + on conflict (assistant_message_id) do update + set feedback = excluded.feedback, updated_at = now() + returning feedback + ` + return row.feedback + } +} diff --git a/lib/thread-chat/infrastructure/repositories/index.ts b/lib/thread-chat/infrastructure/repositories/index.ts new file mode 100644 index 00000000..9ad64624 --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/index.ts @@ -0,0 +1,42 @@ +import type postgres from "postgres" +import { ArtifactRepository } from "./artifact-repository" +import { FeedbackRepository } from "./feedback-repository" +import { MessageRepository } from "./message-repository" +import { MessageRunRepository } from "./message-run-repository" +import { ProjectRepository } from "./project-repository" +import { ThreadRepository } from "./thread-repository" +import type { ThreadChatSql } from "./database" + +export type ThreadChatRepositories = ReturnType< + typeof createThreadChatRepositories +> + +export function createThreadChatRepositories(sql: ThreadChatSql) { + return { + projects: new ProjectRepository(sql), + threads: new ThreadRepository(sql), + messages: new MessageRepository(sql), + messageRuns: new MessageRunRepository(sql), + artifacts: new ArtifactRepository(sql), + feedback: new FeedbackRepository(sql), + } +} + +export class ThreadChatUnitOfWork { + constructor(private readonly sql: postgres.Sql) {} + + transaction( + callback: (repositories: ThreadChatRepositories) => Promise + ): Promise { + return this.sql.begin((transactionSql) => + callback(createThreadChatRepositories(transactionSql)) + ) as Promise + } +} + +export { ArtifactRepository } from "./artifact-repository" +export { FeedbackRepository } from "./feedback-repository" +export { MessageRepository } from "./message-repository" +export { MessageRunRepository } from "./message-run-repository" +export { ProjectRepository } from "./project-repository" +export { ThreadRepository } from "./thread-repository" diff --git a/lib/thread-chat/infrastructure/repositories/message-repository.ts b/lib/thread-chat/infrastructure/repositories/message-repository.ts new file mode 100644 index 00000000..d0eca030 --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/message-repository.ts @@ -0,0 +1,189 @@ +import type { UIMessage } from "ai" +import { assertMessageCanBeReplaced, type Message } from "../../domain/message" +import { invariant } from "../../domain/domain-error" +import type { MessageId, ThreadId, UserId } from "../../domain/ids" +import { mapMessage, toSqlJson, type ThreadChatSql } from "./database" + +export class MessageRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async findOwnedById( + actorId: UserId, + messageId: MessageId + ): Promise { + const [row] = await this.sql[]>` + select + m.id, + m.thread_id as "threadId", + m.sequence, + m.role, + m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", + m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id = ${messageId} and p.owner_user_id = ${actorId} + ` + return row ? mapMessage(row) : null + } + + async listEffectiveOwned( + actorId: UserId, + threadId: ThreadId, + limit = 200 + ): Promise { + const rows = await this.sql[]>` + select * from ( + select + m.id, + m.thread_id as "threadId", + m.sequence, + m.role, + m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", + m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.thread_id = ${threadId} + and m.superseded_at is null + and p.owner_user_id = ${actorId} + order by m.sequence desc + limit ${limit} + ) window + order by sequence asc + ` + return rows.map(mapMessage) + } + + /** 锁定 Thread 后分配 sequence,并在同一事务完成 insert。 */ + async append(input: { + actorId: UserId + id: MessageId + threadId: ThreadId + role: Message["role"] + parts: UIMessage["parts"] | null + finalizedAt: Date | null + replacesMessageId?: MessageId | null + }): Promise { + const [thread] = await this.sql` + select t.id + from thread_chat.threads t + join thread_chat.projects p on p.id = t.project_id + where t.id = ${input.threadId} and p.owner_user_id = ${input.actorId} + for update of t + ` + invariant( + thread, + "project_owner_mismatch", + "Thread 不存在或不属于当前 actor。" + ) + + let source: Message | null = null + if (input.replacesMessageId) { + const [sourceRow] = await this.sql[]>` + select + id, + thread_id as "threadId", + sequence, + role, + parts, + replaces_message_id as "replacesMessageId", + superseded_at as "supersededAt", + finalized_at as "finalizedAt", + created_at as "createdAt" + from thread_chat.messages + where id = ${input.replacesMessageId} + for update + ` + invariant(sourceRow, "entity_not_found", "replacement source 不存在。") + source = mapMessage(sourceRow) + assertMessageCanBeReplaced(source, { + threadId: input.threadId, + role: input.role, + replacesMessageId: input.replacesMessageId, + }) + } + + const [sequenceRow] = await this.sql<{ nextSequence: number }[]>` + select coalesce(max(sequence), 0)::integer + 1 as "nextSequence" + from thread_chat.messages + where thread_id = ${input.threadId} + ` + const [row] = await this.sql[]>` + insert into thread_chat.messages ( + id, + thread_id, + sequence, + role, + parts, + replaces_message_id, + finalized_at + ) values ( + ${input.id}, + ${input.threadId}, + ${sequenceRow.nextSequence}, + ${input.role}, + ${input.parts === null ? null : this.sql.json(toSqlJson(input.parts))}, + ${input.replacesMessageId ?? null}, + ${input.finalizedAt} + ) + returning + id, + thread_id as "threadId", + sequence, + role, + parts, + replaces_message_id as "replacesMessageId", + superseded_at as "supersededAt", + finalized_at as "finalizedAt", + created_at as "createdAt" + ` + + if (source) { + await this.sql` + update thread_chat.messages + set superseded_at = now() + where id = ${source.id} and superseded_at is null + ` + } + return mapMessage(row) + } + + /** finalized assistant parts 是单次封存,不提供 finalized Message 的通用更新入口。 */ + async finalizeAssistantOnce(input: { + actorId: UserId + messageId: MessageId + parts: UIMessage["parts"] + finalizedAt: Date + }): Promise { + const [row] = await this.sql[]>` + update thread_chat.messages m + set parts = ${this.sql.json(toSqlJson(input.parts))}, + finalized_at = ${input.finalizedAt} + from thread_chat.threads t, thread_chat.projects p + where m.id = ${input.messageId} + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + and m.role = 'assistant' + and m.finalized_at is null + returning + m.id, + m.thread_id as "threadId", + m.sequence, + m.role, + m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", + m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + ` + return row ? mapMessage(row) : null + } +} diff --git a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts new file mode 100644 index 00000000..f1a9e40f --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts @@ -0,0 +1,207 @@ +import type { UIMessage } from "ai" +import { invariant } from "../../domain/domain-error" +import type { MessageId, MessageRunId, UserId } from "../../domain/ids" +import { + assertMessageRunTransition, + type MessageRun, + type MessageRunStatus, +} from "../../domain/message-run" +import { mapMessageRun, toSqlJson, type ThreadChatSql } from "./database" + +export class MessageRunRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async insertQueued(input: { + actorId: UserId + id: MessageRunId + assistantMessageId: MessageId + modelId: string + }): Promise { + const [message] = await this.sql` + select m.id + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id = ${input.assistantMessageId} + and m.role = 'assistant' + and p.owner_user_id = ${input.actorId} + for update of m + ` + invariant( + message, + "project_owner_mismatch", + "Message 不属于 actor,或不是 assistant Message。" + ) + const [row] = await this.sql[]>` + insert into thread_chat.message_runs ( + id, assistant_message_id, status, model_id + ) values ( + ${input.id}, ${input.assistantMessageId}, 'queued', ${input.modelId} + ) + returning + id, + assistant_message_id as "assistantMessageId", + status, + model_id as "modelId", + event_sequence as "eventSequence", + checkpoint_parts as "checkpointParts", + error_code as "errorCode", + error_message as "errorMessage", + heartbeat_at as "heartbeatAt", + stop_requested_at as "stopRequestedAt", + finished_at as "finishedAt", + created_at as "createdAt", + updated_at as "updatedAt" + ` + return mapMessageRun(row) + } + + async findOwnedByAssistantMessageId( + actorId: UserId, + assistantMessageId: MessageId + ): Promise { + const [row] = await this.sql[]>` + select + r.id, + r.assistant_message_id as "assistantMessageId", + r.status, + r.model_id as "modelId", + r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", + r.error_code as "errorCode", + r.error_message as "errorMessage", + r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", + r.finished_at as "finishedAt", + r.created_at as "createdAt", + r.updated_at as "updatedAt" + from thread_chat.message_runs r + join thread_chat.messages m on m.id = r.assistant_message_id + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where r.assistant_message_id = ${assistantMessageId} + and p.owner_user_id = ${actorId} + ` + return row ? mapMessageRun(row) : null + } + + async transition(input: { + actorId: UserId + messageRunId: MessageRunId + expectedStatus: MessageRunStatus + nextStatus: MessageRunStatus + finishedAt?: Date | null + error?: { code: string; message: string } | null + }): Promise { + assertMessageRunTransition(input.expectedStatus, input.nextStatus) + const [row] = await this.sql[]>` + update thread_chat.message_runs r + set status = ${input.nextStatus}, + finished_at = ${input.finishedAt ?? null}, + error_code = ${input.error?.code ?? null}, + error_message = ${input.error?.message ?? null}, + updated_at = now() + from thread_chat.messages m, + thread_chat.threads t, + thread_chat.projects p + where r.id = ${input.messageRunId} + and r.status = ${input.expectedStatus} + and r.assistant_message_id = m.id + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning + r.id, + r.assistant_message_id as "assistantMessageId", + r.status, + r.model_id as "modelId", + r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", + r.error_code as "errorCode", + r.error_message as "errorMessage", + r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", + r.finished_at as "finishedAt", + r.created_at as "createdAt", + r.updated_at as "updatedAt" + ` + return row ? mapMessageRun(row) : null + } + + async checkpoint(input: { + actorId: UserId + messageRunId: MessageRunId + expectedEventSequence: number + checkpointParts: UIMessage["parts"] + heartbeatAt: Date + }): Promise { + const [row] = await this.sql[]>` + update thread_chat.message_runs r + set checkpoint_parts = ${this.sql.json(toSqlJson(input.checkpointParts))}, + event_sequence = event_sequence + 1, + heartbeat_at = ${input.heartbeatAt}, + updated_at = now() + from thread_chat.messages m, + thread_chat.threads t, + thread_chat.projects p + where r.id = ${input.messageRunId} + and r.status = 'running' + and r.event_sequence = ${input.expectedEventSequence} + and r.assistant_message_id = m.id + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning + r.id, + r.assistant_message_id as "assistantMessageId", + r.status, + r.model_id as "modelId", + r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", + r.error_code as "errorCode", + r.error_message as "errorMessage", + r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", + r.finished_at as "finishedAt", + r.created_at as "createdAt", + r.updated_at as "updatedAt" + ` + return row ? mapMessageRun(row) : null + } + + async requestStop( + actorId: UserId, + assistantMessageId: MessageId, + requestedAt: Date + ): Promise { + const [row] = await this.sql[]>` + update thread_chat.message_runs r + set stop_requested_at = coalesce(r.stop_requested_at, ${requestedAt}), + updated_at = now() + from thread_chat.messages m, + thread_chat.threads t, + thread_chat.projects p + where r.assistant_message_id = ${assistantMessageId} + and r.status in ('queued', 'running') + and r.assistant_message_id = m.id + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${actorId} + returning + r.id, + r.assistant_message_id as "assistantMessageId", + r.status, + r.model_id as "modelId", + r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", + r.error_code as "errorCode", + r.error_message as "errorMessage", + r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", + r.finished_at as "finishedAt", + r.created_at as "createdAt", + r.updated_at as "updatedAt" + ` + return row ? mapMessageRun(row) : null + } +} diff --git a/lib/thread-chat/infrastructure/repositories/project-repository.ts b/lib/thread-chat/infrastructure/repositories/project-repository.ts new file mode 100644 index 00000000..3a60e58d --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/project-repository.ts @@ -0,0 +1,89 @@ +import { invariant } from "../../domain/domain-error" +import type { ProjectId, UserId } from "../../domain/ids" +import type { Project, ProjectTarget } from "../../domain/project" +import { mapProject, type ThreadChatSql } from "./database" + +export class ProjectRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async findOwnedById( + actorId: UserId, + projectId: ProjectId + ): Promise { + const [row] = await this.sql[]>` + select + id, + owner_user_id as "ownerUserId", + auto_title as "autoTitle", + custom_title as "customTitle", + target, + instruction, + archived_at as "archivedAt", + artifact_change_sequence as "artifactChangeSequence", + created_at as "createdAt", + updated_at as "updatedAt" + from thread_chat.projects + where id = ${projectId} and owner_user_id = ${actorId} + ` + return row ? mapProject(row) : null + } + + async insert(input: { + id: ProjectId + ownerUserId: UserId + autoTitle?: string | null + customTitle?: string | null + target?: ProjectTarget | null + instruction?: string | null + }): Promise { + const [row] = await this.sql[]>` + insert into thread_chat.projects ( + id, owner_user_id, auto_title, custom_title, target, instruction + ) values ( + ${input.id}, + ${input.ownerUserId}, + ${input.autoTitle ?? null}, + ${input.customTitle ?? null}, + ${input.target ? this.sql.json(input.target) : null}, + ${input.instruction ?? null} + ) + returning + id, + owner_user_id as "ownerUserId", + auto_title as "autoTitle", + custom_title as "customTitle", + target, + instruction, + archived_at as "archivedAt", + artifact_change_sequence as "artifactChangeSequence", + created_at as "createdAt", + updated_at as "updatedAt" + ` + return mapProject(row) + } + + async assertOwnedForUpdate( + actorId: UserId, + projectId: ProjectId + ): Promise { + const [row] = await this.sql` + select id + from thread_chat.projects + where id = ${projectId} and owner_user_id = ${actorId} + for update + ` + invariant( + row, + "project_owner_mismatch", + "Project 不存在或不属于当前 actor。" + ) + } + + async deleteOwned(actorId: UserId, projectId: ProjectId): Promise { + const result = await this.sql` + delete from thread_chat.projects + where id = ${projectId} and owner_user_id = ${actorId} + ` + return result.count === 1 + } +} diff --git a/lib/thread-chat/infrastructure/repositories/thread-repository.ts b/lib/thread-chat/infrastructure/repositories/thread-repository.ts new file mode 100644 index 00000000..18059f5d --- /dev/null +++ b/lib/thread-chat/infrastructure/repositories/thread-repository.ts @@ -0,0 +1,158 @@ +import { + validateBaseContext, + type BaseContextV1, +} from "../../domain/base-context" +import { invariant } from "../../domain/domain-error" +import type { ProjectId, ThreadId, UserId } from "../../domain/ids" +import type { ForkSourceSnapshot, Thread } from "../../domain/thread" +import { mapThread, type ThreadChatSql } from "./database" +import { ProjectRepository } from "./project-repository" + +export class ThreadRepository { + constructor(private readonly sql: ThreadChatSql) {} + + async findOwnedById( + actorId: UserId, + threadId: ThreadId + ): Promise { + const [row] = await this.sql[]>` + select + t.id, + t.project_id as "projectId", + t.parent_thread_id as "parentThreadId", + t.source_message_id as "sourceMessageId", + t.fork_source_snapshot as "forkSourceSnapshot", + t.base_context as "baseContext", + t.auto_title as "autoTitle", + t.custom_title as "customTitle", + t.archived_at as "archivedAt", + t.created_at as "createdAt", + t.updated_at as "updatedAt" + from thread_chat.threads t + join thread_chat.projects p on p.id = t.project_id + where t.id = ${threadId} and p.owner_user_id = ${actorId} + ` + return row ? mapThread(row) : null + } + + async listOwnedTopology( + actorId: UserId, + projectId: ProjectId + ): Promise { + const rows = await this.sql[]>` + select + t.id, + t.project_id as "projectId", + t.parent_thread_id as "parentThreadId", + t.source_message_id as "sourceMessageId", + t.fork_source_snapshot as "forkSourceSnapshot", + t.base_context as "baseContext", + t.auto_title as "autoTitle", + t.custom_title as "customTitle", + t.archived_at as "archivedAt", + t.created_at as "createdAt", + t.updated_at as "updatedAt" + from thread_chat.threads t + join thread_chat.projects p on p.id = t.project_id + where t.project_id = ${projectId} and p.owner_user_id = ${actorId} + order by t.created_at, t.id + ` + return rows.map(mapThread) + } + + async insertRoot(input: { + actorId: UserId + id: ThreadId + projectId: ProjectId + }): Promise { + await new ProjectRepository(this.sql).assertOwnedForUpdate( + input.actorId, + input.projectId + ) + return this.insert({ id: input.id, projectId: input.projectId }) + } + + async insertBranch(input: { + actorId: UserId + id: ThreadId + projectId: ProjectId + parentThreadId: ThreadId + sourceMessageId: string + forkSourceSnapshot: ForkSourceSnapshot + baseContext: unknown + }): Promise { + await new ProjectRepository(this.sql).assertOwnedForUpdate( + input.actorId, + input.projectId + ) + const baseContext = validateBaseContext(input.baseContext) + const [relation] = await this.sql` + select 1 + from thread_chat.threads parent + join thread_chat.messages source + on source.id = ${input.sourceMessageId} + and source.thread_id = parent.id + where parent.id = ${input.parentThreadId} + and parent.project_id = ${input.projectId} + for update of parent, source + ` + invariant( + relation, + "thread_source_invalid", + "Parent/source Message 必须属于同一 Project 与 Parent Thread。" + ) + + return this.insert({ + id: input.id, + projectId: input.projectId, + parentThreadId: input.parentThreadId, + sourceMessageId: input.sourceMessageId, + forkSourceSnapshot: input.forkSourceSnapshot, + baseContext, + }) + } + + private async insert(input: { + id: ThreadId + projectId: ProjectId + parentThreadId?: ThreadId + sourceMessageId?: string + forkSourceSnapshot?: ForkSourceSnapshot + baseContext?: BaseContextV1 + }): Promise { + const [row] = await this.sql[]>` + insert into thread_chat.threads ( + id, + project_id, + parent_thread_id, + source_message_id, + fork_source_snapshot, + base_context + ) values ( + ${input.id}, + ${input.projectId}, + ${input.parentThreadId ?? null}, + ${input.sourceMessageId ?? null}, + ${ + input.forkSourceSnapshot + ? this.sql.json(input.forkSourceSnapshot) + : null + }, + ${input.baseContext ? this.sql.json(input.baseContext) : null} + ) + returning + id, + project_id as "projectId", + parent_thread_id as "parentThreadId", + source_message_id as "sourceMessageId", + fork_source_snapshot as "forkSourceSnapshot", + base_context as "baseContext", + auto_title as "autoTitle", + custom_title as "customTitle", + archived_at as "archivedAt", + created_at as "createdAt", + updated_at as "updatedAt" + ` + return mapThread(row) + } +} diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index 1b8294f3..35a746a7 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -27,14 +27,14 @@ ## 3. 实现纯领域模型与 Repository -- [ ] 3.1 按 design 的模块骨架建立不依赖 React、HTTP、Drizzle 或具体 AI Runtime 的 Project、Thread、Message、MessageRun、BaseContext 与 Artifact 领域类型。 -- [ ] 3.2 实现 Root/Branch 关系判定、唯一 Root、同 Project Parent/source、无环拓扑与 ForkFacts 完整性验证。 -- [ ] 3.3 实现 Thread 内服务端 sequence 分配,并用数据库唯一约束与并发测试保证无重复 sequence。 -- [ ] 3.4 实现 finalized Message 不可变与 replacement 规则;Repository 的更新入口必须明确禁止原地覆盖 finalized parts 和 sequence。 -- [ ] 3.5 实现 BaseContextV1 的验证、持久化和有序 Message ID 解析;BaseContext 只能由服务端计算。 -- [ ] 3.6 实现 MessageRun 的 queued/running/completed/failed/stopped 条件状态转换、checkpointParts、eventSequence 与 Stop 请求持久化。 -- [ ] 3.7 实现 Project、Thread、Message、MessageRun、Artifact 与 feedback Repository;所有 Query 从 actor 校验 Project owner scope。 -- [ ] 3.8 实现 Artifact 独立持久化及 `sourceMessageId` provenance;Message 的 AI SDK v7 tool output 只保存 `artifactId`,不复制 Markdown 正文。 +- [x] 3.1 按 design 的模块骨架建立不依赖 React、HTTP、Drizzle 或具体 AI Runtime 的 Project、Thread、Message、MessageRun、BaseContext 与 Artifact 领域类型。 +- [x] 3.2 实现 Root/Branch 关系判定、唯一 Root、同 Project Parent/source、无环拓扑与 ForkFacts 完整性验证。 +- [x] 3.3 实现 Thread 内服务端 sequence 分配,并用数据库唯一约束与并发测试保证无重复 sequence。 +- [x] 3.4 实现 finalized Message 不可变与 replacement 规则;Repository 的更新入口必须明确禁止原地覆盖 finalized parts 和 sequence。 +- [x] 3.5 实现 BaseContextV1 的验证、持久化和有序 Message ID 解析;BaseContext 只能由服务端计算。 +- [x] 3.6 实现 MessageRun 的 queued/running/completed/failed/stopped 条件状态转换、checkpointParts、eventSequence 与 Stop 请求持久化。 +- [x] 3.7 实现 Project、Thread、Message、MessageRun、Artifact 与 feedback Repository;所有 Query 从 actor 校验 Project owner scope。 +- [x] 3.8 实现 Artifact 独立持久化及 `sourceMessageId` provenance;Message 的 AI SDK v7 tool output 只保存 `artifactId`,不复制 Markdown 正文。 ## 4. 实现核心 Application Command 与 Query diff --git a/tests/factories/thread-chat-factories.ts b/tests/factories/thread-chat-factories.ts index 33a9a0f8..2cf3207d 100644 --- a/tests/factories/thread-chat-factories.ts +++ b/tests/factories/thread-chat-factories.ts @@ -1,8 +1,9 @@ import { randomUUID } from "node:crypto" -import type { UIMessage } from "ai" - -type JsonValue = - string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } +import type { Artifact } from "@/lib/thread-chat/domain/artifact" +import type { Message } from "@/lib/thread-chat/domain/message" +import type { MessageRun } from "@/lib/thread-chat/domain/message-run" +import type { Project } from "@/lib/thread-chat/domain/project" +import type { Thread } from "@/lib/thread-chat/domain/thread" const FIXTURE_TIME = new Date("2026-01-01T00:00:00.000Z") @@ -15,69 +16,11 @@ export type UserFixture = { updatedAt: Date } -export type ProjectFixture = { - id: string - ownerUserId: string - autoTitle: string | null - customTitle: string | null - target: JsonValue | null - instruction: JsonValue | null - archivedAt: Date | null - createdAt: Date - updatedAt: Date -} - -export type ThreadFixture = { - id: string - projectId: string - parentThreadId: string | null - sourceMessageId: string | null - forkSourceSnapshot: JsonValue | null - baseContext: { schemaVersion: 1; messageIds: string[] } | null - autoTitle: string | null - customTitle: string | null - archivedAt: Date | null - createdAt: Date - updatedAt: Date -} - -export type MessageFixture = { - id: string - threadId: string - sequence: number - role: "user" | "assistant" - parts: UIMessage["parts"] | null - replacesMessageId: string | null - supersededAt: Date | null - finalizedAt: Date | null - createdAt: Date -} - -export type ArtifactFixture = { - id: string - projectId: string - sourceMessageId: string - kind: string - title: string - content: JsonValue - createdAt: Date -} - -export type MessageRunFixture = { - id: string - assistantMessageId: string - status: "queued" | "running" | "completed" | "failed" | "stopped" - modelId: string - eventSequence: number - checkpointParts: UIMessage["parts"] - errorCode: string | null - errorMessage: string | null - heartbeatAt: Date | null - stopRequestedAt: Date | null - finishedAt: Date | null - createdAt: Date - updatedAt: Date -} +export type ProjectFixture = Project +export type ThreadFixture = Thread +export type MessageFixture = Message +export type ArtifactFixture = Artifact +export type MessageRunFixture = MessageRun export function createUserFixture( overrides: Partial = {} @@ -105,6 +48,7 @@ export function createProjectFixture( target: null, instruction: null, archivedAt: null, + artifactChangeSequence: 0, createdAt: FIXTURE_TIME, updatedAt: FIXTURE_TIME, ...overrides, @@ -166,6 +110,7 @@ export function createArtifactFixture( id: randomUUID(), projectId: randomUUID(), sourceMessageId: randomUUID(), + changeSequence: 1, kind: "markdown", title: "测试 Artifact", content: { markdown: "# 测试" }, diff --git a/tests/integration/repositories.test.ts b/tests/integration/repositories.test.ts new file mode 100644 index 00000000..70091b2d --- /dev/null +++ b/tests/integration/repositories.test.ts @@ -0,0 +1,386 @@ +import { randomUUID } from "node:crypto" +import { afterAll, describe, expect, it } from "vitest" +import postgres from "postgres" +import { assertSafeTestDatabaseUrl } from "../../scripts/lib/test-database-safety.mjs" +import { + createThreadChatRepositories, + ThreadChatUnitOfWork, +} from "@/lib/thread-chat/infrastructure/repositories" + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 10 }) +const unitOfWork = new ThreadChatUnitOfWork(sql) +const now = new Date("2026-01-01T00:00:00.000Z") + +async function createUser(): Promise { + const userId = randomUUID() + await sql` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${userId}, + 'Repository Test', + ${`${userId}@thread-chat.test`}, + true, + now(), + now() + ) + ` + return userId +} + +async function createProjectWithRoot(actorId: string) { + return unitOfWork.transaction(async (repositories) => { + const project = await repositories.projects.insert({ + id: randomUUID(), + ownerUserId: actorId, + }) + const root = await repositories.threads.insertRoot({ + actorId, + id: randomUUID(), + projectId: project.id, + }) + return { project, root } + }) +} + +async function deleteUser(userId: string): Promise { + await sql`delete from thread_chat."user" where id = ${userId}` +} + +afterAll(async () => { + await sql.end() +}) + +describe("ThreadChat Repositories", () => { + it("所有 owner-scoped Query 隔离其他 actor", async () => { + const ownerId = await createUser() + const otherId = await createUser() + try { + const { project, root } = await createProjectWithRoot(ownerId) + const repositories = createThreadChatRepositories(sql) + + expect( + await repositories.projects.findOwnedById(ownerId, project.id) + ).toMatchObject({ id: project.id, ownerUserId: ownerId }) + expect( + await repositories.projects.findOwnedById(otherId, project.id) + ).toBeNull() + expect( + await repositories.threads.findOwnedById(otherId, root.id) + ).toBeNull() + expect( + await repositories.threads.listOwnedTopology(otherId, project.id) + ).toEqual([]) + } finally { + await deleteUser(ownerId) + await deleteUser(otherId) + } + }) + + it("并发 append 通过 Thread 行锁分配唯一递增 sequence", async () => { + const ownerId = await createUser() + try { + const { root } = await createProjectWithRoot(ownerId) + const appended = await Promise.all( + Array.from({ length: 8 }, (_, index) => + unitOfWork.transaction((repositories) => + repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "user", + parts: [{ type: "text", text: `消息 ${index}` }], + finalizedAt: now, + }) + ) + ) + ) + + expect(appended.map((message) => message.sequence).toSorted()).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, + ]) + } finally { + await deleteUser(ownerId) + } + }) + + it("finalized Message 只允许 replacement,assistant 只能封存一次", async () => { + const ownerId = await createUser() + try { + const { root } = await createProjectWithRoot(ownerId) + const result = await unitOfWork.transaction(async (repositories) => { + const source = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "user", + parts: [{ type: "text", text: "旧内容" }], + finalizedAt: now, + }) + const replacement = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "user", + parts: [{ type: "text", text: "新内容" }], + finalizedAt: now, + replacesMessageId: source.id, + }) + const assistant = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "assistant", + parts: null, + finalizedAt: null, + }) + const finalized = await repositories.messages.finalizeAssistantOnce({ + actorId: ownerId, + messageId: assistant.id, + parts: [{ type: "text", text: "最终内容" }], + finalizedAt: now, + }) + const duplicateFinalize = + await repositories.messages.finalizeAssistantOnce({ + actorId: ownerId, + messageId: assistant.id, + parts: [{ type: "text", text: "覆盖内容" }], + finalizedAt: now, + }) + return { source, replacement, finalized, duplicateFinalize } + }) + + const sourceAfter = await createThreadChatRepositories( + sql + ).messages.findOwnedById(ownerId, result.source.id) + expect(sourceAfter).toMatchObject({ + parts: [{ type: "text", text: "旧内容" }], + sequence: result.source.sequence, + }) + expect(sourceAfter?.supersededAt).toBeInstanceOf(Date) + expect(result.replacement.replacesMessageId).toBe(result.source.id) + expect(result.finalized?.parts).toEqual([ + { type: "text", text: "最终内容" }, + ]) + expect(result.duplicateFinalize).toBeNull() + } finally { + await deleteUser(ownerId) + } + }) + + it("事务内拒绝跨 Project Fork 与 Artifact provenance", async () => { + const ownerId = await createUser() + try { + const first = await createProjectWithRoot(ownerId) + const second = await createProjectWithRoot(ownerId) + const source = await unitOfWork.transaction((repositories) => + repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: first.root.id, + role: "user", + parts: [{ type: "text", text: "来源" }], + finalizedAt: now, + }) + ) + + await expect( + unitOfWork.transaction((repositories) => + repositories.threads.insertBranch({ + actorId: ownerId, + id: randomUUID(), + projectId: second.project.id, + parentThreadId: second.root.id, + sourceMessageId: source.id, + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "user", + sourceSequence: source.sequence, + }, + baseContext: { schemaVersion: 1, messageIds: [source.id] }, + }) + ) + ).rejects.toMatchObject({ code: "thread_source_invalid" }) + + await expect( + unitOfWork.transaction((repositories) => + repositories.artifacts.insert({ + actorId: ownerId, + id: randomUUID(), + projectId: second.project.id, + sourceMessageId: source.id, + kind: "markdown", + title: "非法跨 Project Artifact", + content: "# invalid", + }) + ) + ).rejects.toMatchObject({ code: "artifact_provenance_invalid" }) + + const childCount = await sql<{ count: number }[]>` + select count(*)::integer as count + from thread_chat.threads + where project_id = ${second.project.id} + ` + expect(childCount[0].count).toBe(1) + } finally { + await deleteUser(ownerId) + } + }) + + it("assistant Message 只有一个 Run,并持久化状态机、checkpoint 与 Stop", async () => { + const ownerId = await createUser() + try { + const { root } = await createProjectWithRoot(ownerId) + const result = await unitOfWork.transaction(async (repositories) => { + const assistant = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "assistant", + parts: null, + finalizedAt: null, + }) + const queued = await repositories.messageRuns.insertQueued({ + actorId: ownerId, + id: randomUUID(), + assistantMessageId: assistant.id, + modelId: "fake/test-model", + }) + return { assistant, queued } + }) + + await expect( + unitOfWork.transaction((repositories) => + repositories.messageRuns.insertQueued({ + actorId: ownerId, + id: randomUUID(), + assistantMessageId: result.assistant.id, + modelId: "fake/test-model", + }) + ) + ).rejects.toMatchObject({ code: "23505" }) + + const running = await unitOfWork.transaction((repositories) => + repositories.messageRuns.transition({ + actorId: ownerId, + messageRunId: result.queued.id, + expectedStatus: "queued", + nextStatus: "running", + }) + ) + const checkpoint = await unitOfWork.transaction((repositories) => + repositories.messageRuns.checkpoint({ + actorId: ownerId, + messageRunId: result.queued.id, + expectedEventSequence: 0, + checkpointParts: [{ type: "text", text: "部分内容" }], + heartbeatAt: now, + }) + ) + const stopped = await unitOfWork.transaction((repositories) => + repositories.messageRuns.requestStop(ownerId, result.assistant.id, now) + ) + + expect(running?.status).toBe("running") + expect(checkpoint).toMatchObject({ + eventSequence: 1, + checkpointParts: [{ type: "text", text: "部分内容" }], + }) + expect(stopped?.stopRequestedAt).toEqual(now) + } finally { + await deleteUser(ownerId) + } + }) + + it("Artifact 分配单调 changeSequence,feedback 只接受合格 assistant", async () => { + const ownerId = await createUser() + try { + const { project, root } = await createProjectWithRoot(ownerId) + const { assistant, user } = await unitOfWork.transaction( + async (repositories) => { + const user = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "user", + parts: [{ type: "text", text: "用户" }], + finalizedAt: now, + }) + const assistant = await repositories.messages.append({ + actorId: ownerId, + id: randomUUID(), + threadId: root.id, + role: "assistant", + parts: [{ type: "text", text: "回答" }], + finalizedAt: now, + }) + const queued = await repositories.messageRuns.insertQueued({ + actorId: ownerId, + id: randomUUID(), + assistantMessageId: assistant.id, + modelId: "fake/test-model", + }) + await repositories.messageRuns.transition({ + actorId: ownerId, + messageRunId: queued.id, + expectedStatus: "queued", + nextStatus: "running", + }) + await repositories.messageRuns.transition({ + actorId: ownerId, + messageRunId: queued.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: now, + }) + return { assistant, user } + } + ) + + const artifacts = await unitOfWork.transaction(async (repositories) => [ + await repositories.artifacts.insert({ + actorId: ownerId, + id: randomUUID(), + projectId: project.id, + sourceMessageId: assistant.id, + kind: "markdown", + title: "A", + content: "# A", + }), + await repositories.artifacts.insert({ + actorId: ownerId, + id: randomUUID(), + projectId: project.id, + sourceMessageId: assistant.id, + kind: "markdown", + title: "B", + content: "# B", + }), + ]) + expect(artifacts.map((artifact) => artifact.changeSequence)).toEqual([ + 1, 2, + ]) + expect( + await unitOfWork.transaction((repositories) => + repositories.feedback.set({ + actorId: ownerId, + assistantMessageId: assistant.id, + feedback: "positive", + }) + ) + ).toBe("positive") + await expect( + unitOfWork.transaction((repositories) => + repositories.feedback.set({ + actorId: ownerId, + assistantMessageId: user.id, + feedback: "positive", + }) + ) + ).rejects.toMatchObject({ code: "feedback_not_eligible" }) + } finally { + await deleteUser(ownerId) + } + }) +}) diff --git a/tests/unit/domain-model.test.ts b/tests/unit/domain-model.test.ts new file mode 100644 index 00000000..e8dd3d53 --- /dev/null +++ b/tests/unit/domain-model.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, it } from "vitest" +import { + resolveBaseContextMessages, + validateBaseContext, +} from "@/lib/thread-chat/domain/base-context" +import { + assertMessageCanBeReplaced, + assertMessageForkEligible, + selectEffectiveMessages, + type Message, +} from "@/lib/thread-chat/domain/message" +import { + assertMessageRunTransition, + nextEventSequence, + type MessageRun, +} from "@/lib/thread-chat/domain/message-run" +import { + validateThreadTopology, + type Thread, +} from "@/lib/thread-chat/domain/thread" +import { + assertArtifactProvenance, + toMarkdownArtifactToolOutput, +} from "@/lib/thread-chat/domain/artifact" + +const now = new Date("2026-01-01T00:00:00.000Z") + +function message(overrides: Partial = {}): Message { + return { + id: "message-1", + threadId: "root", + sequence: 1, + role: "user", + parts: [{ type: "text", text: "测试" }], + replacesMessageId: null, + supersededAt: null, + finalizedAt: now, + createdAt: now, + ...overrides, + } +} + +function run(overrides: Partial = {}): MessageRun { + return { + id: "run-1", + assistantMessageId: "message-1", + status: "completed", + modelId: "fake/test-model", + eventSequence: 0, + checkpointParts: [], + errorCode: null, + errorMessage: null, + heartbeatAt: null, + stopRequestedAt: null, + finishedAt: now, + createdAt: now, + updatedAt: now, + ...overrides, + } +} + +function thread(overrides: Partial = {}): Thread { + return { + id: "root", + projectId: "project-1", + parentThreadId: null, + sourceMessageId: null, + forkSourceSnapshot: null, + baseContext: null, + autoTitle: null, + customTitle: null, + archivedAt: null, + createdAt: now, + updatedAt: now, + ...overrides, + } +} + +describe("BaseContext", () => { + it("验证版本、去重并按 messageIds 顺序解析", () => { + const first = message({ id: "first", sequence: 1 }) + const second = message({ id: "second", sequence: 2 }) + const context = validateBaseContext({ + schemaVersion: 1, + messageIds: ["second", "first"], + }) + + expect( + resolveBaseContextMessages( + context, + new Map([ + [first.id, first], + [second.id, second], + ]) + ).map((entry) => entry.id) + ).toEqual(["second", "first"]) + expect(() => + validateBaseContext({ schemaVersion: 1, messageIds: ["first", "first"] }) + ).toThrow(/不得重复/) + }) +}) + +describe("Thread topology", () => { + it("接受唯一 Root 与同 Project 的嵌套 Branch", () => { + const source = message({ id: "source", threadId: "root" }) + const child = thread({ + id: "child", + parentThreadId: "root", + sourceMessageId: source.id, + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "user", + sourceSequence: 1, + }, + baseContext: { schemaVersion: 1, messageIds: [source.id] }, + }) + + expect(() => + validateThreadTopology( + "project-1", + [thread(), child], + new Map([[source.id, source]]) + ) + ).not.toThrow() + }) + + it("拒绝跨 Project Parent/source 与环", () => { + const invalidChild = thread({ + id: "child", + parentThreadId: "root", + sourceMessageId: "source", + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "user", + sourceSequence: 1, + }, + baseContext: { schemaVersion: 1, messageIds: [] }, + }) + expect(() => + validateThreadTopology( + "project-1", + [thread(), invalidChild], + new Map([["source", message({ id: "source", threadId: "other" })]]) + ) + ).toThrow(/Parent Thread/) + + const branchA = thread({ + id: "a", + parentThreadId: "b", + sourceMessageId: "source-b", + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "user", + sourceSequence: 1, + }, + baseContext: { schemaVersion: 1, messageIds: [] }, + }) + const branchB = thread({ + id: "b", + parentThreadId: "a", + sourceMessageId: "source-a", + forkSourceSnapshot: { + schemaVersion: 1, + sourceRole: "user", + sourceSequence: 1, + }, + baseContext: { schemaVersion: 1, messageIds: [] }, + }) + expect(() => + validateThreadTopology( + "project-1", + [thread(), branchA, branchB], + new Map([ + ["source-a", message({ id: "source-a", threadId: "a" })], + ["source-b", message({ id: "source-b", threadId: "b" })], + ]) + ) + ).toThrow(/不得形成环/) + }) +}) + +describe("Message replacement 与 Fork", () => { + it("replacement 保持来源不可变并追加到有效时间线", () => { + const source = message({ id: "source", sequence: 1, supersededAt: now }) + const replacement = message({ + id: "replacement", + sequence: 3, + replacesMessageId: source.id, + }) + expect( + selectEffectiveMessages([replacement, source]).map((item) => item.id) + ).toEqual(["replacement"]) + + const activeSource = { ...source, supersededAt: null } + expect(() => + assertMessageCanBeReplaced(activeSource, replacement) + ).not.toThrow() + expect(() => + assertMessageCanBeReplaced(activeSource, { + ...replacement, + threadId: "other", + }) + ).toThrow(/同 Thread/) + }) + + it("只允许 completed assistant 或 finalized user 作为 Fork source", () => { + const assistant = message({ role: "assistant" }) + expect(() => assertMessageForkEligible(assistant, run())).not.toThrow() + expect(() => + assertMessageForkEligible(assistant, run({ status: "running" })) + ).toThrow(/completed/) + expect(() => assertMessageForkEligible(message(), null)).not.toThrow() + }) +}) + +describe("MessageRun 状态机", () => { + it("只允许条件状态转换和非负 eventSequence", () => { + expect(() => assertMessageRunTransition("queued", "running")).not.toThrow() + expect(() => + assertMessageRunTransition("running", "completed") + ).not.toThrow() + expect(() => assertMessageRunTransition("completed", "running")).toThrow( + /不允许/ + ) + expect(nextEventSequence(7)).toBe(8) + expect(() => nextEventSequence(-1)).toThrow(/非负/) + }) +}) + +describe("Artifact provenance", () => { + it("只投影 artifactId,并拒绝跨 Project 来源", () => { + const source = message() + const root = thread() + const artifact = { + id: "artifact-1", + projectId: root.projectId, + sourceMessageId: source.id, + } + expect(() => assertArtifactProvenance(artifact, source, root)).not.toThrow() + expect(toMarkdownArtifactToolOutput(artifact)).toEqual({ + artifactId: "artifact-1", + }) + expect(() => + assertArtifactProvenance( + { ...artifact, projectId: "other-project" }, + source, + root + ) + ).toThrow(/同一 Project/) + }) +}) From 3237929d271f53f081809811b2d26956c51d1f5b Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:23:45 +0800 Subject: [PATCH 10/23] feat(thread-chat): implement application commands and queries --- .../application/application-types.ts | 72 +++ lib/thread-chat/application/prompt-history.ts | 55 +++ .../application/thread-chat-commands.ts | 430 ++++++++++++++++++ .../application/thread-chat-queries.ts | 118 +++++ lib/thread-chat/domain/domain-error.ts | 8 + .../repositories/artifact-repository.ts | 26 ++ .../infrastructure/repositories/index.ts | 1 + .../repositories/message-repository.ts | 135 +++++- .../repositories/message-run-repository.ts | 66 +++ .../repositories/project-repository.ts | 93 ++++ .../repositories/thread-repository.ts | 66 +++ .../define-thread-chat-domain-model/tasks.md | 16 +- tests/integration/application.test.ts | 331 ++++++++++++++ 13 files changed, 1408 insertions(+), 9 deletions(-) create mode 100644 lib/thread-chat/application/application-types.ts create mode 100644 lib/thread-chat/application/prompt-history.ts create mode 100644 lib/thread-chat/application/thread-chat-commands.ts create mode 100644 lib/thread-chat/application/thread-chat-queries.ts create mode 100644 tests/integration/application.test.ts diff --git a/lib/thread-chat/application/application-types.ts b/lib/thread-chat/application/application-types.ts new file mode 100644 index 00000000..b9419247 --- /dev/null +++ b/lib/thread-chat/application/application-types.ts @@ -0,0 +1,72 @@ +import type { UIMessage } from "ai" +import type { Artifact } from "../domain/artifact" +import type { Message } from "../domain/message" +import type { MessageRun } from "../domain/message-run" +import type { Project, ProjectTarget } from "../domain/project" +import type { Thread } from "../domain/thread" + +export type IdGenerator = () => string + +export type ThreadChatApplicationDependencies = { + generateId: IdGenerator + now: () => Date + resolveModelId: (requestedModelId?: string) => string +} + +export type ProjectArtifactSummary = { + changeSequence: number + total: number + byKind: Record +} + +export type ThreadMessageBundle = { + threadId: string + messages: Message[] + assistantRuns: MessageRun[] + hasOlderMessages: boolean + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} + +export type CreationBundle = { + project: Project + rootThread: Thread + artifactSummary: ProjectArtifactSummary + userMessage: Message + assistantMessage: Message + assistantRun: MessageRun +} + +export type MessageCreationBundle = Pick< + CreationBundle, + "userMessage" | "assistantMessage" | "assistantRun" +> + +export type ReplacementBundle = { + supersededMessageIds: string[] + createdMessages: Message[] + assistantRun: MessageRun +} + +export type ProjectPatch = { + customTitle?: string | null + target?: ProjectTarget | null + instruction?: string | null +} + +export type UserMessageInput = UIMessage["parts"] + +export type ProjectBootstrap = { + project: Project + threadTopology: Thread[] + artifactSummary: ProjectArtifactSummary + initialThread: ThreadMessageBundle +} + +export type ProjectSummary = Project & { + displayTitle: string + threadCount: number + messageCount: number +} + +export type ArtifactResult = Artifact diff --git a/lib/thread-chat/application/prompt-history.ts b/lib/thread-chat/application/prompt-history.ts new file mode 100644 index 00000000..af303d74 --- /dev/null +++ b/lib/thread-chat/application/prompt-history.ts @@ -0,0 +1,55 @@ +import { invariant } from "../domain/domain-error" +import type { Message } from "../domain/message" +import { + createThreadChatRepositories, + type ThreadChatSql, +} from "../infrastructure/repositories" + +export async function loadPromptHistory( + sql: ThreadChatSql, + input: { actorId: string; threadId: string } +): Promise { + const repositories = createThreadChatRepositories(sql) + const thread = await repositories.threads.findOwnedById( + input.actorId, + input.threadId + ) + invariant(thread, "entity_not_found", "Thread 不存在。") + const baseIds = thread.baseContext?.messageIds ?? [] + const [baseMessages, currentMessages] = await Promise.all([ + repositories.messages.listByIdsOwned(input.actorId, baseIds), + repositories.messages.listEffectiveOwned(input.actorId, thread.id, 100_000), + ]) + const byId = new Map(baseMessages.map((message) => [message.id, message])) + const orderedBase = baseIds.map((messageId) => { + const message = byId.get(messageId) + invariant( + message, + "base_context_message_missing", + `BaseContext 引用的 Message ${messageId} 不存在。` + ) + return message + }) + const candidates = [...orderedBase, ...currentMessages] + const assistantIds = candidates + .filter((message) => message.role === "assistant") + .map((message) => message.id) + const runs = await repositories.messageRuns.findOwnedByAssistantMessageIds( + input.actorId, + assistantIds + ) + const completedAssistantIds = new Set( + runs + .filter((run) => run.status === "completed") + .map((run) => run.assistantMessageId) + ) + const seen = new Set() + return candidates.filter((message) => { + if (seen.has(message.id)) return false + seen.add(message.id) + if (message.finalizedAt === null) return false + return ( + message.role === "user" || completedAssistantIds.has(message.id) + ) + }) +} diff --git a/lib/thread-chat/application/thread-chat-commands.ts b/lib/thread-chat/application/thread-chat-commands.ts new file mode 100644 index 00000000..53442d00 --- /dev/null +++ b/lib/thread-chat/application/thread-chat-commands.ts @@ -0,0 +1,430 @@ +import { invariant } from "../domain/domain-error" +import type { UserId } from "../domain/ids" +import { assertMessageForkEligible } from "../domain/message" +import type { + ThreadChatRepositories, + ThreadChatUnitOfWork, +} from "../infrastructure/repositories" +import type { + CreationBundle, + MessageCreationBundle, + ProjectPatch, + ReplacementBundle, + ThreadChatApplicationDependencies, + UserMessageInput, +} from "./application-types" + +export class ThreadChatCommands { + constructor( + private readonly unitOfWork: ThreadChatUnitOfWork, + private readonly dependencies: ThreadChatApplicationDependencies + ) {} + + createProject(input: { + actorId: UserId + parts: UserMessageInput + requestedModelId?: string + }): Promise { + const modelId = this.dependencies.resolveModelId(input.requestedModelId) + return this.unitOfWork.transaction(async (repositories) => { + const project = await repositories.projects.insert({ + id: this.dependencies.generateId(), + ownerUserId: input.actorId, + }) + const rootThread = await repositories.threads.insertRoot({ + actorId: input.actorId, + id: this.dependencies.generateId(), + projectId: project.id, + }) + const created = await this.appendTurn(repositories, { + actorId: input.actorId, + threadId: rootThread.id, + parts: input.parts, + modelId, + threadAlreadyLocked: true, + }) + return { + project, + rootThread, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + ...created, + } + }) + } + + sendMessage(input: { + actorId: UserId + threadId: string + parts: UserMessageInput + requestedModelId?: string + }): Promise { + const modelId = this.dependencies.resolveModelId(input.requestedModelId) + return this.unitOfWork.transaction((repositories) => + this.appendTurn(repositories, { ...input, modelId }) + ) + } + + forkThread(input: { + actorId: UserId + sourceThreadId: string + sourceMessageId: string + anchor?: { + exactQuote: string + textPosition?: { start: number; end: number } + } + }) { + return this.unitOfWork.transaction(async (repositories) => { + const parent = await repositories.threads.findOwnedByIdForUpdate( + input.actorId, + input.sourceThreadId + ) + invariant(parent, "entity_not_found", "Parent Thread 不存在。") + const source = await repositories.messages.findOwnedByIdForUpdate( + input.actorId, + input.sourceMessageId + ) + invariant(source, "entity_not_found", "Fork source Message 不存在。") + invariant( + source.threadId === parent.id, + "thread_source_invalid", + "Fork source 不属于指定 Parent Thread。" + ) + const sourceRun = + source.role === "assistant" + ? await repositories.messageRuns.findOwnedByAssistantMessageIdForUpdate( + input.actorId, + source.id + ) + : null + assertMessageForkEligible(source, sourceRun) + if (input.anchor?.textPosition) { + const sourceText = (source.parts ?? []) + .filter( + (part): part is Extract => + part.type === "text" + ) + .map((part) => part.text) + .join("\n") + const { start, end } = input.anchor.textPosition + invariant( + start >= 0 && + end > start && + sourceText.slice(start, end) === input.anchor.exactQuote, + "fork_anchor_mismatch", + "Fork anchor 与来源 Message 的规范文本投影不一致。" + ) + } + + const inheritedIds = parent.baseContext?.messageIds ?? [] + const current = await repositories.messages.listEffectiveOwned( + input.actorId, + parent.id, + 100_000 + ) + const currentAssistantIds = current + .filter((message) => message.role === "assistant") + .map((message) => message.id) + const currentRuns = + await repositories.messageRuns.findOwnedByAssistantMessageIds( + input.actorId, + currentAssistantIds + ) + const completedAssistantIds = new Set( + currentRuns + .filter((run) => run.status === "completed") + .map((run) => run.assistantMessageId) + ) + const eligibleIds = current + .filter( + (message) => + message.sequence <= source.sequence && + message.finalizedAt !== null && + (message.role === "user" || + completedAssistantIds.has(message.id)) + ) + .map((message) => message.id) + const baseContext = { + schemaVersion: 1 as const, + messageIds: [...new Set([...inheritedIds, ...eligibleIds])], + } + return repositories.threads.insertBranch({ + actorId: input.actorId, + id: this.dependencies.generateId(), + projectId: parent.projectId, + parentThreadId: parent.id, + sourceMessageId: source.id, + forkSourceSnapshot: { + schemaVersion: 1, + ...(input.anchor === undefined + ? {} + : { quote: input.anchor.exactQuote }), + sourceRole: source.role, + sourceSequence: source.sequence, + }, + baseContext, + }) + }) + } + + regenerate(input: { + actorId: UserId + sourceAssistantMessageId: string + requestedModelId?: string + }): Promise { + const modelId = this.dependencies.resolveModelId(input.requestedModelId) + return this.unitOfWork.transaction(async (repositories) => { + const found = await repositories.messages.findOwnedById( + input.actorId, + input.sourceAssistantMessageId + ) + invariant(found, "entity_not_found", "Message 不存在。") + await repositories.threads.findOwnedByIdForUpdate( + input.actorId, + found.threadId + ) + const source = await repositories.messages.findOwnedByIdForUpdate( + input.actorId, + found.id + ) + invariant(source, "entity_not_found", "Message 不存在。") + invariant( + source.role === "assistant" && + source.finalizedAt !== null && + source.supersededAt === null, + "message_not_regeneratable", + "Message 不满足 Regenerate 资格。" + ) + invariant( + await repositories.messages.isLastEffective(source), + "fork_required", + "历史位置需要通过 Fork 保留另一条路线。" + ) + const run = + await repositories.messageRuns.findOwnedByAssistantMessageIdForUpdate( + input.actorId, + source.id + ) + invariant( + run?.status === "completed", + "message_not_regeneratable", + "只有 completed assistant Message 可以 Regenerate。" + ) + await repositories.messageRuns.assertNoActiveForThread( + input.actorId, + source.threadId + ) + const replacement = await repositories.messages.append({ + actorId: input.actorId, + id: this.dependencies.generateId(), + threadId: source.threadId, + role: "assistant", + parts: null, + finalizedAt: null, + replacesMessageId: source.id, + }) + const assistantRun = await repositories.messageRuns.insertQueued({ + actorId: input.actorId, + id: this.dependencies.generateId(), + assistantMessageId: replacement.id, + modelId, + }) + return { + supersededMessageIds: [source.id], + createdMessages: [replacement], + assistantRun, + } + }) + } + + editLastUser(input: { + actorId: UserId + sourceUserMessageId: string + parts: UserMessageInput + requestedModelId?: string + }): Promise { + const modelId = this.dependencies.resolveModelId(input.requestedModelId) + return this.unitOfWork.transaction(async (repositories) => { + const found = await repositories.messages.findOwnedById( + input.actorId, + input.sourceUserMessageId + ) + invariant(found, "entity_not_found", "Message 不存在。") + await repositories.threads.findOwnedByIdForUpdate( + input.actorId, + found.threadId + ) + const source = await repositories.messages.findOwnedByIdForUpdate( + input.actorId, + found.id + ) + invariant(source, "entity_not_found", "Message 不存在。") + invariant( + source.role === "user" && + source.finalizedAt !== null && + source.supersededAt === null, + "message_not_editable", + "Message 不满足 Edit 资格。" + ) + invariant( + await repositories.messages.isLastEffectiveUser(source), + "fork_required", + "只有最后一条有效 user Message 可以 Edit。" + ) + await repositories.messageRuns.assertNoActiveForThread( + input.actorId, + source.threadId + ) + const replacementUser = await repositories.messages.append({ + actorId: input.actorId, + id: this.dependencies.generateId(), + threadId: source.threadId, + role: "user", + parts: input.parts, + finalizedAt: this.dependencies.now(), + replacesMessageId: source.id, + }) + const suffixIds = await repositories.messages.supersedeEffectiveRange({ + actorId: input.actorId, + threadId: source.threadId, + afterSequence: source.sequence, + beforeSequence: replacementUser.sequence, + supersededAt: this.dependencies.now(), + }) + const replacementAssistant = await repositories.messages.append({ + actorId: input.actorId, + id: this.dependencies.generateId(), + threadId: source.threadId, + role: "assistant", + parts: null, + finalizedAt: null, + }) + const assistantRun = await repositories.messageRuns.insertQueued({ + actorId: input.actorId, + id: this.dependencies.generateId(), + assistantMessageId: replacementAssistant.id, + modelId, + }) + return { + supersededMessageIds: [source.id, ...suffixIds], + createdMessages: [replacementUser, replacementAssistant], + assistantRun, + } + }) + } + + patchProject(input: { + actorId: UserId + projectId: string + patch: ProjectPatch + }) { + return this.unitOfWork.transaction(async (repositories) => { + const project = await repositories.projects.updateMetadata({ + actorId: input.actorId, + projectId: input.projectId, + ...input.patch, + }) + invariant(project, "entity_not_found", "Project 不存在。") + return project + }) + } + + setProjectArchived(input: { + actorId: UserId + projectId: string + archived: boolean + }) { + return this.unitOfWork.transaction(async (repositories) => { + const project = await repositories.projects.setArchived({ + ...input, + now: this.dependencies.now(), + }) + invariant(project, "entity_not_found", "Project 不存在。") + return project + }) + } + + patchBranch(input: { + actorId: UserId + threadId: string + customTitle?: string | null + archived?: boolean + }) { + return this.unitOfWork.transaction(async (repositories) => { + const thread = await repositories.threads.updateBranchMetadata({ + ...input, + now: this.dependencies.now(), + }) + invariant(thread, "entity_not_found", "Thread 不存在。") + return thread + }) + } + + setFeedback(input: { + actorId: UserId + assistantMessageId: string + feedback: "positive" | "negative" | null + }) { + return this.unitOfWork.transaction(async (repositories) => ({ + messageId: input.assistantMessageId, + value: await repositories.feedback.set(input), + updatedAt: this.dependencies.now(), + })) + } + + deleteProject(input: { actorId: UserId; projectId: string }): Promise { + return this.unitOfWork.transaction(async (repositories) => { + const deleted = await repositories.projects.deleteOwned( + input.actorId, + input.projectId + ) + invariant(deleted, "entity_not_found", "Project 不存在。") + }) + } + + private async appendTurn( + repositories: ThreadChatRepositories, + input: { + actorId: UserId + threadId: string + parts: UserMessageInput + modelId: string + threadAlreadyLocked?: boolean + } + ): Promise { + const thread = input.threadAlreadyLocked + ? await repositories.threads.findOwnedById(input.actorId, input.threadId) + : await repositories.threads.findOwnedByIdForUpdate( + input.actorId, + input.threadId + ) + invariant(thread, "entity_not_found", "Thread 不存在。") + invariant(!thread.archivedAt, "thread_archived", "已归档 Thread 不能发送消息。") + await repositories.messageRuns.assertNoActiveForThread( + input.actorId, + input.threadId + ) + const userMessage = await repositories.messages.append({ + actorId: input.actorId, + id: this.dependencies.generateId(), + threadId: input.threadId, + role: "user", + parts: input.parts, + finalizedAt: this.dependencies.now(), + }) + const assistantMessage = await repositories.messages.append({ + actorId: input.actorId, + id: this.dependencies.generateId(), + threadId: input.threadId, + role: "assistant", + parts: null, + finalizedAt: null, + }) + const assistantRun = await repositories.messageRuns.insertQueued({ + actorId: input.actorId, + id: this.dependencies.generateId(), + assistantMessageId: assistantMessage.id, + modelId: input.modelId, + }) + return { userMessage, assistantMessage, assistantRun } + } +} diff --git a/lib/thread-chat/application/thread-chat-queries.ts b/lib/thread-chat/application/thread-chat-queries.ts new file mode 100644 index 00000000..1491eee1 --- /dev/null +++ b/lib/thread-chat/application/thread-chat-queries.ts @@ -0,0 +1,118 @@ +import { invariant } from "../domain/domain-error" +import { getProjectDisplayTitle } from "../domain/project" +import { + createThreadChatRepositories, + type ThreadChatSql, +} from "../infrastructure/repositories" +import type { + ProjectBootstrap, + ProjectSummary, + ThreadMessageBundle, +} from "./application-types" + +export class ThreadChatQueries { + constructor(private readonly sql: ThreadChatSql) {} + + async listProjects(input: { + actorId: string + status?: "active" | "archived" | "all" + limit?: number + before?: { updatedAt: Date; id: string } + }): Promise { + const repositories = createThreadChatRepositories(this.sql) + const projects = await repositories.projects.listOwned({ + actorId: input.actorId, + status: input.status ?? "active", + limit: input.limit ?? 50, + before: input.before, + }) + return projects.map((project) => ({ + ...project, + displayTitle: getProjectDisplayTitle(project), + })) + } + + async projectBootstrap(input: { + actorId: string + projectId: string + }): Promise { + const repositories = createThreadChatRepositories(this.sql) + const project = await repositories.projects.findOwnedById( + input.actorId, + input.projectId + ) + invariant(project, "entity_not_found", "Project 不存在。") + const threadTopology = await repositories.threads.listOwnedTopology( + input.actorId, + input.projectId + ) + const root = threadTopology.find((thread) => thread.parentThreadId === null) + invariant(root, "project_root_invalid", "Project 缺少唯一 Root Thread。") + const artifactSummary = await repositories.artifacts.summarizeOwnedProject( + input.actorId, + input.projectId + ) + invariant(artifactSummary, "entity_not_found", "Project 不存在。") + return { + project, + threadTopology, + artifactSummary, + initialThread: await this.threadMessages({ + actorId: input.actorId, + threadId: root.id, + }), + } + } + + async threadMessages(input: { + actorId: string + threadId: string + limit?: number + beforeSequence?: number + }): Promise { + const repositories = createThreadChatRepositories(this.sql) + const thread = await repositories.threads.findOwnedById( + input.actorId, + input.threadId + ) + invariant(thread, "entity_not_found", "Thread 不存在。") + const limit = input.limit ?? 200 + const fetched = await repositories.messages.listEffectiveWindow({ + actorId: input.actorId, + threadId: input.threadId, + beforeSequence: input.beforeSequence, + limit: limit + 1, + }) + const hasOlderMessages = fetched.length > limit + const messages = hasOlderMessages ? fetched.slice(1) : fetched + const assistantMessageIds = messages + .filter((message) => message.role === "assistant") + .map((message) => message.id) + const assistantRuns = + await repositories.messageRuns.findOwnedByAssistantMessageIds( + input.actorId, + assistantMessageIds + ) + invariant( + assistantRuns.length === assistantMessageIds.length, + "entity_not_found", + "assistant Message 缺少唯一 MessageRun。" + ) + return { + threadId: input.threadId, + messages, + assistantRuns, + hasOlderMessages, + oldestReturnedSequence: messages[0]?.sequence ?? null, + newestReturnedSequence: messages.at(-1)?.sequence ?? null, + } + } + + async artifactById(input: { actorId: string; artifactId: string }) { + const artifact = await createThreadChatRepositories( + this.sql + ).artifacts.findOwnedById(input.actorId, input.artifactId) + invariant(artifact, "entity_not_found", "Artifact 不存在。") + return artifact + } +} diff --git a/lib/thread-chat/domain/domain-error.ts b/lib/thread-chat/domain/domain-error.ts index 4d943ab0..ed5ba127 100644 --- a/lib/thread-chat/domain/domain-error.ts +++ b/lib/thread-chat/domain/domain-error.ts @@ -14,6 +14,14 @@ export type ThreadChatDomainErrorCode = | "message_run_transition_invalid" | "artifact_provenance_invalid" | "feedback_not_eligible" + | "thread_archived" + | "thread_generation_in_progress" + | "root_thread_title_owned_by_project" + | "root_thread_archive_owned_by_project" + | "message_not_editable" + | "message_not_regeneratable" + | "fork_required" + | "fork_anchor_mismatch" | "entity_not_found" export class ThreadChatDomainError extends Error { diff --git a/lib/thread-chat/infrastructure/repositories/artifact-repository.ts b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts index 407e5047..669a0546 100644 --- a/lib/thread-chat/infrastructure/repositories/artifact-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts @@ -133,4 +133,30 @@ export class ArtifactRepository { ` return row ? mapArtifact(row) : null } + + async summarizeOwnedProject(actorId: UserId, projectId: ProjectId): Promise<{ + changeSequence: number + total: number + byKind: Record + } | null> { + const rows = await this.sql<{ changeSequence: number; kind: string | null; count: number }[]>` + select + p.artifact_change_sequence::integer as "changeSequence", + a.kind, + count(a.id)::integer as count + from thread_chat.projects p + left join thread_chat.artifacts a on a.project_id = p.id + where p.id = ${projectId} and p.owner_user_id = ${actorId} + group by p.id, p.artifact_change_sequence, a.kind + ` + if (rows.length === 0) return null + const byKind = Object.fromEntries( + rows.filter((row) => row.kind !== null).map((row) => [row.kind!, Number(row.count)]) + ) + return { + changeSequence: Number(rows[0].changeSequence), + total: Object.values(byKind).reduce((sum, count) => sum + count, 0), + byKind, + } + } } diff --git a/lib/thread-chat/infrastructure/repositories/index.ts b/lib/thread-chat/infrastructure/repositories/index.ts index 9ad64624..6fec6db5 100644 --- a/lib/thread-chat/infrastructure/repositories/index.ts +++ b/lib/thread-chat/infrastructure/repositories/index.ts @@ -40,3 +40,4 @@ export { MessageRepository } from "./message-repository" export { MessageRunRepository } from "./message-run-repository" export { ProjectRepository } from "./project-repository" export { ThreadRepository } from "./thread-repository" +export type { ThreadChatSql } from "./database" diff --git a/lib/thread-chat/infrastructure/repositories/message-repository.ts b/lib/thread-chat/infrastructure/repositories/message-repository.ts index d0eca030..92b8e903 100644 --- a/lib/thread-chat/infrastructure/repositories/message-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/message-repository.ts @@ -55,12 +55,145 @@ export class MessageRepository { and p.owner_user_id = ${actorId} order by m.sequence desc limit ${limit} - ) window + ) message_window order by sequence asc ` return rows.map(mapMessage) } + async listEffectiveWindow(input: { + actorId: UserId + threadId: ThreadId + limit: number + beforeSequence?: number + }): Promise { + const rows = await this.sql[]>` + select * from ( + select + m.id, m.thread_id as "threadId", m.sequence, m.role, m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.thread_id = ${input.threadId} + and m.superseded_at is null + and p.owner_user_id = ${input.actorId} + and (${input.beforeSequence ?? null}::integer is null or m.sequence < ${input.beforeSequence ?? null}) + order by m.sequence desc + limit ${input.limit} + ) message_window + order by sequence asc + ` + return rows.map(mapMessage) + } + + async listByIdsOwned( + actorId: UserId, + messageIds: readonly MessageId[] + ): Promise { + if (messageIds.length === 0) return [] + const rows = await this.sql[]>` + select + m.id, m.thread_id as "threadId", m.sequence, m.role, m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id in ${this.sql(messageIds)} and p.owner_user_id = ${actorId} + ` + return rows.map(mapMessage) + } + + async findOwnedByIdForUpdate( + actorId: UserId, + messageId: MessageId + ): Promise { + const [row] = await this.sql[]>` + select + m.id, m.thread_id as "threadId", m.sequence, m.role, m.parts, + m.replaces_message_id as "replacesMessageId", + m.superseded_at as "supersededAt", m.finalized_at as "finalizedAt", + m.created_at as "createdAt" + from thread_chat.messages m + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where m.id = ${messageId} and p.owner_user_id = ${actorId} + for update of m + ` + return row ? mapMessage(row) : null + } + + async isLastEffective(message: Message): Promise { + const [row] = await this.sql` + select 1 from thread_chat.messages + where thread_id = ${message.threadId} + and superseded_at is null + and sequence > ${message.sequence} + limit 1 + ` + return !row + } + + async isLastEffectiveUser(message: Message): Promise { + const [row] = await this.sql` + select 1 from thread_chat.messages + where thread_id = ${message.threadId} + and role = 'user' + and superseded_at is null + and sequence > ${message.sequence} + limit 1 + ` + return !row + } + + async supersedeEffectiveSuffix(input: { + actorId: UserId + threadId: ThreadId + fromSequence: number + supersededAt: Date + }): Promise { + const rows = await this.sql<{ id: string }[]>` + update thread_chat.messages m + set superseded_at = ${input.supersededAt} + from thread_chat.threads t, thread_chat.projects p + where m.thread_id = ${input.threadId} + and m.sequence >= ${input.fromSequence} + and m.superseded_at is null + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning m.id + ` + return rows.map((row) => row.id) + } + + async supersedeEffectiveRange(input: { + actorId: UserId + threadId: ThreadId + afterSequence: number + beforeSequence: number + supersededAt: Date + }): Promise { + const rows = await this.sql<{ id: string }[]>` + update thread_chat.messages m + set superseded_at = ${input.supersededAt} + from thread_chat.threads t, thread_chat.projects p + where m.thread_id = ${input.threadId} + and m.sequence > ${input.afterSequence} + and m.sequence < ${input.beforeSequence} + and m.superseded_at is null + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning m.id + ` + return rows.map((row) => row.id) + } + /** 锁定 Thread 后分配 sequence,并在同一事务完成 insert。 */ async append(input: { actorId: UserId diff --git a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts index f1a9e40f..bafb653c 100644 --- a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts @@ -85,6 +85,72 @@ export class MessageRunRepository { return row ? mapMessageRun(row) : null } + async findOwnedByAssistantMessageIdForUpdate( + actorId: UserId, + assistantMessageId: MessageId + ): Promise { + const [row] = await this.sql[]>` + select + r.id, r.assistant_message_id as "assistantMessageId", r.status, + r.model_id as "modelId", r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", r.error_code as "errorCode", + r.error_message as "errorMessage", r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", r.finished_at as "finishedAt", + r.created_at as "createdAt", r.updated_at as "updatedAt" + from thread_chat.message_runs r + join thread_chat.messages m on m.id = r.assistant_message_id + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where r.assistant_message_id = ${assistantMessageId} + and p.owner_user_id = ${actorId} + for update of r + ` + return row ? mapMessageRun(row) : null + } + + async findOwnedByAssistantMessageIds( + actorId: UserId, + assistantMessageIds: readonly MessageId[] + ): Promise { + if (assistantMessageIds.length === 0) return [] + const rows = await this.sql[]>` + select + r.id, r.assistant_message_id as "assistantMessageId", r.status, + r.model_id as "modelId", r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", r.error_code as "errorCode", + r.error_message as "errorMessage", r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", r.finished_at as "finishedAt", + r.created_at as "createdAt", r.updated_at as "updatedAt" + from thread_chat.message_runs r + join thread_chat.messages m on m.id = r.assistant_message_id + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where r.assistant_message_id in ${this.sql(assistantMessageIds)} + and p.owner_user_id = ${actorId} + ` + return rows.map(mapMessageRun) + } + + async assertNoActiveForThread(actorId: UserId, threadId: string): Promise { + const [row] = await this.sql` + select r.id + from thread_chat.threads t + join thread_chat.projects p on p.id = t.project_id + join thread_chat.messages m on m.thread_id = t.id + join thread_chat.message_runs r on r.assistant_message_id = m.id + where t.id = ${threadId} + and p.owner_user_id = ${actorId} + and r.status in ('queued', 'running') + for update of r + limit 1 + ` + invariant( + !row, + "thread_generation_in_progress", + "Thread 已有 queued 或 running MessageRun。" + ) + } + async transition(input: { actorId: UserId messageRunId: MessageRunId diff --git a/lib/thread-chat/infrastructure/repositories/project-repository.ts b/lib/thread-chat/infrastructure/repositories/project-repository.ts index 3a60e58d..81da07f8 100644 --- a/lib/thread-chat/infrastructure/repositories/project-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/project-repository.ts @@ -62,6 +62,99 @@ export class ProjectRepository { return mapProject(row) } + async listOwned(input: { + actorId: UserId + status: "active" | "archived" | "all" + limit: number + before?: { updatedAt: Date; id: ProjectId } + }): Promise> { + const rows = await this.sql[]>` + select + p.id, + p.owner_user_id as "ownerUserId", + p.auto_title as "autoTitle", + p.custom_title as "customTitle", + p.target, + p.instruction, + p.archived_at as "archivedAt", + p.artifact_change_sequence as "artifactChangeSequence", + p.created_at as "createdAt", + p.updated_at as "updatedAt", + count(distinct t.id)::integer as "threadCount", + count(distinct m.id)::integer as "messageCount" + from thread_chat.projects p + left join thread_chat.threads t on t.project_id = p.id + left join thread_chat.messages m on m.thread_id = t.id + where p.owner_user_id = ${input.actorId} + and (${input.status} = 'all' + or (${input.status} = 'active' and p.archived_at is null) + or (${input.status} = 'archived' and p.archived_at is not null)) + and (${input.before?.updatedAt ?? null}::timestamptz is null + or (p.updated_at, p.id) < (${input.before?.updatedAt ?? null}, ${input.before?.id ?? null}::uuid)) + group by p.id + order by p.updated_at desc, p.id desc + limit ${input.limit} + ` + return rows.map((row) => ({ + ...mapProject(row), + threadCount: Number(row.threadCount), + messageCount: Number(row.messageCount), + })) + } + + async updateMetadata(input: { + actorId: UserId + projectId: ProjectId + customTitle?: string | null + target?: ProjectTarget | null + instruction?: string | null + }): Promise { + const [row] = await this.sql[]>` + update thread_chat.projects + set custom_title = case when ${input.customTitle !== undefined} then ${input.customTitle ?? null} else custom_title end, + target = case when ${input.target !== undefined} then ${input.target ? this.sql.json(input.target) : null} else target end, + instruction = case when ${input.instruction !== undefined} then ${input.instruction ?? null} else instruction end, + updated_at = now() + where id = ${input.projectId} and owner_user_id = ${input.actorId} + returning + id, owner_user_id as "ownerUserId", auto_title as "autoTitle", + custom_title as "customTitle", target, instruction, + archived_at as "archivedAt", + artifact_change_sequence as "artifactChangeSequence", + created_at as "createdAt", updated_at as "updatedAt" + ` + return row ? mapProject(row) : null + } + + async setArchived(input: { + actorId: UserId + projectId: ProjectId + archived: boolean + now: Date + }): Promise { + const [row] = await this.sql[]>` + update thread_chat.projects + set archived_at = case + when ${input.archived} then coalesce(archived_at, ${input.now}) + else null + end, + updated_at = case + when (${input.archived} and archived_at is null) + or (not ${input.archived} and archived_at is not null) + then now() + else updated_at + end + where id = ${input.projectId} and owner_user_id = ${input.actorId} + returning + id, owner_user_id as "ownerUserId", auto_title as "autoTitle", + custom_title as "customTitle", target, instruction, + archived_at as "archivedAt", + artifact_change_sequence as "artifactChangeSequence", + created_at as "createdAt", updated_at as "updatedAt" + ` + return row ? mapProject(row) : null + } + async assertOwnedForUpdate( actorId: UserId, projectId: ProjectId diff --git a/lib/thread-chat/infrastructure/repositories/thread-repository.ts b/lib/thread-chat/infrastructure/repositories/thread-repository.ts index 18059f5d..f1378809 100644 --- a/lib/thread-chat/infrastructure/repositories/thread-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/thread-repository.ts @@ -35,6 +35,25 @@ export class ThreadRepository { return row ? mapThread(row) : null } + async findOwnedByIdForUpdate( + actorId: UserId, + threadId: ThreadId + ): Promise { + const [row] = await this.sql[]>` + select + t.id, t.project_id as "projectId", t.parent_thread_id as "parentThreadId", + t.source_message_id as "sourceMessageId", t.fork_source_snapshot as "forkSourceSnapshot", + t.base_context as "baseContext", t.auto_title as "autoTitle", + t.custom_title as "customTitle", t.archived_at as "archivedAt", + t.created_at as "createdAt", t.updated_at as "updatedAt" + from thread_chat.threads t + join thread_chat.projects p on p.id = t.project_id + where t.id = ${threadId} and p.owner_user_id = ${actorId} + for update of t + ` + return row ? mapThread(row) : null + } + async listOwnedTopology( actorId: UserId, projectId: ProjectId @@ -112,6 +131,53 @@ export class ThreadRepository { }) } + async updateBranchMetadata(input: { + actorId: UserId + threadId: ThreadId + customTitle?: string | null + archived?: boolean + now: Date + }): Promise { + const current = await this.findOwnedById(input.actorId, input.threadId) + if (!current) return null + invariant( + current.parentThreadId !== null, + input.archived === undefined + ? "root_thread_title_owned_by_project" + : "root_thread_archive_owned_by_project", + "Root Thread 的 metadata 由 Project 管理。" + ) + const hasArchived = input.archived !== undefined + const archived = input.archived ?? false + const [row] = await this.sql[]>` + update thread_chat.threads t + set custom_title = case when ${input.customTitle !== undefined} then ${input.customTitle ?? null} else t.custom_title end, + archived_at = case + when ${!hasArchived} then t.archived_at + when ${archived} then coalesce(t.archived_at, ${input.now}) + else null + end, + updated_at = case + when ${input.customTitle !== undefined} + or (${hasArchived && archived} and t.archived_at is null) + or (${hasArchived && !archived} and t.archived_at is not null) + then now() + else t.updated_at + end + from thread_chat.projects p + where t.id = ${input.threadId} + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning + t.id, t.project_id as "projectId", t.parent_thread_id as "parentThreadId", + t.source_message_id as "sourceMessageId", t.fork_source_snapshot as "forkSourceSnapshot", + t.base_context as "baseContext", t.auto_title as "autoTitle", + t.custom_title as "customTitle", t.archived_at as "archivedAt", + t.created_at as "createdAt", t.updated_at as "updatedAt" + ` + return row ? mapThread(row) : null + } + private async insert(input: { id: ThreadId projectId: ProjectId diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index 35a746a7..d13c9c67 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -38,14 +38,14 @@ ## 4. 实现核心 Application Command 与 Query -- [ ] 4.1 原子创建 `Project + Root Thread + U1 + A1 + queued MessageRun`,所有新实体 ID 由服务端生成,提交前不启动 AI Runtime。 -- [ ] 4.2 原子追加 `user Message + assistant Message + queued MessageRun`,不把 user/assistant 角色交替建成数据库不变量。 -- [ ] 4.3 实现 Fork Thread:验证 finalized source、冻结 ForkSourceSnapshot 和 BaseContext、创建 Child Thread,任一步失败整体回滚。 -- [ ] 4.4 实现 Regenerate:保留旧 assistant Message 内容和 sequence,标记 superseded,追加 replacement assistant Message 与新 MessageRun。 -- [ ] 4.5 实现 Edit last user Message:追加 replacement user Message,将依赖旧输入的有效后缀 supersede,并创建新 assistant Message 与 MessageRun。 -- [ ] 4.6 实现 Project metadata、Branch metadata、archive/unarchive、feedback 与 Project 永久删除 Application Command。 -- [ ] 4.7 实现 Project 列表、ProjectBootstrap、Thread Message 和 Artifact-by-ID Query;Bootstrap 只返回轻量 topology 与 Root bundle,Thread 默认最多 200 条有效 Message。 -- [ ] 4.8 实现 Prompt History:`BaseContext.messageIds + 当前 Thread 有效 Prompt Message`,排除不合格 assistant 状态,且不依赖客户端已加载窗口。 +- [x] 4.1 原子创建 `Project + Root Thread + U1 + A1 + queued MessageRun`,所有新实体 ID 由服务端生成,提交前不启动 AI Runtime。 +- [x] 4.2 原子追加 `user Message + assistant Message + queued MessageRun`,不把 user/assistant 角色交替建成数据库不变量。 +- [x] 4.3 实现 Fork Thread:验证 finalized source、冻结 ForkSourceSnapshot 和 BaseContext、创建 Child Thread,任一步失败整体回滚。 +- [x] 4.4 实现 Regenerate:保留旧 assistant Message 内容和 sequence,标记 superseded,追加 replacement assistant Message 与新 MessageRun。 +- [x] 4.5 实现 Edit last user Message:追加 replacement user Message,将依赖旧输入的有效后缀 supersede,并创建新 assistant Message 与 MessageRun。 +- [x] 4.6 实现 Project metadata、Branch metadata、archive/unarchive、feedback 与 Project 永久删除 Application Command。 +- [x] 4.7 实现 Project 列表、ProjectBootstrap、Thread Message 和 Artifact-by-ID Query;Bootstrap 只返回轻量 topology 与 Root bundle,Thread 默认最多 200 条有效 Message。 +- [x] 4.8 实现 Prompt History:`BaseContext.messageIds + 当前 Thread 有效 Prompt Message`,排除不合格 assistant 状态,且不依赖客户端已加载窗口。 ## 5. 实现 MessageRun 后台执行 diff --git a/tests/integration/application.test.ts b/tests/integration/application.test.ts new file mode 100644 index 00000000..2cdfe4f6 --- /dev/null +++ b/tests/integration/application.test.ts @@ -0,0 +1,331 @@ +import { randomUUID } from "node:crypto" +import { afterAll, describe, expect, it } from "vitest" +import postgres from "postgres" +import { ThreadChatCommands } from "@/lib/thread-chat/application/thread-chat-commands" +import { ThreadChatQueries } from "@/lib/thread-chat/application/thread-chat-queries" +import { loadPromptHistory } from "@/lib/thread-chat/application/prompt-history" +import { + createThreadChatRepositories, + ThreadChatUnitOfWork, +} from "@/lib/thread-chat/infrastructure/repositories" +import { assertSafeTestDatabaseUrl } from "../../scripts/lib/test-database-safety.mjs" + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 10 }) +const unitOfWork = new ThreadChatUnitOfWork(sql) +const fixedNow = new Date("2026-08-25T00:00:00.000Z") +const commands = new ThreadChatCommands(unitOfWork, { + generateId: randomUUID, + now: () => fixedNow, + resolveModelId: (requested) => requested ?? "test/model", +}) +const queries = new ThreadChatQueries(sql) + +async function createUser(): Promise { + const id = randomUUID() + await sql` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${id}, 'Application Test', ${`${id}@thread-chat.test`}, true, now(), now() + ) + ` + return id +} + +async function deleteUser(id: string): Promise { + await sql`delete from thread_chat."user" where id = ${id}` +} + +async function completeAssistant(actorId: string, messageId: string) { + const repositories = createThreadChatRepositories(sql) + const run = await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + messageId + ) + expect(run).not.toBeNull() + const running = await repositories.messageRuns.transition({ + actorId, + messageRunId: run!.id, + expectedStatus: "queued", + nextStatus: "running", + }) + expect(running?.status).toBe("running") + await repositories.messages.finalizeAssistantOnce({ + actorId, + messageId, + parts: [{ type: "text", text: `answer:${messageId}` }], + finalizedAt: fixedNow, + }) + const completed = await repositories.messageRuns.transition({ + actorId, + messageRunId: run!.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: fixedNow, + }) + expect(completed?.status).toBe("completed") +} + +afterAll(async () => { + await sql.end() +}) + +describe("ThreadChat Application", () => { + it("原子创建与追加 turn,并拒绝同 Thread 的并发 generation", async () => { + const actorId = await createUser() + try { + const creation = await commands.createProject({ + actorId, + parts: [{ type: "text", text: "first" }], + }) + expect(creation.userMessage.sequence).toBe(1) + expect(creation.assistantMessage.sequence).toBe(2) + expect(creation.assistantRun).toMatchObject({ + assistantMessageId: creation.assistantMessage.id, + status: "queued", + }) + await expect( + commands.sendMessage({ + actorId, + threadId: creation.rootThread.id, + parts: [{ type: "text", text: "blocked" }], + }) + ).rejects.toMatchObject({ code: "thread_generation_in_progress" }) + expect( + (await queries.threadMessages({ actorId, threadId: creation.rootThread.id })) + .messages + ).toHaveLength(2) + + await completeAssistant(actorId, creation.assistantMessage.id) + const turn = await commands.sendMessage({ + actorId, + threadId: creation.rootThread.id, + parts: [{ type: "text", text: "second" }], + requestedModelId: "test/other-model", + }) + expect([turn.userMessage.sequence, turn.assistantMessage.sequence]).toEqual([ + 3, 4, + ]) + expect(turn.assistantRun.modelId).toBe("test/other-model") + expect( + (await loadPromptHistory(sql, { actorId, threadId: creation.rootThread.id })).map( + (message) => message.id + ) + ).toEqual([ + creation.userMessage.id, + creation.assistantMessage.id, + turn.userMessage.id, + ]) + } finally { + await deleteUser(actorId) + } + }) + + it("Fork 冻结 BaseContext,Prompt History 不依赖客户端窗口", async () => { + const actorId = await createUser() + try { + const creation = await commands.createProject({ + actorId, + parts: [{ type: "text", text: "fork me" }], + }) + await completeAssistant(actorId, creation.assistantMessage.id) + await expect( + commands.forkThread({ + actorId, + sourceThreadId: creation.rootThread.id, + sourceMessageId: creation.assistantMessage.id, + anchor: { + exactQuote: "wrong", + textPosition: { start: 0, end: 5 }, + }, + }) + ).rejects.toMatchObject({ code: "fork_anchor_mismatch" }) + const branch = await commands.forkThread({ + actorId, + sourceThreadId: creation.rootThread.id, + sourceMessageId: creation.assistantMessage.id, + anchor: { exactQuote: "answer" }, + }) + expect(branch).toMatchObject({ + parentThreadId: creation.rootThread.id, + sourceMessageId: creation.assistantMessage.id, + forkSourceSnapshot: { sourceRole: "assistant", sourceSequence: 2 }, + }) + expect(branch.baseContext?.messageIds).toEqual([ + creation.userMessage.id, + creation.assistantMessage.id, + ]) + expect( + (await loadPromptHistory(sql, { actorId, threadId: branch.id })).map( + (message) => message.id + ) + ).toEqual(branch.baseContext?.messageIds) + } finally { + await deleteUser(actorId) + } + }) + + it("Edit supersede 有效后缀,Regenerate 保留旧事实并追加 replacement", async () => { + const actorId = await createUser() + try { + const creation = await commands.createProject({ + actorId, + parts: [{ type: "text", text: "u1" }], + }) + await completeAssistant(actorId, creation.assistantMessage.id) + const second = await commands.sendMessage({ + actorId, + threadId: creation.rootThread.id, + parts: [{ type: "text", text: "u2" }], + }) + await completeAssistant(actorId, second.assistantMessage.id) + + const edited = await commands.editLastUser({ + actorId, + sourceUserMessageId: second.userMessage.id, + parts: [{ type: "text", text: "u2 edited" }], + }) + expect(new Set(edited.supersededMessageIds)).toEqual( + new Set([second.userMessage.id, second.assistantMessage.id]) + ) + expect(edited.createdMessages.map((message) => message.role)).toEqual([ + "user", + "assistant", + ]) + expect(edited.createdMessages[0].replacesMessageId).toBe( + second.userMessage.id + ) + await completeAssistant(actorId, edited.createdMessages[1].id) + + const oldAssistant = edited.createdMessages[1] + const regenerated = await commands.regenerate({ + actorId, + sourceAssistantMessageId: oldAssistant.id, + }) + expect(regenerated.supersededMessageIds).toEqual([oldAssistant.id]) + expect(regenerated.createdMessages[0]).toMatchObject({ + role: "assistant", + replacesMessageId: oldAssistant.id, + }) + const persistedOld = await createThreadChatRepositories( + sql + ).messages.findOwnedById(actorId, oldAssistant.id) + expect(persistedOld).toMatchObject({ + sequence: oldAssistant.sequence, + parts: [{ type: "text", text: `answer:${oldAssistant.id}` }], + }) + expect(persistedOld?.supersededAt).not.toBeNull() + } finally { + await deleteUser(actorId) + } + }) + + it("metadata、archive、feedback、Bootstrap、Artifact Query 与级联删除受 owner scope 保护", async () => { + const actorId = await createUser() + const otherId = await createUser() + try { + const creation = await commands.createProject({ + actorId, + parts: [{ type: "text", text: "metadata" }], + }) + await completeAssistant(actorId, creation.assistantMessage.id) + const project = await commands.patchProject({ + actorId, + projectId: creation.project.id, + patch: { customTitle: "Custom", instruction: "instruction" }, + }) + expect(project).toMatchObject({ + customTitle: "Custom", + instruction: "instruction", + }) + await commands.setFeedback({ + actorId, + assistantMessageId: creation.assistantMessage.id, + feedback: "positive", + }) + const branch = await commands.forkThread({ + actorId, + sourceThreadId: creation.rootThread.id, + sourceMessageId: creation.assistantMessage.id, + }) + expect( + await commands.patchBranch({ + actorId, + threadId: branch.id, + customTitle: "Branch", + archived: true, + }) + ).toMatchObject({ customTitle: "Branch", archivedAt: fixedNow }) + await expect( + commands.patchBranch({ + actorId, + threadId: creation.rootThread.id, + customTitle: "Root", + }) + ).rejects.toMatchObject({ code: "root_thread_title_owned_by_project" }) + const artifact = await unitOfWork.transaction((repositories) => + repositories.artifacts.insert({ + actorId, + id: randomUUID(), + projectId: creation.project.id, + sourceMessageId: creation.assistantMessage.id, + kind: "markdown", + title: "Artifact", + content: "# body", + }) + ) + const bootstrap = await queries.projectBootstrap({ + actorId, + projectId: creation.project.id, + }) + expect(bootstrap.initialThread.threadId).toBe(creation.rootThread.id) + expect(bootstrap.artifactSummary).toEqual({ + changeSequence: 1, + total: 1, + byKind: { markdown: 1 }, + }) + expect(await queries.artifactById({ actorId, artifactId: artifact.id })).toEqual( + artifact + ) + await expect( + queries.artifactById({ actorId: otherId, artifactId: artifact.id }) + ).rejects.toMatchObject({ code: "entity_not_found" }) + expect((await queries.listProjects({ actorId }))[0].displayTitle).toBe( + "Custom" + ) + await commands.setProjectArchived({ + actorId, + projectId: creation.project.id, + archived: true, + }) + expect(await queries.listProjects({ actorId })).toEqual([]) + expect( + await queries.listProjects({ actorId, status: "archived" }) + ).toHaveLength(1) + await commands.deleteProject({ actorId, projectId: creation.project.id }) + await expect( + queries.projectBootstrap({ actorId, projectId: creation.project.id }) + ).rejects.toMatchObject({ code: "entity_not_found" }) + } finally { + await deleteUser(actorId) + await deleteUser(otherId) + } + }) + + it("任一创建失败时不留下 Project 半成品", async () => { + const missingActorId = randomUUID() + await expect( + commands.createProject({ + actorId: missingActorId, + parts: [{ type: "text", text: "rollback" }], + }) + ).rejects.toBeTruthy() + const [row] = await sql<{ count: number }[]>` + select count(*)::integer as count + from thread_chat.projects + where owner_user_id = ${missingActorId} + ` + expect(row.count).toBe(0) + }) +}) From b59ca5f54fac837b0603d9283eb58dd88a0f0d1c Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:31:50 +0800 Subject: [PATCH 11/23] feat(thread-chat): execute durable message runs --- .../application/application-types.ts | 2 + lib/thread-chat/application/message-runner.ts | 289 ++++++++++++++++++ .../application/ports/ai-runtime.ts | 1 + .../application/thread-chat-commands.ts | 34 ++- .../infrastructure/ai-sdk-runtime.ts | 111 +++++++ .../repositories/message-run-repository.ts | 80 ++++- .../define-thread-chat-domain-model/tasks.md | 12 +- tests/integration/message-runner.test.ts | 286 +++++++++++++++++ 8 files changed, 799 insertions(+), 16 deletions(-) create mode 100644 lib/thread-chat/application/message-runner.ts create mode 100644 lib/thread-chat/infrastructure/ai-sdk-runtime.ts create mode 100644 tests/integration/message-runner.test.ts diff --git a/lib/thread-chat/application/application-types.ts b/lib/thread-chat/application/application-types.ts index b9419247..a640a41c 100644 --- a/lib/thread-chat/application/application-types.ts +++ b/lib/thread-chat/application/application-types.ts @@ -11,6 +11,8 @@ export type ThreadChatApplicationDependencies = { generateId: IdGenerator now: () => Date resolveModelId: (requestedModelId?: string) => string + wakeRunAfterCommit?: (messageRunId: string) => void | Promise + onWakeError?: (error: unknown) => void } export type ProjectArtifactSummary = { diff --git a/lib/thread-chat/application/message-runner.ts b/lib/thread-chat/application/message-runner.ts new file mode 100644 index 00000000..172d9e57 --- /dev/null +++ b/lib/thread-chat/application/message-runner.ts @@ -0,0 +1,289 @@ +import type { UIMessage } from "ai" +import { toMarkdownArtifactToolOutput } from "../domain/artifact" +import { invariant } from "../domain/domain-error" +import type { MessageRun } from "../domain/message-run" +import { + createThreadChatRepositories, + type ThreadChatSql, + type ThreadChatUnitOfWork, +} from "../infrastructure/repositories" +import { loadPromptHistory } from "./prompt-history" +import type { AiRuntime } from "./ports/ai-runtime" + +export type MessageRunExecutionResult = + | { outcome: "not_claimed" } + | { outcome: "completed" | "failed" | "stopped"; run: MessageRun } + +export class MessageRunner { + private readonly controllers = new Map() + + constructor( + private readonly sql: ThreadChatSql, + private readonly unitOfWork: ThreadChatUnitOfWork, + private readonly runtime: AiRuntime, + private readonly dependencies: { + generateId: () => string + now: () => Date + heartbeatIntervalMs?: number + } + ) {} + + async execute(messageRunId: string): Promise { + const repositories = createThreadChatRepositories(this.sql) + const context = await repositories.messageRuns.findExecutionContext( + messageRunId + ) + if (!context) return { outcome: "not_claimed" } + const claimed = await repositories.messageRuns.transition({ + actorId: context.actorId, + messageRunId, + expectedStatus: "queued", + nextStatus: "running", + }) + if (!claimed) return { outcome: "not_claimed" } + + const controller = new AbortController() + this.controllers.set(messageRunId, controller) + const heartbeatTimer = setInterval(() => { + void repositories.messageRuns + .heartbeat({ + actorId: context.actorId, + messageRunId, + heartbeatAt: this.dependencies.now(), + }) + .catch(() => undefined) + }, this.dependencies.heartbeatIntervalMs ?? 15_000) + heartbeatTimer.unref() + + try { + const promptMessages = await loadPromptHistory(this.sql, { + actorId: context.actorId, + threadId: context.threadId, + }) + let checkpointParts = claimed.checkpointParts + let eventSequence = claimed.eventSequence + const artifactParts: UIMessage["parts"] = [] + const events = this.runtime.execute( + { + messageRunId, + assistantMessageId: claimed.assistantMessageId, + modelId: claimed.modelId, + prompt: promptMessages.map((message) => ({ + id: message.id, + role: message.role, + parts: message.parts ?? [], + })), + }, + { signal: controller.signal } + ) + + for await (const event of events) { + if (await this.stopWasRequested(context.actorId, messageRunId)) { + controller.abort() + return this.finish(context.actorId, claimed, "stopped") + } + + if (event.type === "delta") { + checkpointParts = [...checkpointParts, ...event.partsDelta] + const checkpointed = await repositories.messageRuns.checkpoint({ + actorId: context.actorId, + messageRunId, + expectedEventSequence: eventSequence, + checkpointParts, + heartbeatAt: this.dependencies.now(), + }) + if (!checkpointed) return { outcome: "not_claimed" } + eventSequence = checkpointed.eventSequence + continue + } + + if (event.type === "artifact") { + const artifact = await this.unitOfWork.transaction(async (tx) => { + const created = await tx.artifacts.insert({ + actorId: context.actorId, + id: this.dependencies.generateId(), + projectId: context.projectId, + sourceMessageId: claimed.assistantMessageId, + kind: event.output.kind, + title: event.output.title, + content: event.output.content, + }) + const toolPart = { + type: "dynamic-tool" as const, + toolName: "createMarkdownArtifact", + toolCallId: event.output.toolCallId ?? created.id, + state: "output-available" as const, + input: { + title: event.output.title, + content: event.output.content, + }, + output: toMarkdownArtifactToolOutput(created), + } + artifactParts.push(toolPart) + checkpointParts = [...checkpointParts, toolPart] + const checkpointed = await tx.messageRuns.checkpoint({ + actorId: context.actorId, + messageRunId, + expectedEventSequence: eventSequence, + checkpointParts, + heartbeatAt: this.dependencies.now(), + }) + invariant( + checkpointed, + "message_run_transition_invalid", + "Artifact checkpoint 写入时 MessageRun 已改变。" + ) + return { artifact: created, checkpointed } + }) + eventSequence = artifact.checkpointed.eventSequence + continue + } + + if (event.type === "completed") { + return this.complete( + context.actorId, + claimed, + [...artifactParts, ...event.parts] + ) + } + if (event.type === "failed") { + return this.finish(context.actorId, claimed, "failed", event.error) + } + return this.finish(context.actorId, claimed, "stopped") + } + + return this.finish(context.actorId, claimed, "failed", { + code: "runtime_ended_without_terminal_event", + message: "AI Runtime 未产生终态事件。", + }) + } catch (error) { + if ( + controller.signal.aborted || + (await this.stopWasRequested(context.actorId, messageRunId)) + ) { + return this.finish(context.actorId, claimed, "stopped") + } + return this.finish(context.actorId, claimed, "failed", { + code: "runtime_execution_failed", + message: error instanceof Error ? error.message : "AI Runtime 执行失败。", + }) + } finally { + clearInterval(heartbeatTimer) + this.controllers.delete(messageRunId) + } + } + + async scanQueued(limit = 20): Promise[]> { + const ids = await createThreadChatRepositories( + this.sql + ).messageRuns.listQueuedIds(limit) + return Promise.allSettled(ids.map((id) => this.execute(id))) + } + + async requestStop(input: { + actorId: string + assistantMessageId: string + }): Promise { + const run = await this.unitOfWork.transaction(async (repositories) => { + const requested = await repositories.messageRuns.requestStop( + input.actorId, + input.assistantMessageId, + this.dependencies.now() + ) + if (!requested) { + const current = + await repositories.messageRuns.findOwnedByAssistantMessageId( + input.actorId, + input.assistantMessageId + ) + invariant(current, "entity_not_found", "MessageRun 不存在。") + return current + } + if (requested.status !== "queued") return requested + const stopped = await repositories.messageRuns.transition({ + actorId: input.actorId, + messageRunId: requested.id, + expectedStatus: "queued", + nextStatus: "stopped", + finishedAt: this.dependencies.now(), + incrementEventSequence: true, + }) + invariant( + stopped, + "message_run_transition_invalid", + "queued MessageRun Stop 转换失败。" + ) + return stopped + }) + this.controllers.get(run.id)?.abort() + return run + } + + private async stopWasRequested( + actorId: string, + messageRunId: string + ): Promise { + const context = await createThreadChatRepositories( + this.sql + ).messageRuns.findExecutionContext(messageRunId) + return context?.actorId === actorId && context.run.stopRequestedAt !== null + } + + private complete( + actorId: string, + claimed: MessageRun, + finalParts: UIMessage["parts"] + ): Promise { + return this.unitOfWork.transaction(async (repositories) => { + const message = await repositories.messages.finalizeAssistantOnce({ + actorId, + messageId: claimed.assistantMessageId, + parts: finalParts, + finalizedAt: this.dependencies.now(), + }) + invariant( + message, + "message_run_transition_invalid", + "assistant Message 无法封存。" + ) + const run = await repositories.messageRuns.transition({ + actorId, + messageRunId: claimed.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: this.dependencies.now(), + incrementEventSequence: true, + }) + invariant( + run, + "message_run_transition_invalid", + "MessageRun completed 条件更新失败。" + ) + return { outcome: "completed", run } + }) + } + + private async finish( + actorId: string, + claimed: MessageRun, + outcome: "failed" | "stopped", + error?: { code: string; message: string } + ): Promise { + const repositories = createThreadChatRepositories(this.sql) + const run = await repositories.messageRuns.transition({ + actorId, + messageRunId: claimed.id, + expectedStatus: "running", + nextStatus: outcome, + finishedAt: this.dependencies.now(), + error: outcome === "failed" ? error : null, + incrementEventSequence: true, + }) + if (run) return { outcome, run } + const current = await repositories.messageRuns.findExecutionContext( + claimed.id + ) + invariant(current, "entity_not_found", "MessageRun 不存在。") + return { outcome: current.run.status as "failed" | "stopped", run: current.run } + } +} diff --git a/lib/thread-chat/application/ports/ai-runtime.ts b/lib/thread-chat/application/ports/ai-runtime.ts index cfc0b3df..0d59adfb 100644 --- a/lib/thread-chat/application/ports/ai-runtime.ts +++ b/lib/thread-chat/application/ports/ai-runtime.ts @@ -5,6 +5,7 @@ export type MarkdownArtifactRuntimeOutput = { kind: "markdown" title: string content: string + toolCallId?: string } /** AI Runtime 的供应商无关输出;持久化游标由 MessageRun runner 分配。 */ diff --git a/lib/thread-chat/application/thread-chat-commands.ts b/lib/thread-chat/application/thread-chat-commands.ts index 53442d00..c4197bf4 100644 --- a/lib/thread-chat/application/thread-chat-commands.ts +++ b/lib/thread-chat/application/thread-chat-commands.ts @@ -20,13 +20,13 @@ export class ThreadChatCommands { private readonly dependencies: ThreadChatApplicationDependencies ) {} - createProject(input: { + async createProject(input: { actorId: UserId parts: UserMessageInput requestedModelId?: string }): Promise { const modelId = this.dependencies.resolveModelId(input.requestedModelId) - return this.unitOfWork.transaction(async (repositories) => { + const result = await this.unitOfWork.transaction(async (repositories) => { const project = await repositories.projects.insert({ id: this.dependencies.generateId(), ownerUserId: input.actorId, @@ -50,21 +50,25 @@ export class ThreadChatCommands { ...created, } }) + await this.wakeAfterCommit(result.assistantRun.id) + return result } - sendMessage(input: { + async sendMessage(input: { actorId: UserId threadId: string parts: UserMessageInput requestedModelId?: string }): Promise { const modelId = this.dependencies.resolveModelId(input.requestedModelId) - return this.unitOfWork.transaction((repositories) => + const result = await this.unitOfWork.transaction((repositories) => this.appendTurn(repositories, { ...input, modelId }) ) + await this.wakeAfterCommit(result.assistantRun.id) + return result } - forkThread(input: { + async forkThread(input: { actorId: UserId sourceThreadId: string sourceMessageId: string @@ -166,13 +170,13 @@ export class ThreadChatCommands { }) } - regenerate(input: { + async regenerate(input: { actorId: UserId sourceAssistantMessageId: string requestedModelId?: string }): Promise { const modelId = this.dependencies.resolveModelId(input.requestedModelId) - return this.unitOfWork.transaction(async (repositories) => { + const result = await this.unitOfWork.transaction(async (repositories) => { const found = await repositories.messages.findOwnedById( input.actorId, input.sourceAssistantMessageId @@ -234,16 +238,18 @@ export class ThreadChatCommands { assistantRun, } }) + await this.wakeAfterCommit(result.assistantRun.id) + return result } - editLastUser(input: { + async editLastUser(input: { actorId: UserId sourceUserMessageId: string parts: UserMessageInput requestedModelId?: string }): Promise { const modelId = this.dependencies.resolveModelId(input.requestedModelId) - return this.unitOfWork.transaction(async (repositories) => { + const result = await this.unitOfWork.transaction(async (repositories) => { const found = await repositories.messages.findOwnedById( input.actorId, input.sourceUserMessageId @@ -310,6 +316,8 @@ export class ThreadChatCommands { assistantRun, } }) + await this.wakeAfterCommit(result.assistantRun.id) + return result } patchProject(input: { @@ -427,4 +435,12 @@ export class ThreadChatCommands { }) return { userMessage, assistantMessage, assistantRun } } + + private async wakeAfterCommit(messageRunId: string): Promise { + try { + await this.dependencies.wakeRunAfterCommit?.(messageRunId) + } catch (error) { + this.dependencies.onWakeError?.(error) + } + } } diff --git a/lib/thread-chat/infrastructure/ai-sdk-runtime.ts b/lib/thread-chat/infrastructure/ai-sdk-runtime.ts new file mode 100644 index 00000000..22b20306 --- /dev/null +++ b/lib/thread-chat/infrastructure/ai-sdk-runtime.ts @@ -0,0 +1,111 @@ +import { + convertToModelMessages, + isStepCount, + streamText, + tool, + type LanguageModel, +} from "ai" +import { + isExplicitMarkdownArtifactRequest, + MARKDOWN_ARTIFACT_TOOL_DESCRIPTION, + MARKDOWN_ARTIFACT_TOOL_NAME, + markdownArtifactInputSchema, +} from "@/lib/chat/markdown-artifact" +import { resolveChatModel } from "@/lib/ai/provider" +import type { + AiRuntime, + AiRuntimeEvent, + AiRuntimeRequest, +} from "../application/ports/ai-runtime" + +const markdownArtifactTool = tool({ + description: MARKDOWN_ARTIFACT_TOOL_DESCRIPTION, + inputSchema: markdownArtifactInputSchema, + execute: async () => ({ created: true as const }), +}) + +export class AiSdkRuntime implements AiRuntime { + constructor( + private readonly resolveModel: (modelId: string) => LanguageModel = + resolveChatModel + ) {} + + async *execute( + request: AiRuntimeRequest, + options: { signal?: AbortSignal } = {} + ): AsyncIterable { + const lastUserText = request.prompt + .findLast((message) => message.role === "user") + ?.parts.filter((part) => part.type === "text") + .map((part) => part.text) + .join("\n") + const tools = { [MARKDOWN_ARTIFACT_TOOL_NAME]: markdownArtifactTool } + const markdownArtifactEnabled = isExplicitMarkdownArtifactRequest( + lastUserText ?? "" + ) + const result = streamText({ + model: this.resolveModel(request.modelId), + messages: await convertToModelMessages(request.prompt, { tools }), + tools, + activeTools: markdownArtifactEnabled + ? [MARKDOWN_ARTIFACT_TOOL_NAME] + : [], + abortSignal: options.signal, + stopWhen: isStepCount(5), + }) + let text = "" + + for await (const part of result.fullStream) { + if (part.type === "text-delta") { + text += part.text + yield { + type: "delta", + partsDelta: [{ type: "text", text: part.text }], + } + continue + } + if ( + part.type === "tool-result" && + part.toolName === MARKDOWN_ARTIFACT_TOOL_NAME + ) { + const input = markdownArtifactInputSchema.parse(part.input) + yield { + type: "artifact", + output: { + kind: "markdown", + title: input.title, + content: input.content, + toolCallId: part.toolCallId, + }, + } + continue + } + if (part.type === "abort") { + yield { type: "stopped" } + return + } + if (part.type === "error") { + if (options.signal?.aborted) { + yield { type: "stopped" } + return + } + yield { + type: "failed", + error: { + code: "ai_sdk_stream_error", + message: + part.error instanceof Error + ? part.error.message + : "AI SDK stream failed.", + }, + } + return + } + } + + yield { + type: "completed", + parts: text ? [{ type: "text", text }] : [], + } + } +} diff --git a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts index bafb653c..0a483610 100644 --- a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts @@ -1,6 +1,12 @@ import type { UIMessage } from "ai" import { invariant } from "../../domain/domain-error" -import type { MessageId, MessageRunId, UserId } from "../../domain/ids" +import type { + MessageId, + MessageRunId, + ProjectId, + ThreadId, + UserId, +} from "../../domain/ids" import { assertMessageRunTransition, type MessageRun, @@ -131,6 +137,48 @@ export class MessageRunRepository { return rows.map(mapMessageRun) } + async findExecutionContext(messageRunId: MessageRunId): Promise<{ + run: MessageRun + actorId: UserId + projectId: ProjectId + threadId: ThreadId + } | null> { + const [row] = await this.sql[]>` + select + r.id, r.assistant_message_id as "assistantMessageId", r.status, + r.model_id as "modelId", r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", r.error_code as "errorCode", + r.error_message as "errorMessage", r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", r.finished_at as "finishedAt", + r.created_at as "createdAt", r.updated_at as "updatedAt", + p.owner_user_id as "actorId", p.id as "projectId", t.id as "threadId" + from thread_chat.message_runs r + join thread_chat.messages m on m.id = r.assistant_message_id + join thread_chat.threads t on t.id = m.thread_id + join thread_chat.projects p on p.id = t.project_id + where r.id = ${messageRunId} + ` + return row + ? { + run: mapMessageRun(row), + actorId: String(row.actorId), + projectId: String(row.projectId), + threadId: String(row.threadId), + } + : null + } + + async listQueuedIds(limit: number): Promise { + const rows = await this.sql<{ id: string }[]>` + select id + from thread_chat.message_runs + where status = 'queued' and stop_requested_at is null + order by created_at, id + limit ${limit} + ` + return rows.map((row) => row.id) + } + async assertNoActiveForThread(actorId: UserId, threadId: string): Promise { const [row] = await this.sql` select r.id @@ -158,11 +206,13 @@ export class MessageRunRepository { nextStatus: MessageRunStatus finishedAt?: Date | null error?: { code: string; message: string } | null + incrementEventSequence?: boolean }): Promise { assertMessageRunTransition(input.expectedStatus, input.nextStatus) const [row] = await this.sql[]>` update thread_chat.message_runs r set status = ${input.nextStatus}, + event_sequence = event_sequence + ${input.incrementEventSequence ? 1 : 0}, finished_at = ${input.finishedAt ?? null}, error_code = ${input.error?.code ?? null}, error_message = ${input.error?.message ?? null}, @@ -235,6 +285,34 @@ export class MessageRunRepository { return row ? mapMessageRun(row) : null } + async heartbeat(input: { + actorId: UserId + messageRunId: MessageRunId + heartbeatAt: Date + }): Promise { + const [row] = await this.sql[]>` + update thread_chat.message_runs r + set heartbeat_at = ${input.heartbeatAt}, updated_at = now() + from thread_chat.messages m, + thread_chat.threads t, + thread_chat.projects p + where r.id = ${input.messageRunId} + and r.status = 'running' + and r.assistant_message_id = m.id + and m.thread_id = t.id + and t.project_id = p.id + and p.owner_user_id = ${input.actorId} + returning + r.id, r.assistant_message_id as "assistantMessageId", r.status, + r.model_id as "modelId", r.event_sequence as "eventSequence", + r.checkpoint_parts as "checkpointParts", r.error_code as "errorCode", + r.error_message as "errorMessage", r.heartbeat_at as "heartbeatAt", + r.stop_requested_at as "stopRequestedAt", r.finished_at as "finishedAt", + r.created_at as "createdAt", r.updated_at as "updatedAt" + ` + return row ? mapMessageRun(row) : null + } + async requestStop( actorId: UserId, assistantMessageId: MessageId, diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index d13c9c67..12de3c4f 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -49,12 +49,12 @@ ## 5. 实现 MessageRun 后台执行 -- [ ] 5.1 将 `message_runs.status=queued` 作为持久化待执行事实;事务提交后才尝试唤醒执行器,不建立通用任务平台。 -- [ ] 5.2 实现条件领取 queued Run、heartbeat、checkpoint/eventSequence 持久化和 completed/failed/stopped 原子终态提交,防止同一 Run 重复执行。 -- [ ] 5.3 实现最小 queued scanner,补偿事务已提交但即时唤醒失败的 Run;不得创建第二条 assistant Message 或 MessageRun。 -- [ ] 5.4 接入真实 AI Runtime adapter,并确保领域/Application 只依赖 capability;自动测试使用 Fake AI Runtime,不发起真实计费请求。 -- [ ] 5.5 完成 Markdown Artifact 工具结果投影:创建独立 Artifact,最终 Message tool output 保存稳定 `artifactId`。 -- [ ] 5.6 实现显式 Stop;浏览器刷新、断开连接或客户端 Runtime 销毁不得调用 Stop,也不得终止后台执行。 +- [x] 5.1 将 `message_runs.status=queued` 作为持久化待执行事实;事务提交后才尝试唤醒执行器,不建立通用任务平台。 +- [x] 5.2 实现条件领取 queued Run、heartbeat、checkpoint/eventSequence 持久化和 completed/failed/stopped 原子终态提交,防止同一 Run 重复执行。 +- [x] 5.3 实现最小 queued scanner,补偿事务已提交但即时唤醒失败的 Run;不得创建第二条 assistant Message 或 MessageRun。 +- [x] 5.4 接入真实 AI Runtime adapter,并确保领域/Application 只依赖 capability;自动测试使用 Fake AI Runtime,不发起真实计费请求。 +- [x] 5.5 完成 Markdown Artifact 工具结果投影:创建独立 Artifact,最终 Message tool output 保存稳定 `artifactId`。 +- [x] 5.6 实现显式 Stop;浏览器刷新、断开连接或客户端 Runtime 销毁不得调用 Stop,也不得终止后台执行。 ## 6. 后端领域验收门 diff --git a/tests/integration/message-runner.test.ts b/tests/integration/message-runner.test.ts new file mode 100644 index 00000000..70c6dd4b --- /dev/null +++ b/tests/integration/message-runner.test.ts @@ -0,0 +1,286 @@ +import { randomUUID } from "node:crypto" +import { afterAll, describe, expect, it } from "vitest" +import postgres from "postgres" +import { MessageRunner } from "@/lib/thread-chat/application/message-runner" +import type { + AiRuntime, + AiRuntimeRequest, +} from "@/lib/thread-chat/application/ports/ai-runtime" +import { ThreadChatCommands } from "@/lib/thread-chat/application/thread-chat-commands" +import { + createThreadChatRepositories, + ThreadChatUnitOfWork, +} from "@/lib/thread-chat/infrastructure/repositories" +import { FakeAiRuntime } from "../fakes/fake-ai-runtime" +import { assertSafeTestDatabaseUrl } from "../../scripts/lib/test-database-safety.mjs" + +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const sql = postgres(testDatabaseUrl, { max: 10 }) +const unitOfWork = new ThreadChatUnitOfWork(sql) +const now = new Date("2026-08-25T01:00:00.000Z") + +function createCommands( + overrides: { + wakeRunAfterCommit?: (messageRunId: string) => void | Promise + onWakeError?: (error: unknown) => void + } = {} +) { + return new ThreadChatCommands(unitOfWork, { + generateId: randomUUID, + now: () => now, + resolveModelId: () => "fake/model", + ...overrides, + }) +} + +function createRunner(runtime: AiRuntime) { + return new MessageRunner(sql, unitOfWork, runtime, { + generateId: randomUUID, + now: () => now, + heartbeatIntervalMs: 60_000, + }) +} + +async function createUser(): Promise { + const id = randomUUID() + await sql` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${id}, 'Runner Test', ${`${id}@thread-chat.test`}, true, now(), now() + ) + ` + return id +} + +async function deleteUser(id: string): Promise { + await sql`delete from thread_chat."user" where id = ${id}` +} + +afterAll(async () => { + await sql.end() +}) + +describe("MessageRunner", () => { + it("条件领取、checkpoint、Artifact 引用与 completed 在持久层收敛", async () => { + const actorId = await createUser() + try { + const creation = await createCommands().createProject({ + actorId, + parts: [{ type: "text", text: "生成 Markdown 文档" }], + }) + const runtime = new FakeAiRuntime() + runtime.setScenario(creation.assistantRun.id, { + events: [ + { + type: "delta", + partsDelta: [{ type: "text", text: "working" }], + }, + { + type: "artifact", + output: { + kind: "markdown", + title: "Result", + content: "# Result", + toolCallId: "tool-call-1", + }, + }, + { + type: "completed", + parts: [{ type: "text", text: "done" }], + }, + ], + }) + const runner = createRunner(runtime) + const [first, duplicate] = await Promise.all([ + runner.execute(creation.assistantRun.id), + runner.execute(creation.assistantRun.id), + ]) + expect([first.outcome, duplicate.outcome].toSorted()).toEqual([ + "completed", + "not_claimed", + ]) + expect(runtime.invocations).toHaveLength(1) + + const repositories = createThreadChatRepositories(sql) + const run = await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + creation.assistantMessage.id + ) + expect(run).toMatchObject({ + status: "completed", + eventSequence: 3, + checkpointParts: [ + { type: "text", text: "working" }, + { + type: "dynamic-tool", + toolName: "createMarkdownArtifact", + toolCallId: "tool-call-1", + state: "output-available", + output: { artifactId: expect.any(String) }, + }, + ], + heartbeatAt: now, + finishedAt: now, + }) + const message = await repositories.messages.findOwnedById( + actorId, + creation.assistantMessage.id + ) + expect(message?.finalizedAt).toEqual(now) + const toolPart = message?.parts?.find( + (part) => part.type === "dynamic-tool" + ) + expect(toolPart).toMatchObject({ + output: { artifactId: expect.any(String) }, + }) + const artifactId = (toolPart as { output: { artifactId: string } }).output + .artifactId + expect( + (toolPart as { output: { artifactId: string } }).output + ).toEqual({ artifactId }) + expect( + await repositories.artifacts.findOwnedById(actorId, artifactId) + ).toMatchObject({ + sourceMessageId: creation.assistantMessage.id, + content: "# Result", + }) + expect(runtime.invocations[0].prompt.map((message) => message.id)).toEqual([ + creation.userMessage.id, + ]) + } finally { + await deleteUser(actorId) + } + }) + + it("queued scanner 补偿唤醒失败并保存 failed 终态", async () => { + const actorId = await createUser() + try { + const wakeErrors: unknown[] = [] + let committedAtWake = false + const runtime = new FakeAiRuntime() + const commands = createCommands({ + wakeRunAfterCommit: async (messageRunId) => { + committedAtWake = Boolean( + await createThreadChatRepositories( + sql + ).messageRuns.findExecutionContext(messageRunId) + ) + runtime.setScenario(messageRunId, { + events: [ + { + type: "failed", + error: { code: "provider_failed", message: "provider down" }, + }, + ], + }) + throw new Error("wake unavailable") + }, + onWakeError: (error) => wakeErrors.push(error), + }) + const creation = await commands.createProject({ + actorId, + parts: [{ type: "text", text: "scanner" }], + }) + expect(wakeErrors).toHaveLength(1) + expect(committedAtWake).toBe(true) + expect(creation.assistantRun.status).toBe("queued") + + const scan = await createRunner(runtime).scanQueued() + expect(scan).toHaveLength(1) + const run = await createThreadChatRepositories( + sql + ).messageRuns.findOwnedByAssistantMessageId( + actorId, + creation.assistantMessage.id + ) + expect(run).toMatchObject({ + status: "failed", + errorCode: "provider_failed", + errorMessage: "provider down", + eventSequence: 1, + finishedAt: now, + }) + } finally { + await deleteUser(actorId) + } + }) + + it("显式 Stop 终止 queued Run,且不启动 Runtime", async () => { + const actorId = await createUser() + try { + const creation = await createCommands().createProject({ + actorId, + parts: [{ type: "text", text: "stop queued" }], + }) + const runtime = new FakeAiRuntime() + const runner = createRunner(runtime) + const stopped = await runner.requestStop({ + actorId, + assistantMessageId: creation.assistantMessage.id, + }) + expect(stopped).toMatchObject({ + status: "stopped", + stopRequestedAt: now, + finishedAt: now, + eventSequence: 1, + }) + expect(await runner.execute(creation.assistantRun.id)).toEqual({ + outcome: "not_claimed", + }) + expect(runtime.invocations).toEqual([]) + } finally { + await deleteUser(actorId) + } + }) + + it("running Run 只在显式 Stop 后中止;执行器取消与浏览器订阅无关", async () => { + const actorId = await createUser() + try { + let releaseStarted!: () => void + const started = new Promise((resolve) => { + releaseStarted = resolve + }) + const blockingRuntime: AiRuntime = { + async *execute( + _request: AiRuntimeRequest, + options?: { signal?: AbortSignal } + ) { + yield { + type: "delta" as const, + partsDelta: [{ type: "text" as const, text: "partial" }], + } + releaseStarted() + await new Promise((resolve) => { + options?.signal?.addEventListener("abort", () => resolve(), { + once: true, + }) + }) + throw new Error("provider aborted") + }, + } + const creation = await createCommands().createProject({ + actorId, + parts: [{ type: "text", text: "stop running" }], + }) + const runner = createRunner(blockingRuntime) + const execution = runner.execute(creation.assistantRun.id) + await started + const accepted = await runner.requestStop({ + actorId, + assistantMessageId: creation.assistantMessage.id, + }) + expect(accepted).toMatchObject({ status: "running", stopRequestedAt: now }) + expect(await execution).toMatchObject({ outcome: "stopped" }) + const run = await createThreadChatRepositories( + sql + ).messageRuns.findOwnedByAssistantMessageId( + actorId, + creation.assistantMessage.id + ) + expect(run).toMatchObject({ status: "stopped", finishedAt: now }) + } finally { + await deleteUser(actorId) + } + }) +}) From 7637932225e493db38b2f95d900ec5bccbb5040f Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:34:26 +0800 Subject: [PATCH 12/23] test(thread-chat): enforce backend domain acceptance gate --- lib/thread-chat/application/prompt-history.ts | 32 ++++------------- lib/thread-chat/domain/prompt-history.ts | 36 +++++++++++++++++++ .../backend-verification.md | 21 +++++++++++ .../define-thread-chat-domain-model/tasks.md | 10 +++--- tests/integration/application.test.ts | 18 ++++++++++ tests/unit/domain-model.test.ts | 35 ++++++++++++++++++ 6 files changed, 122 insertions(+), 30 deletions(-) create mode 100644 lib/thread-chat/domain/prompt-history.ts create mode 100644 openspec/changes/define-thread-chat-domain-model/backend-verification.md diff --git a/lib/thread-chat/application/prompt-history.ts b/lib/thread-chat/application/prompt-history.ts index af303d74..7647e479 100644 --- a/lib/thread-chat/application/prompt-history.ts +++ b/lib/thread-chat/application/prompt-history.ts @@ -1,5 +1,6 @@ import { invariant } from "../domain/domain-error" import type { Message } from "../domain/message" +import { buildPromptHistory } from "../domain/prompt-history" import { createThreadChatRepositories, type ThreadChatSql, @@ -20,36 +21,17 @@ export async function loadPromptHistory( repositories.messages.listByIdsOwned(input.actorId, baseIds), repositories.messages.listEffectiveOwned(input.actorId, thread.id, 100_000), ]) - const byId = new Map(baseMessages.map((message) => [message.id, message])) - const orderedBase = baseIds.map((messageId) => { - const message = byId.get(messageId) - invariant( - message, - "base_context_message_missing", - `BaseContext 引用的 Message ${messageId} 不存在。` - ) - return message - }) - const candidates = [...orderedBase, ...currentMessages] - const assistantIds = candidates + const assistantIds = [...baseMessages, ...currentMessages] .filter((message) => message.role === "assistant") .map((message) => message.id) const runs = await repositories.messageRuns.findOwnedByAssistantMessageIds( input.actorId, assistantIds ) - const completedAssistantIds = new Set( - runs - .filter((run) => run.status === "completed") - .map((run) => run.assistantMessageId) - ) - const seen = new Set() - return candidates.filter((message) => { - if (seen.has(message.id)) return false - seen.add(message.id) - if (message.finalizedAt === null) return false - return ( - message.role === "user" || completedAssistantIds.has(message.id) - ) + return buildPromptHistory({ + baseMessageIds: baseIds, + baseMessages, + currentMessages, + assistantRuns: runs, }) } diff --git a/lib/thread-chat/domain/prompt-history.ts b/lib/thread-chat/domain/prompt-history.ts new file mode 100644 index 00000000..d9b50115 --- /dev/null +++ b/lib/thread-chat/domain/prompt-history.ts @@ -0,0 +1,36 @@ +import { invariant } from "./domain-error" +import type { MessageId } from "./ids" +import type { Message } from "./message" +import type { MessageRun } from "./message-run" + +export function buildPromptHistory(input: { + baseMessageIds: readonly MessageId[] + baseMessages: readonly Message[] + currentMessages: readonly Message[] + assistantRuns: readonly MessageRun[] +}): Message[] { + const baseById = new Map( + input.baseMessages.map((message) => [message.id, message]) + ) + const orderedBase = input.baseMessageIds.map((messageId) => { + const message = baseById.get(messageId) + invariant( + message, + "base_context_message_missing", + `BaseContext 引用的 Message ${messageId} 不存在。` + ) + return message + }) + const completedAssistantIds = new Set( + input.assistantRuns + .filter((run) => run.status === "completed") + .map((run) => run.assistantMessageId) + ) + const seen = new Set() + return [...orderedBase, ...input.currentMessages].filter((message) => { + if (seen.has(message.id)) return false + seen.add(message.id) + if (message.finalizedAt === null) return false + return message.role === "user" || completedAssistantIds.has(message.id) + }) +} diff --git a/openspec/changes/define-thread-chat-domain-model/backend-verification.md b/openspec/changes/define-thread-chat-domain-model/backend-verification.md new file mode 100644 index 00000000..1beef65e --- /dev/null +++ b/openspec/changes/define-thread-chat-domain-model/backend-verification.md @@ -0,0 +1,21 @@ +# 后端领域验收证据 + +## 自动化门槛 + +| 门槛 | 命令 | 结果 | +|---|---|---| +| 领域单元测试 | `pnpm test:unit` | 4 个文件、14 个测试通过 | +| Repository / Application / Runtime 集成测试 | `pnpm test:integration` | 5 个文件、20 个测试通过;使用隔离的 `thread-chat-test` PostgreSQL | +| TypeScript | `pnpm typecheck` | 通过 | +| OpenSpec | `pnpm openspec:validate` | 27 项严格校验通过 | + +## 覆盖映射 + +| 领域能力 | 自动化证据 | +|---|---| +| Root / Branch、无环拓扑、BaseContext、replacement、Fork 资格、MessageRun 状态机、Prompt History | `tests/unit/domain-model.test.ts` | +| owner scope、唯一 Root、并发 sequence、finalized 不可变、Artifact provenance、单一 MessageRun | `tests/integration/normalized-schema.test.ts`、`tests/integration/repositories.test.ts` | +| create、send、Fork、Edit、Regenerate、metadata、feedback、delete 及失败回滚 | `tests/integration/application.test.ts` | +| 条件领取、delta checkpoint、eventSequence、heartbeat、completed、failed、Stop、queued scanner、Artifact tool output | `tests/integration/message-runner.test.ts` | + +自动化测试只使用 `FakeAiRuntime`,不会请求真实模型供应商。 diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/define-thread-chat-domain-model/tasks.md index 12de3c4f..4c343f6b 100644 --- a/openspec/changes/define-thread-chat-domain-model/tasks.md +++ b/openspec/changes/define-thread-chat-domain-model/tasks.md @@ -58,11 +58,11 @@ ## 6. 后端领域验收门 -- [ ] 6.1 完成领域单元测试:Root/Branch、拓扑无环、sequence、replacement、Fork 资格、BaseContext、Prompt History 与 MessageRun 状态机。 -- [ ] 6.2 完成 Repository 集成测试:真实 PostgreSQL owner scope、唯一 Root、并发 sequence、finalized 不可变、replacement、Artifact provenance 与单一 MessageRun。 -- [ ] 6.3 完成 Application 集成测试:create/send/Fork/Edit/Regenerate/Stop/delete 的事务提交与整体回滚。 -- [ ] 6.4 使用 Fake AI Runtime 测试 delta、完成、失败、Stop、queued scanner 补偿、刷新时 checkpoint 恢复与 Artifact tool output。 -- [ ] 6.5 执行 `pnpm typecheck`、后端 unit/integration tests 和 `pnpm openspec:validate`,全部通过后记录实现证据;未通过不得进入 API Route 实现。 +- [x] 6.1 完成领域单元测试:Root/Branch、拓扑无环、sequence、replacement、Fork 资格、BaseContext、Prompt History 与 MessageRun 状态机。 +- [x] 6.2 完成 Repository 集成测试:真实 PostgreSQL owner scope、唯一 Root、并发 sequence、finalized 不可变、replacement、Artifact provenance 与单一 MessageRun。 +- [x] 6.3 完成 Application 集成测试:create/send/Fork/Edit/Regenerate/Stop/delete 的事务提交与整体回滚。 +- [x] 6.4 使用 Fake AI Runtime 测试 delta、完成、失败、Stop、queued scanner 补偿、刷新时 checkpoint 恢复与 Artifact tool output。 +- [x] 6.5 执行 `pnpm typecheck`、后端 unit/integration tests 和 `pnpm openspec:validate`,全部通过后记录实现证据;未通过不得进入 API Route 实现。 ## 7. E2E 通过后的旧后端退役 diff --git a/tests/integration/application.test.ts b/tests/integration/application.test.ts index 2cdfe4f6..0843d695 100644 --- a/tests/integration/application.test.ts +++ b/tests/integration/application.test.ts @@ -181,6 +181,24 @@ describe("ThreadChat Application", () => { }) await completeAssistant(actorId, second.assistantMessage.id) + await expect( + commands.editLastUser({ + actorId, + sourceUserMessageId: creation.userMessage.id, + parts: [{ type: "text", text: "historical edit" }], + }) + ).rejects.toMatchObject({ code: "fork_required" }) + await expect( + commands.regenerate({ + actorId, + sourceAssistantMessageId: creation.assistantMessage.id, + }) + ).rejects.toMatchObject({ code: "fork_required" }) + expect( + (await queries.threadMessages({ actorId, threadId: creation.rootThread.id })) + .messages + ).toHaveLength(4) + const edited = await commands.editLastUser({ actorId, sourceUserMessageId: second.userMessage.id, diff --git a/tests/unit/domain-model.test.ts b/tests/unit/domain-model.test.ts index e8dd3d53..ba8ca62d 100644 --- a/tests/unit/domain-model.test.ts +++ b/tests/unit/domain-model.test.ts @@ -22,6 +22,7 @@ import { assertArtifactProvenance, toMarkdownArtifactToolOutput, } from "@/lib/thread-chat/domain/artifact" +import { buildPromptHistory } from "@/lib/thread-chat/domain/prompt-history" const now = new Date("2026-01-01T00:00:00.000Z") @@ -227,6 +228,40 @@ describe("MessageRun 状态机", () => { }) }) +describe("Prompt History", () => { + it("保留 BaseContext 顺序并排除非 completed assistant", () => { + const baseAssistant = message({ + id: "base-assistant", + threadId: "parent", + role: "assistant", + supersededAt: now, + }) + const currentUser = message({ id: "current-user", sequence: 1 }) + const queuedAssistant = message({ + id: "queued-assistant", + sequence: 2, + role: "assistant", + finalizedAt: null, + }) + + expect( + buildPromptHistory({ + baseMessageIds: [baseAssistant.id], + baseMessages: [baseAssistant], + currentMessages: [currentUser, queuedAssistant], + assistantRuns: [ + run({ assistantMessageId: baseAssistant.id }), + run({ + assistantMessageId: queuedAssistant.id, + status: "queued", + finishedAt: null, + }), + ], + }).map((entry) => entry.id) + ).toEqual([baseAssistant.id, currentUser.id]) + }) +}) + describe("Artifact provenance", () => { it("只投影 artifactId,并拒绝跨 Project 来源", () => { const source = message() From c1b7f647e885ccacca9d0c2410d30e85fab16a51 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 18:46:10 +0800 Subject: [PATCH 13/23] feat(thread-chat): expose v1 command and streaming API --- app/api/v1/artifacts/[artifactId]/route.ts | 13 + .../[assistantMessageId]/events/route.ts | 15 + .../[assistantMessageId]/stop/route.ts | 13 + .../v1/messages/[messageId]/edits/route.ts | 13 + .../v1/messages/[messageId]/feedback/route.ts | 13 + .../[messageId]/regenerations/route.ts | 13 + .../v1/projects/[projectId]/archive/route.ts | 13 + .../projects/[projectId]/bootstrap/route.ts | 13 + app/api/v1/projects/[projectId]/route.ts | 23 + .../projects/[projectId]/unarchive/route.ts | 13 + app/api/v1/projects/route.ts | 10 + .../v1/threads/[threadId]/archive/route.ts | 13 + app/api/v1/threads/[threadId]/forks/route.ts | 13 + .../v1/threads/[threadId]/messages/route.ts | 23 + app/api/v1/threads/[threadId]/route.ts | 13 + .../v1/threads/[threadId]/unarchive/route.ts | 13 + lib/thread-chat/api/capabilities.ts | 95 ++++ lib/thread-chat/api/client-error.ts | 13 + lib/thread-chat/api/contracts.ts | 315 +++++++++++++ lib/thread-chat/api/json-transport.ts | 269 +++++++++++ lib/thread-chat/api/routes.ts | 34 ++ lib/thread-chat/api/server/cursor.ts | 45 ++ lib/thread-chat/api/server/errors.ts | 119 +++++ lib/thread-chat/api/server/handlers.ts | 435 ++++++++++++++++++ lib/thread-chat/api/server/http.ts | 32 ++ lib/thread-chat/api/server/mappers.ts | 105 +++++ lib/thread-chat/api/server/runtime.ts | 44 ++ lib/thread-chat/application/message-runner.ts | 5 +- .../application/thread-chat-commands.ts | 28 +- .../application/thread-chat-queries.ts | 32 ++ lib/thread-chat/domain/domain-error.ts | 5 + .../design-thread-chat-client-api/tasks.md | 52 +-- tests/fixtures/thread-chat-api-fixtures.ts | 95 ++++ tests/integration/message-runner.test.ts | 2 + 34 files changed, 1915 insertions(+), 37 deletions(-) create mode 100644 app/api/v1/artifacts/[artifactId]/route.ts create mode 100644 app/api/v1/assistant-messages/[assistantMessageId]/events/route.ts create mode 100644 app/api/v1/assistant-messages/[assistantMessageId]/stop/route.ts create mode 100644 app/api/v1/messages/[messageId]/edits/route.ts create mode 100644 app/api/v1/messages/[messageId]/feedback/route.ts create mode 100644 app/api/v1/messages/[messageId]/regenerations/route.ts create mode 100644 app/api/v1/projects/[projectId]/archive/route.ts create mode 100644 app/api/v1/projects/[projectId]/bootstrap/route.ts create mode 100644 app/api/v1/projects/[projectId]/route.ts create mode 100644 app/api/v1/projects/[projectId]/unarchive/route.ts create mode 100644 app/api/v1/projects/route.ts create mode 100644 app/api/v1/threads/[threadId]/archive/route.ts create mode 100644 app/api/v1/threads/[threadId]/forks/route.ts create mode 100644 app/api/v1/threads/[threadId]/messages/route.ts create mode 100644 app/api/v1/threads/[threadId]/route.ts create mode 100644 app/api/v1/threads/[threadId]/unarchive/route.ts create mode 100644 lib/thread-chat/api/capabilities.ts create mode 100644 lib/thread-chat/api/client-error.ts create mode 100644 lib/thread-chat/api/contracts.ts create mode 100644 lib/thread-chat/api/json-transport.ts create mode 100644 lib/thread-chat/api/routes.ts create mode 100644 lib/thread-chat/api/server/cursor.ts create mode 100644 lib/thread-chat/api/server/errors.ts create mode 100644 lib/thread-chat/api/server/handlers.ts create mode 100644 lib/thread-chat/api/server/http.ts create mode 100644 lib/thread-chat/api/server/mappers.ts create mode 100644 lib/thread-chat/api/server/runtime.ts create mode 100644 tests/fixtures/thread-chat-api-fixtures.ts diff --git a/app/api/v1/artifacts/[artifactId]/route.ts b/app/api/v1/artifacts/[artifactId]/route.ts new file mode 100644 index 00000000..ca72e094 --- /dev/null +++ b/app/api/v1/artifacts/[artifactId]/route.ts @@ -0,0 +1,13 @@ +import { loadArtifact } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function GET( + _request: Request, + context: { params: Promise<{ artifactId: string }> } +) { + const { artifactId } = await context.params + return withActor( + (actorId) => loadArtifact(actorId, artifactId), + "artifact_not_found" + ) +} diff --git a/app/api/v1/assistant-messages/[assistantMessageId]/events/route.ts b/app/api/v1/assistant-messages/[assistantMessageId]/events/route.ts new file mode 100644 index 00000000..9ec2382e --- /dev/null +++ b/app/api/v1/assistant-messages/[assistantMessageId]/events/route.ts @@ -0,0 +1,15 @@ +import { assistantEvents } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export const maxDuration = 300 + +export async function GET( + request: Request, + context: { params: Promise<{ assistantMessageId: string }> } +) { + const { assistantMessageId } = await context.params + return withActor( + (actorId) => assistantEvents(actorId, assistantMessageId, request), + "assistant_message_not_found" + ) +} diff --git a/app/api/v1/assistant-messages/[assistantMessageId]/stop/route.ts b/app/api/v1/assistant-messages/[assistantMessageId]/stop/route.ts new file mode 100644 index 00000000..32e5481e --- /dev/null +++ b/app/api/v1/assistant-messages/[assistantMessageId]/stop/route.ts @@ -0,0 +1,13 @@ +import { stopAssistant } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + _request: Request, + context: { params: Promise<{ assistantMessageId: string }> } +) { + const { assistantMessageId } = await context.params + return withActor( + (actorId) => stopAssistant(actorId, assistantMessageId), + "assistant_message_not_found" + ) +} diff --git a/app/api/v1/messages/[messageId]/edits/route.ts b/app/api/v1/messages/[messageId]/edits/route.ts new file mode 100644 index 00000000..3e300aeb --- /dev/null +++ b/app/api/v1/messages/[messageId]/edits/route.ts @@ -0,0 +1,13 @@ +import { editMessage } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + request: Request, + context: { params: Promise<{ messageId: string }> } +) { + const { messageId } = await context.params + return withActor( + (actorId) => editMessage(actorId, messageId, request), + "message_not_found" + ) +} diff --git a/app/api/v1/messages/[messageId]/feedback/route.ts b/app/api/v1/messages/[messageId]/feedback/route.ts new file mode 100644 index 00000000..af574d3b --- /dev/null +++ b/app/api/v1/messages/[messageId]/feedback/route.ts @@ -0,0 +1,13 @@ +import { setFeedback } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function PUT( + request: Request, + context: { params: Promise<{ messageId: string }> } +) { + const { messageId } = await context.params + return withActor( + (actorId) => setFeedback(actorId, messageId, request), + "message_not_found" + ) +} diff --git a/app/api/v1/messages/[messageId]/regenerations/route.ts b/app/api/v1/messages/[messageId]/regenerations/route.ts new file mode 100644 index 00000000..da2653ba --- /dev/null +++ b/app/api/v1/messages/[messageId]/regenerations/route.ts @@ -0,0 +1,13 @@ +import { regenerateMessage } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + request: Request, + context: { params: Promise<{ messageId: string }> } +) { + const { messageId } = await context.params + return withActor( + (actorId) => regenerateMessage(actorId, messageId, request), + "message_not_found" + ) +} diff --git a/app/api/v1/projects/[projectId]/archive/route.ts b/app/api/v1/projects/[projectId]/archive/route.ts new file mode 100644 index 00000000..63a77921 --- /dev/null +++ b/app/api/v1/projects/[projectId]/archive/route.ts @@ -0,0 +1,13 @@ +import { setProjectArchived } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + _request: Request, + context: { params: Promise<{ projectId: string }> } +) { + const { projectId } = await context.params + return withActor( + (actorId) => setProjectArchived(actorId, projectId, true), + "project_not_found" + ) +} diff --git a/app/api/v1/projects/[projectId]/bootstrap/route.ts b/app/api/v1/projects/[projectId]/bootstrap/route.ts new file mode 100644 index 00000000..00d25daf --- /dev/null +++ b/app/api/v1/projects/[projectId]/bootstrap/route.ts @@ -0,0 +1,13 @@ +import { bootstrapProject } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function GET( + _request: Request, + context: { params: Promise<{ projectId: string }> } +) { + const { projectId } = await context.params + return withActor( + (actorId) => bootstrapProject(actorId, projectId), + "project_not_found" + ) +} diff --git a/app/api/v1/projects/[projectId]/route.ts b/app/api/v1/projects/[projectId]/route.ts new file mode 100644 index 00000000..7e9aa0d3 --- /dev/null +++ b/app/api/v1/projects/[projectId]/route.ts @@ -0,0 +1,23 @@ +import { + deleteProject, + patchProject, +} from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +type Context = { params: Promise<{ projectId: string }> } + +export async function PATCH(request: Request, context: Context) { + const { projectId } = await context.params + return withActor( + (actorId) => patchProject(actorId, projectId, request), + "project_not_found" + ) +} + +export async function DELETE(_request: Request, context: Context) { + const { projectId } = await context.params + return withActor( + (actorId) => deleteProject(actorId, projectId), + "project_not_found" + ) +} diff --git a/app/api/v1/projects/[projectId]/unarchive/route.ts b/app/api/v1/projects/[projectId]/unarchive/route.ts new file mode 100644 index 00000000..6f9f7dea --- /dev/null +++ b/app/api/v1/projects/[projectId]/unarchive/route.ts @@ -0,0 +1,13 @@ +import { setProjectArchived } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + _request: Request, + context: { params: Promise<{ projectId: string }> } +) { + const { projectId } = await context.params + return withActor( + (actorId) => setProjectArchived(actorId, projectId, false), + "project_not_found" + ) +} diff --git a/app/api/v1/projects/route.ts b/app/api/v1/projects/route.ts new file mode 100644 index 00000000..d0c98e5c --- /dev/null +++ b/app/api/v1/projects/route.ts @@ -0,0 +1,10 @@ +import { createProject, listProjects } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function GET(request: Request) { + return withActor((actorId) => listProjects(actorId, request)) +} + +export async function POST(request: Request) { + return withActor((actorId) => createProject(actorId, request)) +} diff --git a/app/api/v1/threads/[threadId]/archive/route.ts b/app/api/v1/threads/[threadId]/archive/route.ts new file mode 100644 index 00000000..ebd1c9ac --- /dev/null +++ b/app/api/v1/threads/[threadId]/archive/route.ts @@ -0,0 +1,13 @@ +import { setThreadArchived } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + _request: Request, + context: { params: Promise<{ threadId: string }> } +) { + const { threadId } = await context.params + return withActor( + (actorId) => setThreadArchived(actorId, threadId, true), + "thread_not_found" + ) +} diff --git a/app/api/v1/threads/[threadId]/forks/route.ts b/app/api/v1/threads/[threadId]/forks/route.ts new file mode 100644 index 00000000..4edd8d1c --- /dev/null +++ b/app/api/v1/threads/[threadId]/forks/route.ts @@ -0,0 +1,13 @@ +import { forkThread } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + request: Request, + context: { params: Promise<{ threadId: string }> } +) { + const { threadId } = await context.params + return withActor( + (actorId) => forkThread(actorId, threadId, request), + "thread_not_found" + ) +} diff --git a/app/api/v1/threads/[threadId]/messages/route.ts b/app/api/v1/threads/[threadId]/messages/route.ts new file mode 100644 index 00000000..900844a2 --- /dev/null +++ b/app/api/v1/threads/[threadId]/messages/route.ts @@ -0,0 +1,23 @@ +import { + loadThreadMessages, + sendMessage, +} from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +type Context = { params: Promise<{ threadId: string }> } + +export async function GET(request: Request, context: Context) { + const { threadId } = await context.params + return withActor( + (actorId) => loadThreadMessages(actorId, threadId, request), + "thread_not_found" + ) +} + +export async function POST(request: Request, context: Context) { + const { threadId } = await context.params + return withActor( + (actorId) => sendMessage(actorId, threadId, request), + "thread_not_found" + ) +} diff --git a/app/api/v1/threads/[threadId]/route.ts b/app/api/v1/threads/[threadId]/route.ts new file mode 100644 index 00000000..dda89036 --- /dev/null +++ b/app/api/v1/threads/[threadId]/route.ts @@ -0,0 +1,13 @@ +import { patchThread } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function PATCH( + request: Request, + context: { params: Promise<{ threadId: string }> } +) { + const { threadId } = await context.params + return withActor( + (actorId) => patchThread(actorId, threadId, request), + "thread_not_found" + ) +} diff --git a/app/api/v1/threads/[threadId]/unarchive/route.ts b/app/api/v1/threads/[threadId]/unarchive/route.ts new file mode 100644 index 00000000..53c6d1b7 --- /dev/null +++ b/app/api/v1/threads/[threadId]/unarchive/route.ts @@ -0,0 +1,13 @@ +import { setThreadArchived } from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" + +export async function POST( + _request: Request, + context: { params: Promise<{ threadId: string }> } +) { + const { threadId } = await context.params + return withActor( + (actorId) => setThreadArchived(actorId, threadId, false), + "thread_not_found" + ) +} diff --git a/lib/thread-chat/api/capabilities.ts b/lib/thread-chat/api/capabilities.ts new file mode 100644 index 00000000..cb8942f9 --- /dev/null +++ b/lib/thread-chat/api/capabilities.ts @@ -0,0 +1,95 @@ +import type { z } from "zod" +import type { + artifactSchema, + assistantMessageEventSchema, + assistantRunStateSchema, + creationBundleSchema, + feedbackSchema, + forkThreadRequestSchema, + listProjectsResultSchema, + messageCreationBundleSchema, + projectBootstrapSchema, + projectSchema, + projectTargetSchema, + replacementBundleSchema, + threadMessageBundleSchema, + threadSchema, + UserMessageParts, +} from "./contracts" + +export interface ThreadChatApiCapabilities { + listProjects(input?: { + status?: "active" | "archived" | "all" + limit?: number + cursor?: string + signal?: AbortSignal + }): Promise> + createProject(input: { + parts: UserMessageParts + requestedModelId?: string + signal?: AbortSignal + }): Promise> + bootstrapProject( + projectId: string, + signal?: AbortSignal + ): Promise> + patchProject(input: { + projectId: string + customTitle?: string | null + target?: z.infer | null + instruction?: string | null + }): Promise> + setProjectArchived( + projectId: string, + archived: boolean + ): Promise> + deleteProject(projectId: string): Promise + loadThreadMessages(input: { + threadId: string + limit?: number + beforeSequence?: number + signal?: AbortSignal + }): Promise> + patchThread( + threadId: string, + customTitle: string | null + ): Promise> + setThreadArchived( + threadId: string, + archived: boolean + ): Promise> + sendMessage(input: { + threadId: string + parts: UserMessageParts + requestedModelId?: string + }): Promise> + forkThread( + threadId: string, + input: z.infer + ): Promise<{ thread: z.infer }> + editMessage(input: { + messageId: string + parts: UserMessageParts + requestedModelId?: string + }): Promise> + regenerateMessage(input: { + messageId: string + requestedModelId?: string + }): Promise> + setFeedback( + messageId: string, + value: "positive" | "negative" | null + ): Promise> + loadArtifact( + artifactId: string, + signal?: AbortSignal + ): Promise> + subscribeAssistantEvents(input: { + assistantMessageId: string + afterEventSequence?: number + signal?: AbortSignal + }): AsyncIterable> + stopAssistant( + assistantMessageId: string + ): Promise> +} diff --git a/lib/thread-chat/api/client-error.ts b/lib/thread-chat/api/client-error.ts new file mode 100644 index 00000000..52e74657 --- /dev/null +++ b/lib/thread-chat/api/client-error.ts @@ -0,0 +1,13 @@ +import type { ApiErrorCode } from "./contracts" + +export class ThreadChatClientError extends Error { + constructor( + readonly code: ApiErrorCode, + message: string, + readonly status: number, + readonly details?: unknown + ) { + super(message) + this.name = "ThreadChatClientError" + } +} diff --git a/lib/thread-chat/api/contracts.ts b/lib/thread-chat/api/contracts.ts new file mode 100644 index 00000000..6679f433 --- /dev/null +++ b/lib/thread-chat/api/contracts.ts @@ -0,0 +1,315 @@ +import { z } from "zod" + +export const idSchema = z.uuid() +export const dateTimeSchema = z.iso.datetime({ offset: true }) +export const jsonValueSchema = z.json() + +const textPartSchema = z.strictObject({ + type: z.literal("text"), + text: z.string(), +}) +const filePartSchema = z.strictObject({ + type: z.literal("file"), + mediaType: z.string().min(1), + filename: z.string().optional(), + url: z.string().min(1), +}) + +export const userMessagePartsSchema = z + .array(z.discriminatedUnion("type", [textPartSchema, filePartSchema])) + .min(1) + .refine( + (parts) => + parts.some( + (part) => + (part.type === "text" && part.text.trim().length > 0) || + part.type === "file" + ), + "Message parts must contain meaningful content." + ) + +export const messagePartsSchema = z.array( + z.looseObject({ type: z.string().min(1) }) +) +export const markdownArtifactToolOutputSchema = z.strictObject({ + artifactId: idSchema, +}) + +export const projectTargetSchema = z.strictObject({ + ultimate: z.string().max(4_000).nullable(), + shortTerm: z.array(z.string().trim().min(1).max(500)).max(50), + midTerm: z.array(z.string().trim().min(1).max(500)).max(50), +}) + +export const projectSchema = z.strictObject({ + id: idSchema, + ownerUserId: z.string().min(1), + autoTitle: z.string().nullable(), + customTitle: z.string().nullable(), + target: projectTargetSchema.nullable(), + instruction: z.string().nullable(), + archivedAt: dateTimeSchema.nullable(), + createdAt: dateTimeSchema, + updatedAt: dateTimeSchema, +}) + +export const projectSummarySchema = z.strictObject({ + id: idSchema, + displayTitle: z.string(), + archivedAt: dateTimeSchema.nullable(), + updatedAt: dateTimeSchema, + threadCount: z.int().nonnegative(), + messageCount: z.int().nonnegative(), +}) + +export const forkSourceSnapshotSchema = z.strictObject({ + schemaVersion: z.literal(1), + quote: z.string().optional(), + sourceRole: z.enum(["user", "assistant"]), + sourceSequence: z.int().positive(), +}) + +export const threadSchema = z.strictObject({ + id: idSchema, + projectId: idSchema, + parentThreadId: idSchema.nullable(), + sourceMessageId: idSchema.nullable(), + forkSourceSnapshot: forkSourceSnapshotSchema.nullable(), + autoTitle: z.string().nullable(), + customTitle: z.string().nullable(), + archivedAt: dateTimeSchema.nullable(), + createdAt: dateTimeSchema, + updatedAt: dateTimeSchema, +}) + +export const messageSchema = z.strictObject({ + id: idSchema, + threadId: idSchema, + sequence: z.int().positive(), + role: z.enum(["user", "assistant"]), + parts: messagePartsSchema.nullable(), + replacesMessageId: idSchema.nullable(), + supersededAt: dateTimeSchema.nullable(), + finalizedAt: dateTimeSchema.nullable(), + createdAt: dateTimeSchema, +}) + +export const assistantRunStateSchema = z.strictObject({ + assistantMessageId: idSchema, + status: z.enum(["queued", "running", "completed", "failed", "stopped"]), + modelId: z.string().min(1), + checkpointParts: messagePartsSchema, + eventSequence: z.int().nonnegative(), + error: z + .strictObject({ code: z.string(), message: z.string() }) + .nullable(), + stopRequestedAt: dateTimeSchema.nullable(), + finishedAt: dateTimeSchema.nullable(), +}) + +export const artifactSchema = z.strictObject({ + id: idSchema, + projectId: idSchema, + sourceMessageId: idSchema, + kind: z.string(), + title: z.string(), + content: jsonValueSchema, + createdAt: dateTimeSchema, +}) + +export const artifactSummarySchema = z.strictObject({ + changeSequence: z.int().nonnegative(), + total: z.int().nonnegative(), + byKind: z.record(z.string(), z.int().nonnegative()), +}) + +export const feedbackSchema = z.strictObject({ + messageId: idSchema, + value: z.enum(["positive", "negative"]).nullable(), + updatedAt: dateTimeSchema, +}) + +export const threadMessageBundleSchema = z.strictObject({ + threadId: idSchema, + messages: z.array(messageSchema), + assistantRuns: z.array(assistantRunStateSchema), + hasOlderMessages: z.boolean(), + oldestReturnedSequence: z.int().positive().nullable(), + newestReturnedSequence: z.int().positive().nullable(), +}) + +export const projectBootstrapSchema = z.strictObject({ + project: projectSchema, + threadTopology: z.array(threadSchema), + artifactSummary: artifactSummarySchema, + initialThread: threadMessageBundleSchema, +}) + +export const creationBundleSchema = z.strictObject({ + project: projectSchema, + rootThread: threadSchema, + artifactSummary: artifactSummarySchema, + userMessage: messageSchema, + assistantMessage: messageSchema, + assistantRun: assistantRunStateSchema, +}) + +export const messageCreationBundleSchema = creationBundleSchema.pick({ + userMessage: true, + assistantMessage: true, + assistantRun: true, +}) + +export const replacementBundleSchema = z.strictObject({ + supersededMessageIds: z.array(idSchema), + createdMessages: z.array(messageSchema), + assistantRun: assistantRunStateSchema, +}) + +export const listProjectsResultSchema = z.strictObject({ + items: z.array(projectSummarySchema), + nextCursor: z.string().nullable(), +}) + +export const listProjectsQuerySchema = z.strictObject({ + status: z.enum(["active", "archived", "all"]).default("active"), + limit: z.coerce.number().int().min(1).max(100).default(50), + cursor: z.string().min(1).optional(), +}) +export const threadMessagesQuerySchema = z.strictObject({ + limit: z.coerce.number().int().min(1).max(200).default(200), + beforeSequence: z.coerce.number().int().positive().optional(), +}) +export const assistantEventsQuerySchema = z.strictObject({ + afterEventSequence: z.coerce.number().int().nonnegative().default(0), +}) + +export const createProjectRequestSchema = z.strictObject({ + initialMessage: z.strictObject({ parts: userMessagePartsSchema }), + requestedModelId: z.string().min(1).optional(), +}) +export const sendMessageRequestSchema = z.strictObject({ + parts: userMessagePartsSchema, + requestedModelId: z.string().min(1).optional(), +}) +export const patchProjectRequestSchema = z + .strictObject({ + customTitle: z.string().trim().min(1).max(120).nullable().optional(), + target: projectTargetSchema.nullable().optional(), + instruction: z.string().max(20_000).nullable().optional(), + }) + .refine((value) => Object.keys(value).length > 0, "At least one field is required.") +export const patchThreadRequestSchema = z.strictObject({ + customTitle: z.string().trim().min(1).max(120).nullable(), +}) +export const forkThreadRequestSchema = z.strictObject({ + sourceMessageId: idSchema, + anchor: z + .strictObject({ + exactQuote: z.string().min(1), + textPosition: z + .strictObject({ + start: z.int().nonnegative(), + end: z.int().positive(), + }) + .refine((position) => position.end > position.start) + .optional(), + }) + .optional(), +}) +export const editMessageRequestSchema = sendMessageRequestSchema +export const regenerateMessageRequestSchema = z.strictObject({ + requestedModelId: z.string().min(1).optional(), +}) +export const putFeedbackRequestSchema = z.strictObject({ + value: z.enum(["positive", "negative"]).nullable(), +}) + +export const apiErrorCodeSchema = z.enum([ + "validation_error", + "invalid_query", + "invalid_cursor", + "invalid_event_cursor", + "unauthorized", + "forbidden", + "project_not_found", + "thread_not_found", + "message_not_found", + "assistant_message_not_found", + "message_run_not_found", + "artifact_not_found", + "model_not_available", + "thread_archived", + "thread_generation_in_progress", + "root_thread_title_owned_by_project", + "root_thread_archive_owned_by_project", + "source_message_not_found", + "fork_source_thread_mismatch", + "fork_source_not_finalized", + "fork_source_superseded", + "fork_anchor_mismatch", + "message_not_editable", + "message_not_regeneratable", + "message_not_feedback_eligible", + "fork_required", + "project_delete_conflict", + "internal_error", +]) + +export const apiErrorResponseSchema = z.strictObject({ + error: z.strictObject({ + code: apiErrorCodeSchema, + message: z.string(), + details: jsonValueSchema.optional(), + }), +}) + +const uiMessageChunkSchema = z.looseObject({ type: z.string().min(1) }) +export const runSnapshotEventSchema = z.strictObject({ + type: z.literal("run.snapshot"), + cursor: z.int().nonnegative(), + run: assistantRunStateSchema, + message: messageSchema, + artifactSummary: artifactSummarySchema, +}) +export const runDeltaEventSchema = z.strictObject({ + type: z.literal("run.delta"), + eventSequence: z.int().nonnegative(), + chunk: uiMessageChunkSchema, +}) +export const runCompletedEventSchema = z.strictObject({ + type: z.literal("run.completed"), + eventSequence: z.int().nonnegative(), + run: assistantRunStateSchema, + message: messageSchema, + artifactSummary: artifactSummarySchema, +}) +export const runFailedEventSchema = z.strictObject({ + type: z.literal("run.failed"), + eventSequence: z.int().nonnegative(), + run: assistantRunStateSchema, +}) +export const runStoppedEventSchema = z.strictObject({ + type: z.literal("run.stopped"), + eventSequence: z.int().nonnegative(), + run: assistantRunStateSchema, + message: messageSchema, +}) +export const assistantMessageEventSchema = z.discriminatedUnion("type", [ + runSnapshotEventSchema, + runDeltaEventSchema, + runCompletedEventSchema, + runFailedEventSchema, + runStoppedEventSchema, +]) + +export type ProjectDTO = z.infer +export type ThreadDTO = z.infer +export type MessageDTO = z.infer +export type AssistantRunStateDTO = z.infer +export type AssistantMessageEvent = z.infer +export type ApiErrorCode = z.infer +export type UserMessageParts = z.infer +export function apiResponseSchema(data: T) { + return z.strictObject({ data }) +} diff --git a/lib/thread-chat/api/json-transport.ts b/lib/thread-chat/api/json-transport.ts new file mode 100644 index 00000000..7b56eddc --- /dev/null +++ b/lib/thread-chat/api/json-transport.ts @@ -0,0 +1,269 @@ +import { z } from "zod" +import { ThreadChatClientError } from "./client-error" +import type { ThreadChatApiCapabilities } from "./capabilities" +import { + apiErrorResponseSchema, + apiResponseSchema, + artifactSchema, + assistantMessageEventSchema, + assistantRunStateSchema, + creationBundleSchema, + feedbackSchema, + listProjectsResultSchema, + messageCreationBundleSchema, + projectBootstrapSchema, + projectSchema, + replacementBundleSchema, + threadMessageBundleSchema, + threadSchema, +} from "./contracts" +import { threadChatApiRoutes } from "./routes" + +type RequestOptions = { + method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE" + body?: unknown + signal?: AbortSignal + schema: z.ZodType +} + +export class JsonThreadChatTransport implements ThreadChatApiCapabilities { + constructor( + private readonly fetcher: typeof fetch = fetch, + private readonly baseUrl = "" + ) {} + + listProjects(input: Parameters[0] = {}) { + const query = new URLSearchParams() + if (input.status) query.set("status", input.status) + if (input.limit !== undefined) query.set("limit", String(input.limit)) + if (input.cursor) query.set("cursor", input.cursor) + return this.request( + `${threadChatApiRoutes.projects()}${query.size ? `?${query}` : ""}`, + { schema: listProjectsResultSchema, signal: input.signal } + ) + } + + createProject(input: Parameters[0]) { + return this.request(threadChatApiRoutes.projects(), { + method: "POST", + body: { + initialMessage: { parts: input.parts }, + ...(input.requestedModelId + ? { requestedModelId: input.requestedModelId } + : {}), + }, + schema: creationBundleSchema, + signal: input.signal, + }) + } + + bootstrapProject(projectId: string, signal?: AbortSignal) { + return this.request(threadChatApiRoutes.projectBootstrap(projectId), { + schema: projectBootstrapSchema, + signal, + }) + } + + patchProject(input: Parameters[0]) { + const { projectId, ...body } = input + return this.request(threadChatApiRoutes.project(projectId), { + method: "PATCH", + body, + schema: projectSchema, + }) + } + + setProjectArchived(projectId: string, archived: boolean) { + return this.request(threadChatApiRoutes.projectArchive(projectId, archived), { + method: "POST", + schema: projectSchema, + }) + } + + async deleteProject(projectId: string): Promise { + await this.requestEmpty(threadChatApiRoutes.project(projectId), "DELETE") + } + + loadThreadMessages(input: Parameters[0]) { + const query = new URLSearchParams() + if (input.limit !== undefined) query.set("limit", String(input.limit)) + if (input.beforeSequence !== undefined) + query.set("beforeSequence", String(input.beforeSequence)) + return this.request( + `${threadChatApiRoutes.threadMessages(input.threadId)}${query.size ? `?${query}` : ""}`, + { schema: threadMessageBundleSchema, signal: input.signal } + ) + } + + patchThread(threadId: string, customTitle: string | null) { + return this.request(threadChatApiRoutes.thread(threadId), { + method: "PATCH", + body: { customTitle }, + schema: threadSchema, + }) + } + + setThreadArchived(threadId: string, archived: boolean) { + return this.request(threadChatApiRoutes.threadArchive(threadId, archived), { + method: "POST", + schema: threadSchema, + }) + } + + sendMessage(input: Parameters[0]) { + return this.request(threadChatApiRoutes.threadMessages(input.threadId), { + method: "POST", + body: { + parts: input.parts, + ...(input.requestedModelId + ? { requestedModelId: input.requestedModelId } + : {}), + }, + schema: messageCreationBundleSchema, + }) + } + + forkThread( + threadId: string, + input: Parameters[1] + ) { + return this.request(threadChatApiRoutes.threadForks(threadId), { + method: "POST", + body: input, + schema: z.strictObject({ thread: threadSchema }), + }) + } + + editMessage(input: Parameters[0]) { + return this.request(threadChatApiRoutes.messageEdits(input.messageId), { + method: "POST", + body: { + parts: input.parts, + ...(input.requestedModelId + ? { requestedModelId: input.requestedModelId } + : {}), + }, + schema: replacementBundleSchema, + }) + } + + regenerateMessage( + input: Parameters[0] + ) { + return this.request( + threadChatApiRoutes.messageRegenerations(input.messageId), + { + method: "POST", + body: input.requestedModelId + ? { requestedModelId: input.requestedModelId } + : {}, + schema: replacementBundleSchema, + } + ) + } + + setFeedback( + messageId: string, + value: "positive" | "negative" | null + ) { + return this.request(threadChatApiRoutes.messageFeedback(messageId), { + method: "PUT", + body: { value }, + schema: feedbackSchema, + }) + } + + loadArtifact(artifactId: string, signal?: AbortSignal) { + return this.request(threadChatApiRoutes.artifact(artifactId), { + schema: artifactSchema, + signal, + }) + } + + async *subscribeAssistantEvents( + input: Parameters[0] + ) { + const query = new URLSearchParams() + if (input.afterEventSequence !== undefined) + query.set("afterEventSequence", String(input.afterEventSequence)) + const response = await this.fetcher( + this.url( + `${threadChatApiRoutes.assistantEvents(input.assistantMessageId)}${query.size ? `?${query}` : ""}` + ), + { + headers: { Accept: "text/event-stream" }, + credentials: "same-origin", + signal: input.signal, + } + ) + await this.assertSuccess(response) + if (!response.body) throw new Error("SSE response body is missing.") + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() + let buffer = "" + try { + while (true) { + const { value, done } = await reader.read() + if (done) break + buffer += value + let boundary = buffer.indexOf("\n\n") + while (boundary >= 0) { + const block = buffer.slice(0, boundary) + buffer = buffer.slice(boundary + 2) + const data = block + .split("\n") + .filter((line) => line.startsWith("data:")) + .map((line) => line.slice(5).trimStart()) + .join("\n") + if (data) yield assistantMessageEventSchema.parse(JSON.parse(data)) + boundary = buffer.indexOf("\n\n") + } + } + } finally { + reader.releaseLock() + } + } + + stopAssistant(assistantMessageId: string) { + return this.request(threadChatApiRoutes.assistantStop(assistantMessageId), { + method: "POST", + schema: assistantRunStateSchema, + }) + } + + private async request(path: string, options: RequestOptions): Promise { + const response = await this.fetcher(this.url(path), { + method: options.method ?? "GET", + headers: options.body === undefined ? undefined : { "Content-Type": "application/json" }, + body: options.body === undefined ? undefined : JSON.stringify(options.body), + credentials: "same-origin", + signal: options.signal, + }) + await this.assertSuccess(response) + const envelope = apiResponseSchema(options.schema).parse(await response.json()) + return envelope.data + } + + private async requestEmpty(path: string, method: "DELETE"): Promise { + const response = await this.fetcher(this.url(path), { + method, + credentials: "same-origin", + }) + await this.assertSuccess(response) + } + + private async assertSuccess(response: Response): Promise { + if (response.ok) return + const parsed = apiErrorResponseSchema.safeParse(await response.json()) + if (!parsed.success) throw new Error(`Invalid API error response (${response.status}).`) + throw new ThreadChatClientError( + parsed.data.error.code, + parsed.data.error.message, + response.status, + parsed.data.error.details + ) + } + + private url(path: string): string { + return `${this.baseUrl}${path}` + } +} diff --git a/lib/thread-chat/api/routes.ts b/lib/thread-chat/api/routes.ts new file mode 100644 index 00000000..ebb8301c --- /dev/null +++ b/lib/thread-chat/api/routes.ts @@ -0,0 +1,34 @@ +export const threadChatRoutes = { + newProject: () => "/thread-chat/new", + project: (projectId: string) => `/thread-chat/${encodeURIComponent(projectId)}`, +} as const + +export const threadChatApiRoutes = { + projects: () => "/api/v1/projects", + project: (projectId: string) => + `/api/v1/projects/${encodeURIComponent(projectId)}`, + projectBootstrap: (projectId: string) => + `/api/v1/projects/${encodeURIComponent(projectId)}/bootstrap`, + projectArchive: (projectId: string, archived: boolean) => + `/api/v1/projects/${encodeURIComponent(projectId)}/${archived ? "archive" : "unarchive"}`, + thread: (threadId: string) => + `/api/v1/threads/${encodeURIComponent(threadId)}`, + threadMessages: (threadId: string) => + `/api/v1/threads/${encodeURIComponent(threadId)}/messages`, + threadArchive: (threadId: string, archived: boolean) => + `/api/v1/threads/${encodeURIComponent(threadId)}/${archived ? "archive" : "unarchive"}`, + threadForks: (threadId: string) => + `/api/v1/threads/${encodeURIComponent(threadId)}/forks`, + messageEdits: (messageId: string) => + `/api/v1/messages/${encodeURIComponent(messageId)}/edits`, + messageRegenerations: (messageId: string) => + `/api/v1/messages/${encodeURIComponent(messageId)}/regenerations`, + messageFeedback: (messageId: string) => + `/api/v1/messages/${encodeURIComponent(messageId)}/feedback`, + assistantEvents: (messageId: string) => + `/api/v1/assistant-messages/${encodeURIComponent(messageId)}/events`, + assistantStop: (messageId: string) => + `/api/v1/assistant-messages/${encodeURIComponent(messageId)}/stop`, + artifact: (artifactId: string) => + `/api/v1/artifacts/${encodeURIComponent(artifactId)}`, +} as const diff --git a/lib/thread-chat/api/server/cursor.ts b/lib/thread-chat/api/server/cursor.ts new file mode 100644 index 00000000..cee044de --- /dev/null +++ b/lib/thread-chat/api/server/cursor.ts @@ -0,0 +1,45 @@ +import { createHmac, timingSafeEqual } from "node:crypto" +import { z } from "zod" +import { ThreadChatApiError } from "./errors" + +const cursorPayloadSchema = z.strictObject({ + actorId: z.string(), + status: z.enum(["active", "archived", "all"]), + updatedAt: z.iso.datetime({ offset: true }), + id: z.uuid(), +}) + +function secret(): string { + return process.env.BETTER_AUTH_SECRET ?? "thread-chat-local-cursor-secret" +} + +function signature(payload: string): Buffer { + return createHmac("sha256", secret()).update(payload).digest() +} + +export function encodeProjectCursor(input: z.infer) { + const payload = Buffer.from(JSON.stringify(input)).toString("base64url") + return `${payload}.${signature(payload).toString("base64url")}` +} + +export function decodeProjectCursor( + cursor: string, + binding: { actorId: string; status: "active" | "archived" | "all" } +) { + try { + const [payload, encodedSignature, extra] = cursor.split(".") + if (!payload || !encodedSignature || extra) throw new Error("shape") + const actual = Buffer.from(encodedSignature, "base64url") + const expected = signature(payload) + if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) + throw new Error("signature") + const decoded = cursorPayloadSchema.parse( + JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) + ) + if (decoded.actorId !== binding.actorId || decoded.status !== binding.status) + throw new Error("binding") + return { updatedAt: new Date(decoded.updatedAt), id: decoded.id } + } catch { + throw new ThreadChatApiError("invalid_cursor", 400, "Invalid cursor.") + } +} diff --git a/lib/thread-chat/api/server/errors.ts b/lib/thread-chat/api/server/errors.ts new file mode 100644 index 00000000..2ee542f8 --- /dev/null +++ b/lib/thread-chat/api/server/errors.ts @@ -0,0 +1,119 @@ +import { ZodError } from "zod" +import { ThreadChatDomainError } from "../../domain/domain-error" +import type { ApiErrorCode } from "../contracts" + +export class ThreadChatApiError extends Error { + constructor( + readonly code: ApiErrorCode, + readonly status: number, + message: string, + readonly details?: unknown + ) { + super(message) + this.name = "ThreadChatApiError" + } +} + +const domainErrorMap: Partial< + Record +> = { + project_owner_mismatch: { code: "forbidden", status: 403 }, + thread_archived: { code: "thread_archived", status: 409 }, + thread_generation_in_progress: { + code: "thread_generation_in_progress", + status: 409, + }, + root_thread_title_owned_by_project: { + code: "root_thread_title_owned_by_project", + status: 422, + }, + root_thread_archive_owned_by_project: { + code: "root_thread_archive_owned_by_project", + status: 422, + }, + message_not_editable: { code: "message_not_editable", status: 422 }, + message_not_regeneratable: { + code: "message_not_regeneratable", + status: 422, + }, + feedback_not_eligible: { + code: "message_not_feedback_eligible", + status: 422, + }, + fork_required: { code: "fork_required", status: 422 }, + fork_anchor_mismatch: { code: "fork_anchor_mismatch", status: 422 }, + message_not_fork_eligible: { + code: "fork_source_not_finalized", + status: 422, + }, + message_not_finalized: { + code: "fork_source_not_finalized", + status: 422, + }, + message_superseded: { code: "fork_source_superseded", status: 422 }, + thread_source_invalid: { + code: "fork_source_thread_mismatch", + status: 422, + }, + thread_not_found: { code: "thread_not_found", status: 404 }, + message_not_found: { code: "message_not_found", status: 404 }, + source_message_not_found: { + code: "source_message_not_found", + status: 404, + }, + assistant_message_not_found: { + code: "assistant_message_not_found", + status: 404, + }, + message_run_not_found: { code: "message_run_not_found", status: 404 }, +} + +export function errorResponse( + error: unknown, + fallbackNotFound: ApiErrorCode = "internal_error" +): Response { + if (error instanceof ThreadChatApiError) { + return Response.json( + { + error: { + code: error.code, + message: error.message, + ...(error.details === undefined ? {} : { details: error.details }), + }, + }, + { status: error.status } + ) + } + if (error instanceof ZodError) { + return Response.json( + { + error: { + code: "validation_error", + message: "Request validation failed.", + details: error.issues, + }, + }, + { status: 400 } + ) + } + if (error instanceof ThreadChatDomainError) { + if (error.code === "entity_not_found") { + return Response.json( + { error: { code: fallbackNotFound, message: error.message } }, + { status: 404 } + ) + } + const mapped = domainErrorMap[error.code] + if (mapped) { + return Response.json( + { error: { code: mapped.code, message: error.message } }, + { status: mapped.status } + ) + } + } + console.error("[thread-chat-api] unhandled error", error) + return Response.json( + { error: { code: "internal_error", message: "Internal server error." } }, + { status: 500 } + ) +} diff --git a/lib/thread-chat/api/server/handlers.ts b/lib/thread-chat/api/server/handlers.ts new file mode 100644 index 00000000..0be2df57 --- /dev/null +++ b/lib/thread-chat/api/server/handlers.ts @@ -0,0 +1,435 @@ +import { z } from "zod" +import type { UIMessage } from "ai" +import { + assistantEventsQuerySchema, + createProjectRequestSchema, + editMessageRequestSchema, + forkThreadRequestSchema, + idSchema, + listProjectsQuerySchema, + patchProjectRequestSchema, + patchThreadRequestSchema, + putFeedbackRequestSchema, + regenerateMessageRequestSchema, + sendMessageRequestSchema, + threadMessagesQuerySchema, +} from "../contracts" +import { decodeProjectCursor, encodeProjectCursor } from "./cursor" +import { ThreadChatApiError } from "./errors" +import { jsonData, readJson } from "./http" +import { + toArtifactDTO, + toAssistantRunStateDTO, + toMessageDTO, + toProjectDTO, + toProjectSummaryDTO, + toThreadDTO, + toThreadMessageBundleDTO, +} from "./mappers" +import { threadChatServer } from "./runtime" + +function queryObject(request: Request): Record { + const entries = [...new URL(request.url).searchParams.entries()] + if (new Set(entries.map(([key]) => key)).size !== entries.length) + throw new ThreadChatApiError("invalid_query", 400, "Duplicate query parameter.") + return Object.fromEntries(entries) +} + +function validatePathIds(...ids: string[]): void { + for (const id of ids) idSchema.parse(id) +} + +export async function listProjects(actorId: string, request: Request) { + let query: z.infer + try { + query = listProjectsQuerySchema.parse(queryObject(request)) + } catch (error) { + if (error instanceof ThreadChatApiError) throw error + throw new ThreadChatApiError("invalid_query", 400, "Invalid project query.") + } + const before = query.cursor + ? decodeProjectCursor(query.cursor, { + actorId, + status: query.status, + }) + : undefined + const projects = await threadChatServer.queries.listProjects({ + actorId, + status: query.status, + limit: query.limit + 1, + before, + }) + const hasNext = projects.length > query.limit + const items = projects.slice(0, query.limit) + const last = items.at(-1) + return jsonData({ + items: items.map(toProjectSummaryDTO), + nextCursor: + hasNext && last + ? encodeProjectCursor({ + actorId, + status: query.status, + updatedAt: last.updatedAt.toISOString(), + id: last.id, + }) + : null, + }) +} + +export async function createProject(actorId: string, request: Request) { + const body = createProjectRequestSchema.parse(await readJson(request)) + const result = await threadChatServer.commands().createProject({ + actorId, + parts: body.initialMessage.parts as UIMessage["parts"], + requestedModelId: body.requestedModelId, + }) + return jsonData( + { + project: toProjectDTO(result.project), + rootThread: toThreadDTO(result.rootThread), + artifactSummary: result.artifactSummary, + userMessage: toMessageDTO(result.userMessage), + assistantMessage: toMessageDTO(result.assistantMessage), + assistantRun: toAssistantRunStateDTO(result.assistantRun), + }, + 201 + ) +} + +export async function bootstrapProject(actorId: string, projectId: string) { + validatePathIds(projectId) + const result = await threadChatServer.queries.projectBootstrap({ + actorId, + projectId, + }) + return jsonData({ + project: toProjectDTO(result.project), + threadTopology: result.threadTopology.map(toThreadDTO), + artifactSummary: result.artifactSummary, + initialThread: toThreadMessageBundleDTO(result.initialThread), + }) +} + +export async function patchProject( + actorId: string, + projectId: string, + request: Request +) { + validatePathIds(projectId) + const patch = patchProjectRequestSchema.parse(await readJson(request)) + const project = await threadChatServer.commands().patchProject({ + actorId, + projectId, + patch, + }) + return jsonData(toProjectDTO(project)) +} + +export async function setProjectArchived( + actorId: string, + projectId: string, + archived: boolean +) { + validatePathIds(projectId) + const project = await threadChatServer.commands().setProjectArchived({ + actorId, + projectId, + archived, + }) + return jsonData(toProjectDTO(project)) +} + +export async function deleteProject(actorId: string, projectId: string) { + validatePathIds(projectId) + await threadChatServer.commands().deleteProject({ actorId, projectId }) + return new Response(null, { status: 204 }) +} + +export async function loadThreadMessages( + actorId: string, + threadId: string, + request: Request +) { + validatePathIds(threadId) + let query: z.infer + try { + query = threadMessagesQuerySchema.parse(queryObject(request)) + } catch { + throw new ThreadChatApiError("invalid_query", 400, "Invalid message query.") + } + const bundle = await threadChatServer.queries.threadMessages({ + actorId, + threadId, + ...query, + }) + return jsonData(toThreadMessageBundleDTO(bundle)) +} + +export async function sendMessage( + actorId: string, + threadId: string, + request: Request +) { + validatePathIds(threadId) + const body = sendMessageRequestSchema.parse(await readJson(request)) + const result = await threadChatServer.commands().sendMessage({ + actorId, + threadId, + parts: body.parts as UIMessage["parts"], + requestedModelId: body.requestedModelId, + }) + return jsonData( + { + userMessage: toMessageDTO(result.userMessage), + assistantMessage: toMessageDTO(result.assistantMessage), + assistantRun: toAssistantRunStateDTO(result.assistantRun), + }, + 201 + ) +} + +export async function patchThread( + actorId: string, + threadId: string, + request: Request +) { + validatePathIds(threadId) + const body = patchThreadRequestSchema.parse(await readJson(request)) + const thread = await threadChatServer.commands().patchBranch({ + actorId, + threadId, + customTitle: body.customTitle, + }) + return jsonData(toThreadDTO(thread)) +} + +export async function setThreadArchived( + actorId: string, + threadId: string, + archived: boolean +) { + validatePathIds(threadId) + const thread = await threadChatServer.commands().patchBranch({ + actorId, + threadId, + archived, + }) + return jsonData(toThreadDTO(thread)) +} + +export async function forkThread( + actorId: string, + threadId: string, + request: Request +) { + validatePathIds(threadId) + const body = forkThreadRequestSchema.parse(await readJson(request)) + const thread = await threadChatServer.commands().forkThread({ + actorId, + sourceThreadId: threadId, + sourceMessageId: body.sourceMessageId, + anchor: body.anchor, + }) + return jsonData({ thread: toThreadDTO(thread) }, 201) +} + +export async function editMessage( + actorId: string, + messageId: string, + request: Request +) { + validatePathIds(messageId) + const body = editMessageRequestSchema.parse(await readJson(request)) + const result = await threadChatServer.commands().editLastUser({ + actorId, + sourceUserMessageId: messageId, + parts: body.parts as UIMessage["parts"], + requestedModelId: body.requestedModelId, + }) + return jsonData( + { + supersededMessageIds: result.supersededMessageIds, + createdMessages: result.createdMessages.map(toMessageDTO), + assistantRun: toAssistantRunStateDTO(result.assistantRun), + }, + 201 + ) +} + +export async function regenerateMessage( + actorId: string, + messageId: string, + request: Request +) { + validatePathIds(messageId) + const body = regenerateMessageRequestSchema.parse(await readJson(request)) + const result = await threadChatServer.commands().regenerate({ + actorId, + sourceAssistantMessageId: messageId, + requestedModelId: body.requestedModelId, + }) + return jsonData( + { + supersededMessageIds: result.supersededMessageIds, + createdMessages: result.createdMessages.map(toMessageDTO), + assistantRun: toAssistantRunStateDTO(result.assistantRun), + }, + 201 + ) +} + +export async function setFeedback( + actorId: string, + messageId: string, + request: Request +) { + validatePathIds(messageId) + const body = putFeedbackRequestSchema.parse(await readJson(request)) + const feedback = await threadChatServer.commands().setFeedback({ + actorId, + assistantMessageId: messageId, + feedback: body.value, + }) + return jsonData({ + messageId: feedback.messageId, + value: feedback.value, + updatedAt: feedback.updatedAt.toISOString(), + }) +} + +export async function loadArtifact(actorId: string, artifactId: string) { + validatePathIds(artifactId) + const artifact = await threadChatServer.queries.artifactById({ + actorId, + artifactId, + }) + return jsonData(toArtifactDTO(artifact)) +} + +export async function stopAssistant(actorId: string, assistantMessageId: string) { + validatePathIds(assistantMessageId) + const run = await threadChatServer.runner.requestStop({ + actorId, + assistantMessageId, + }) + return jsonData(toAssistantRunStateDTO(run)) +} + +export async function assistantEvents( + actorId: string, + assistantMessageId: string, + request: Request +) { + validatePathIds(assistantMessageId) + let query: z.infer + try { + query = assistantEventsQuerySchema.parse(queryObject(request)) + } catch { + throw new ThreadChatApiError( + "invalid_query", + 400, + "Invalid event query." + ) + } + const initial = await threadChatServer.queries.assistantSnapshot({ + actorId, + assistantMessageId, + }) + if (query.afterEventSequence > initial.run.eventSequence) + throw new ThreadChatApiError( + "invalid_event_cursor", + 409, + "Event cursor is ahead of the server cursor." + ) + + const encoder = new TextEncoder() + let cancelled = false + const encodeEvent = (event: unknown) => + encoder.encode(`data: ${JSON.stringify(event)}\n\n`) + const stream = new ReadableStream({ + async start(controller) { + let snapshot = initial + let cursor = snapshot.run.eventSequence + controller.enqueue( + encodeEvent({ + type: "run.snapshot", + cursor, + run: toAssistantRunStateDTO(snapshot.run), + message: toMessageDTO(snapshot.message), + artifactSummary: snapshot.artifactSummary, + }) + ) + if (["completed", "failed", "stopped"].includes(snapshot.run.status)) { + controller.close() + return + } + while (!cancelled && !request.signal.aborted) { + await new Promise((resolve) => setTimeout(resolve, 100)) + snapshot = await threadChatServer.queries.assistantSnapshot({ + actorId, + assistantMessageId, + }) + if (snapshot.run.eventSequence <= cursor) continue + cursor = snapshot.run.eventSequence + if (snapshot.run.status === "completed") { + controller.enqueue( + encodeEvent({ + type: "run.completed", + eventSequence: cursor, + run: toAssistantRunStateDTO(snapshot.run), + message: toMessageDTO(snapshot.message), + artifactSummary: snapshot.artifactSummary, + }) + ) + controller.close() + return + } + if (snapshot.run.status === "failed") { + controller.enqueue( + encodeEvent({ + type: "run.failed", + eventSequence: cursor, + run: toAssistantRunStateDTO(snapshot.run), + }) + ) + controller.close() + return + } + if (snapshot.run.status === "stopped") { + controller.enqueue( + encodeEvent({ + type: "run.stopped", + eventSequence: cursor, + run: toAssistantRunStateDTO(snapshot.run), + message: toMessageDTO(snapshot.message), + }) + ) + controller.close() + return + } + controller.enqueue( + encodeEvent({ + type: "run.delta", + eventSequence: cursor, + chunk: { + type: "data-run-checkpoint", + id: assistantMessageId, + data: { checkpointParts: snapshot.run.checkpointParts }, + }, + }) + ) + } + if (!cancelled) controller.close() + }, + cancel() { + cancelled = true + }, + }) + return new Response(stream, { + headers: { + "Cache-Control": "no-cache, no-transform", + "Content-Type": "text/event-stream; charset=utf-8", + Connection: "keep-alive", + }, + }) +} diff --git a/lib/thread-chat/api/server/http.ts b/lib/thread-chat/api/server/http.ts new file mode 100644 index 00000000..5e222ad0 --- /dev/null +++ b/lib/thread-chat/api/server/http.ts @@ -0,0 +1,32 @@ +import { getCurrentUserId } from "@/lib/auth/server" +import type { ApiErrorCode } from "../contracts" +import { errorResponse, ThreadChatApiError } from "./errors" + +export const jsonData = (data: unknown, status = 200) => + Response.json({ data }, { status }) + +export async function readJson(request: Request): Promise { + try { + return await request.json() + } catch { + throw new ThreadChatApiError( + "validation_error", + 400, + "Request body must be valid JSON." + ) + } +} + +export async function withActor( + action: (actorId: string) => Promise, + fallbackNotFound: ApiErrorCode = "internal_error" +): Promise { + try { + const actorId = await getCurrentUserId() + if (!actorId) + throw new ThreadChatApiError("unauthorized", 401, "Authentication required.") + return await action(actorId) + } catch (error) { + return errorResponse(error, fallbackNotFound) + } +} diff --git a/lib/thread-chat/api/server/mappers.ts b/lib/thread-chat/api/server/mappers.ts new file mode 100644 index 00000000..541f8ad4 --- /dev/null +++ b/lib/thread-chat/api/server/mappers.ts @@ -0,0 +1,105 @@ +import type { Artifact } from "../../domain/artifact" +import type { Message } from "../../domain/message" +import type { MessageRun } from "../../domain/message-run" +import type { Project } from "../../domain/project" +import type { Thread } from "../../domain/thread" +import type { ProjectSummary } from "../../application/application-types" + +const iso = (value: Date | null) => value?.toISOString() ?? null + +export function toProjectDTO(project: Project) { + return { + id: project.id, + ownerUserId: project.ownerUserId, + autoTitle: project.autoTitle, + customTitle: project.customTitle, + target: project.target, + instruction: project.instruction, + archivedAt: iso(project.archivedAt), + createdAt: project.createdAt.toISOString(), + updatedAt: project.updatedAt.toISOString(), + } +} + +export function toProjectSummaryDTO(project: ProjectSummary) { + return { + id: project.id, + displayTitle: project.displayTitle, + archivedAt: iso(project.archivedAt), + updatedAt: project.updatedAt.toISOString(), + threadCount: project.threadCount, + messageCount: project.messageCount, + } +} + +export function toThreadDTO(thread: Thread) { + return { + id: thread.id, + projectId: thread.projectId, + parentThreadId: thread.parentThreadId, + sourceMessageId: thread.sourceMessageId, + forkSourceSnapshot: thread.forkSourceSnapshot, + autoTitle: thread.autoTitle, + customTitle: thread.customTitle, + archivedAt: iso(thread.archivedAt), + createdAt: thread.createdAt.toISOString(), + updatedAt: thread.updatedAt.toISOString(), + } +} + +export function toMessageDTO(message: Message) { + return { + id: message.id, + threadId: message.threadId, + sequence: message.sequence, + role: message.role, + parts: message.parts, + replacesMessageId: message.replacesMessageId, + supersededAt: iso(message.supersededAt), + finalizedAt: iso(message.finalizedAt), + createdAt: message.createdAt.toISOString(), + } +} + +export function toAssistantRunStateDTO(run: MessageRun) { + return { + assistantMessageId: run.assistantMessageId, + status: run.status, + modelId: run.modelId, + checkpointParts: run.checkpointParts, + eventSequence: run.eventSequence, + error: + run.errorCode && run.errorMessage + ? { code: run.errorCode, message: run.errorMessage } + : null, + stopRequestedAt: iso(run.stopRequestedAt), + finishedAt: iso(run.finishedAt), + } +} + +export function toArtifactDTO(artifact: Artifact) { + return { + id: artifact.id, + projectId: artifact.projectId, + sourceMessageId: artifact.sourceMessageId, + kind: artifact.kind, + title: artifact.title, + content: artifact.content, + createdAt: artifact.createdAt.toISOString(), + } +} + +export function toThreadMessageBundleDTO(bundle: { + threadId: string + messages: Message[] + assistantRuns: MessageRun[] + hasOlderMessages: boolean + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +}) { + return { + ...bundle, + messages: bundle.messages.map(toMessageDTO), + assistantRuns: bundle.assistantRuns.map(toAssistantRunStateDTO), + } +} diff --git a/lib/thread-chat/api/server/runtime.ts b/lib/thread-chat/api/server/runtime.ts new file mode 100644 index 00000000..b5a66e2a --- /dev/null +++ b/lib/thread-chat/api/server/runtime.ts @@ -0,0 +1,44 @@ +import { after } from "next/server" +import { + DEFAULT_THREAD_CHAT_MODEL_ID, + isThreadChatModelId, +} from "@/constants/model" +import { dbClient } from "@/lib/db" +import { MessageRunner } from "../../application/message-runner" +import { ThreadChatCommands } from "../../application/thread-chat-commands" +import { ThreadChatQueries } from "../../application/thread-chat-queries" +import { AiSdkRuntime } from "../../infrastructure/ai-sdk-runtime" +import { ThreadChatUnitOfWork } from "../../infrastructure/repositories" +import { ThreadChatApiError } from "./errors" + +const unitOfWork = new ThreadChatUnitOfWork(dbClient) +const runner = new MessageRunner(dbClient, unitOfWork, new AiSdkRuntime(), { + generateId: crypto.randomUUID, + now: () => new Date(), +}) + +export const threadChatServer = { + queries: new ThreadChatQueries(dbClient), + runner, + commands() { + return new ThreadChatCommands(unitOfWork, { + generateId: crypto.randomUUID, + now: () => new Date(), + resolveModelId(requestedModelId) { + if (requestedModelId === undefined) return DEFAULT_THREAD_CHAT_MODEL_ID + if (isThreadChatModelId(requestedModelId)) return requestedModelId + throw new ThreadChatApiError( + "model_not_available", + 422, + "Requested model is not available for ThreadChat." + ) + }, + wakeRunAfterCommit(messageRunId) { + after(() => runner.execute(messageRunId)) + }, + onWakeError(error) { + console.error("[thread-chat-api] run wake failed", error) + }, + }) + }, +} diff --git a/lib/thread-chat/application/message-runner.ts b/lib/thread-chat/application/message-runner.ts index 172d9e57..f3ca2315 100644 --- a/lib/thread-chat/application/message-runner.ts +++ b/lib/thread-chat/application/message-runner.ts @@ -113,10 +113,7 @@ export class MessageRunner { toolName: "createMarkdownArtifact", toolCallId: event.output.toolCallId ?? created.id, state: "output-available" as const, - input: { - title: event.output.title, - content: event.output.content, - }, + input: { title: event.output.title }, output: toMarkdownArtifactToolOutput(created), } artifactParts.push(toolPart) diff --git a/lib/thread-chat/application/thread-chat-commands.ts b/lib/thread-chat/application/thread-chat-commands.ts index c4197bf4..130565b9 100644 --- a/lib/thread-chat/application/thread-chat-commands.ts +++ b/lib/thread-chat/application/thread-chat-commands.ts @@ -82,12 +82,16 @@ export class ThreadChatCommands { input.actorId, input.sourceThreadId ) - invariant(parent, "entity_not_found", "Parent Thread 不存在。") + invariant(parent, "thread_not_found", "Parent Thread 不存在。") const source = await repositories.messages.findOwnedByIdForUpdate( input.actorId, input.sourceMessageId ) - invariant(source, "entity_not_found", "Fork source Message 不存在。") + invariant( + source, + "source_message_not_found", + "Fork source Message 不存在。" + ) invariant( source.threadId === parent.id, "thread_source_invalid", @@ -100,6 +104,16 @@ export class ThreadChatCommands { source.id ) : null + invariant( + source.finalizedAt !== null, + "message_not_finalized", + "Fork source 尚未 finalized。" + ) + invariant( + source.supersededAt === null, + "message_superseded", + "Fork source 已 superseded。" + ) assertMessageForkEligible(source, sourceRun) if (input.anchor?.textPosition) { const sourceText = (source.parts ?? []) @@ -181,7 +195,7 @@ export class ThreadChatCommands { input.actorId, input.sourceAssistantMessageId ) - invariant(found, "entity_not_found", "Message 不存在。") + invariant(found, "message_not_found", "Message 不存在。") await repositories.threads.findOwnedByIdForUpdate( input.actorId, found.threadId @@ -190,7 +204,7 @@ export class ThreadChatCommands { input.actorId, found.id ) - invariant(source, "entity_not_found", "Message 不存在。") + invariant(source, "message_not_found", "Message 不存在。") invariant( source.role === "assistant" && source.finalizedAt !== null && @@ -254,7 +268,7 @@ export class ThreadChatCommands { input.actorId, input.sourceUserMessageId ) - invariant(found, "entity_not_found", "Message 不存在。") + invariant(found, "message_not_found", "Message 不存在。") await repositories.threads.findOwnedByIdForUpdate( input.actorId, found.threadId @@ -263,7 +277,7 @@ export class ThreadChatCommands { input.actorId, found.id ) - invariant(source, "entity_not_found", "Message 不存在。") + invariant(source, "message_not_found", "Message 不存在。") invariant( source.role === "user" && source.finalizedAt !== null && @@ -405,7 +419,7 @@ export class ThreadChatCommands { input.actorId, input.threadId ) - invariant(thread, "entity_not_found", "Thread 不存在。") + invariant(thread, "thread_not_found", "Thread 不存在。") invariant(!thread.archivedAt, "thread_archived", "已归档 Thread 不能发送消息。") await repositories.messageRuns.assertNoActiveForThread( input.actorId, diff --git a/lib/thread-chat/application/thread-chat-queries.ts b/lib/thread-chat/application/thread-chat-queries.ts index 1491eee1..0386d3c6 100644 --- a/lib/thread-chat/application/thread-chat-queries.ts +++ b/lib/thread-chat/application/thread-chat-queries.ts @@ -115,4 +115,36 @@ export class ThreadChatQueries { invariant(artifact, "entity_not_found", "Artifact 不存在。") return artifact } + + async assistantSnapshot(input: { + actorId: string + assistantMessageId: string + }) { + const repositories = createThreadChatRepositories(this.sql) + const message = await repositories.messages.findOwnedById( + input.actorId, + input.assistantMessageId + ) + invariant( + message?.role === "assistant", + "assistant_message_not_found", + "assistant Message 不存在。" + ) + const thread = await repositories.threads.findOwnedById( + input.actorId, + message.threadId + ) + invariant(thread, "entity_not_found", "Thread 不存在。") + const run = await repositories.messageRuns.findOwnedByAssistantMessageId( + input.actorId, + message.id + ) + invariant(run, "message_run_not_found", "MessageRun 不存在。") + const artifactSummary = await repositories.artifacts.summarizeOwnedProject( + input.actorId, + thread.projectId + ) + invariant(artifactSummary, "entity_not_found", "Project 不存在。") + return { message, run, artifactSummary } + } } diff --git a/lib/thread-chat/domain/domain-error.ts b/lib/thread-chat/domain/domain-error.ts index ed5ba127..b3277da3 100644 --- a/lib/thread-chat/domain/domain-error.ts +++ b/lib/thread-chat/domain/domain-error.ts @@ -22,6 +22,11 @@ export type ThreadChatDomainErrorCode = | "message_not_regeneratable" | "fork_required" | "fork_anchor_mismatch" + | "thread_not_found" + | "message_not_found" + | "source_message_not_found" + | "assistant_message_not_found" + | "message_run_not_found" | "entity_not_found" export class ThreadChatDomainError extends Error { diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md index bedc1f60..650594c5 100644 --- a/openspec/changes/design-thread-chat-client-api/tasks.md +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -9,41 +9,41 @@ ## 1. 建立共享 V1 契约与 Transport 边界 -- [ ] 1.1 为 `api-contracts.md` 中全部 ID、DTO、Request、Response、Error 和 SSE Event 建立前后端共享严格 Zod Schema。 -- [ ] 1.2 为 AI SDK v7 `UIMessage.parts` 建立共享校验入口,限制客户端提交合法 user parts,并定义 Markdown tool output 的 `artifactId` 结构。 -- [ ] 1.3 定义不依赖 Zustand、React 或具体 HTTP 库的 `ThreadChatApiCapabilities`,覆盖 Project、Thread、Message、Artifact、feedback、SSE 与 Stop。 -- [ ] 1.4 实现 JSON Transport 的 Session/认证处理、请求编码、响应 Schema 校验和统一 `ClientError` 映射。 -- [ ] 1.5 实现集中式 `threadChatRoutes`;页面 URL 只由客户端构造,服务端 DTO 不包含 `canonicalUrl` 或其他页面路径。 -- [ ] 1.6 建立契约 fixture,覆盖成功 DTO、结构化错误、未知字段拒绝、错误实体归属和非法 AI SDK part。 +- [x] 1.1 为 `api-contracts.md` 中全部 ID、DTO、Request、Response、Error 和 SSE Event 建立前后端共享严格 Zod Schema。 +- [x] 1.2 为 AI SDK v7 `UIMessage.parts` 建立共享校验入口,限制客户端提交合法 user parts,并定义 Markdown tool output 的 `artifactId` 结构。 +- [x] 1.3 定义不依赖 Zustand、React 或具体 HTTP 库的 `ThreadChatApiCapabilities`,覆盖 Project、Thread、Message、Artifact、feedback、SSE 与 Stop。 +- [x] 1.4 实现 JSON Transport 的 Session/认证处理、请求编码、响应 Schema 校验和统一 `ClientError` 映射。 +- [x] 1.5 实现集中式 `threadChatRoutes`;页面 URL 只由客户端构造,服务端 DTO 不包含 `canonicalUrl` 或其他页面路径。 +- [x] 1.6 建立契约 fixture,覆盖成功 DTO、结构化错误、未知字段拒绝、错误实体归属和非法 AI SDK part。 ## 2. 实现 Project 与 Thread Query API -- [ ] 2.1 实现 `GET /api/v1/projects` 的 owner scope、active/archived 过滤、稳定排序、limit 和绑定查询条件的不透明 cursor。 -- [ ] 2.2 实现 `GET /api/v1/projects/{projectId}/bootstrap`,返回 Project、全量轻量 topology、ProjectArtifactSummary 与唯一 Root ThreadMessageBundle。 -- [ ] 2.3 实现 `GET /api/v1/threads/{threadId}/messages`,按有效 Message sequence 查询最新最多 200 条并返回窗口边界。 -- [ ] 2.4 确保 Bootstrap 和 MessageBundle 不返回 BaseContext、Prompt History、未打开 Branch Message 或 Artifact 正文。 -- [ ] 2.5 实现 `GET /api/v1/artifacts/{artifactId}`,从 Artifact 所属 Project 校验 actor,并只在按 ID 请求时返回完整内容。 -- [ ] 2.6 实现 ProjectArtifactSummary 的服务端统计和单调 `changeSequence`,不得从客户端已加载 Artifact 数量反推。 +- [x] 2.1 实现 `GET /api/v1/projects` 的 owner scope、active/archived 过滤、稳定排序、limit 和绑定查询条件的不透明 cursor。 +- [x] 2.2 实现 `GET /api/v1/projects/{projectId}/bootstrap`,返回 Project、全量轻量 topology、ProjectArtifactSummary 与唯一 Root ThreadMessageBundle。 +- [x] 2.3 实现 `GET /api/v1/threads/{threadId}/messages`,按有效 Message sequence 查询最新最多 200 条并返回窗口边界。 +- [x] 2.4 确保 Bootstrap 和 MessageBundle 不返回 BaseContext、Prompt History、未打开 Branch Message 或 Artifact 正文。 +- [x] 2.5 实现 `GET /api/v1/artifacts/{artifactId}`,从 Artifact 所属 Project 校验 actor,并只在按 ID 请求时返回完整内容。 +- [x] 2.6 实现 ProjectArtifactSummary 的服务端统计和单调 `changeSequence`,不得从客户端已加载 Artifact 数量反推。 ## 3. 实现 Project、Thread 与 Message Command API -- [ ] 3.1 实现 `POST /api/v1/projects`,调用后端 Application 原子创建 Project、Root、U1、A1 与 queued Run,并在响应中返回 CreationBundle。 -- [ ] 3.2 实现 Project metadata、archive/unarchive 和永久删除 API,保持缺省字段与显式 null 的不同语义。 -- [ ] 3.3 实现 Branch Thread title、archive/unarchive API,并拒绝通过 Thread metadata 修改 Root 标题或归档 Root。 -- [ ] 3.4 实现 `POST /api/v1/threads/{threadId}/messages`,原子返回 user Message、assistant Message 与 queued Run;P0 不增加 user-only append API。 -- [ ] 3.5 实现 Fork API,只接受既有 sourceThreadId、sourceMessageId 与 anchor,不接受 Child ID、BaseContext 或 ForkSourceSnapshot 权威字段。 -- [ ] 3.6 实现 Edit 与 Regenerate replacement API,返回 ReplacementBundle,保证旧 finalized Message parts 与 sequence 不变。 -- [ ] 3.7 实现 feedback API,只允许对合格 assistant Message 设置 positive、negative 或 null。 -- [ ] 3.8 为全部 Route 加入 Session actor、owner scope、归档状态、业务资格、严格字段校验和统一错误映射。 +- [x] 3.1 实现 `POST /api/v1/projects`,调用后端 Application 原子创建 Project、Root、U1、A1 与 queued Run,并在响应中返回 CreationBundle。 +- [x] 3.2 实现 Project metadata、archive/unarchive 和永久删除 API,保持缺省字段与显式 null 的不同语义。 +- [x] 3.3 实现 Branch Thread title、archive/unarchive API,并拒绝通过 Thread metadata 修改 Root 标题或归档 Root。 +- [x] 3.4 实现 `POST /api/v1/threads/{threadId}/messages`,原子返回 user Message、assistant Message 与 queued Run;P0 不增加 user-only append API。 +- [x] 3.5 实现 Fork API,只接受既有 sourceThreadId、sourceMessageId 与 anchor,不接受 Child ID、BaseContext 或 ForkSourceSnapshot 权威字段。 +- [x] 3.6 实现 Edit 与 Regenerate replacement API,返回 ReplacementBundle,保证旧 finalized Message parts 与 sequence 不变。 +- [x] 3.7 实现 feedback API,只允许对合格 assistant Message 设置 positive、negative 或 null。 +- [x] 3.8 为全部 Route 加入 Session actor、owner scope、归档状态、业务资格、严格字段校验和统一错误映射。 ## 4. 实现 SSE 生成恢复与 Stop -- [ ] 4.1 实现 `GET /api/v1/assistant-messages/{id}/events` SSE;每次连接首个业务事件固定为持久化 `run.snapshot`。 -- [ ] 4.2 实现 `afterEventSequence` 校验、重复/倒序过滤和 snapshot 后严格递增的 live event。 -- [ ] 4.3 实现 `run.delta`、`run.completed`、`run.failed`、`run.stopped` Schema,并在 snapshot/completed 携带最新 Artifact Summary。 -- [ ] 4.4 确保 finalized Message tool output 只包含 `artifactId`,SSE 不复制 Artifact 正文。 -- [ ] 4.5 实现显式 Stop API;关闭页面、刷新、切换 Thread 或取消 SSE 不得触发 Stop。 -- [ ] 4.6 验证 queued/running Run 刷新后复用同一 assistantMessageId 和 MessageRun,不启动第二次执行。 +- [x] 4.1 实现 `GET /api/v1/assistant-messages/{id}/events` SSE;每次连接首个业务事件固定为持久化 `run.snapshot`。 +- [x] 4.2 实现 `afterEventSequence` 校验、重复/倒序过滤和 snapshot 后严格递增的 live event。 +- [x] 4.3 实现 `run.delta`、`run.completed`、`run.failed`、`run.stopped` Schema,并在 snapshot/completed 携带最新 Artifact Summary。 +- [x] 4.4 确保 finalized Message tool output 只包含 `artifactId`,SSE 不复制 Artifact 正文。 +- [x] 4.5 实现显式 Stop API;关闭页面、刷新、切换 Thread 或取消 SSE 不得触发 Stop。 +- [x] 4.6 验证 queued/running Run 刷新后复用同一 assistantMessageId 和 MessageRun,不启动第二次执行。 ## 5. 后端 API 验收门 diff --git a/tests/fixtures/thread-chat-api-fixtures.ts b/tests/fixtures/thread-chat-api-fixtures.ts new file mode 100644 index 00000000..2d8c0250 --- /dev/null +++ b/tests/fixtures/thread-chat-api-fixtures.ts @@ -0,0 +1,95 @@ +const ids = { + project: "00000000-0000-4000-8000-000000000101", + thread: "00000000-0000-4000-8000-000000000102", + userMessage: "00000000-0000-4000-8000-000000000103", + assistantMessage: "00000000-0000-4000-8000-000000000104", + artifact: "00000000-0000-4000-8000-000000000105", +} +const timestamp = "2026-08-25T00:00:00.000Z" + +export const projectDTOFixture = { + id: ids.project, + ownerUserId: "user-fixture", + autoTitle: null, + customTitle: null, + target: null, + instruction: null, + archivedAt: null, + createdAt: timestamp, + updatedAt: timestamp, +} + +export const rootThreadDTOFixture = { + id: ids.thread, + projectId: ids.project, + parentThreadId: null, + sourceMessageId: null, + forkSourceSnapshot: null, + autoTitle: null, + customTitle: null, + archivedAt: null, + createdAt: timestamp, + updatedAt: timestamp, +} + +export const userMessageDTOFixture = { + id: ids.userMessage, + threadId: ids.thread, + sequence: 1, + role: "user" as const, + parts: [{ type: "text", text: "hello" }], + replacesMessageId: null, + supersededAt: null, + finalizedAt: timestamp, + createdAt: timestamp, +} + +export const assistantMessageDTOFixture = { + id: ids.assistantMessage, + threadId: ids.thread, + sequence: 2, + role: "assistant" as const, + parts: null, + replacesMessageId: null, + supersededAt: null, + finalizedAt: null, + createdAt: timestamp, +} + +export const assistantRunDTOFixture = { + assistantMessageId: ids.assistantMessage, + status: "queued" as const, + modelId: "fake/model", + checkpointParts: [], + eventSequence: 0, + error: null, + stopRequestedAt: null, + finishedAt: null, +} + +export const creationBundleDTOFixture = { + project: projectDTOFixture, + rootThread: rootThreadDTOFixture, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + userMessage: userMessageDTOFixture, + assistantMessage: assistantMessageDTOFixture, + assistantRun: assistantRunDTOFixture, +} + +export const apiErrorFixture = { + error: { + code: "validation_error" as const, + message: "Request validation failed.", + details: [{ path: ["initialMessage", "parts"] }], + }, +} + +export const ownershipMismatchFixture = { + ...assistantMessageDTOFixture, + threadId: "00000000-0000-4000-8000-000000000999", +} + +export const invalidUserPartFixture = { + type: "reasoning", + text: "client-forged", +} diff --git a/tests/integration/message-runner.test.ts b/tests/integration/message-runner.test.ts index 70c6dd4b..a3e5a881 100644 --- a/tests/integration/message-runner.test.ts +++ b/tests/integration/message-runner.test.ts @@ -117,6 +117,7 @@ describe("MessageRunner", () => { toolName: "createMarkdownArtifact", toolCallId: "tool-call-1", state: "output-available", + input: { title: "Result" }, output: { artifactId: expect.any(String) }, }, ], @@ -139,6 +140,7 @@ describe("MessageRunner", () => { expect( (toolPart as { output: { artifactId: string } }).output ).toEqual({ artifactId }) + expect(JSON.stringify(toolPart)).not.toContain("# Result") expect( await repositories.artifacts.findOwnedById(actorId, artifactId) ).toMatchObject({ From 11727fe3ed7b9aa12c89429ecddfa5cf023ccbfc Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 19:02:27 +0800 Subject: [PATCH 14/23] test(thread-chat): enforce v1 API acceptance gate --- lib/thread-chat/api/contracts.ts | 182 ++++-- lib/thread-chat/api/server/http.ts | 11 +- lib/thread-chat/api/server/runtime.ts | 5 +- .../repositories/artifact-repository.ts | 17 +- .../infrastructure/repositories/database.ts | 69 ++- .../repositories/message-repository.ts | 19 +- .../repositories/message-run-repository.ts | 26 +- .../repositories/project-repository.ts | 20 +- .../repositories/thread-repository.ts | 18 +- .../backend-api-verification.md | 28 + .../design-thread-chat-client-api/tasks.md | 12 +- tests/api/contracts.test.ts | 56 ++ tests/api/errors.test.ts | 115 ++++ tests/api/handlers.test.ts | 558 ++++++++++++++++++ tests/api/sse.test.ts | 238 ++++++++ tests/api/transport.test.ts | 104 ++++ tests/setup/require-test-database.ts | 3 +- 17 files changed, 1369 insertions(+), 112 deletions(-) create mode 100644 openspec/changes/design-thread-chat-client-api/backend-api-verification.md create mode 100644 tests/api/contracts.test.ts create mode 100644 tests/api/errors.test.ts create mode 100644 tests/api/handlers.test.ts create mode 100644 tests/api/sse.test.ts create mode 100644 tests/api/transport.test.ts diff --git a/lib/thread-chat/api/contracts.ts b/lib/thread-chat/api/contracts.ts index 6679f433..3bdbfc08 100644 --- a/lib/thread-chat/api/contracts.ts +++ b/lib/thread-chat/api/contracts.ts @@ -69,18 +69,34 @@ export const forkSourceSnapshotSchema = z.strictObject({ sourceSequence: z.int().positive(), }) -export const threadSchema = z.strictObject({ - id: idSchema, - projectId: idSchema, - parentThreadId: idSchema.nullable(), - sourceMessageId: idSchema.nullable(), - forkSourceSnapshot: forkSourceSnapshotSchema.nullable(), - autoTitle: z.string().nullable(), - customTitle: z.string().nullable(), - archivedAt: dateTimeSchema.nullable(), - createdAt: dateTimeSchema, - updatedAt: dateTimeSchema, -}) +export const threadSchema = z + .strictObject({ + id: idSchema, + projectId: idSchema, + parentThreadId: idSchema.nullable(), + sourceMessageId: idSchema.nullable(), + forkSourceSnapshot: forkSourceSnapshotSchema.nullable(), + autoTitle: z.string().nullable(), + customTitle: z.string().nullable(), + archivedAt: dateTimeSchema.nullable(), + createdAt: dateTimeSchema, + updatedAt: dateTimeSchema, + }) + .superRefine((thread, context) => { + const rootFactsValid = + thread.parentThreadId === null && + thread.sourceMessageId === null && + thread.forkSourceSnapshot === null + const branchFactsValid = + thread.parentThreadId !== null && + thread.sourceMessageId !== null && + thread.forkSourceSnapshot !== null + if (!rootFactsValid && !branchFactsValid) + context.addIssue({ + code: "custom", + message: "Thread ForkFacts are inconsistent.", + }) + }) export const messageSchema = z.strictObject({ id: idSchema, @@ -100,9 +116,7 @@ export const assistantRunStateSchema = z.strictObject({ modelId: z.string().min(1), checkpointParts: messagePartsSchema, eventSequence: z.int().nonnegative(), - error: z - .strictObject({ code: z.string(), message: z.string() }) - .nullable(), + error: z.strictObject({ code: z.string(), message: z.string() }).nullable(), stopRequestedAt: dateTimeSchema.nullable(), finishedAt: dateTimeSchema.nullable(), }) @@ -117,11 +131,18 @@ export const artifactSchema = z.strictObject({ createdAt: dateTimeSchema, }) -export const artifactSummarySchema = z.strictObject({ - changeSequence: z.int().nonnegative(), - total: z.int().nonnegative(), - byKind: z.record(z.string(), z.int().nonnegative()), -}) +export const artifactSummarySchema = z + .strictObject({ + changeSequence: z.int().nonnegative(), + total: z.int().nonnegative(), + byKind: z.record(z.string(), z.int().nonnegative()), + }) + .refine( + (summary) => + Object.values(summary.byKind).reduce((sum, count) => sum + count, 0) === + summary.total, + "Artifact summary total must equal byKind counts." + ) export const feedbackSchema = z.strictObject({ messageId: idSchema, @@ -129,36 +150,98 @@ export const feedbackSchema = z.strictObject({ updatedAt: dateTimeSchema, }) -export const threadMessageBundleSchema = z.strictObject({ - threadId: idSchema, - messages: z.array(messageSchema), - assistantRuns: z.array(assistantRunStateSchema), - hasOlderMessages: z.boolean(), - oldestReturnedSequence: z.int().positive().nullable(), - newestReturnedSequence: z.int().positive().nullable(), -}) +export const threadMessageBundleSchema = z + .strictObject({ + threadId: idSchema, + messages: z.array(messageSchema), + assistantRuns: z.array(assistantRunStateSchema), + hasOlderMessages: z.boolean(), + oldestReturnedSequence: z.int().positive().nullable(), + newestReturnedSequence: z.int().positive().nullable(), + }) + .superRefine((bundle, context) => { + if (bundle.messages.some((message) => message.threadId !== bundle.threadId)) + context.addIssue({ + code: "custom", + message: "Message belongs to another Thread.", + }) + const assistantIds = bundle.messages + .filter((message) => message.role === "assistant") + .map((message) => message.id) + .toSorted() + const runIds = bundle.assistantRuns + .map((run) => run.assistantMessageId) + .toSorted() + if (JSON.stringify(assistantIds) !== JSON.stringify(runIds)) + context.addIssue({ + code: "custom", + message: "Assistant Run coverage is invalid.", + }) + }) -export const projectBootstrapSchema = z.strictObject({ - project: projectSchema, - threadTopology: z.array(threadSchema), - artifactSummary: artifactSummarySchema, - initialThread: threadMessageBundleSchema, -}) +export const projectBootstrapSchema = z + .strictObject({ + project: projectSchema, + threadTopology: z.array(threadSchema), + artifactSummary: artifactSummarySchema, + initialThread: threadMessageBundleSchema, + }) + .superRefine((bootstrap, context) => { + const roots = bootstrap.threadTopology.filter( + (thread) => thread.parentThreadId === null + ) + if ( + roots.length !== 1 || + roots[0].id !== bootstrap.initialThread.threadId || + bootstrap.threadTopology.some( + (thread) => thread.projectId !== bootstrap.project.id + ) + ) + context.addIssue({ + code: "custom", + message: "Project Bootstrap ownership is invalid.", + }) + }) -export const creationBundleSchema = z.strictObject({ - project: projectSchema, - rootThread: threadSchema, - artifactSummary: artifactSummarySchema, - userMessage: messageSchema, - assistantMessage: messageSchema, - assistantRun: assistantRunStateSchema, -}) +export const creationBundleSchema = z + .strictObject({ + project: projectSchema, + rootThread: threadSchema, + artifactSummary: artifactSummarySchema, + userMessage: messageSchema, + assistantMessage: messageSchema, + assistantRun: assistantRunStateSchema, + }) + .superRefine((bundle, context) => { + if ( + bundle.rootThread.projectId !== bundle.project.id || + bundle.rootThread.parentThreadId !== null || + bundle.userMessage.threadId !== bundle.rootThread.id || + bundle.assistantMessage.threadId !== bundle.rootThread.id || + bundle.assistantRun.assistantMessageId !== bundle.assistantMessage.id + ) + context.addIssue({ + code: "custom", + message: "Creation Bundle ownership is invalid.", + }) + }) -export const messageCreationBundleSchema = creationBundleSchema.pick({ - userMessage: true, - assistantMessage: true, - assistantRun: true, -}) +export const messageCreationBundleSchema = z + .strictObject({ + userMessage: messageSchema, + assistantMessage: messageSchema, + assistantRun: assistantRunStateSchema, + }) + .superRefine((bundle, context) => { + if ( + bundle.userMessage.threadId !== bundle.assistantMessage.threadId || + bundle.assistantRun.assistantMessageId !== bundle.assistantMessage.id + ) + context.addIssue({ + code: "custom", + message: "Message Bundle ownership is invalid.", + }) + }) export const replacementBundleSchema = z.strictObject({ supersededMessageIds: z.array(idSchema), @@ -198,7 +281,10 @@ export const patchProjectRequestSchema = z target: projectTargetSchema.nullable().optional(), instruction: z.string().max(20_000).nullable().optional(), }) - .refine((value) => Object.keys(value).length > 0, "At least one field is required.") + .refine( + (value) => Object.keys(value).length > 0, + "At least one field is required." + ) export const patchThreadRequestSchema = z.strictObject({ customTitle: z.string().trim().min(1).max(120).nullable(), }) diff --git a/lib/thread-chat/api/server/http.ts b/lib/thread-chat/api/server/http.ts index 5e222ad0..2801a31c 100644 --- a/lib/thread-chat/api/server/http.ts +++ b/lib/thread-chat/api/server/http.ts @@ -19,12 +19,17 @@ export async function readJson(request: Request): Promise { export async function withActor( action: (actorId: string) => Promise, - fallbackNotFound: ApiErrorCode = "internal_error" + fallbackNotFound: ApiErrorCode = "internal_error", + resolveActor: () => Promise = getCurrentUserId ): Promise { try { - const actorId = await getCurrentUserId() + const actorId = await resolveActor() if (!actorId) - throw new ThreadChatApiError("unauthorized", 401, "Authentication required.") + throw new ThreadChatApiError( + "unauthorized", + 401, + "Authentication required." + ) return await action(actorId) } catch (error) { return errorResponse(error, fallbackNotFound) diff --git a/lib/thread-chat/api/server/runtime.ts b/lib/thread-chat/api/server/runtime.ts index b5a66e2a..bebe3f99 100644 --- a/lib/thread-chat/api/server/runtime.ts +++ b/lib/thread-chat/api/server/runtime.ts @@ -1,4 +1,5 @@ import { after } from "next/server" +import { randomUUID } from "node:crypto" import { DEFAULT_THREAD_CHAT_MODEL_ID, isThreadChatModelId, @@ -13,7 +14,7 @@ import { ThreadChatApiError } from "./errors" const unitOfWork = new ThreadChatUnitOfWork(dbClient) const runner = new MessageRunner(dbClient, unitOfWork, new AiSdkRuntime(), { - generateId: crypto.randomUUID, + generateId: randomUUID, now: () => new Date(), }) @@ -22,7 +23,7 @@ export const threadChatServer = { runner, commands() { return new ThreadChatCommands(unitOfWork, { - generateId: crypto.randomUUID, + generateId: randomUUID, now: () => new Date(), resolveModelId(requestedModelId) { if (requestedModelId === undefined) return DEFAULT_THREAD_CHAT_MODEL_ID diff --git a/lib/thread-chat/infrastructure/repositories/artifact-repository.ts b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts index 669a0546..e5bcac1b 100644 --- a/lib/thread-chat/infrastructure/repositories/artifact-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/artifact-repository.ts @@ -6,7 +6,7 @@ import { mapArtifact, mapMessage, mapThread, - toSqlJson, + toSqlJsonText, type ThreadChatSql, } from "./database" @@ -98,7 +98,7 @@ export class ArtifactRepository { ${counter.changeSequence}, ${input.kind}, ${input.title}, - ${this.sql.json(toSqlJson(input.content))} + ${toSqlJsonText(input.content)}::jsonb ) returning id, @@ -134,12 +134,17 @@ export class ArtifactRepository { return row ? mapArtifact(row) : null } - async summarizeOwnedProject(actorId: UserId, projectId: ProjectId): Promise<{ + async summarizeOwnedProject( + actorId: UserId, + projectId: ProjectId + ): Promise<{ changeSequence: number total: number byKind: Record } | null> { - const rows = await this.sql<{ changeSequence: number; kind: string | null; count: number }[]>` + const rows = await this.sql< + { changeSequence: number; kind: string | null; count: number }[] + >` select p.artifact_change_sequence::integer as "changeSequence", a.kind, @@ -151,7 +156,9 @@ export class ArtifactRepository { ` if (rows.length === 0) return null const byKind = Object.fromEntries( - rows.filter((row) => row.kind !== null).map((row) => [row.kind!, Number(row.count)]) + rows + .filter((row) => row.kind !== null) + .map((row) => [row.kind!, Number(row.count)]) ) return { changeSequence: Number(rows[0].changeSequence), diff --git a/lib/thread-chat/infrastructure/repositories/database.ts b/lib/thread-chat/infrastructure/repositories/database.ts index bb679849..5d89ab8a 100644 --- a/lib/thread-chat/infrastructure/repositories/database.ts +++ b/lib/thread-chat/infrastructure/repositories/database.ts @@ -7,8 +7,29 @@ import type { Thread } from "../../domain/thread" export type ThreadChatSql = postgres.Sql | postgres.TransactionSql -export function toSqlJson(value: unknown): postgres.JSONValue { - return value as postgres.JSONValue +export function toSqlJsonText(value: unknown): string { + return JSON.stringify(value) +} + +export function toSqlTimestamp(value: Date | null | undefined): string | null { + return value?.toISOString() ?? null +} + +function toDate(value: unknown): Date { + return value instanceof Date ? value : new Date(String(value)) +} + +function toNullableDate(value: unknown): Date | null { + return value === null || value === undefined ? null : toDate(value) +} + +function fromSqlJson(value: unknown): unknown { + if (typeof value !== "string") return value + try { + return JSON.parse(value) + } catch { + return value + } } export function mapProject(row: Record): Project { @@ -19,10 +40,10 @@ export function mapProject(row: Record): Project { customTitle: (row.customTitle as string | null) ?? null, target: (row.target as Project["target"]) ?? null, instruction: (row.instruction as string | null) ?? null, - archivedAt: (row.archivedAt as Date | null) ?? null, + archivedAt: toNullableDate(row.archivedAt), artifactChangeSequence: Number(row.artifactChangeSequence), - createdAt: row.createdAt as Date, - updatedAt: row.updatedAt as Date, + createdAt: toDate(row.createdAt), + updatedAt: toDate(row.updatedAt), } } @@ -33,13 +54,15 @@ export function mapThread(row: Record): Thread { parentThreadId: (row.parentThreadId as string | null) ?? null, sourceMessageId: (row.sourceMessageId as string | null) ?? null, forkSourceSnapshot: - (row.forkSourceSnapshot as Thread["forkSourceSnapshot"]) ?? null, - baseContext: (row.baseContext as Thread["baseContext"]) ?? null, + (fromSqlJson(row.forkSourceSnapshot) as Thread["forkSourceSnapshot"]) ?? + null, + baseContext: + (fromSqlJson(row.baseContext) as Thread["baseContext"]) ?? null, autoTitle: (row.autoTitle as string | null) ?? null, customTitle: (row.customTitle as string | null) ?? null, - archivedAt: (row.archivedAt as Date | null) ?? null, - createdAt: row.createdAt as Date, - updatedAt: row.updatedAt as Date, + archivedAt: toNullableDate(row.archivedAt), + createdAt: toDate(row.createdAt), + updatedAt: toDate(row.updatedAt), } } @@ -49,11 +72,11 @@ export function mapMessage(row: Record): Message { threadId: String(row.threadId), sequence: Number(row.sequence), role: row.role as Message["role"], - parts: (row.parts as Message["parts"]) ?? null, + parts: (fromSqlJson(row.parts) as Message["parts"]) ?? null, replacesMessageId: (row.replacesMessageId as string | null) ?? null, - supersededAt: (row.supersededAt as Date | null) ?? null, - finalizedAt: (row.finalizedAt as Date | null) ?? null, - createdAt: row.createdAt as Date, + supersededAt: toNullableDate(row.supersededAt), + finalizedAt: toNullableDate(row.finalizedAt), + createdAt: toDate(row.createdAt), } } @@ -64,14 +87,16 @@ export function mapMessageRun(row: Record): MessageRun { status: row.status as MessageRun["status"], modelId: String(row.modelId), eventSequence: Number(row.eventSequence), - checkpointParts: row.checkpointParts as MessageRun["checkpointParts"], + checkpointParts: fromSqlJson( + row.checkpointParts + ) as MessageRun["checkpointParts"], errorCode: (row.errorCode as string | null) ?? null, errorMessage: (row.errorMessage as string | null) ?? null, - heartbeatAt: (row.heartbeatAt as Date | null) ?? null, - stopRequestedAt: (row.stopRequestedAt as Date | null) ?? null, - finishedAt: (row.finishedAt as Date | null) ?? null, - createdAt: row.createdAt as Date, - updatedAt: row.updatedAt as Date, + heartbeatAt: toNullableDate(row.heartbeatAt), + stopRequestedAt: toNullableDate(row.stopRequestedAt), + finishedAt: toNullableDate(row.finishedAt), + createdAt: toDate(row.createdAt), + updatedAt: toDate(row.updatedAt), } } @@ -83,7 +108,7 @@ export function mapArtifact(row: Record): Artifact { changeSequence: Number(row.changeSequence), kind: String(row.kind), title: String(row.title), - content: row.content, - createdAt: row.createdAt as Date, + content: fromSqlJson(row.content), + createdAt: toDate(row.createdAt), } } diff --git a/lib/thread-chat/infrastructure/repositories/message-repository.ts b/lib/thread-chat/infrastructure/repositories/message-repository.ts index 92b8e903..f3a0af0b 100644 --- a/lib/thread-chat/infrastructure/repositories/message-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/message-repository.ts @@ -2,7 +2,12 @@ import type { UIMessage } from "ai" import { assertMessageCanBeReplaced, type Message } from "../../domain/message" import { invariant } from "../../domain/domain-error" import type { MessageId, ThreadId, UserId } from "../../domain/ids" -import { mapMessage, toSqlJson, type ThreadChatSql } from "./database" +import { + mapMessage, + toSqlJsonText, + toSqlTimestamp, + type ThreadChatSql, +} from "./database" export class MessageRepository { constructor(private readonly sql: ThreadChatSql) {} @@ -158,7 +163,7 @@ export class MessageRepository { }): Promise { const rows = await this.sql<{ id: string }[]>` update thread_chat.messages m - set superseded_at = ${input.supersededAt} + set superseded_at = ${toSqlTimestamp(input.supersededAt)}::timestamptz from thread_chat.threads t, thread_chat.projects p where m.thread_id = ${input.threadId} and m.sequence >= ${input.fromSequence} @@ -180,7 +185,7 @@ export class MessageRepository { }): Promise { const rows = await this.sql<{ id: string }[]>` update thread_chat.messages m - set superseded_at = ${input.supersededAt} + set superseded_at = ${toSqlTimestamp(input.supersededAt)}::timestamptz from thread_chat.threads t, thread_chat.projects p where m.thread_id = ${input.threadId} and m.sequence > ${input.afterSequence} @@ -262,9 +267,9 @@ export class MessageRepository { ${input.threadId}, ${sequenceRow.nextSequence}, ${input.role}, - ${input.parts === null ? null : this.sql.json(toSqlJson(input.parts))}, + ${input.parts === null ? null : toSqlJsonText(input.parts)}::jsonb, ${input.replacesMessageId ?? null}, - ${input.finalizedAt} + ${toSqlTimestamp(input.finalizedAt)}::timestamptz ) returning id, @@ -297,8 +302,8 @@ export class MessageRepository { }): Promise { const [row] = await this.sql[]>` update thread_chat.messages m - set parts = ${this.sql.json(toSqlJson(input.parts))}, - finalized_at = ${input.finalizedAt} + set parts = ${toSqlJsonText(input.parts)}::jsonb, + finalized_at = ${toSqlTimestamp(input.finalizedAt)}::timestamptz from thread_chat.threads t, thread_chat.projects p where m.id = ${input.messageId} and m.thread_id = t.id diff --git a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts index 0a483610..f2b23915 100644 --- a/lib/thread-chat/infrastructure/repositories/message-run-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/message-run-repository.ts @@ -12,7 +12,12 @@ import { type MessageRun, type MessageRunStatus, } from "../../domain/message-run" -import { mapMessageRun, toSqlJson, type ThreadChatSql } from "./database" +import { + mapMessageRun, + toSqlJsonText, + toSqlTimestamp, + type ThreadChatSql, +} from "./database" export class MessageRunRepository { constructor(private readonly sql: ThreadChatSql) {} @@ -179,7 +184,10 @@ export class MessageRunRepository { return rows.map((row) => row.id) } - async assertNoActiveForThread(actorId: UserId, threadId: string): Promise { + async assertNoActiveForThread( + actorId: UserId, + threadId: string + ): Promise { const [row] = await this.sql` select r.id from thread_chat.threads t @@ -213,7 +221,7 @@ export class MessageRunRepository { update thread_chat.message_runs r set status = ${input.nextStatus}, event_sequence = event_sequence + ${input.incrementEventSequence ? 1 : 0}, - finished_at = ${input.finishedAt ?? null}, + finished_at = ${toSqlTimestamp(input.finishedAt)}::timestamptz, error_code = ${input.error?.code ?? null}, error_message = ${input.error?.message ?? null}, updated_at = now() @@ -253,9 +261,9 @@ export class MessageRunRepository { }): Promise { const [row] = await this.sql[]>` update thread_chat.message_runs r - set checkpoint_parts = ${this.sql.json(toSqlJson(input.checkpointParts))}, + set checkpoint_parts = ${toSqlJsonText(input.checkpointParts)}::jsonb, event_sequence = event_sequence + 1, - heartbeat_at = ${input.heartbeatAt}, + heartbeat_at = ${toSqlTimestamp(input.heartbeatAt)}::timestamptz, updated_at = now() from thread_chat.messages m, thread_chat.threads t, @@ -292,7 +300,8 @@ export class MessageRunRepository { }): Promise { const [row] = await this.sql[]>` update thread_chat.message_runs r - set heartbeat_at = ${input.heartbeatAt}, updated_at = now() + set heartbeat_at = ${toSqlTimestamp(input.heartbeatAt)}::timestamptz, + updated_at = now() from thread_chat.messages m, thread_chat.threads t, thread_chat.projects p @@ -320,7 +329,10 @@ export class MessageRunRepository { ): Promise { const [row] = await this.sql[]>` update thread_chat.message_runs r - set stop_requested_at = coalesce(r.stop_requested_at, ${requestedAt}), + set stop_requested_at = coalesce( + r.stop_requested_at, + ${toSqlTimestamp(requestedAt)}::timestamptz + ), updated_at = now() from thread_chat.messages m, thread_chat.threads t, diff --git a/lib/thread-chat/infrastructure/repositories/project-repository.ts b/lib/thread-chat/infrastructure/repositories/project-repository.ts index 81da07f8..c43dc137 100644 --- a/lib/thread-chat/infrastructure/repositories/project-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/project-repository.ts @@ -1,7 +1,12 @@ import { invariant } from "../../domain/domain-error" import type { ProjectId, UserId } from "../../domain/ids" import type { Project, ProjectTarget } from "../../domain/project" -import { mapProject, type ThreadChatSql } from "./database" +import { + mapProject, + toSqlJsonText, + toSqlTimestamp, + type ThreadChatSql, +} from "./database" export class ProjectRepository { constructor(private readonly sql: ThreadChatSql) {} @@ -44,7 +49,7 @@ export class ProjectRepository { ${input.ownerUserId}, ${input.autoTitle ?? null}, ${input.customTitle ?? null}, - ${input.target ? this.sql.json(input.target) : null}, + ${input.target ? toSqlJsonText(input.target) : null}::jsonb, ${input.instruction ?? null} ) returning @@ -89,8 +94,8 @@ export class ProjectRepository { and (${input.status} = 'all' or (${input.status} = 'active' and p.archived_at is null) or (${input.status} = 'archived' and p.archived_at is not null)) - and (${input.before?.updatedAt ?? null}::timestamptz is null - or (p.updated_at, p.id) < (${input.before?.updatedAt ?? null}, ${input.before?.id ?? null}::uuid)) + and (${toSqlTimestamp(input.before?.updatedAt)}::timestamptz is null + or (p.updated_at, p.id) < (${toSqlTimestamp(input.before?.updatedAt)}::timestamptz, ${input.before?.id ?? null}::uuid)) group by p.id order by p.updated_at desc, p.id desc limit ${input.limit} @@ -112,7 +117,7 @@ export class ProjectRepository { const [row] = await this.sql[]>` update thread_chat.projects set custom_title = case when ${input.customTitle !== undefined} then ${input.customTitle ?? null} else custom_title end, - target = case when ${input.target !== undefined} then ${input.target ? this.sql.json(input.target) : null} else target end, + target = case when ${input.target !== undefined} then ${input.target ? toSqlJsonText(input.target) : null}::jsonb else target end, instruction = case when ${input.instruction !== undefined} then ${input.instruction ?? null} else instruction end, updated_at = now() where id = ${input.projectId} and owner_user_id = ${input.actorId} @@ -135,7 +140,10 @@ export class ProjectRepository { const [row] = await this.sql[]>` update thread_chat.projects set archived_at = case - when ${input.archived} then coalesce(archived_at, ${input.now}) + when ${input.archived} then coalesce( + archived_at, + ${toSqlTimestamp(input.now)}::timestamptz + ) else null end, updated_at = case diff --git a/lib/thread-chat/infrastructure/repositories/thread-repository.ts b/lib/thread-chat/infrastructure/repositories/thread-repository.ts index f1378809..5c4a27db 100644 --- a/lib/thread-chat/infrastructure/repositories/thread-repository.ts +++ b/lib/thread-chat/infrastructure/repositories/thread-repository.ts @@ -5,7 +5,12 @@ import { import { invariant } from "../../domain/domain-error" import type { ProjectId, ThreadId, UserId } from "../../domain/ids" import type { ForkSourceSnapshot, Thread } from "../../domain/thread" -import { mapThread, type ThreadChatSql } from "./database" +import { + mapThread, + toSqlJsonText, + toSqlTimestamp, + type ThreadChatSql, +} from "./database" import { ProjectRepository } from "./project-repository" export class ThreadRepository { @@ -154,7 +159,10 @@ export class ThreadRepository { set custom_title = case when ${input.customTitle !== undefined} then ${input.customTitle ?? null} else t.custom_title end, archived_at = case when ${!hasArchived} then t.archived_at - when ${archived} then coalesce(t.archived_at, ${input.now}) + when ${archived} then coalesce( + t.archived_at, + ${toSqlTimestamp(input.now)}::timestamptz + ) else null end, updated_at = case @@ -201,10 +209,10 @@ export class ThreadRepository { ${input.sourceMessageId ?? null}, ${ input.forkSourceSnapshot - ? this.sql.json(input.forkSourceSnapshot) + ? toSqlJsonText(input.forkSourceSnapshot) : null - }, - ${input.baseContext ? this.sql.json(input.baseContext) : null} + }::jsonb, + ${input.baseContext ? toSqlJsonText(input.baseContext) : null}::jsonb ) returning id, diff --git a/openspec/changes/design-thread-chat-client-api/backend-api-verification.md b/openspec/changes/design-thread-chat-client-api/backend-api-verification.md new file mode 100644 index 00000000..cb48bc28 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/backend-api-verification.md @@ -0,0 +1,28 @@ +# 后端 API 验收证据 + +## 自动化门槛 + +| 门槛 | 命令 | 结果 | +|---|---|---| +| 领域单元测试 | `pnpm test:unit` | 4 个文件、14 个测试通过 | +| Repository / Application / Runtime 集成测试 | `pnpm test:integration` | 5 个文件、20 个测试通过;使用隔离的 `thread-chat-test` PostgreSQL | +| API 合同与集成测试 | `pnpm test:api` | 5 个文件、32 个测试通过;使用 Fake AI Runtime,不请求真实模型 | +| TypeScript | `pnpm typecheck` | 通过 | +| 生产构建 | `pnpm build --webpack` | Next.js 16.3.1 官方 webpack 构建通过;编译、类型检查、静态生成和全部 `/api/v1` Route 收集完成 | +| OpenSpec | `pnpm openspec:validate` | 27 项严格校验通过 | + +默认 Turbopack 构建已执行到 PostCSS 处理阶段,但当前受管执行环境禁止其内部进程绑定端口并触发 Turbopack panic。使用 Next.js 16.3.1 官方 `--webpack` 回退后生产构建完整通过,未修改项目默认构建器。 + +## 覆盖映射 + +| API 能力 | 自动化证据 | +|---|---| +| 严格 Zod DTO、实体归属、非法 user part、Artifact tool output 仅含 `artifactId` | `tests/api/contracts.test.ts` | +| JSON Transport 请求编码、严格响应、ClientError 与 SSE frame 解析 | `tests/api/transport.test.ts` | +| Session、owner scope、Project cursor、空 Query、窗口边界、metadata、archive、delete、feedback | `tests/api/handlers.test.ts` | +| create、send、Fork、Edit、Regenerate、Stop 的关联响应、严格输入、资格失败与无半成品回滚 | `tests/api/handlers.test.ts` | +| 公开领域错误、Route fallback、Zod details 与未知异常隐藏 | `tests/api/errors.test.ts` | +| snapshot、cursor、重复连接、取消订阅、completed、failed、stopped 与断线重连 | `tests/api/sse.test.ts` | +| Artifact 正文只由按 ID Query 返回,Bootstrap、MessageBundle 与 SSE 只携带引用或摘要 | `tests/api/handlers.test.ts`、`tests/api/sse.test.ts` | + +API 自动化测试不会唤醒真实后台模型执行,也不会请求真实模型供应商。 diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md index 650594c5..62d09ddd 100644 --- a/openspec/changes/design-thread-chat-client-api/tasks.md +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -47,12 +47,12 @@ ## 5. 后端 API 验收门 -- [ ] 5.1 在 domain change 的 Vitest 与隔离 PostgreSQL 基础上完成 API 合同测试;自动测试统一使用 Fake AI Runtime,不调用真实模型。 -- [ ] 5.2 覆盖 Project list/bootstrap、Thread Message window、Artifact-by-ID、metadata、archive/delete、feedback 和全部错误码。 -- [ ] 5.3 覆盖 create/send/Fork/Edit/Regenerate/Stop 的权限、严格输入、原子响应与事务回滚;确认服务端不返回页面 URL。 -- [ ] 5.4 覆盖 SSE snapshot、eventSequence、delta、terminal、断开重连、重复连接和显式 Stop。 -- [ ] 5.5 覆盖 Markdown Artifact:Message 只保存 `artifactId`、Bootstrap/MessageBundle/SSE 无正文、Artifact Query 按 ID 返回正文。 -- [ ] 5.6 执行 `pnpm typecheck`、domain unit/integration、API tests、`pnpm build` 与 `pnpm openspec:validate`;全部通过并记录证据后才能开始第 6 节前端工作。 +- [x] 5.1 在 domain change 的 Vitest 与隔离 PostgreSQL 基础上完成 API 合同测试;自动测试统一使用 Fake AI Runtime,不调用真实模型。 +- [x] 5.2 覆盖 Project list/bootstrap、Thread Message window、Artifact-by-ID、metadata、archive/delete、feedback 和全部错误码。 +- [x] 5.3 覆盖 create/send/Fork/Edit/Regenerate/Stop 的权限、严格输入、原子响应与事务回滚;确认服务端不返回页面 URL。 +- [x] 5.4 覆盖 SSE snapshot、eventSequence、delta、terminal、断开重连、重复连接和显式 Stop。 +- [x] 5.5 覆盖 Markdown Artifact:Message 只保存 `artifactId`、Bootstrap/MessageBundle/SSE 无正文、Artifact Query 按 ID 返回正文。 +- [x] 5.6 执行 `pnpm typecheck`、domain unit/integration、API tests、`pnpm build` 与 `pnpm openspec:validate`;全部通过并记录证据后才能开始第 6 节前端工作。 ## 6. 建立前端测试基础与 Zustand Store diff --git a/tests/api/contracts.test.ts b/tests/api/contracts.test.ts new file mode 100644 index 00000000..75b987b1 --- /dev/null +++ b/tests/api/contracts.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest" +import { + apiErrorResponseSchema, + creationBundleSchema, + createProjectRequestSchema, + markdownArtifactToolOutputSchema, + userMessagePartsSchema, +} from "@/lib/thread-chat/api/contracts" +import { + apiErrorFixture, + creationBundleDTOFixture, + invalidUserPartFixture, + ownershipMismatchFixture, +} from "../fixtures/thread-chat-api-fixtures" + +describe("ThreadChat V1 contracts", () => { + it("接受权威 Creation Bundle 与结构化错误", () => { + expect(creationBundleSchema.parse(creationBundleDTOFixture)).toEqual( + creationBundleDTOFixture + ) + expect(apiErrorResponseSchema.parse(apiErrorFixture)).toEqual( + apiErrorFixture + ) + }) + + it("拒绝未知请求字段、错误实体归属与非法 user part", () => { + expect(() => + createProjectRequestSchema.parse({ + initialMessage: { parts: [{ type: "text", text: "hello" }] }, + clientProjectId: crypto.randomUUID(), + }) + ).toThrow() + expect(() => + creationBundleSchema.parse({ + ...creationBundleDTOFixture, + assistantMessage: ownershipMismatchFixture, + }) + ).toThrow(/ownership/) + expect(() => + userMessagePartsSchema.parse([invalidUserPartFixture]) + ).toThrow() + }) + + it("Markdown tool output 只能携带 artifactId", () => { + const artifactId = crypto.randomUUID() + expect(markdownArtifactToolOutputSchema.parse({ artifactId })).toEqual({ + artifactId, + }) + expect(() => + markdownArtifactToolOutputSchema.parse({ + artifactId, + content: "# duplicated", + }) + ).toThrow() + }) +}) diff --git a/tests/api/errors.test.ts b/tests/api/errors.test.ts new file mode 100644 index 00000000..ee607eea --- /dev/null +++ b/tests/api/errors.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it, vi } from "vitest" +import { ZodError } from "zod" +import { ThreadChatDomainError } from "@/lib/thread-chat/domain/domain-error" +import { + errorResponse, + ThreadChatApiError, +} from "@/lib/thread-chat/api/server/errors" + +async function readError(response: Response) { + return { + status: response.status, + body: await response.json(), + } +} + +describe("ThreadChat API error mapping", () => { + it.each([ + ["project_owner_mismatch", "forbidden", 403], + ["thread_archived", "thread_archived", 409], + ["thread_generation_in_progress", "thread_generation_in_progress", 409], + [ + "root_thread_title_owned_by_project", + "root_thread_title_owned_by_project", + 422, + ], + [ + "root_thread_archive_owned_by_project", + "root_thread_archive_owned_by_project", + 422, + ], + ["message_not_editable", "message_not_editable", 422], + ["message_not_regeneratable", "message_not_regeneratable", 422], + ["feedback_not_eligible", "message_not_feedback_eligible", 422], + ["fork_required", "fork_required", 422], + ["fork_anchor_mismatch", "fork_anchor_mismatch", 422], + ["message_not_fork_eligible", "fork_source_not_finalized", 422], + ["message_not_finalized", "fork_source_not_finalized", 422], + ["message_superseded", "fork_source_superseded", 422], + ["thread_source_invalid", "fork_source_thread_mismatch", 422], + ["thread_not_found", "thread_not_found", 404], + ["message_not_found", "message_not_found", 404], + ["source_message_not_found", "source_message_not_found", 404], + ["assistant_message_not_found", "assistant_message_not_found", 404], + ["message_run_not_found", "message_run_not_found", 404], + ] as const)("映射 %s", async (domainCode, apiCode, status) => { + expect( + await readError( + errorResponse(new ThreadChatDomainError(domainCode, "mapped")) + ) + ).toEqual({ + status, + body: { error: { code: apiCode, message: "mapped" } }, + }) + }) + + it("按 Route fallback 映射 entity_not_found", async () => { + expect( + await readError( + errorResponse( + new ThreadChatDomainError("entity_not_found", "missing"), + "artifact_not_found" + ) + ) + ).toEqual({ + status: 404, + body: { error: { code: "artifact_not_found", message: "missing" } }, + }) + }) + + it("保留显式 API 错误与 Zod details,隐藏未知异常", async () => { + expect( + await readError( + errorResponse( + new ThreadChatApiError("project_delete_conflict", 409, "busy", { + projectId: "project", + }) + ) + ) + ).toEqual({ + status: 409, + body: { + error: { + code: "project_delete_conflict", + message: "busy", + details: { projectId: "project" }, + }, + }, + }) + + const zodError = new ZodError([ + { + code: "custom", + path: ["field"], + message: "invalid", + }, + ]) + const validation = await readError(errorResponse(zodError)) + expect(validation.status).toBe(400) + expect(validation.body.error).toMatchObject({ code: "validation_error" }) + + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + expect( + await readError(errorResponse(new Error("database secret"))) + ).toEqual({ + status: 500, + body: { + error: { + code: "internal_error", + message: "Internal server error.", + }, + }, + }) + consoleError.mockRestore() + }) +}) diff --git a/tests/api/handlers.test.ts b/tests/api/handlers.test.ts new file mode 100644 index 00000000..fd9e8485 --- /dev/null +++ b/tests/api/handlers.test.ts @@ -0,0 +1,558 @@ +import { randomUUID } from "node:crypto" +import { describe, expect, it, vi } from "vitest" + +vi.mock("next/server", () => ({ after: vi.fn() })) + +import { dbClient } from "@/lib/db" +import { + creationBundleSchema, + messageCreationBundleSchema, + projectBootstrapSchema, + replacementBundleSchema, + threadMessageBundleSchema, +} from "@/lib/thread-chat/api/contracts" +import { + bootstrapProject, + createProject, + deleteProject, + editMessage, + forkThread, + listProjects, + loadArtifact, + loadThreadMessages, + patchProject, + patchThread, + regenerateMessage, + sendMessage, + setFeedback, + setProjectArchived, + setThreadArchived, + stopAssistant, +} from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" +import { + createThreadChatRepositories, + ThreadChatUnitOfWork, +} from "@/lib/thread-chat/infrastructure/repositories" + +const unitOfWork = new ThreadChatUnitOfWork(dbClient) + +async function createUser(): Promise { + const id = randomUUID() + await dbClient` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${id}, 'API Test', ${`${id}@thread-chat.test`}, true, now(), now() + ) + ` + return id +} + +async function deleteUser(id: string): Promise { + await dbClient`delete from thread_chat."user" where id = ${id}` +} + +function jsonRequest(url: string, method: string, body?: unknown) { + return new Request(url, { + method, + headers: + body === undefined ? undefined : { "Content-Type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }) +} + +async function call( + actorId: string | null, + action: (actorId: string) => Promise, + fallback: Parameters[1] = "internal_error" +) { + return withActor(action, fallback, async () => actorId) +} + +async function data(response: Response) { + return (await response.json()).data +} + +async function errorCode(response: Response) { + return (await response.json()).error.code as string +} + +async function aggregateCounts(actorId: string) { + const [row] = await dbClient< + { projects: number; threads: number; messages: number; runs: number }[] + >` + select + count(distinct p.id)::integer as projects, + count(distinct t.id)::integer as threads, + count(distinct m.id)::integer as messages, + count(distinct r.id)::integer as runs + from thread_chat.projects p + left join thread_chat.threads t on t.project_id = p.id + left join thread_chat.messages m on m.thread_id = t.id + left join thread_chat.message_runs r on r.assistant_message_id = m.id + where p.owner_user_id = ${actorId} + ` + return row +} + +async function completeAssistant( + actorId: string, + assistantMessageId: string, + parts: Array> = [{ type: "text", text: "completed" }] +) { + const repositories = createThreadChatRepositories(dbClient) + const run = await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + assistantMessageId + ) + await repositories.messageRuns.transition({ + actorId, + messageRunId: run!.id, + expectedStatus: "queued", + nextStatus: "running", + }) + await repositories.messages.finalizeAssistantOnce({ + actorId, + messageId: assistantMessageId, + parts: parts as never, + finalizedAt: new Date(), + }) + await repositories.messageRuns.transition({ + actorId, + messageRunId: run!.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: new Date(), + incrementEventSequence: true, + }) +} + +describe("ThreadChat API handlers", () => { + it("Session、严格输入、Project cursor 与 owner scope 使用统一合同", async () => { + const actorId = await createUser() + const otherId = await createUser() + try { + const unauthorized = await call(null, (actor) => + listProjects(actor, new Request("http://test/api/v1/projects")) + ) + expect(unauthorized.status).toBe(401) + + const invalid = await call(actorId, (actor) => + createProject( + actor, + jsonRequest("http://test/api/v1/projects", "POST", { + initialMessage: { parts: [{ type: "text", text: "valid" }] }, + clientProjectId: randomUUID(), + }) + ) + ) + expect(invalid.status).toBe(400) + expect((await invalid.json()).error.code).toBe("validation_error") + + const creations: Array> = [] + for (const text of ["one", "two", "three"]) { + const response = await call(actorId, (actor) => + createProject( + actor, + jsonRequest("http://test/api/v1/projects", "POST", { + initialMessage: { parts: [{ type: "text", text }] }, + }) + ) + ) + expect(response.status).toBe(201) + const creation = creationBundleSchema.parse(await data(response)) + expect(JSON.stringify(creation)).not.toContain("canonicalUrl") + creations.push(creation) + } + + const firstPage = await data( + await call(actorId, (actor) => + listProjects( + actor, + new Request("http://test/api/v1/projects?limit=2&status=active") + ) + ) + ) + expect(firstPage.items).toHaveLength(2) + expect(firstPage.nextCursor).toEqual(expect.any(String)) + const secondPage = await data( + await call(actorId, (actor) => + listProjects( + actor, + new Request( + `http://test/api/v1/projects?limit=2&status=active&cursor=${encodeURIComponent(firstPage.nextCursor)}` + ) + ) + ) + ) + expect(secondPage.items).toHaveLength(1) + expect( + new Set( + [...firstPage.items, ...secondPage.items].map((item) => item.id) + ).size + ).toBe(3) + const rebound = await call(actorId, (actor) => + listProjects( + actor, + new Request( + `http://test/api/v1/projects?status=archived&cursor=${encodeURIComponent(firstPage.nextCursor)}` + ) + ) + ) + expect(rebound.status).toBe(400) + expect((await rebound.json()).error.code).toBe("invalid_cursor") + + const forbidden = await call( + otherId, + (actor) => bootstrapProject(actor, creations[0].project.id), + "project_not_found" + ) + expect(forbidden.status).toBe(404) + expect((await forbidden.json()).error.code).toBe("project_not_found") + } finally { + await deleteUser(actorId) + await deleteUser(otherId) + } + }) + + it("Query 边界与失败 Command 返回合同错误且不留下半成品", async () => { + const actorId = await createUser() + try { + expect( + await data( + await call(actorId, (actor) => + listProjects(actor, new Request("http://test/api/v1/projects")) + ) + ) + ).toEqual({ items: [], nextCursor: null }) + + const duplicateQuery = await call(actorId, (actor) => + listProjects( + actor, + new Request("http://test/api/v1/projects?limit=2&limit=3") + ) + ) + expect(duplicateQuery.status).toBe(400) + expect(await errorCode(duplicateQuery)).toBe("invalid_query") + + const invalidModel = await call(actorId, (actor) => + createProject( + actor, + jsonRequest("http://test/api/v1/projects", "POST", { + initialMessage: { + parts: [{ type: "text", text: "invalid model" }], + }, + requestedModelId: "not/available", + }) + ) + ) + expect(invalidModel.status).toBe(422) + expect(await errorCode(invalidModel)).toBe("model_not_available") + expect(await aggregateCounts(actorId)).toEqual({ + projects: 0, + threads: 0, + messages: 0, + runs: 0, + }) + + const creation = creationBundleSchema.parse( + await data( + await call(actorId, (actor) => + createProject( + actor, + jsonRequest("http://test/api/v1/projects", "POST", { + initialMessage: { parts: [{ type: "text", text: "queued" }] }, + }) + ) + ) + ) + ) + const before = await aggregateCounts(actorId) + + const sendConflict = await call(actorId, (actor) => + sendMessage( + actor, + creation.rootThread.id, + jsonRequest("http://test/messages", "POST", { + parts: [{ type: "text", text: "must rollback" }], + }) + ) + ) + expect(sendConflict.status).toBe(409) + expect(await errorCode(sendConflict)).toBe( + "thread_generation_in_progress" + ) + + const forkConflict = await call(actorId, (actor) => + forkThread( + actor, + creation.rootThread.id, + jsonRequest("http://test/forks", "POST", { + sourceMessageId: creation.assistantMessage.id, + }) + ) + ) + expect(forkConflict.status).toBe(422) + expect(await errorCode(forkConflict)).toBe("fork_source_not_finalized") + + const rootTitle = await call(actorId, (actor) => + patchThread( + actor, + creation.rootThread.id, + jsonRequest("http://test/thread", "PATCH", { customTitle: "Root" }) + ) + ) + expect(rootTitle.status).toBe(422) + expect(await errorCode(rootTitle)).toBe( + "root_thread_title_owned_by_project" + ) + const rootArchive = await call(actorId, (actor) => + setThreadArchived(actor, creation.rootThread.id, true) + ) + expect(rootArchive.status).toBe(422) + expect(await errorCode(rootArchive)).toBe( + "root_thread_archive_owned_by_project" + ) + + const feedbackConflict = await call(actorId, (actor) => + setFeedback( + actor, + creation.userMessage.id, + jsonRequest("http://test/feedback", "PUT", { value: "negative" }) + ) + ) + expect(feedbackConflict.status).toBe(422) + expect(await errorCode(feedbackConflict)).toBe( + "message_not_feedback_eligible" + ) + + expect(await aggregateCounts(actorId)).toEqual(before) + } finally { + await deleteUser(actorId) + } + }) + + it("Query、metadata、Artifact 与 command 响应保持原子关系", async () => { + const actorId = await createUser() + const otherId = await createUser() + try { + const creationResponse = await call(actorId, (actor) => + createProject( + actor, + jsonRequest("http://test/api/v1/projects", "POST", { + initialMessage: { + parts: [{ type: "text", text: "create artifact" }], + }, + }) + ) + ) + const creation = creationBundleSchema.parse(await data(creationResponse)) + const artifact = await unitOfWork.transaction((repositories) => + repositories.artifacts.insert({ + actorId, + id: randomUUID(), + projectId: creation.project.id, + sourceMessageId: creation.assistantMessage.id, + kind: "markdown", + title: "API Artifact", + content: "# API Artifact body", + }) + ) + await completeAssistant(actorId, creation.assistantMessage.id, [ + { type: "text", text: "done" }, + { + type: "dynamic-tool", + toolName: "createMarkdownArtifact", + toolCallId: "tool-api", + state: "output-available", + input: { title: "API Artifact" }, + output: { artifactId: artifact.id }, + }, + ]) + + const bootstrapResponse = await call(actorId, (actor) => + bootstrapProject(actor, creation.project.id) + ) + const bootstrap = projectBootstrapSchema.parse( + await data(bootstrapResponse) + ) + expect(bootstrap.artifactSummary).toEqual({ + changeSequence: 1, + total: 1, + byKind: { markdown: 1 }, + }) + expect(JSON.stringify(bootstrap)).not.toContain("# API Artifact body") + const artifactResponse = await call(actorId, (actor) => + loadArtifact(actor, artifact.id) + ) + expect(await data(artifactResponse)).toMatchObject({ + id: artifact.id, + content: "# API Artifact body", + }) + const hiddenArtifact = await call( + otherId, + (actor) => loadArtifact(actor, artifact.id), + "artifact_not_found" + ) + expect(hiddenArtifact.status).toBe(404) + + const patchResponse = await call(actorId, (actor) => + patchProject( + actor, + creation.project.id, + jsonRequest("http://test/project", "PATCH", { + customTitle: "Patched", + }) + ) + ) + expect(await data(patchResponse)).toMatchObject({ + customTitle: "Patched", + target: null, + }) + expect( + ( + await data( + await call(actorId, (actor) => + setProjectArchived(actor, creation.project.id, true) + ) + ) + ).archivedAt + ).not.toBeNull() + await call(actorId, (actor) => + setProjectArchived(actor, creation.project.id, false) + ) + + const feedback = await call(actorId, (actor) => + setFeedback( + actor, + creation.assistantMessage.id, + jsonRequest("http://test/feedback", "PUT", { value: "positive" }) + ) + ) + expect(await data(feedback)).toMatchObject({ value: "positive" }) + + const sendResponse = await call(actorId, (actor) => + sendMessage( + actor, + creation.rootThread.id, + jsonRequest("http://test/messages", "POST", { + parts: [{ type: "text", text: "second" }], + }) + ) + ) + const sent = messageCreationBundleSchema.parse(await data(sendResponse)) + expect(sent.assistantRun.assistantMessageId).toBe( + sent.assistantMessage.id + ) + await completeAssistant(actorId, sent.assistantMessage.id) + + const forkResponse = await call(actorId, (actor) => + forkThread( + actor, + creation.rootThread.id, + jsonRequest("http://test/forks", "POST", { + sourceMessageId: sent.assistantMessage.id, + }) + ) + ) + const branch = (await data(forkResponse)).thread + expect(branch).toMatchObject({ + parentThreadId: creation.rootThread.id, + sourceMessageId: sent.assistantMessage.id, + }) + expect( + await data( + await call(actorId, (actor) => + patchThread( + actor, + branch.id, + jsonRequest("http://test/thread", "PATCH", { + customTitle: "Branch", + }) + ) + ) + ) + ).toMatchObject({ customTitle: "Branch" }) + expect( + ( + await data( + await call(actorId, (actor) => + setThreadArchived(actor, branch.id, true) + ) + ) + ).archivedAt + ).not.toBeNull() + + const invalidFork = await call(actorId, (actor) => + forkThread( + actor, + creation.rootThread.id, + jsonRequest("http://test/forks", "POST", { + sourceMessageId: sent.assistantMessage.id, + baseContext: { schemaVersion: 1, messageIds: [] }, + }) + ) + ) + expect(invalidFork.status).toBe(400) + + const editResponse = await call(actorId, (actor) => + editMessage( + actor, + sent.userMessage.id, + jsonRequest("http://test/edit", "POST", { + parts: [{ type: "text", text: "second edited" }], + }) + ) + ) + const edited = replacementBundleSchema.parse(await data(editResponse)) + expect(new Set(edited.supersededMessageIds)).toEqual( + new Set([sent.userMessage.id, sent.assistantMessage.id]) + ) + await completeAssistant(actorId, edited.createdMessages[1].id) + + const regenerateResponse = await call(actorId, (actor) => + regenerateMessage( + actor, + edited.createdMessages[1].id, + jsonRequest("http://test/regenerate", "POST", {}) + ) + ) + const regenerated = replacementBundleSchema.parse( + await data(regenerateResponse) + ) + expect(regenerated.createdMessages[0].replacesMessageId).toBe( + edited.createdMessages[1].id + ) + const stopped = await call(actorId, (actor) => + stopAssistant(actor, regenerated.createdMessages[0].id) + ) + expect(await data(stopped)).toMatchObject({ status: "stopped" }) + + const window = threadMessageBundleSchema.parse( + await data( + await call(actorId, (actor) => + loadThreadMessages( + actor, + creation.rootThread.id, + new Request("http://test/messages?limit=2") + ) + ) + ) + ) + expect(window.messages).toHaveLength(2) + expect(window.hasOlderMessages).toBe(true) + expect(window.messages[0].sequence).toBeLessThan( + window.messages[1].sequence + ) + + const deleted = await call(actorId, (actor) => + deleteProject(actor, creation.project.id) + ) + expect(deleted.status).toBe(204) + } finally { + await deleteUser(actorId) + await deleteUser(otherId) + } + }) +}) diff --git a/tests/api/sse.test.ts b/tests/api/sse.test.ts new file mode 100644 index 00000000..9e6f55eb --- /dev/null +++ b/tests/api/sse.test.ts @@ -0,0 +1,238 @@ +import { randomUUID } from "node:crypto" +import { describe, expect, it, vi } from "vitest" + +vi.mock("next/server", () => ({ after: vi.fn() })) + +import { dbClient } from "@/lib/db" +import { assistantMessageEventSchema } from "@/lib/thread-chat/api/contracts" +import { + assistantEvents, + createProject, + stopAssistant, +} from "@/lib/thread-chat/api/server/handlers" +import { withActor } from "@/lib/thread-chat/api/server/http" +import { createThreadChatRepositories } from "@/lib/thread-chat/infrastructure/repositories" + +async function createUser(): Promise { + const id = randomUUID() + await dbClient` + insert into thread_chat."user" ( + id, name, email, email_verified, created_at, updated_at + ) values ( + ${id}, 'SSE Test', ${`${id}@thread-chat.test`}, true, now(), now() + ) + ` + return id +} + +async function deleteUser(id: string): Promise { + await dbClient`delete from thread_chat."user" where id = ${id}` +} + +async function createQueued(actorId: string) { + const response = await withActor( + (actor) => + createProject( + actor, + new Request("http://test/api/v1/projects", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + initialMessage: { parts: [{ type: "text", text: "SSE" }] }, + }), + }) + ), + "internal_error", + async () => actorId + ) + return (await response.json()).data +} + +async function readEvent(reader: ReadableStreamDefaultReader) { + const { value, done } = await reader.read() + expect(done).toBe(false) + const block = new TextDecoder().decode(value) + const line = block.split("\n").find((entry) => entry.startsWith("data:")) + return assistantMessageEventSchema.parse(JSON.parse(line!.slice(5).trim())) +} + +describe("ThreadChat SSE", () => { + it("snapshot、delta cursor、completed、重连与重复连接复用同一 Run", async () => { + const actorId = await createUser() + try { + const creation = await createQueued(actorId) + const repositories = createThreadChatRepositories(dbClient) + const queued = + await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + creation.assistantMessage.id + ) + await repositories.messageRuns.transition({ + actorId, + messageRunId: queued!.id, + expectedStatus: "queued", + nextStatus: "running", + }) + await repositories.messageRuns.checkpoint({ + actorId, + messageRunId: queued!.id, + expectedEventSequence: 0, + checkpointParts: [{ type: "text", text: "partial" }], + heartbeatAt: new Date(), + }) + + const firstResponse = await assistantEvents( + actorId, + creation.assistantMessage.id, + new Request("http://test/events?afterEventSequence=0") + ) + const firstReader = firstResponse.body!.getReader() + expect(await readEvent(firstReader)).toMatchObject({ + type: "run.snapshot", + cursor: 1, + run: { + assistantMessageId: creation.assistantMessage.id, + checkpointParts: [{ type: "text", text: "partial" }], + }, + }) + await firstReader.cancel() + expect( + await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + creation.assistantMessage.id + ) + ).toMatchObject({ status: "running" }) + + const responseA = await assistantEvents( + actorId, + creation.assistantMessage.id, + new Request("http://test/events?afterEventSequence=1") + ) + const responseB = await assistantEvents( + actorId, + creation.assistantMessage.id, + new Request("http://test/events?afterEventSequence=1") + ) + const readerA = responseA.body!.getReader() + const readerB = responseB.body!.getReader() + expect((await readEvent(readerA)).type).toBe("run.snapshot") + expect((await readEvent(readerB)).type).toBe("run.snapshot") + + await repositories.messages.finalizeAssistantOnce({ + actorId, + messageId: creation.assistantMessage.id, + parts: [{ type: "text", text: "final" }], + finalizedAt: new Date(), + }) + await repositories.messageRuns.transition({ + actorId, + messageRunId: queued!.id, + expectedStatus: "running", + nextStatus: "completed", + finishedAt: new Date(), + incrementEventSequence: true, + }) + expect(await readEvent(readerA)).toMatchObject({ + type: "run.completed", + eventSequence: 2, + message: { parts: [{ type: "text", text: "final" }] }, + }) + expect(await readEvent(readerB)).toMatchObject({ + type: "run.completed", + eventSequence: 2, + }) + + const reconnect = await assistantEvents( + actorId, + creation.assistantMessage.id, + new Request("http://test/events?afterEventSequence=1") + ) + const reconnectReader = reconnect.body!.getReader() + expect(await readEvent(reconnectReader)).toMatchObject({ + type: "run.snapshot", + cursor: 2, + run: { status: "completed" }, + }) + expect((await reconnectReader.read()).done).toBe(true) + + const [count] = await dbClient<{ count: number }[]>` + select count(*)::integer as count + from thread_chat.message_runs + where assistant_message_id = ${creation.assistantMessage.id} + ` + expect(count.count).toBe(1) + } finally { + await deleteUser(actorId) + } + }) + + it("拒绝超前 cursor,并发送 live failed 与 stopped 终态", async () => { + const actorId = await createUser() + try { + const failedCreation = await createQueued(actorId) + const repositories = createThreadChatRepositories(dbClient) + const failedRun = + await repositories.messageRuns.findOwnedByAssistantMessageId( + actorId, + failedCreation.assistantMessage.id + ) + const invalid = await withActor( + (actor) => + assistantEvents( + actor, + failedCreation.assistantMessage.id, + new Request("http://test/events?afterEventSequence=99") + ), + "assistant_message_not_found", + async () => actorId + ) + expect(invalid.status).toBe(409) + expect((await invalid.json()).error.code).toBe("invalid_event_cursor") + + await repositories.messageRuns.transition({ + actorId, + messageRunId: failedRun!.id, + expectedStatus: "queued", + nextStatus: "running", + }) + const failedResponse = await assistantEvents( + actorId, + failedCreation.assistantMessage.id, + new Request("http://test/events") + ) + const failedReader = failedResponse.body!.getReader() + expect((await readEvent(failedReader)).type).toBe("run.snapshot") + await repositories.messageRuns.transition({ + actorId, + messageRunId: failedRun!.id, + expectedStatus: "running", + nextStatus: "failed", + error: { code: "provider_failed", message: "failed" }, + finishedAt: new Date(), + incrementEventSequence: true, + }) + expect(await readEvent(failedReader)).toMatchObject({ + type: "run.failed", + eventSequence: 1, + run: { status: "failed" }, + }) + + const stoppedCreation = await createQueued(actorId) + const stoppedResponse = await assistantEvents( + actorId, + stoppedCreation.assistantMessage.id, + new Request("http://test/events") + ) + const stoppedReader = stoppedResponse.body!.getReader() + expect((await readEvent(stoppedReader)).type).toBe("run.snapshot") + await stopAssistant(actorId, stoppedCreation.assistantMessage.id) + expect(await readEvent(stoppedReader)).toMatchObject({ + type: "run.stopped", + eventSequence: 1, + run: { status: "stopped" }, + }) + } finally { + await deleteUser(actorId) + } + }) +}) diff --git a/tests/api/transport.test.ts b/tests/api/transport.test.ts new file mode 100644 index 00000000..586a60cf --- /dev/null +++ b/tests/api/transport.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from "vitest" +import { JsonThreadChatTransport } from "@/lib/thread-chat/api/json-transport" +import { ThreadChatClientError } from "@/lib/thread-chat/api/client-error" +import { + assistantMessageDTOFixture, + assistantRunDTOFixture, +} from "../fixtures/thread-chat-api-fixtures" + +describe("JsonThreadChatTransport", () => { + it("编码请求并严格校验成功响应", async () => { + const fetcher = vi.fn().mockResolvedValue( + Response.json({ + data: { + items: [], + nextCursor: null, + }, + }) + ) + const transport = new JsonThreadChatTransport(fetcher) + expect( + await transport.listProjects({ status: "archived", limit: 10 }) + ).toEqual({ items: [], nextCursor: null }) + expect(fetcher).toHaveBeenCalledWith( + "/api/v1/projects?status=archived&limit=10", + expect.objectContaining({ credentials: "same-origin" }) + ) + + fetcher.mockResolvedValueOnce( + Response.json({ data: { items: [], nextCursor: null, unknown: true } }) + ) + await expect(transport.listProjects()).rejects.toThrow() + }) + + it("将结构化错误映射为 ClientError", async () => { + const fetcher = vi.fn().mockResolvedValue( + Response.json( + { + error: { + code: "project_not_found", + message: "missing", + }, + }, + { status: 404 } + ) + ) + await expect( + new JsonThreadChatTransport(fetcher).bootstrapProject(crypto.randomUUID()) + ).rejects.toEqual( + expect.objectContaining>({ + code: "project_not_found", + status: 404, + message: "missing", + }) + ) + }) + + it("按 SSE data frame 解析并校验事件", async () => { + const events = [ + { + type: "run.snapshot", + cursor: 0, + run: assistantRunDTOFixture, + message: assistantMessageDTOFixture, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + }, + { + type: "run.stopped", + eventSequence: 1, + run: { + ...assistantRunDTOFixture, + status: "stopped", + eventSequence: 1, + stopRequestedAt: "2026-08-25T00:00:00.000Z", + finishedAt: "2026-08-25T00:00:00.000Z", + }, + message: assistantMessageDTOFixture, + }, + ] + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder() + for (const event of events) + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(event)}\n\n`) + ) + controller.close() + }, + }) + const fetcher = vi.fn().mockResolvedValue( + new Response(stream, { + headers: { "Content-Type": "text/event-stream" }, + }) + ) + const received = [] + for await (const event of new JsonThreadChatTransport( + fetcher + ).subscribeAssistantEvents({ + assistantMessageId: assistantMessageDTOFixture.id, + })) { + received.push(event) + } + expect(received).toEqual(events) + }) +}) diff --git a/tests/setup/require-test-database.ts b/tests/setup/require-test-database.ts index 3016b8db..13f0e96c 100644 --- a/tests/setup/require-test-database.ts +++ b/tests/setup/require-test-database.ts @@ -4,4 +4,5 @@ import { } from "../../scripts/lib/test-database-safety.mjs" loadTestDatabaseEnvironment() -assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +const testDatabaseUrl = assertSafeTestDatabaseUrl(process.env.TEST_DATABASE_URL) +process.env.DATABASE_URL = testDatabaseUrl From c3ec38b0fa0c020346f31312537e87a57df28a5d Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 19:25:23 +0800 Subject: [PATCH 15/23] feat(thread-chat): establish client state runtime --- lib/thread-chat/client/app-store.ts | 121 +++ lib/thread-chat/client/commands.ts | 414 ++++++++ lib/thread-chat/client/errors.ts | 28 + .../client/generation-coordinator.ts | 137 +++ lib/thread-chat/client/hooks.ts | 182 ++++ lib/thread-chat/client/index.ts | 10 + lib/thread-chat/client/loaders.ts | 126 +++ lib/thread-chat/client/normalizer.ts | 174 ++++ lib/thread-chat/client/project-store.ts | 962 ++++++++++++++++++ lib/thread-chat/client/providers.tsx | 159 +++ lib/thread-chat/client/runtime.ts | 209 ++++ lib/thread-chat/client/selectors.ts | 255 +++++ lib/thread-chat/client/types.ts | 354 +++++++ .../frontend-state-verification.md | 23 + .../design-thread-chat-client-api/tasks.md | 34 +- package.json | 6 +- pnpm-lock.yaml | 427 +++++++- tests/client/providers.test.tsx | 179 ++++ tests/client/runtime.test.ts | 365 +++++++ tests/client/store.test.ts | 262 +++++ tests/client/test-api.ts | 31 + vitest.client.config.ts | 11 + 22 files changed, 4446 insertions(+), 23 deletions(-) create mode 100644 lib/thread-chat/client/app-store.ts create mode 100644 lib/thread-chat/client/commands.ts create mode 100644 lib/thread-chat/client/errors.ts create mode 100644 lib/thread-chat/client/generation-coordinator.ts create mode 100644 lib/thread-chat/client/hooks.ts create mode 100644 lib/thread-chat/client/index.ts create mode 100644 lib/thread-chat/client/loaders.ts create mode 100644 lib/thread-chat/client/normalizer.ts create mode 100644 lib/thread-chat/client/project-store.ts create mode 100644 lib/thread-chat/client/providers.tsx create mode 100644 lib/thread-chat/client/runtime.ts create mode 100644 lib/thread-chat/client/selectors.ts create mode 100644 lib/thread-chat/client/types.ts create mode 100644 openspec/changes/design-thread-chat-client-api/frontend-state-verification.md create mode 100644 tests/client/providers.test.tsx create mode 100644 tests/client/runtime.test.ts create mode 100644 tests/client/store.test.ts create mode 100644 tests/client/test-api.ts create mode 100644 vitest.client.config.ts diff --git a/lib/thread-chat/client/app-store.ts b/lib/thread-chat/client/app-store.ts new file mode 100644 index 00000000..892bf95d --- /dev/null +++ b/lib/thread-chat/client/app-store.ts @@ -0,0 +1,121 @@ +import { createStore } from "zustand/vanilla" +import type { + ListProjectsResult, + ProjectSummary, + ThreadChatAppState, + ThreadChatAppStore, +} from "./types" + +const compareProjects = (left: ProjectSummary, right: ProjectSummary) => + right.updatedAt.localeCompare(left.updatedAt) || + right.id.localeCompare(left.id) + +function createInitialState( + initialCatalog?: ListProjectsResult +): ThreadChatAppState { + const items = initialCatalog?.items ?? [] + return { + catalog: { + projectsById: Object.fromEntries(items.map((item) => [item.id, item])), + orderedProjectIds: items.toSorted(compareProjects).map((item) => item.id), + loadState: initialCatalog ? { status: "ready" } : { status: "idle" }, + activeFilter: "active", + nextCursor: initialCatalog?.nextCursor ?? null, + }, + shellUi: { + sidebarOpen: true, + sidebarWidth: 280, + projectSearchQuery: "", + pendingProjectId: null, + }, + } +} + +export function createThreadChatAppStore(initialCatalog?: ListProjectsResult) { + return createStore()((set) => ({ + ...createInitialState(initialCatalog), + mergeProjectPage(result, reset = false) { + set((state) => { + const projectsById = reset ? {} : { ...state.catalog.projectsById } + for (const item of result.items) projectsById[item.id] = item + return { + catalog: { + ...state.catalog, + projectsById, + orderedProjectIds: Object.values(projectsById) + .toSorted(compareProjects) + .map((item) => item.id), + loadState: { status: "ready" }, + nextCursor: result.nextCursor, + }, + } + }) + }, + upsertProjectSummary(summary) { + set((state) => { + const projectsById = { + ...state.catalog.projectsById, + [summary.id]: summary, + } + return { + catalog: { + ...state.catalog, + projectsById, + orderedProjectIds: Object.values(projectsById) + .toSorted(compareProjects) + .map((item) => item.id), + }, + } + }) + }, + removeProjectSummary(projectId) { + set((state) => { + const projectsById = { ...state.catalog.projectsById } + delete projectsById[projectId] + return { + catalog: { + ...state.catalog, + projectsById, + orderedProjectIds: state.catalog.orderedProjectIds.filter( + (id) => id !== projectId + ), + }, + } + }) + }, + setCatalogLoadState(loadState) { + set((state) => ({ catalog: { ...state.catalog, loadState } })) + }, + setCatalogFilter(activeFilter) { + set((state) => ({ + catalog: { + ...state.catalog, + activeFilter, + projectsById: {}, + orderedProjectIds: [], + loadState: { status: "idle" }, + nextCursor: null, + }, + })) + }, + setProjectRoutePending(pendingProjectId) { + set((state) => ({ shellUi: { ...state.shellUi, pendingProjectId } })) + }, + setSidebarOpen(sidebarOpen) { + set((state) => ({ shellUi: { ...state.shellUi, sidebarOpen } })) + }, + setSidebarWidth(sidebarWidth) { + set((state) => ({ + shellUi: { + ...state.shellUi, + sidebarWidth: Math.max(200, Math.min(520, sidebarWidth)), + }, + })) + }, + setProjectSearchQuery(projectSearchQuery) { + set((state) => ({ + shellUi: { ...state.shellUi, projectSearchQuery }, + })) + }, + })) +} diff --git a/lib/thread-chat/client/commands.ts b/lib/thread-chat/client/commands.ts new file mode 100644 index 00000000..bc22f2da --- /dev/null +++ b/lib/thread-chat/client/commands.ts @@ -0,0 +1,414 @@ +import type { StoreApi } from "zustand/vanilla" +import type { ThreadChatApiCapabilities } from "../api/capabilities" +import { ThreadChatClientError } from "../api/client-error" +import { + assistantRunStateSchema, + creationBundleSchema, + feedbackSchema, + listProjectsResultSchema, + messageCreationBundleSchema, + projectBootstrapSchema, + projectSchema, + replacementBundleSchema, + threadSchema, +} from "../api/contracts" +import { threadChatRoutes } from "../api/routes" +import { clientInvariant, isAbortError, normalizeClientError } from "./errors" +import { selectForkAvailability } from "./selectors" +import type { + GenerationCoordinator, + NavigationCapability, + NewProjectDraftStore, + ProjectRuntimeRegistry, + ThreadChatAppCommands, + ThreadChatAppStore, + ThreadChatProjectCommands, + ThreadChatProjectStore, + ThreadMessageLoader, + ArtifactLoader, +} from "./types" + +async function runProjectCommand(input: { + store: StoreApi + scope: string + execute(): Promise +}): Promise { + if ( + input.store.getState().requests.commandByScope[input.scope]?.status === + "submitting" + ) + return + input.store.getState().setCommandState(input.scope, { status: "submitting" }) + try { + await input.execute() + input.store.getState().setCommandState(input.scope, null) + } catch (error) { + input.store.getState().setCommandState(input.scope, { + status: "error", + error: normalizeClientError(error), + }) + } +} + +export function createThreadChatProjectCommands(input: { + projectId: string + api: ThreadChatApiCapabilities + store: StoreApi + messageLoader: ThreadMessageLoader + artifactLoader: ArtifactLoader + generationCoordinator: GenerationCoordinator +}): { commands: ThreadChatProjectCommands; destroy(): void } { + let bootstrapPromise: Promise | null = null + let bootstrapController: AbortController | null = null + let disposed = false + + const commands: ThreadChatProjectCommands = { + loadProjectBootstrap() { + const state = input.store.getState() + if (state.requests.bootstrap.status === "ready") { + input.generationCoordinator.resumeLoadedRuns() + return Promise.resolve() + } + if (bootstrapPromise) return bootstrapPromise + state.setBootstrapLoadState({ status: "loading" }) + bootstrapController = new AbortController() + bootstrapPromise = input.api + .bootstrapProject(input.projectId, bootstrapController.signal) + .then((rawBootstrap) => { + if (disposed) return + const bootstrap = projectBootstrapSchema.parse(rawBootstrap) + input.store.getState().mergeBootstrap(bootstrap) + input.generationCoordinator.resumeLoadedRuns() + }) + .catch((error: unknown) => { + if (disposed || isAbortError(error)) return + input.store.getState().setBootstrapLoadState({ + status: "error", + error: normalizeClientError(error), + }) + }) + .finally(() => { + bootstrapPromise = null + bootstrapController = null + }) + return bootstrapPromise + }, + ensureThreadMessages: input.messageLoader.ensure, + ensureArtifact: input.artifactLoader.ensure, + updateProject(patch) { + return runProjectCommand({ + store: input.store, + scope: `project:update:${input.projectId}`, + execute: async () => { + input.store.getState().applyProject( + projectSchema.parse( + await input.api.patchProject({ + projectId: input.projectId, + ...patch, + }) + ) + ) + }, + }) + }, + updateThread(threadId, customTitle) { + return runProjectCommand({ + store: input.store, + scope: `thread:update:${threadId}`, + execute: async () => { + input.store + .getState() + .applyThread( + threadSchema.parse( + await input.api.patchThread(threadId, customTitle) + ) + ) + }, + }) + }, + setProjectArchived(archived) { + return runProjectCommand({ + store: input.store, + scope: `project:archive:${input.projectId}`, + execute: async () => { + input.store + .getState() + .applyProject( + projectSchema.parse( + await input.api.setProjectArchived(input.projectId, archived) + ) + ) + }, + }) + }, + setThreadArchived(threadId, archived) { + return runProjectCommand({ + store: input.store, + scope: `thread:archive:${threadId}`, + execute: async () => { + input.store + .getState() + .applyThread( + threadSchema.parse( + await input.api.setThreadArchived(threadId, archived) + ) + ) + }, + }) + }, + deleteProject() { + return runProjectCommand({ + store: input.store, + scope: `project:delete:${input.projectId}`, + execute: () => input.api.deleteProject(input.projectId), + }) + }, + sendMessage(threadId, parts, requestedModelId) { + return runProjectCommand({ + store: input.store, + scope: `send:${threadId}`, + execute: async () => { + const bundle = messageCreationBundleSchema.parse( + await input.api.sendMessage({ + threadId, + parts, + requestedModelId, + }) + ) + input.store.getState().applyMessageCreationBundle(bundle) + input.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId + ) + }, + }) + }, + forkThread(command) { + return runProjectCommand({ + store: input.store, + scope: `fork:${command.sourceMessageId}`, + execute: async () => { + const availability = selectForkAvailability( + input.store.getState(), + command.sourceMessageId + ) + if (!availability.allowed) clientInvariant(false, availability.reason) + const rawResult = await input.api.forkThread(command.sourceThreadId, { + sourceMessageId: command.sourceMessageId, + anchor: command.anchor, + }) + const result = { thread: threadSchema.parse(rawResult.thread) } + input.store.getState().applyThreadCreated(result.thread) + input.store + .getState() + .openThread(result.thread.id, command.sourceSlotId) + }, + }) + }, + editMessage(messageId, parts, requestedModelId) { + return runProjectCommand({ + store: input.store, + scope: `edit:${messageId}`, + execute: async () => { + const bundle = replacementBundleSchema.parse( + await input.api.editMessage({ + messageId, + parts, + requestedModelId, + }) + ) + input.store.getState().applyReplacementBundle(bundle) + input.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId + ) + }, + }) + }, + regenerateMessage(messageId, requestedModelId) { + return runProjectCommand({ + store: input.store, + scope: `regenerate:${messageId}`, + execute: async () => { + const bundle = replacementBundleSchema.parse( + await input.api.regenerateMessage({ + messageId, + requestedModelId, + }) + ) + input.store.getState().applyReplacementBundle(bundle) + input.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId + ) + }, + }) + }, + setFeedback(messageId, value) { + return runProjectCommand({ + store: input.store, + scope: `feedback:${messageId}`, + execute: async () => { + input.store + .getState() + .applyFeedback( + feedbackSchema.parse( + await input.api.setFeedback(messageId, value) + ) + ) + }, + }) + }, + stopAssistant(assistantMessageId) { + return runProjectCommand({ + store: input.store, + scope: `stop:${assistantMessageId}`, + execute: async () => { + input.store + .getState() + .applyAssistantRun( + assistantRunStateSchema.parse( + await input.api.stopAssistant(assistantMessageId) + ) + ) + }, + }) + }, + } + + return { + commands, + destroy() { + disposed = true + bootstrapController?.abort() + bootstrapController = null + bootstrapPromise = null + }, + } +} + +export function createThreadChatAppCommands(input: { + api: ThreadChatApiCapabilities + store: StoreApi + navigation: NavigationCapability +}): { commands: ThreadChatAppCommands; destroy(): void } { + let catalogPromise: Promise | null = null + let controller: AbortController | null = null + let disposed = false + + return { + commands: { + loadProjectCatalog({ reset = false } = {}) { + if (catalogPromise) return catalogPromise + const state = input.store.getState() + if ( + !reset && + state.catalog.loadState.status === "ready" && + !state.catalog.nextCursor + ) + return Promise.resolve() + state.setCatalogLoadState({ status: "loading" }) + controller = new AbortController() + catalogPromise = input.api + .listProjects({ + status: state.catalog.activeFilter, + cursor: reset ? undefined : (state.catalog.nextCursor ?? undefined), + signal: controller.signal, + }) + .then((rawResult) => { + if (!disposed) + input.store + .getState() + .mergeProjectPage( + listProjectsResultSchema.parse(rawResult), + reset + ) + }) + .catch((error: unknown) => { + if (disposed || isAbortError(error)) return + input.store.getState().setCatalogLoadState({ + status: "error", + error: normalizeClientError(error), + }) + }) + .finally(() => { + catalogPromise = null + controller = null + }) + return catalogPromise + }, + async setProjectArchived(projectId, archived) { + const project = projectSchema.parse( + await input.api.setProjectArchived(projectId, archived) + ) + const current = input.store.getState().catalog.projectsById[projectId] + if (!current) return + input.store.getState().upsertProjectSummary({ + ...current, + displayTitle: project.customTitle ?? project.autoTitle ?? "", + archivedAt: project.archivedAt, + updatedAt: project.updatedAt, + }) + }, + async deleteProject(projectId) { + await input.api.deleteProject(projectId) + input.store.getState().removeProjectSummary(projectId) + if (input.navigation.currentProjectId?.() === projectId) + input.navigation.replace(threadChatRoutes.newProject()) + }, + }, + destroy() { + disposed = true + controller?.abort() + controller = null + catalogPromise = null + }, + } +} + +export async function submitNewProjectDraft(input: { + api: ThreadChatApiCapabilities + appStore: StoreApi + registry: ProjectRuntimeRegistry + navigation: NavigationCapability + draftStore: StoreApi +}): Promise { + const draft = input.draftStore.getState() + if (draft.status === "submitting") return + const parts = structuredClone(draft.draftParts) + draft.markSubmitting() + try { + const bundle = creationBundleSchema.parse( + await input.api.createProject({ + parts, + requestedModelId: draft.requestedModelId, + }) + ) + const runtime = input.registry.seedFromCreation(bundle) + input.appStore.getState().upsertProjectSummary({ + id: bundle.project.id, + displayTitle: + bundle.project.customTitle ?? bundle.project.autoTitle ?? "New project", + archivedAt: bundle.project.archivedAt, + updatedAt: bundle.project.updatedAt, + threadCount: 1, + messageCount: 2, + }) + input.appStore.getState().setProjectRoutePending(bundle.project.id) + runtime.generationCoordinator.subscribeAssistant( + bundle.assistantRun.assistantMessageId + ) + input.navigation.replace(threadChatRoutes.project(bundle.project.id)) + } catch (error) { + const normalized = normalizeClientError(error) + input.draftStore + .getState() + .markError( + normalized.status === 0 + ? new ThreadChatClientError( + normalized.code, + "Project creation result is unknown. Check the project list before retrying.", + normalized.status, + normalized.details + ) + : normalized + ) + } +} diff --git a/lib/thread-chat/client/errors.ts b/lib/thread-chat/client/errors.ts new file mode 100644 index 00000000..4c4d6c71 --- /dev/null +++ b/lib/thread-chat/client/errors.ts @@ -0,0 +1,28 @@ +import { ThreadChatClientError } from "../api/client-error" + +export function normalizeClientError(error: unknown): ThreadChatClientError { + if (error instanceof ThreadChatClientError) return error + if (error instanceof Error && error.name === "AbortError") + return new ThreadChatClientError( + "internal_error", + "Request was aborted.", + 0 + ) + return new ThreadChatClientError( + "internal_error", + error instanceof Error ? error.message : "Unexpected client error.", + 0 + ) +} + +export function clientInvariant( + condition: unknown, + message: string +): asserts condition { + if (!condition) + throw new ThreadChatClientError("validation_error", message, 0) +} + +export function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === "AbortError" +} diff --git a/lib/thread-chat/client/generation-coordinator.ts b/lib/thread-chat/client/generation-coordinator.ts new file mode 100644 index 00000000..1bef33a2 --- /dev/null +++ b/lib/thread-chat/client/generation-coordinator.ts @@ -0,0 +1,137 @@ +import type { StoreApi } from "zustand/vanilla" +import type { ThreadChatApiCapabilities } from "../api/capabilities" +import { assistantMessageEventSchema } from "../api/contracts" +import { isAbortError } from "./errors" +import type { + AssistantMessageEvent, + GenerationCoordinator, + ThreadChatProjectStore, +} from "./types" + +function defaultScheduleFlush(callback: () => void): () => void { + if (typeof requestAnimationFrame === "function") { + const id = requestAnimationFrame(callback) + return () => cancelAnimationFrame(id) + } + let cancelled = false + queueMicrotask(() => { + if (!cancelled) callback() + }) + return () => { + cancelled = true + } +} + +function defaultReconnectWait(signal: AbortSignal): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(resolve, 250) + signal.addEventListener( + "abort", + () => { + clearTimeout(timeout) + resolve() + }, + { once: true } + ) + }) +} + +function isTerminal(event: AssistantMessageEvent): boolean { + return ( + event.type === "run.completed" || + event.type === "run.failed" || + event.type === "run.stopped" || + (event.type === "run.snapshot" && + ["completed", "failed", "stopped"].includes(event.run.status)) + ) +} + +export function createGenerationCoordinator(input: { + api: ThreadChatApiCapabilities + store: StoreApi + scheduleFlush?: (callback: () => void) => () => void + waitForReconnect?: (signal: AbortSignal) => Promise +}): GenerationCoordinator { + const controllers = new Map() + const cancelFlushByMessageId = new Map void>() + const scheduleFlush = input.scheduleFlush ?? defaultScheduleFlush + const waitForReconnect = input.waitForReconnect ?? defaultReconnectWait + let disposed = false + + const scheduleMessageFlush = (assistantMessageId: string) => { + if (cancelFlushByMessageId.has(assistantMessageId)) return + cancelFlushByMessageId.set( + assistantMessageId, + scheduleFlush(() => { + cancelFlushByMessageId.delete(assistantMessageId) + if (!disposed) input.store.getState().flushRunBuffer(assistantMessageId) + }) + ) + } + + const consume = async ( + assistantMessageId: string, + controller: AbortController + ) => { + while (!disposed && !controller.signal.aborted) { + const run = + input.store.getState().runs.byAssistantMessageId[assistantMessageId] + if (!run || ["completed", "failed", "stopped"].includes(run.status)) + return + try { + for await (const rawEvent of input.api.subscribeAssistantEvents({ + assistantMessageId, + afterEventSequence: run.eventSequence, + signal: controller.signal, + })) { + if (disposed || controller.signal.aborted) return + const event = assistantMessageEventSchema.parse(rawEvent) + input.store.getState().applyRunEvent(event, assistantMessageId) + if (event.type === "run.delta") + scheduleMessageFlush(assistantMessageId) + if (isTerminal(event)) return + } + } catch (error) { + if (disposed || controller.signal.aborted || isAbortError(error)) return + } + await waitForReconnect(controller.signal) + } + } + + const coordinator: GenerationCoordinator = { + resumeLoadedRuns() { + for (const run of Object.values( + input.store.getState().runs.byAssistantMessageId + )) + if (run.status === "queued" || run.status === "running") + coordinator.subscribeAssistant(run.assistantMessageId) + }, + subscribeAssistant(assistantMessageId) { + if (disposed || controllers.has(assistantMessageId)) return + const run = + input.store.getState().runs.byAssistantMessageId[assistantMessageId] + if (!run || ["completed", "failed", "stopped"].includes(run.status)) + return + const controller = new AbortController() + controllers.set(assistantMessageId, controller) + void consume(assistantMessageId, controller).finally(() => { + if (controllers.get(assistantMessageId) === controller) + controllers.delete(assistantMessageId) + }) + }, + unsubscribeAssistant(assistantMessageId) { + controllers.get(assistantMessageId)?.abort() + controllers.delete(assistantMessageId) + cancelFlushByMessageId.get(assistantMessageId)?.() + cancelFlushByMessageId.delete(assistantMessageId) + }, + destroy() { + disposed = true + for (const controller of controllers.values()) controller.abort() + controllers.clear() + for (const cancel of cancelFlushByMessageId.values()) cancel() + cancelFlushByMessageId.clear() + }, + } + return coordinator +} diff --git a/lib/thread-chat/client/hooks.ts b/lib/thread-chat/client/hooks.ts new file mode 100644 index 00000000..7b67572c --- /dev/null +++ b/lib/thread-chat/client/hooks.ts @@ -0,0 +1,182 @@ +"use client" + +import { useCallback, useEffect, useMemo } from "react" +import { useStore } from "zustand" +import { submitNewProjectDraft } from "./commands" +import { + selectAppShellUi, + selectArtifact, + selectAssistantRun, + selectFocusedColumnId, + selectFocusedThreadId, + selectForkAvailability, + selectProject, + selectProjectCatalog, + selectProjectHeaderView, + selectProjectTarget, + selectProjectTreeRows, + selectThread, + selectThreadColumnHeaderView, + selectThreadColumnView, + selectThreadMessages, + selectVisibleThreadColumns, +} from "./selectors" +import { + useNewProjectDraftStoreApi, + useThreadChatAppRuntime, + useThreadChatProjectRuntime, +} from "./providers" +import type { + NewProjectDraftStore, + ThreadChatAppStore, + ThreadChatProjectStore, +} from "./types" + +export function useThreadChatAppStore( + selector: (state: ThreadChatAppStore) => T +): T { + return useStore(useThreadChatAppRuntime().appStore, selector) +} + +export function useThreadChatStore( + selector: (state: ThreadChatProjectStore) => T +): T { + return useStore(useThreadChatProjectRuntime().store, selector) +} + +export function useNewProjectDraftStore( + selector: (state: NewProjectDraftStore) => T +): T { + return useStore(useNewProjectDraftStoreApi(), selector) +} + +export const useProjectCatalog = () => + useThreadChatAppStore(selectProjectCatalog) +export const useAppShellUi = () => useThreadChatAppStore(selectAppShellUi) +export const useProject = () => useThreadChatStore(selectProject) +export const useProjectTarget = () => useThreadChatStore(selectProjectTarget) +export const useThread = (threadId: string) => + useThreadChatStore((state) => selectThread(state, threadId)) +export const useThreadMessages = (threadId: string) => + useThreadChatStore((state) => selectThreadMessages(state, threadId)) +export const useThreadColumnView = (slotId: "root" | string) => + useThreadChatStore((state) => selectThreadColumnView(state, slotId)) +export const useThreadColumnHeaderView = (slotId: "root" | string) => + useThreadChatStore((state) => selectThreadColumnHeaderView(state, slotId)) +export const useProjectTreeRows = () => + useThreadChatStore(selectProjectTreeRows) +export const useAssistantRun = (assistantMessageId: string) => + useThreadChatStore((state) => selectAssistantRun(state, assistantMessageId)) +export const useArtifact = (artifactId: string) => + useThreadChatStore((state) => selectArtifact(state, artifactId)) +export const useForkAvailability = (messageId: string) => + useThreadChatStore((state) => selectForkAvailability(state, messageId)) +export const useVisibleThreadColumns = () => + useThreadChatStore(selectVisibleThreadColumns) +export const useProjectHeaderView = () => + useThreadChatStore(selectProjectHeaderView) +export const useFocusedColumnId = () => + useThreadChatStore(selectFocusedColumnId) +export const useFocusedThreadId = () => + useThreadChatStore(selectFocusedThreadId) + +export function useAppShellCommands() { + const runtime = useThreadChatAppRuntime() + return useMemo( + () => ({ + setSidebarOpen: runtime.appStore.getState().setSidebarOpen, + setSidebarWidth: runtime.appStore.getState().setSidebarWidth, + setProjectSearchQuery: runtime.appStore.getState().setProjectSearchQuery, + setCatalogFilter: runtime.appStore.getState().setCatalogFilter, + setProjectRoutePending: + runtime.appStore.getState().setProjectRoutePending, + loadProjectCatalog: runtime.commands.loadProjectCatalog, + }), + [runtime] + ) +} + +export function useProjectCommands() { + const runtime = useThreadChatProjectRuntime() + return runtime.commands +} + +export function useThreadCommands(threadId: string) { + const runtime = useThreadChatProjectRuntime() + return useMemo( + () => ({ + send: runtime.commands.sendMessage.bind(null, threadId), + updateTitle: runtime.commands.updateThread.bind(null, threadId), + setArchived: runtime.commands.setThreadArchived.bind(null, threadId), + ensureLoaded: runtime.commands.ensureThreadMessages.bind(null, threadId), + fork: ( + sourceSlotId: "root" | string, + sourceMessageId: string, + anchor?: { + exactQuote: string + textPosition?: { start: number; end: number } + } + ) => + runtime.commands.forkThread({ + sourceSlotId, + sourceThreadId: threadId, + sourceMessageId, + anchor, + }), + }), + [runtime, threadId] + ) +} + +export function useMessageCommands(messageId: string) { + const runtime = useThreadChatProjectRuntime() + return useMemo( + () => ({ + edit: runtime.commands.editMessage.bind(null, messageId), + regenerate: runtime.commands.regenerateMessage.bind(null, messageId), + feedback: runtime.commands.setFeedback.bind(null, messageId), + stop: () => runtime.commands.stopAssistant(messageId), + }), + [messageId, runtime] + ) +} + +export function useSubmitNewProjectDraft() { + const appRuntime = useThreadChatAppRuntime() + const draftStore = useNewProjectDraftStoreApi() + return useCallback( + () => + submitNewProjectDraft({ + api: appRuntime.api, + appStore: appRuntime.appStore, + registry: appRuntime.projectRuntimeRegistry, + navigation: appRuntime.navigation, + draftStore, + }), + [appRuntime, draftStore] + ) +} + +export function useEnsureThreadMessagesLoaded(threadId: string | null) { + const runtime = useThreadChatProjectRuntime() + useEffect(() => { + if (threadId) void runtime.commands.ensureThreadMessages(threadId) + }, [runtime, threadId]) +} + +export function useEnsureArtifactLoaded( + artifactId: string | null, + enabled = true +) { + const runtime = useThreadChatProjectRuntime() + useEffect(() => { + if (enabled && artifactId) void runtime.commands.ensureArtifact(artifactId) + }, [artifactId, enabled, runtime]) +} + +export function useActiveGenerationSubscriptions() { + const runtime = useThreadChatProjectRuntime() + useEffect(() => { + runtime.generationCoordinator.resumeLoadedRuns() + }, [runtime]) +} diff --git a/lib/thread-chat/client/index.ts b/lib/thread-chat/client/index.ts new file mode 100644 index 00000000..df8251fc --- /dev/null +++ b/lib/thread-chat/client/index.ts @@ -0,0 +1,10 @@ +export * from "./app-store" +export * from "./commands" +export * from "./errors" +export * from "./generation-coordinator" +export * from "./loaders" +export * from "./normalizer" +export * from "./project-store" +export * from "./runtime" +export * from "./selectors" +export type * from "./types" diff --git a/lib/thread-chat/client/loaders.ts b/lib/thread-chat/client/loaders.ts new file mode 100644 index 00000000..ec34fd72 --- /dev/null +++ b/lib/thread-chat/client/loaders.ts @@ -0,0 +1,126 @@ +import type { StoreApi } from "zustand/vanilla" +import type { ThreadChatApiCapabilities } from "../api/capabilities" +import { artifactSchema, threadMessageBundleSchema } from "../api/contracts" +import { isAbortError, normalizeClientError, clientInvariant } from "./errors" +import type { + ArtifactLoader, + GenerationCoordinator, + ThreadChatProjectStore, + ThreadMessageLoader, +} from "./types" + +export function createThreadMessageLoader(input: { + projectId: string + api: ThreadChatApiCapabilities + store: StoreApi + generationCoordinator: GenerationCoordinator +}): ThreadMessageLoader { + const inFlight = new Map>() + const controllers = new Map() + let disposed = false + + return { + ensure(threadId) { + const state = input.store.getState() + clientInvariant( + state.entities.threadsById[threadId]?.projectId === input.projectId, + "Thread loader target belongs to another Project." + ) + if ( + state.requests.threadMessagesById[threadId]?.loadState.status === + "ready" + ) + return Promise.resolve() + const existing = inFlight.get(threadId) + if (existing) return existing + + state.setThreadMessageLoadState(threadId, { status: "loading" }) + const controller = new AbortController() + controllers.set(threadId, controller) + const request = input.api + .loadThreadMessages({ threadId, signal: controller.signal }) + .then((rawBundle) => { + if (disposed) return + const bundle = threadMessageBundleSchema.parse(rawBundle) + clientInvariant( + bundle.threadId === threadId, + "Thread loader response identity mismatch." + ) + input.store.getState().applyMessageBundle(bundle) + input.generationCoordinator.resumeLoadedRuns() + }) + .catch((error: unknown) => { + if (disposed || isAbortError(error)) return + input.store.getState().setThreadMessageLoadState(threadId, { + status: "error", + error: normalizeClientError(error), + }) + }) + .finally(() => { + inFlight.delete(threadId) + controllers.delete(threadId) + }) + inFlight.set(threadId, request) + return request + }, + destroy() { + disposed = true + for (const controller of controllers.values()) controller.abort() + controllers.clear() + inFlight.clear() + }, + } +} + +export function createArtifactLoader(input: { + projectId: string + api: ThreadChatApiCapabilities + store: StoreApi +}): ArtifactLoader { + const inFlight = new Map>() + const controllers = new Map() + let disposed = false + + return { + ensure(artifactId) { + const state = input.store.getState() + if (state.entities.artifactsById[artifactId]) return Promise.resolve() + const existing = inFlight.get(artifactId) + if (existing) return existing + state.setArtifactLoadState(artifactId, { status: "loading" }) + const controller = new AbortController() + controllers.set(artifactId, controller) + const request = input.api + .loadArtifact(artifactId, controller.signal) + .then((rawArtifact) => { + if (disposed) return + const artifact = artifactSchema.parse(rawArtifact) + clientInvariant( + artifact.id === artifactId && + artifact.projectId === input.projectId, + "Artifact loader response identity mismatch." + ) + input.store.getState().applyArtifact(artifact) + }) + .catch((error: unknown) => { + if (disposed || isAbortError(error)) return + input.store.getState().setArtifactLoadState(artifactId, { + status: "error", + error: normalizeClientError(error), + }) + }) + .finally(() => { + inFlight.delete(artifactId) + controllers.delete(artifactId) + }) + inFlight.set(artifactId, request) + return request + }, + destroy() { + disposed = true + for (const controller of controllers.values()) controller.abort() + controllers.clear() + inFlight.clear() + }, + } +} diff --git a/lib/thread-chat/client/normalizer.ts b/lib/thread-chat/client/normalizer.ts new file mode 100644 index 00000000..7bf27823 --- /dev/null +++ b/lib/thread-chat/client/normalizer.ts @@ -0,0 +1,174 @@ +import type { + AssistantRunState, + MessageEntity, + ProjectArtifactSummary, + ThreadEntity, + ThreadId, +} from "./types" +import { clientInvariant } from "./errors" + +function sameValue(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right) +} + +export function normalizeThreads(input: { + projectId: string + current: Record + incoming: readonly ThreadEntity[] +}): Record { + const threads = { ...input.current } + for (const thread of input.incoming) { + clientInvariant( + thread.projectId === input.projectId, + "Thread belongs to another Project." + ) + const existing = threads[thread.id] + clientInvariant( + !existing || sameValue(existing, thread), + "Confirmed Thread identity changed." + ) + threads[thread.id] = thread + } + for (const thread of Object.values(threads)) { + if (thread.projectId !== input.projectId) continue + if (thread.parentThreadId) + clientInvariant( + threads[thread.parentThreadId]?.projectId === input.projectId, + "Thread parent is missing from Project topology." + ) + const visited = new Set() + let cursor: ThreadEntity | undefined = thread + while (cursor?.parentThreadId) { + clientInvariant( + !visited.has(cursor.id), + "Thread topology contains a cycle." + ) + visited.add(cursor.id) + cursor = threads[cursor.parentThreadId] + } + } + return threads +} + +export function normalizeMessages(input: { + threadId: ThreadId + currentById: Record + currentIdsByThread: Record + incoming: readonly MessageEntity[] +}): { + messagesById: Record + messageIdsByThreadId: Record +} { + const messagesById = { ...input.currentById } + const ids = new Set(input.currentIdsByThread[input.threadId] ?? []) + for (const message of input.incoming) { + clientInvariant( + message.threadId === input.threadId, + "Message belongs to another Thread." + ) + const existing = messagesById[message.id] + if (existing) { + clientInvariant( + existing.threadId === message.threadId && + existing.sequence === message.sequence && + existing.role === message.role && + existing.replacesMessageId === message.replacesMessageId && + existing.createdAt === message.createdAt, + "Confirmed Message identity changed." + ) + if (existing.finalizedAt) + clientInvariant( + existing.finalizedAt === message.finalizedAt && + sameValue(existing.parts, message.parts), + "Finalized Message content changed." + ) + if (existing.supersededAt) + clientInvariant( + existing.supersededAt === message.supersededAt, + "Message supersededAt changed." + ) + } + messagesById[message.id] = message + ids.add(message.id) + } + const sortedIds = [...ids].toSorted((leftId, rightId) => { + const left = messagesById[leftId] + const right = messagesById[rightId] + return left.sequence - right.sequence || left.id.localeCompare(right.id) + }) + for (let index = 1; index < sortedIds.length; index++) + clientInvariant( + messagesById[sortedIds[index - 1]].sequence !== + messagesById[sortedIds[index]].sequence, + "Thread contains duplicate Message sequence." + ) + return { + messagesById, + messageIdsByThreadId: { + ...input.currentIdsByThread, + [input.threadId]: sortedIds, + }, + } +} + +export function normalizeRuns(input: { + current: Record + incoming: readonly AssistantRunState[] + messagesById: Record +}): Record { + const runs = { ...input.current } + for (const run of input.incoming) { + const message = input.messagesById[run.assistantMessageId] + clientInvariant( + message?.role === "assistant", + "Assistant Run does not reference an assistant Message." + ) + const existing = runs[run.assistantMessageId] + if (existing) { + clientInvariant( + run.eventSequence >= existing.eventSequence, + "Assistant Run eventSequence moved backwards." + ) + clientInvariant( + run.modelId === existing.modelId, + "Assistant Run model identity changed." + ) + const existingTerminal = ["completed", "failed", "stopped"].includes( + existing.status + ) + if (existingTerminal) + clientInvariant( + sameValue(existing, run), + "Terminal Assistant Run changed." + ) + if (run.eventSequence === existing.eventSequence) { + clientInvariant( + existing.status === run.status || + (existing.status === "queued" && run.status === "running"), + "Assistant Run status changed without a valid transition." + ) + clientInvariant( + existing.stopRequestedAt === null || + existing.stopRequestedAt === run.stopRequestedAt, + "Assistant Run stop request moved backwards." + ) + } + } + runs[run.assistantMessageId] = run + } + return runs +} + +export function mergeArtifactSummary( + current: ProjectArtifactSummary | null, + incoming: ProjectArtifactSummary +): ProjectArtifactSummary { + if (!current || incoming.changeSequence > current.changeSequence) + return incoming + if (incoming.changeSequence < current.changeSequence) return current + clientInvariant( + sameValue(current, incoming), + "Artifact Summary changed without advancing changeSequence." + ) + return current +} diff --git a/lib/thread-chat/client/project-store.ts b/lib/thread-chat/client/project-store.ts new file mode 100644 index 00000000..df61981e --- /dev/null +++ b/lib/thread-chat/client/project-store.ts @@ -0,0 +1,962 @@ +import { createStore } from "zustand/vanilla" +import { clientInvariant } from "./errors" +import { + mergeArtifactSummary, + normalizeMessages, + normalizeRuns, + normalizeThreads, +} from "./normalizer" +import type { + AssistantMessageEvent, + AssistantRunState, + MessageEntity, + ThreadChatProjectState, + ThreadChatProjectStore, + ThreadColumnSlot, + ThreadId, + ThreadWorkbenchSnapshotV1, +} from "./types" + +function initialState(): ThreadChatProjectState { + return { + entities: { + project: null, + threadsById: {}, + messagesById: {}, + messageIdsByThreadId: {}, + artifactsById: {}, + feedbackByMessageId: {}, + }, + runs: { + byAssistantMessageId: {}, + streamBuffersByAssistantMessageId: {}, + }, + requests: { + bootstrap: { status: "idle" }, + threadMessagesById: {}, + artifactById: {}, + commandByScope: {}, + }, + readModels: { + artifactSummary: null, + replacementSupersededMessageIds: {}, + }, + ui: { + columnSlots: [], + focusedSlotId: null, + rootColumnWidthPx: null, + forceColumnCount: null, + placementMode: "replace", + viewMode: "columns", + canvasPins: {}, + composerDraftByThreadId: {}, + selectedArtifactId: null, + activationClock: 0, + lastActivatedOrderBySlotId: {}, + overlays: { + selection: null, + threadSwitcherScope: null, + treeListOpen: false, + helpPanelOpen: false, + artifactDrawerOpen: false, + }, + }, + } +} + +function withMessages( + state: ThreadChatProjectState, + threadId: ThreadId, + messages: readonly MessageEntity[], + runs: readonly AssistantRunState[] +) { + const normalized = normalizeMessages({ + threadId, + currentById: state.entities.messagesById, + currentIdsByThread: state.entities.messageIdsByThreadId, + incoming: messages, + }) + return { + entities: { + ...state.entities, + ...normalized, + }, + runs: { + ...state.runs, + byAssistantMessageId: normalizeRuns({ + current: state.runs.byAssistantMessageId, + incoming: runs, + messagesById: normalized.messagesById, + }), + }, + } +} + +function isValidWidth(width: number | null): boolean { + return ( + width === null || (Number.isFinite(width) && width >= 120 && width <= 2_000) + ) +} + +function sanitizeSnapshot( + snapshot: ThreadWorkbenchSnapshotV1, + state: ThreadChatProjectState, + projectId: string +): ThreadWorkbenchSnapshotV1 { + clientInvariant( + snapshot.schemaVersion === 1, + "Unsupported workbench snapshot." + ) + const seenSlots = new Set() + const seenThreads = new Set() + const columnSlots = snapshot.columnSlots.filter((slot) => { + const thread = state.entities.threadsById[slot.threadId] + const valid = + slot.slotId.length > 0 && + !seenSlots.has(slot.slotId) && + !seenThreads.has(slot.threadId) && + thread?.projectId === projectId && + thread.parentThreadId !== null && + isValidWidth(slot.widthPx) + if (valid) { + seenSlots.add(slot.slotId) + seenThreads.add(slot.threadId) + } + return valid + }) + const expandedSlotIds = new Set( + columnSlots.filter((slot) => !slot.folded).map((slot) => slot.slotId) + ) + const focusedSlotId = + snapshot.focusedSlotId === "root" || + expandedSlotIds.has(snapshot.focusedSlotId) + ? snapshot.focusedSlotId + : (columnSlots.find((slot) => !slot.folded)?.slotId ?? "root") + const canvasPins = Object.fromEntries( + Object.entries(snapshot.canvasPins).filter( + ([threadId, point]) => + state.entities.threadsById[threadId]?.projectId === projectId && + Number.isFinite(point.x) && + Number.isFinite(point.y) + ) + ) + return { + ...snapshot, + columnSlots, + focusedSlotId, + rootColumnWidthPx: isValidWidth(snapshot.rootColumnWidthPx) + ? snapshot.rootColumnWidthPx + : null, + forceColumnCount: + snapshot.forceColumnCount === null || + (Number.isInteger(snapshot.forceColumnCount) && + snapshot.forceColumnCount > 0 && + snapshot.forceColumnCount <= 12) + ? snapshot.forceColumnCount + : null, + canvasPins, + } +} + +function findRootThreadId(state: ThreadChatProjectState): string | null { + return ( + Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + )?.id ?? null + ) +} + +function nextFocusedSlot( + slots: readonly ThreadColumnSlot[], + removedIndex: number +): "root" | string { + for (let distance = 0; distance < slots.length; distance++) { + const right = slots[removedIndex + distance] + if (right && !right.folded) return right.slotId + const left = slots[removedIndex - distance - 1] + if (left && !left.folded) return left.slotId + } + return "root" +} + +function checkpointFromEvent(event: AssistantMessageEvent) { + if (event.type !== "run.delta") return null + const chunk = event.chunk as { + type?: string + data?: { checkpointParts?: AssistantRunState["checkpointParts"] } + } + return chunk.type === "data-run-checkpoint" && + Array.isArray(chunk.data?.checkpointParts) + ? chunk.data.checkpointParts + : null +} + +export function createThreadChatProjectStore(input: { + projectId: string + generateSlotId?: () => string +}) { + const generateSlotId = + input.generateSlotId ?? (() => globalThis.crypto.randomUUID()) + + return createStore()((set) => ({ + ...initialState(), + mergeCreationBundle(bundle) { + set((state) => { + clientInvariant( + bundle.project.id === input.projectId, + "Creation Bundle belongs to another Project." + ) + const threadsById = normalizeThreads({ + projectId: input.projectId, + current: state.entities.threadsById, + incoming: [bundle.rootThread], + }) + const merged = withMessages( + { ...state, entities: { ...state.entities, threadsById } }, + bundle.rootThread.id, + [bundle.userMessage, bundle.assistantMessage], + [bundle.assistantRun] + ) + return { + entities: { + ...merged.entities, + project: bundle.project, + }, + runs: merged.runs, + requests: { + ...state.requests, + bootstrap: { status: "ready" }, + threadMessagesById: { + ...state.requests.threadMessagesById, + [bundle.rootThread.id]: { + loadState: { status: "ready" }, + hasOlderMessages: false, + oldestReturnedSequence: bundle.userMessage.sequence, + newestReturnedSequence: bundle.assistantMessage.sequence, + }, + }, + }, + readModels: { + ...state.readModels, + artifactSummary: mergeArtifactSummary( + state.readModels.artifactSummary, + bundle.artifactSummary + ), + }, + ui: { + ...state.ui, + focusedSlotId: "root", + activationClock: state.ui.activationClock + 1, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + root: state.ui.activationClock + 1, + }, + }, + } + }) + }, + mergeBootstrap(bootstrap) { + set((state) => { + clientInvariant( + bootstrap.project.id === input.projectId, + "Bootstrap belongs to another Project." + ) + const threadsById = normalizeThreads({ + projectId: input.projectId, + current: state.entities.threadsById, + incoming: bootstrap.threadTopology, + }) + const merged = withMessages( + { ...state, entities: { ...state.entities, threadsById } }, + bootstrap.initialThread.threadId, + bootstrap.initialThread.messages, + bootstrap.initialThread.assistantRuns + ) + return { + entities: { + ...merged.entities, + project: bootstrap.project, + }, + runs: merged.runs, + requests: { + ...state.requests, + bootstrap: { status: "ready" }, + threadMessagesById: { + ...state.requests.threadMessagesById, + [bootstrap.initialThread.threadId]: { + loadState: { status: "ready" }, + hasOlderMessages: bootstrap.initialThread.hasOlderMessages, + oldestReturnedSequence: + bootstrap.initialThread.oldestReturnedSequence, + newestReturnedSequence: + bootstrap.initialThread.newestReturnedSequence, + }, + }, + }, + readModels: { + ...state.readModels, + artifactSummary: mergeArtifactSummary( + state.readModels.artifactSummary, + bootstrap.artifactSummary + ), + }, + ui: { + ...state.ui, + focusedSlotId: state.ui.focusedSlotId ?? "root", + }, + } + }) + }, + applyMessageBundle(bundle) { + set((state) => { + const thread = state.entities.threadsById[bundle.threadId] + clientInvariant( + thread?.projectId === input.projectId, + "Message Bundle Thread belongs to another Project." + ) + const merged = withMessages( + state, + bundle.threadId, + bundle.messages, + bundle.assistantRuns + ) + return { + entities: merged.entities, + runs: merged.runs, + requests: { + ...state.requests, + threadMessagesById: { + ...state.requests.threadMessagesById, + [bundle.threadId]: { + loadState: { status: "ready" }, + hasOlderMessages: bundle.hasOlderMessages, + oldestReturnedSequence: bundle.oldestReturnedSequence, + newestReturnedSequence: bundle.newestReturnedSequence, + }, + }, + }, + } + }) + }, + applyMessageCreationBundle(bundle) { + set((state) => { + clientInvariant( + bundle.userMessage.threadId === bundle.assistantMessage.threadId && + state.entities.threadsById[bundle.userMessage.threadId] + ?.projectId === input.projectId, + "Message Creation Bundle belongs to another Project." + ) + return withMessages( + state, + bundle.userMessage.threadId, + [bundle.userMessage, bundle.assistantMessage], + [bundle.assistantRun] + ) + }) + }, + applyThreadCreated(thread) { + set((state) => ({ + entities: { + ...state.entities, + threadsById: normalizeThreads({ + projectId: input.projectId, + current: state.entities.threadsById, + incoming: [thread], + }), + }, + })) + }, + applyReplacementBundle(bundle) { + set((state) => { + clientInvariant( + bundle.createdMessages.length > 0, + "Replacement Bundle contains no Message." + ) + const threadId = bundle.createdMessages[0].threadId + clientInvariant( + bundle.createdMessages.every( + (message) => message.threadId === threadId + ) && + state.entities.threadsById[threadId]?.projectId === + input.projectId && + bundle.supersededMessageIds.every( + (messageId) => state.entities.messagesById[messageId] + ), + "Replacement Bundle relations are invalid." + ) + const merged = withMessages(state, threadId, bundle.createdMessages, [ + bundle.assistantRun, + ]) + return { + ...merged, + readModels: { + ...state.readModels, + replacementSupersededMessageIds: { + ...state.readModels.replacementSupersededMessageIds, + ...Object.fromEntries( + bundle.supersededMessageIds.map((messageId) => [ + messageId, + true, + ]) + ), + }, + }, + } + }) + }, + applyRunEvent(event, scopedAssistantMessageId) { + set((state) => { + if (event.type === "run.delta") { + const assistantMessageId = scopedAssistantMessageId + const run = assistantMessageId + ? state.runs.byAssistantMessageId[assistantMessageId] + : undefined + clientInvariant( + assistantMessageId && run, + "Run delta cannot be associated with a loaded assistant Message." + ) + if (event.eventSequence <= run.eventSequence) return state + const currentBuffer = state.runs.streamBuffersByAssistantMessageId[ + assistantMessageId + ] ?? { + pendingChunks: [], + lastReceivedEventSequence: run.eventSequence, + flushScheduled: false, + } + if (event.eventSequence <= currentBuffer.lastReceivedEventSequence) + return state + clientInvariant( + event.eventSequence === currentBuffer.lastReceivedEventSequence + 1, + "Run delta eventSequence contains a gap." + ) + return { + runs: { + byAssistantMessageId: { + ...state.runs.byAssistantMessageId, + [assistantMessageId]: { + ...run, + status: "running", + eventSequence: event.eventSequence, + }, + }, + streamBuffersByAssistantMessageId: { + ...state.runs.streamBuffersByAssistantMessageId, + [assistantMessageId]: { + pendingChunks: [...currentBuffer.pendingChunks, event.chunk], + lastReceivedEventSequence: event.eventSequence, + flushScheduled: true, + }, + }, + }, + } + } + + const run = event.run + const existing = state.runs.byAssistantMessageId[run.assistantMessageId] + if (existing && run.eventSequence < existing.eventSequence) return state + let entities = state.entities + if ( + event.type === "run.snapshot" || + event.type === "run.completed" || + event.type === "run.stopped" + ) { + const normalized = normalizeMessages({ + threadId: event.message.threadId, + currentById: entities.messagesById, + currentIdsByThread: entities.messageIdsByThreadId, + incoming: [event.message], + }) + entities = { ...entities, ...normalized } + } + return { + entities, + runs: { + byAssistantMessageId: normalizeRuns({ + current: state.runs.byAssistantMessageId, + incoming: [run], + messagesById: entities.messagesById, + }), + streamBuffersByAssistantMessageId: Object.fromEntries( + Object.entries( + state.runs.streamBuffersByAssistantMessageId + ).filter( + ([assistantMessageId]) => + assistantMessageId !== run.assistantMessageId + ) + ), + }, + readModels: + event.type === "run.snapshot" || event.type === "run.completed" + ? { + ...state.readModels, + artifactSummary: mergeArtifactSummary( + state.readModels.artifactSummary, + event.artifactSummary + ), + } + : state.readModels, + } + }) + }, + applyAssistantRun(run) { + set((state) => ({ + runs: { + ...state.runs, + byAssistantMessageId: normalizeRuns({ + current: state.runs.byAssistantMessageId, + incoming: [run], + messagesById: state.entities.messagesById, + }), + }, + })) + }, + flushRunBuffer(assistantMessageId) { + set((state) => { + const buffer = + state.runs.streamBuffersByAssistantMessageId[assistantMessageId] + const run = state.runs.byAssistantMessageId[assistantMessageId] + if (!buffer || !run) return state + let checkpointParts = run.checkpointParts + for (const chunk of buffer.pendingChunks) { + const candidate = checkpointFromEvent({ + type: "run.delta", + eventSequence: buffer.lastReceivedEventSequence, + chunk, + }) + if (candidate) checkpointParts = candidate + } + return { + runs: { + byAssistantMessageId: { + ...state.runs.byAssistantMessageId, + [assistantMessageId]: { ...run, checkpointParts }, + }, + streamBuffersByAssistantMessageId: { + ...state.runs.streamBuffersByAssistantMessageId, + [assistantMessageId]: { + ...buffer, + pendingChunks: [], + flushScheduled: false, + }, + }, + }, + } + }) + }, + applyArtifact(artifact) { + set((state) => { + clientInvariant( + artifact.projectId === input.projectId, + "Artifact belongs to another Project." + ) + return { + entities: { + ...state.entities, + artifactsById: { + ...state.entities.artifactsById, + [artifact.id]: artifact, + }, + }, + requests: { + ...state.requests, + artifactById: { + ...state.requests.artifactById, + [artifact.id]: { status: "ready" }, + }, + }, + } + }) + }, + applyProject(project) { + clientInvariant( + project.id === input.projectId, + "Project response belongs to another Runtime." + ) + set((state) => ({ entities: { ...state.entities, project } })) + }, + applyThread(thread) { + set((state) => ({ + entities: { + ...state.entities, + threadsById: { + ...state.entities.threadsById, + ...normalizeThreads({ + projectId: input.projectId, + current: state.entities.threadsById, + incoming: [thread], + }), + }, + }, + })) + }, + applyFeedback(feedback) { + set((state) => { + clientInvariant( + state.entities.messagesById[feedback.messageId]?.role === "assistant", + "Feedback response does not reference a loaded assistant Message." + ) + return { + entities: { + ...state.entities, + feedbackByMessageId: { + ...state.entities.feedbackByMessageId, + [feedback.messageId]: feedback, + }, + }, + } + }) + }, + setBootstrapLoadState(bootstrap) { + set((state) => ({ requests: { ...state.requests, bootstrap } })) + }, + setCommandState(scope, commandState) { + set((state) => { + const commandByScope = { ...state.requests.commandByScope } + if (commandState) commandByScope[scope] = commandState + else delete commandByScope[scope] + return { + requests: { ...state.requests, commandByScope }, + } + }) + }, + setThreadMessageLoadState(threadId, loadState) { + set((state) => ({ + requests: { + ...state.requests, + threadMessagesById: { + ...state.requests.threadMessagesById, + [threadId]: { + ...(state.requests.threadMessagesById[threadId] ?? { + hasOlderMessages: false, + oldestReturnedSequence: null, + newestReturnedSequence: null, + }), + loadState, + }, + }, + }, + })) + }, + setArtifactLoadState(artifactId, loadState) { + set((state) => ({ + requests: { + ...state.requests, + artifactById: { + ...state.requests.artifactById, + [artifactId]: loadState, + }, + }, + })) + }, + restoreWorkbenchSnapshot(snapshot) { + set((state) => { + clientInvariant( + state.requests.bootstrap.status === "ready", + "Workbench can only be restored after Bootstrap." + ) + const sanitized = sanitizeSnapshot(snapshot, state, input.projectId) + return { + ui: { + ...state.ui, + columnSlots: sanitized.columnSlots, + focusedSlotId: sanitized.focusedSlotId, + rootColumnWidthPx: sanitized.rootColumnWidthPx, + forceColumnCount: sanitized.forceColumnCount, + placementMode: sanitized.placementMode, + viewMode: sanitized.viewMode, + canvasPins: sanitized.canvasPins, + }, + } + }) + }, + resetWorkbenchToDefault() { + set((state) => ({ + ui: { + ...state.ui, + columnSlots: [], + focusedSlotId: findRootThreadId(state) ? "root" : null, + rootColumnWidthPx: null, + forceColumnCount: null, + placementMode: "replace", + viewMode: "columns", + canvasPins: {}, + }, + })) + }, + openThread(threadId, sourceSlotId) { + set((state) => { + const thread = state.entities.threadsById[threadId] + clientInvariant( + thread?.projectId === input.projectId && + thread.parentThreadId !== null, + "Only a Branch in this Project can open in a column." + ) + const existingIndex = state.ui.columnSlots.findIndex( + (slot) => slot.threadId === threadId + ) + const clock = state.ui.activationClock + 1 + if (existingIndex >= 0) { + const slots = state.ui.columnSlots.map((slot, index) => + index === existingIndex ? { ...slot, folded: false } : slot + ) + const slotId = slots[existingIndex].slotId + return { + ui: { + ...state.ui, + columnSlots: slots, + focusedSlotId: slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [slotId]: clock, + }, + }, + } + } + + let slots = [...state.ui.columnSlots] + const expanded = slots.filter((slot) => !slot.folded) + const limit = state.ui.forceColumnCount + if (limit !== null && expanded.length + 1 >= limit) { + const candidate = expanded.toSorted( + (left, right) => + (state.ui.lastActivatedOrderBySlotId[left.slotId] ?? 0) - + (state.ui.lastActivatedOrderBySlotId[right.slotId] ?? 0) + )[0] + if (candidate && state.ui.placementMode === "replace") { + const candidateIndex = slots.findIndex( + (slot) => slot.slotId === candidate.slotId + ) + slots[candidateIndex] = { ...candidate, threadId, folded: false } + return { + ui: { + ...state.ui, + columnSlots: slots, + focusedSlotId: candidate.slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [candidate.slotId]: clock, + }, + }, + } + } + if (candidate) + slots = slots.map((slot) => + slot.slotId === candidate.slotId + ? { ...slot, folded: true } + : slot + ) + } + const slotId = generateSlotId() + const sourceIndex = + sourceSlotId === "root" + ? -1 + : slots.findIndex((slot) => slot.slotId === sourceSlotId) + slots.splice(sourceIndex + 1, 0, { + slotId, + threadId, + folded: false, + widthPx: null, + }) + return { + ui: { + ...state.ui, + columnSlots: slots, + focusedSlotId: slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [slotId]: clock, + }, + }, + } + }) + }, + switchColumnThread(slotId, threadId) { + set((state) => { + const thread = state.entities.threadsById[threadId] + clientInvariant( + thread?.projectId === input.projectId && + thread.parentThreadId !== null, + "Column target must be a Branch in this Project." + ) + clientInvariant( + !state.ui.columnSlots.some( + (slot) => slot.slotId !== slotId && slot.threadId === threadId + ), + "Thread is already open in another column." + ) + const clock = state.ui.activationClock + 1 + return { + ui: { + ...state.ui, + columnSlots: state.ui.columnSlots.map((slot) => + slot.slotId === slotId + ? { ...slot, threadId, folded: false } + : slot + ), + focusedSlotId: slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [slotId]: clock, + }, + }, + } + }) + }, + closeColumn(slotId) { + set((state) => { + const index = state.ui.columnSlots.findIndex( + (slot) => slot.slotId === slotId + ) + if (index < 0) return state + const columnSlots = state.ui.columnSlots.filter( + (slot) => slot.slotId !== slotId + ) + const focusedSlotId = + state.ui.focusedSlotId === slotId + ? nextFocusedSlot(columnSlots, index) + : state.ui.focusedSlotId + const lastActivatedOrderBySlotId = { + ...state.ui.lastActivatedOrderBySlotId, + } + delete lastActivatedOrderBySlotId[slotId] + return { + ui: { + ...state.ui, + columnSlots, + focusedSlotId, + lastActivatedOrderBySlotId, + }, + } + }) + }, + setColumnFolded(slotId, folded) { + set((state) => { + const index = state.ui.columnSlots.findIndex( + (slot) => slot.slotId === slotId + ) + if (index < 0) return state + const columnSlots = state.ui.columnSlots.map((slot) => + slot.slotId === slotId ? { ...slot, folded } : slot + ) + return { + ui: { + ...state.ui, + columnSlots, + focusedSlotId: + folded && state.ui.focusedSlotId === slotId + ? nextFocusedSlot(columnSlots, index) + : folded + ? state.ui.focusedSlotId + : slotId, + }, + } + }) + }, + focusColumn(slotId) { + set((state) => { + clientInvariant( + slotId === "root" || + state.ui.columnSlots.some( + (slot) => slot.slotId === slotId && !slot.folded + ), + "Focused column must exist and be expanded." + ) + const clock = state.ui.activationClock + 1 + return { + ui: { + ...state.ui, + focusedSlotId: slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [slotId]: clock, + }, + }, + } + }) + }, + commitColumnWidths(widths) { + set((state) => { + for (const width of Object.values(widths)) + clientInvariant( + width === undefined || isValidWidth(width), + "Column width is invalid." + ) + return { + ui: { + ...state.ui, + rootColumnWidthPx: + widths.root === undefined + ? state.ui.rootColumnWidthPx + : widths.root, + columnSlots: state.ui.columnSlots.map((slot) => { + const width = widths[slot.slotId] + return { + ...slot, + widthPx: width === undefined ? slot.widthPx : width, + } + }), + }, + } + }) + }, + setForceColumnCount(forceColumnCount) { + clientInvariant( + forceColumnCount === null || + (Number.isInteger(forceColumnCount) && forceColumnCount > 0), + "Forced column count is invalid." + ) + set((state) => ({ ui: { ...state.ui, forceColumnCount } })) + }, + setPlacementMode(placementMode) { + set((state) => ({ ui: { ...state.ui, placementMode } })) + }, + setViewMode(viewMode) { + set((state) => ({ ui: { ...state.ui, viewMode } })) + }, + setCanvasPin(threadId, point) { + set((state) => { + clientInvariant( + state.entities.threadsById[threadId]?.projectId === input.projectId, + "Canvas pin Thread belongs to another Project." + ) + const canvasPins = { ...state.ui.canvasPins } + if (point) canvasPins[threadId] = point + else delete canvasPins[threadId] + return { ui: { ...state.ui, canvasPins } } + }) + }, + setComposerDraft(threadId, parts) { + set((state) => { + clientInvariant( + state.entities.threadsById[threadId]?.projectId === input.projectId, + "Composer draft Thread belongs to another Project." + ) + return { + ui: { + ...state.ui, + composerDraftByThreadId: { + ...state.ui.composerDraftByThreadId, + [threadId]: parts, + }, + }, + } + }) + }, + setSelectedArtifact(selectedArtifactId) { + set((state) => ({ ui: { ...state.ui, selectedArtifactId } })) + }, + setOverlays(patch) { + set((state) => ({ + ui: { + ...state.ui, + overlays: { ...state.ui.overlays, ...patch }, + }, + })) + }, + })) +} diff --git a/lib/thread-chat/client/providers.tsx b/lib/thread-chat/client/providers.tsx new file mode 100644 index 00000000..e37d0d25 --- /dev/null +++ b/lib/thread-chat/client/providers.tsx @@ -0,0 +1,159 @@ +"use client" + +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react" +import { useRouter } from "next/navigation" +import type { ThreadChatApiCapabilities } from "../api/capabilities" +import { + createNewProjectDraftStore, + createThreadChatAppRuntime, +} from "./runtime" +import type { + ListProjectsResult, + NavigationCapability, + NewProjectDraftStore, + ThreadChatAppRuntime, + ThreadChatProjectRuntime, +} from "./types" +import type { StoreApi } from "zustand/vanilla" + +const ThreadChatAppRuntimeContext = createContext( + null +) +const ThreadChatProjectRuntimeContext = + createContext(null) +const NewProjectDraftStoreContext = + createContext | null>(null) +const appRuntimeLifecycleVersions = new WeakMap() + +function projectIdFromPathname(pathname: string): string | null { + const match = pathname.match(/^\/thread-chat\/([^/]+)$/) + if (!match || match[1] === "new") return null + return decodeURIComponent(match[1]) +} + +export function ThreadChatAppProvider({ + children, + initialCatalog, + api, + navigation, +}: { + children: ReactNode + initialCatalog?: ListProjectsResult + api?: ThreadChatApiCapabilities + navigation?: NavigationCapability +}) { + const router = useRouter() + const [runtime] = useState(() => + createThreadChatAppRuntime({ + api, + initialCatalog, + navigation: navigation ?? { + replace: (path) => router.replace(path), + currentProjectId: () => + typeof window === "undefined" + ? null + : projectIdFromPathname(window.location.pathname), + }, + }) + ) + useEffect(() => { + const version = (appRuntimeLifecycleVersions.get(runtime) ?? 0) + 1 + appRuntimeLifecycleVersions.set(runtime, version) + return () => { + queueMicrotask(() => { + if (appRuntimeLifecycleVersions.get(runtime) === version) { + appRuntimeLifecycleVersions.delete(runtime) + runtime.destroy() + } + }) + } + }, [runtime]) + return ( + + {children} + + ) +} + +export function ThreadChatProjectProvider({ + children, + projectId, +}: { + children: ReactNode + projectId: string +}) { + const appRuntime = useThreadChatAppRuntime() + const [runtime] = useState(() => + appRuntime.projectRuntimeRegistry.acquire(projectId) + ) + + useEffect(() => { + const bootstrap = runtime.store.getState().requests.bootstrap + const ready = + bootstrap.status === "ready" + ? Promise.resolve() + : runtime.commands.loadProjectBootstrap() + runtime.generationCoordinator.resumeLoadedRuns() + void ready.finally(() => { + if (appRuntime.appStore.getState().shellUi.pendingProjectId === projectId) + appRuntime.appStore.getState().setProjectRoutePending(null) + }) + }, [appRuntime, projectId, runtime]) + + useEffect(() => { + const leased = appRuntime.projectRuntimeRegistry.acquire(projectId) + if (leased !== runtime) + throw new Error("Project Runtime identity changed during Provider lease.") + return () => appRuntime.projectRuntimeRegistry.release(projectId) + }, [appRuntime, projectId, runtime]) + + return ( + + {children} + + ) +} + +export function NewProjectDraftProvider({ + children, + initialRequestedModelId, +}: { + children: ReactNode + initialRequestedModelId?: string +}) { + const [store] = useState(() => + createNewProjectDraftStore(initialRequestedModelId) + ) + return ( + + {children} + + ) +} + +export function useThreadChatAppRuntime(): ThreadChatAppRuntime { + const runtime = useContext(ThreadChatAppRuntimeContext) + if (!runtime) + throw new Error("ThreadChatAppProvider is required for this hook.") + return runtime +} + +export function useThreadChatProjectRuntime(): ThreadChatProjectRuntime { + const runtime = useContext(ThreadChatProjectRuntimeContext) + if (!runtime) + throw new Error("ThreadChatProjectProvider is required for this hook.") + return runtime +} + +export function useNewProjectDraftStoreApi(): StoreApi { + const store = useContext(NewProjectDraftStoreContext) + if (!store) + throw new Error("NewProjectDraftProvider is required for this hook.") + return store +} diff --git a/lib/thread-chat/client/runtime.ts b/lib/thread-chat/client/runtime.ts new file mode 100644 index 00000000..90464373 --- /dev/null +++ b/lib/thread-chat/client/runtime.ts @@ -0,0 +1,209 @@ +import { createStore } from "zustand/vanilla" +import { JsonThreadChatTransport } from "../api/json-transport" +import { createThreadChatAppStore } from "./app-store" +import { + createThreadChatAppCommands, + createThreadChatProjectCommands, +} from "./commands" +import { clientInvariant } from "./errors" +import { createGenerationCoordinator } from "./generation-coordinator" +import { createArtifactLoader, createThreadMessageLoader } from "./loaders" +import { createThreadChatProjectStore } from "./project-store" +import type { + CreationBundle, + ListProjectsResult, + NavigationCapability, + NewProjectDraftStore, + ProjectRuntimeRegistry, + ThreadChatAppRuntime, + ThreadChatProjectRuntime, +} from "./types" +import type { ThreadChatApiCapabilities } from "../api/capabilities" + +export function createNewProjectDraftStore(initialRequestedModelId?: string) { + return createStore()((set) => ({ + draftParts: [], + requestedModelId: initialRequestedModelId, + status: "idle", + error: null, + setDraftParts(draftParts) { + set({ draftParts }) + }, + setRequestedModelId(requestedModelId) { + set({ requestedModelId }) + }, + markSubmitting() { + set({ status: "submitting", error: null }) + }, + markError(error) { + set({ status: "error", error }) + }, + markIdle() { + set({ status: "idle", error: null }) + }, + })) +} + +export function createThreadChatProjectRuntime(input: { + projectId: string + api: ThreadChatApiCapabilities + generateSlotId?: () => string + scheduleFlush?: (callback: () => void) => () => void + waitForReconnect?: (signal: AbortSignal) => Promise +}): ThreadChatProjectRuntime { + const store = createThreadChatProjectStore({ + projectId: input.projectId, + generateSlotId: input.generateSlotId, + }) + const generationCoordinator = createGenerationCoordinator({ + api: input.api, + store, + scheduleFlush: input.scheduleFlush, + waitForReconnect: input.waitForReconnect, + }) + const messageLoader = createThreadMessageLoader({ + projectId: input.projectId, + api: input.api, + store, + generationCoordinator, + }) + const artifactLoader = createArtifactLoader({ + projectId: input.projectId, + api: input.api, + store, + }) + const projectCommands = createThreadChatProjectCommands({ + projectId: input.projectId, + api: input.api, + store, + messageLoader, + artifactLoader, + generationCoordinator, + }) + let disposed = false + return { + projectId: input.projectId, + store, + commands: projectCommands.commands, + messageLoader, + artifactLoader, + generationCoordinator, + destroy() { + if (disposed) return + disposed = true + projectCommands.destroy() + messageLoader.destroy() + artifactLoader.destroy() + generationCoordinator.destroy() + }, + } +} + +export function createProjectRuntimeRegistry(input: { + createRuntime(projectId: string): ThreadChatProjectRuntime +}): ProjectRuntimeRegistry { + const entries = new Map< + string, + { + runtime: ThreadChatProjectRuntime + leased: boolean + handoff: boolean + } + >() + + return { + seedFromCreation(bundle: CreationBundle) { + let entry = entries.get(bundle.project.id) + if (!entry) { + entry = { + runtime: input.createRuntime(bundle.project.id), + leased: false, + handoff: true, + } + entries.set(bundle.project.id, entry) + } else { + clientInvariant( + entry.runtime.store.getState().entities.project === null, + "Cannot seed an initialized Project Runtime." + ) + entry.handoff = true + } + entry.runtime.store.getState().mergeCreationBundle(bundle) + return entry.runtime + }, + acquire(projectId) { + let entry = entries.get(projectId) + if (!entry) { + entry = { + runtime: input.createRuntime(projectId), + leased: true, + handoff: false, + } + entries.set(projectId, entry) + } else { + entry.leased = true + entry.handoff = false + } + return entry.runtime + }, + release(projectId) { + const entry = entries.get(projectId) + if (!entry) return + entry.leased = false + queueMicrotask(() => { + if ( + entries.get(projectId) === entry && + !entry.leased && + !entry.handoff + ) { + entry.runtime.destroy() + entries.delete(projectId) + } + }) + }, + peek(projectId) { + return entries.get(projectId)?.runtime ?? null + }, + destroy() { + for (const entry of entries.values()) entry.runtime.destroy() + entries.clear() + }, + } +} + +export function createThreadChatAppRuntime(input: { + api?: ThreadChatApiCapabilities + navigation: NavigationCapability + initialCatalog?: ListProjectsResult + createProjectRuntime?: ( + projectId: string, + api: ThreadChatApiCapabilities + ) => ThreadChatProjectRuntime +}): ThreadChatAppRuntime { + const api = input.api ?? new JsonThreadChatTransport() + const appStore = createThreadChatAppStore(input.initialCatalog) + const registry = createProjectRuntimeRegistry({ + createRuntime: (projectId) => + input.createProjectRuntime?.(projectId, api) ?? + createThreadChatProjectRuntime({ projectId, api }), + }) + const appCommands = createThreadChatAppCommands({ + api, + store: appStore, + navigation: input.navigation, + }) + let disposed = false + return { + appStore, + projectRuntimeRegistry: registry, + api, + commands: appCommands.commands, + navigation: input.navigation, + destroy() { + if (disposed) return + disposed = true + appCommands.destroy() + registry.destroy() + }, + } +} diff --git a/lib/thread-chat/client/selectors.ts b/lib/thread-chat/client/selectors.ts new file mode 100644 index 00000000..73696234 --- /dev/null +++ b/lib/thread-chat/client/selectors.ts @@ -0,0 +1,255 @@ +import type { + ArtifactEntity, + MessageEntity, + ProjectId, + ThreadChatAppStore, + ThreadChatProjectStore, + ThreadColumnSlot, +} from "./types" + +export const selectProjectCatalog = (state: ThreadChatAppStore) => state.catalog + +export const selectAppShellUi = (state: ThreadChatAppStore) => state.shellUi + +export function selectFilteredProjectIds( + state: ThreadChatAppStore +): ProjectId[] { + const query = state.shellUi.projectSearchQuery.trim().toLocaleLowerCase() + if (!query) return state.catalog.orderedProjectIds + return state.catalog.orderedProjectIds.filter((projectId) => + state.catalog.projectsById[projectId]?.displayTitle + .toLocaleLowerCase() + .includes(query) + ) +} + +export const selectProject = (state: ThreadChatProjectStore) => + state.entities.project + +export const selectProjectTarget = (state: ThreadChatProjectStore) => + state.entities.project?.target ?? null + +export const selectThread = (state: ThreadChatProjectStore, threadId: string) => + state.entities.threadsById[threadId] ?? null + +export const selectRootThread = (state: ThreadChatProjectStore) => + Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + ) ?? null + +export function selectThreadMessages( + state: ThreadChatProjectStore, + threadId: string +): MessageEntity[] { + return (state.entities.messageIdsByThreadId[threadId] ?? []) + .map((messageId) => state.entities.messagesById[messageId]) + .filter( + (message): message is MessageEntity => + Boolean(message) && + message.supersededAt === null && + !state.readModels.replacementSupersededMessageIds[message.id] + ) +} + +export const selectAssistantRun = ( + state: ThreadChatProjectStore, + assistantMessageId: string +) => state.runs.byAssistantMessageId[assistantMessageId] ?? null + +export const selectArtifact = ( + state: ThreadChatProjectStore, + artifactId: string +): ArtifactEntity | null => state.entities.artifactsById[artifactId] ?? null + +export function selectSlotThreadId( + state: ThreadChatProjectStore, + slotId: "root" | string +): string | null { + if (slotId === "root") return selectRootThread(state)?.id ?? null + return ( + state.ui.columnSlots.find((slot) => slot.slotId === slotId)?.threadId ?? + null + ) +} + +export function selectVisibleThreadColumns(state: ThreadChatProjectStore) { + const root = selectRootThread(state) + const rootColumn = root + ? [{ slotId: "root" as const, threadId: root.id, folded: false }] + : [] + return [ + ...rootColumn, + ...state.ui.columnSlots.map((slot) => ({ + slotId: slot.slotId, + threadId: slot.threadId, + folded: slot.folded, + })), + ] +} + +export function selectFocusedThreadId( + state: ThreadChatProjectStore +): string | null { + return state.ui.focusedSlotId + ? selectSlotThreadId(state, state.ui.focusedSlotId) + : null +} + +export const selectFocusedColumnId = (state: ThreadChatProjectStore) => + state.ui.focusedSlotId + +export function selectForkAvailability( + state: ThreadChatProjectStore, + messageId: string +): { allowed: true } | { allowed: false; reason: string } { + const message = state.entities.messagesById[messageId] + if (!message) return { allowed: false, reason: "message_not_loaded" } + if ( + message.finalizedAt === null || + message.supersededAt !== null || + state.readModels.replacementSupersededMessageIds[messageId] + ) + return { allowed: false, reason: "message_not_finalized" } + if (message.role === "assistant") { + const run = state.runs.byAssistantMessageId[messageId] + if (run?.status !== "completed") + return { allowed: false, reason: "assistant_run_not_completed" } + } + return { allowed: true } +} + +export function selectArtifactIdsFromMessage(message: MessageEntity): string[] { + if (!message.parts) return [] + const ids = new Set() + for (const part of message.parts) { + const candidate = part as { + type?: string + output?: { artifactId?: unknown } + } + if ( + candidate.type === "dynamic-tool" && + typeof candidate.output?.artifactId === "string" + ) + ids.add(candidate.output.artifactId) + } + return [...ids] +} + +export function selectThreadColumnView( + state: ThreadChatProjectStore, + slotId: "root" | string +) { + const threadId = selectSlotThreadId(state, slotId) + if (!threadId) return { status: "loading" as const, slotId, threadId: null } + const window = state.requests.threadMessagesById[threadId] + if ( + !window || + window.loadState.status === "idle" || + window.loadState.status === "loading" + ) + return { status: "loading" as const, slotId, threadId } + if (window.loadState.status === "error") + return { + status: "error" as const, + slotId, + threadId, + error: window.loadState.error, + canRetry: true as const, + } + const messages = selectThreadMessages(state, threadId) + return { + status: "ready" as const, + slotId, + threadId, + thread: state.entities.threadsById[threadId], + messages, + runs: Object.fromEntries( + messages + .filter((message) => message.role === "assistant") + .map((message) => [ + message.id, + state.runs.byAssistantMessageId[message.id] ?? null, + ]) + ), + artifactIds: [...new Set(messages.flatMap(selectArtifactIdsFromMessage))], + hasOlderMessages: window.hasOlderMessages, + } +} + +export function selectThreadColumnHeaderView( + state: ThreadChatProjectStore, + slotId: "root" | string +) { + const threadId = selectSlotThreadId(state, slotId) + const thread = threadId ? state.entities.threadsById[threadId] : null + const slot: ThreadColumnSlot | undefined = + slotId === "root" + ? undefined + : state.ui.columnSlots.find((candidate) => candidate.slotId === slotId) + const childCount = threadId + ? Object.values(state.entities.threadsById).filter( + (candidate) => candidate.parentThreadId === threadId + ).length + : 0 + return { + slotId, + threadId, + title: + slotId === "root" + ? (state.entities.project?.customTitle ?? + state.entities.project?.autoTitle ?? + "") + : (thread?.customTitle ?? thread?.autoTitle ?? ""), + folded: slot?.folded ?? false, + childCount, + focused: state.ui.focusedSlotId === slotId, + } +} + +export function selectProjectTreeRows(state: ThreadChatProjectStore) { + const threads = Object.values(state.entities.threadsById) + const childrenByParent = new Map() + for (const thread of threads) { + const siblings = childrenByParent.get(thread.parentThreadId) ?? [] + siblings.push(thread) + childrenByParent.set(thread.parentThreadId, siblings) + } + const rows: Array<{ + threadId: string + parentThreadId: string | null + depth: number + title: string + archived: boolean + }> = [] + const visit = (parentThreadId: string | null, depth: number) => { + for (const thread of (childrenByParent.get(parentThreadId) ?? []).toSorted( + (left, right) => left.createdAt.localeCompare(right.createdAt) + )) { + rows.push({ + threadId: thread.id, + parentThreadId: thread.parentThreadId, + depth, + title: + thread.parentThreadId === null + ? (state.entities.project?.customTitle ?? + state.entities.project?.autoTitle ?? + "") + : (thread.customTitle ?? thread.autoTitle ?? ""), + archived: thread.archivedAt !== null, + }) + visit(thread.id, depth + 1) + } + } + visit(null, 0) + return rows +} + +export function selectProjectHeaderView(state: ThreadChatProjectStore) { + const project = state.entities.project + return { + title: project?.customTitle ?? project?.autoTitle ?? "", + archived: project?.archivedAt !== null && project !== null, + artifactSummary: state.readModels.artifactSummary, + focusedThreadId: selectFocusedThreadId(state), + } +} diff --git a/lib/thread-chat/client/types.ts b/lib/thread-chat/client/types.ts new file mode 100644 index 00000000..e173745a --- /dev/null +++ b/lib/thread-chat/client/types.ts @@ -0,0 +1,354 @@ +import type { z } from "zod" +import type { StoreApi } from "zustand/vanilla" +import type { ThreadChatClientError } from "../api/client-error" +import type { ThreadChatApiCapabilities } from "../api/capabilities" +import type { + artifactSchema, + assistantMessageEventSchema, + assistantRunStateSchema, + creationBundleSchema, + feedbackSchema, + listProjectsResultSchema, + messageCreationBundleSchema, + messageSchema, + projectBootstrapSchema, + projectSchema, + projectSummarySchema, + replacementBundleSchema, + threadMessageBundleSchema, + threadSchema, + UserMessageParts, +} from "../api/contracts" + +export type ProjectEntity = z.infer +export type ProjectSummary = z.infer +export type ThreadEntity = z.infer +export type MessageEntity = z.infer +export type AssistantRunState = z.infer +export type ArtifactEntity = z.infer +export type Feedback = z.infer +export type ListProjectsResult = z.infer +export type CreationBundle = z.infer +export type ProjectBootstrap = z.infer +export type ThreadMessageBundle = z.infer +export type MessageCreationBundle = z.infer +export type ReplacementBundle = z.infer +export type AssistantMessageEvent = z.infer + +export type ProjectId = string +export type ThreadId = string +export type MessageId = string +export type ArtifactId = string +export type ColumnSlotId = string + +export type LoadState = + | { status: "idle" } + | { status: "loading" } + | { status: "ready" } + | { status: "error"; error: ThreadChatClientError } + +export type CommandState = + { status: "submitting" } | { status: "error"; error: ThreadChatClientError } + +export interface ThreadMessageWindowState { + loadState: LoadState + hasOlderMessages: boolean + oldestReturnedSequence: number | null + newestReturnedSequence: number | null +} + +export interface ProjectCatalogState { + projectsById: Record + orderedProjectIds: ProjectId[] + loadState: LoadState + activeFilter: "active" | "archived" + nextCursor: string | null +} + +export interface AppShellUiState { + sidebarOpen: boolean + sidebarWidth: number + projectSearchQuery: string + pendingProjectId: ProjectId | null +} + +export interface ThreadChatAppState { + catalog: ProjectCatalogState + shellUi: AppShellUiState +} + +export interface ThreadChatAppActions { + mergeProjectPage(result: ListProjectsResult, reset?: boolean): void + upsertProjectSummary(summary: ProjectSummary): void + removeProjectSummary(projectId: ProjectId): void + setCatalogLoadState(state: LoadState): void + setCatalogFilter(filter: ProjectCatalogState["activeFilter"]): void + setProjectRoutePending(projectId: ProjectId | null): void + setSidebarOpen(open: boolean): void + setSidebarWidth(width: number): void + setProjectSearchQuery(query: string): void +} + +export type ThreadChatAppStore = ThreadChatAppState & ThreadChatAppActions + +export interface ThreadChatEntitiesState { + project: ProjectEntity | null + threadsById: Record + messagesById: Record + messageIdsByThreadId: Record + artifactsById: Record + feedbackByMessageId: Record +} + +export interface StreamBuffer { + pendingChunks: Extract< + AssistantMessageEvent, + { type: "run.delta" } + >["chunk"][] + lastReceivedEventSequence: number + flushScheduled: boolean +} + +export interface ThreadChatRunsState { + byAssistantMessageId: Record + streamBuffersByAssistantMessageId: Record +} + +export interface ThreadChatRequestsState { + bootstrap: LoadState + threadMessagesById: Record + artifactById: Record + commandByScope: Record +} + +export interface ProjectArtifactSummary { + changeSequence: number + total: number + byKind: Record +} + +export interface ThreadColumnSlot { + slotId: ColumnSlotId + threadId: ThreadId + folded: boolean + widthPx: number | null +} + +export interface Point { + x: number + y: number +} + +export interface TextSelectionState { + messageId: MessageId + exactQuote: string + textPosition?: { start: number; end: number } +} + +export interface OverlayState { + selection: TextSelectionState | null + threadSwitcherScope: + | { kind: "global" } + | { kind: "column"; slotId: ColumnSlotId } + | { kind: "subtree"; rootThreadId: ThreadId } + | null + treeListOpen: boolean + helpPanelOpen: boolean + artifactDrawerOpen: boolean +} + +export interface ThreadChatUiState { + columnSlots: ThreadColumnSlot[] + focusedSlotId: "root" | ColumnSlotId | null + rootColumnWidthPx: number | null + forceColumnCount: number | null + placementMode: "replace" | "fold" + viewMode: "columns" | "canvas" + canvasPins: Record + composerDraftByThreadId: Record + selectedArtifactId: ArtifactId | null + activationClock: number + lastActivatedOrderBySlotId: Record + overlays: OverlayState +} + +export interface ThreadChatProjectState { + entities: ThreadChatEntitiesState + runs: ThreadChatRunsState + requests: ThreadChatRequestsState + readModels: { + artifactSummary: ProjectArtifactSummary | null + replacementSupersededMessageIds: Record + } + ui: ThreadChatUiState +} + +export interface ThreadWorkbenchSnapshotV1 { + schemaVersion: 1 + columnSlots: ThreadColumnSlot[] + focusedSlotId: "root" | ColumnSlotId + rootColumnWidthPx: number | null + forceColumnCount: number | null + placementMode: "replace" | "fold" + viewMode: "columns" | "canvas" + canvasPins: Record +} + +export interface ThreadChatProjectActions { + mergeCreationBundle(bundle: CreationBundle): void + mergeBootstrap(bootstrap: ProjectBootstrap): void + applyMessageBundle(bundle: ThreadMessageBundle): void + applyMessageCreationBundle(bundle: MessageCreationBundle): void + applyThreadCreated(thread: ThreadEntity): void + applyReplacementBundle(bundle: ReplacementBundle): void + applyRunEvent( + event: AssistantMessageEvent, + assistantMessageId?: MessageId + ): void + applyAssistantRun(run: AssistantRunState): void + flushRunBuffer(assistantMessageId: MessageId): void + applyArtifact(artifact: ArtifactEntity): void + applyProject(project: ProjectEntity): void + applyThread(thread: ThreadEntity): void + applyFeedback(feedback: Feedback): void + setBootstrapLoadState(state: LoadState): void + setCommandState(scope: string, state: CommandState | null): void + setThreadMessageLoadState(threadId: ThreadId, state: LoadState): void + setArtifactLoadState(artifactId: ArtifactId, state: LoadState): void + restoreWorkbenchSnapshot(snapshot: ThreadWorkbenchSnapshotV1): void + resetWorkbenchToDefault(): void + openThread(threadId: ThreadId, sourceSlotId: "root" | ColumnSlotId): void + switchColumnThread(slotId: ColumnSlotId, threadId: ThreadId): void + closeColumn(slotId: ColumnSlotId): void + setColumnFolded(slotId: ColumnSlotId, folded: boolean): void + focusColumn(slotId: "root" | ColumnSlotId): void + commitColumnWidths( + widths: Partial> + ): void + setForceColumnCount(count: number | null): void + setPlacementMode(mode: ThreadChatUiState["placementMode"]): void + setViewMode(mode: ThreadChatUiState["viewMode"]): void + setCanvasPin(threadId: ThreadId, point: Point | null): void + setComposerDraft(threadId: ThreadId, parts: UserMessageParts): void + setSelectedArtifact(artifactId: ArtifactId | null): void + setOverlays(patch: Partial): void +} + +export type ThreadChatProjectStore = ThreadChatProjectState & + ThreadChatProjectActions + +export interface GenerationCoordinator { + resumeLoadedRuns(): void + subscribeAssistant(assistantMessageId: MessageId): void + unsubscribeAssistant(assistantMessageId: MessageId): void + destroy(): void +} + +export interface ThreadMessageLoader { + ensure(threadId: ThreadId): Promise + destroy(): void +} + +export interface ArtifactLoader { + ensure(artifactId: ArtifactId): Promise + destroy(): void +} + +export interface ThreadChatProjectCommands { + loadProjectBootstrap(): Promise + ensureThreadMessages(threadId: ThreadId): Promise + ensureArtifact(artifactId: ArtifactId): Promise + updateProject( + patch: Omit< + Parameters[0], + "projectId" + > + ): Promise + updateThread(threadId: ThreadId, customTitle: string | null): Promise + setProjectArchived(archived: boolean): Promise + setThreadArchived(threadId: ThreadId, archived: boolean): Promise + deleteProject(): Promise + sendMessage( + threadId: ThreadId, + parts: UserMessageParts, + requestedModelId?: string + ): Promise + forkThread(input: { + sourceSlotId: "root" | ColumnSlotId + sourceThreadId: ThreadId + sourceMessageId: MessageId + anchor?: { + exactQuote: string + textPosition?: { start: number; end: number } + } + }): Promise + editMessage( + messageId: MessageId, + parts: UserMessageParts, + requestedModelId?: string + ): Promise + regenerateMessage( + messageId: MessageId, + requestedModelId?: string + ): Promise + setFeedback( + messageId: MessageId, + value: "positive" | "negative" | null + ): Promise + stopAssistant(assistantMessageId: MessageId): Promise +} + +export interface ThreadChatProjectRuntime { + projectId: ProjectId + store: StoreApi + commands: ThreadChatProjectCommands + messageLoader: ThreadMessageLoader + artifactLoader: ArtifactLoader + generationCoordinator: GenerationCoordinator + destroy(): void +} + +export interface ProjectRuntimeRegistry { + seedFromCreation(bundle: CreationBundle): ThreadChatProjectRuntime + acquire(projectId: ProjectId): ThreadChatProjectRuntime + release(projectId: ProjectId): void + peek(projectId: ProjectId): ThreadChatProjectRuntime | null + destroy(): void +} + +export interface ThreadChatAppCommands { + loadProjectCatalog(input?: { reset?: boolean }): Promise + setProjectArchived(projectId: ProjectId, archived: boolean): Promise + deleteProject(projectId: ProjectId): Promise +} + +export interface NavigationCapability { + replace(path: string): void + currentProjectId?(): ProjectId | null +} + +export interface ThreadChatAppRuntime { + appStore: StoreApi + projectRuntimeRegistry: ProjectRuntimeRegistry + api: ThreadChatApiCapabilities + commands: ThreadChatAppCommands + navigation: NavigationCapability + destroy(): void +} + +export interface NewProjectDraftState { + draftParts: UserMessageParts + requestedModelId?: string + status: "idle" | "submitting" | "error" + error: ThreadChatClientError | null +} + +export interface NewProjectDraftActions { + setDraftParts(parts: UserMessageParts): void + setRequestedModelId(modelId?: string): void + markSubmitting(): void + markError(error: ThreadChatClientError): void + markIdle(): void +} + +export type NewProjectDraftStore = NewProjectDraftState & NewProjectDraftActions diff --git a/openspec/changes/design-thread-chat-client-api/frontend-state-verification.md b/openspec/changes/design-thread-chat-client-api/frontend-state-verification.md new file mode 100644 index 00000000..9335c56b --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/frontend-state-verification.md @@ -0,0 +1,23 @@ +# 前端状态架构验收证据 + +## 自动化门槛 + +| 门槛 | 命令 | 结果 | +|---|---|---| +| 客户端 Store / Runtime / Hook 测试 | `pnpm test:client` | 3 个文件、13 个测试通过;使用 jsdom、Testing Library 与 user-event | +| 全量自动化测试 | `pnpm test` | 领域单元 14、客户端 13、PostgreSQL 集成 20、API 32,共 79 个测试通过 | +| TypeScript | `pnpm typecheck` | 通过 | +| 客户端静态检查 | `pnpm exec eslint lib/thread-chat/client tests/client vitest.client.config.ts` | 通过 | +| OpenSpec | `pnpm openspec:validate` | 27 项严格校验通过 | + +## 覆盖映射 + +| 客户端能力 | 自动化证据 | +|---|---| +| App Store、Project Store、normalizer、sequence、replacement、乱序 Artifact Summary、稳定 Slot、宽度、折叠与 Snapshot 过滤 | `tests/client/store.test.ts` | +| ThreadMessageLoader 同 ID 去重、跨 Thread 并行、Runtime Abort;ArtifactLoader 按 ID 缓存与 Project 隔离 | `tests/client/runtime.test.ts` | +| GenerationCoordinator 连接去重、checkpoint 合帧、terminal、断线重连、取消订阅不触发 Stop | `tests/client/runtime.test.ts` | +| Application Command 请求边界、服务端 ID 合并与订阅启动;ProjectRuntimeRegistry seed handoff 与 lease | `tests/client/runtime.test.ts` | +| Provider-scoped Runtime、React Strict Mode 生命周期、Selector/Command Hook 与 `/new` 先 seed 后导航 | `tests/client/providers.test.tsx` | + +客户端测试通过注入的 `ThreadChatApiCapabilities` adapter 运行,不接现有 UI,不请求真实后端或模型。 diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md index 62d09ddd..e1d802e0 100644 --- a/openspec/changes/design-thread-chat-client-api/tasks.md +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -56,26 +56,26 @@ ## 6. 建立前端测试基础与 Zustand Store -- [ ] 6.1 增加 Testing Library、user-event 与适配 React Hooks 的 Vitest DOM 环境;不得把 Ego Browser E2E 混入单元测试。 -- [ ] 6.2 使用 vanilla Zustand 建立 Provider-scoped `ThreadChatAppStore`,包含 Project Catalog 与 AppShellUi slices,不保存 selectedProjectId。 -- [ ] 6.3 建立按 projectId 创建的 `ThreadChatProjectStore`,包含 entities、runs、requests、readModels 与 workbench ui slices。 -- [ ] 6.4 实现共享 normalizer,维护 `messagesById + messageIdsByThreadId` 的归属、去重与 sequence 排序。 -- [ ] 6.5 实现 Creation、Bootstrap、Message、replacement、Run Event、Artifact 与 Summary 的语义化 Store Actions。 -- [ ] 6.6 实现稳定 Column Slot、Root/Branch 宽度、折叠、焦点、placement、Canvas pin、overlay 与 Workbench Snapshot Store Actions。 -- [ ] 6.7 确保 Store Action 只同步调用 Zustand `set`/`setState`,不请求 API、不导航、不创建连接,也不原地修改已确认实体。 -- [ ] 6.8 完成 Store/normalizer 测试:归属拒绝、sequence、replacement、重复 DTO、乱序 Summary、局部 Run 更新与 Snapshot 校验。 +- [x] 6.1 增加 Testing Library、user-event 与适配 React Hooks 的 Vitest DOM 环境;不得把 Ego Browser E2E 混入单元测试。 +- [x] 6.2 使用 vanilla Zustand 建立 Provider-scoped `ThreadChatAppStore`,包含 Project Catalog 与 AppShellUi slices,不保存 selectedProjectId。 +- [x] 6.3 建立按 projectId 创建的 `ThreadChatProjectStore`,包含 entities、runs、requests、readModels 与 workbench ui slices。 +- [x] 6.4 实现共享 normalizer,维护 `messagesById + messageIdsByThreadId` 的归属、去重与 sequence 排序。 +- [x] 6.5 实现 Creation、Bootstrap、Message、replacement、Run Event、Artifact 与 Summary 的语义化 Store Actions。 +- [x] 6.6 实现稳定 Column Slot、Root/Branch 宽度、折叠、焦点、placement、Canvas pin、overlay 与 Workbench Snapshot Store Actions。 +- [x] 6.7 确保 Store Action 只同步调用 Zustand `set`/`setState`,不请求 API、不导航、不创建连接,也不原地修改已确认实体。 +- [x] 6.8 完成 Store/normalizer 测试:归属拒绝、sequence、replacement、重复 DTO、乱序 Summary、局部 Run 更新与 Snapshot 校验。 ## 7. 实现 Runtime、Application Commands 与 Hooks -- [ ] 7.1 实现 `ThreadChatAppRuntime`、`ThreadChatProjectRuntime` 与 `ProjectRuntimeRegistry`,保证 Provider 生命周期内 projectId 对应唯一 Runtime。 -- [ ] 7.2 实现 `ThreadChatAppProvider`、`ThreadChatProjectProvider` 与 `NewProjectDraftProvider`,避免 Next.js 服务端模块级 Store 跨请求共享。 -- [ ] 7.3 实现 `/thread-chat/new` seeded Runtime handoff:先合并 CreationBundle,再由客户端 route builder replace,目标 Provider 跳过第二次 Bootstrap。 -- [ ] 7.4 实现 ThreadMessageLoader 的 threadId 级 Promise 去重、跨 Thread 并行与 Runtime destroy 统一 Abort。 -- [ ] 7.5 实现 ArtifactLoader 的 artifactId 级按需缓存,不因 Thread 加载自动请求正文。 -- [ ] 7.6 实现 GenerationCoordinator 的 assistantMessageId 级连接去重、snapshot 合并、断线重连、取消订阅和 destroy。 -- [ ] 7.7 实现 Catalog、Bootstrap、send、Fork、Edit、Regenerate、Stop、feedback 和 metadata Application Commands;客户端不生成服务端实体 ID。 -- [ ] 7.8 实现纯 selectors、Selector Hooks、Command Hooks 与 Lifecycle Hooks;组件不得直接 fetch 或维护第二份领域状态。 -- [ ] 7.9 完成 Runtime、Loader、Coordinator、Command、Selector 与 Hook 的接口注入测试。 +- [x] 7.1 实现 `ThreadChatAppRuntime`、`ThreadChatProjectRuntime` 与 `ProjectRuntimeRegistry`,保证 Provider 生命周期内 projectId 对应唯一 Runtime。 +- [x] 7.2 实现 `ThreadChatAppProvider`、`ThreadChatProjectProvider` 与 `NewProjectDraftProvider`,避免 Next.js 服务端模块级 Store 跨请求共享。 +- [x] 7.3 实现 `/thread-chat/new` seeded Runtime handoff:先合并 CreationBundle,再由客户端 route builder replace,目标 Provider 跳过第二次 Bootstrap。 +- [x] 7.4 实现 ThreadMessageLoader 的 threadId 级 Promise 去重、跨 Thread 并行与 Runtime destroy 统一 Abort。 +- [x] 7.5 实现 ArtifactLoader 的 artifactId 级按需缓存,不因 Thread 加载自动请求正文。 +- [x] 7.6 实现 GenerationCoordinator 的 assistantMessageId 级连接去重、snapshot 合并、断线重连、取消订阅和 destroy。 +- [x] 7.7 实现 Catalog、Bootstrap、send、Fork、Edit、Regenerate、Stop、feedback 和 metadata Application Commands;客户端不生成服务端实体 ID。 +- [x] 7.8 实现纯 selectors、Selector Hooks、Command Hooks 与 Lifecycle Hooks;组件不得直接 fetch 或维护第二份领域状态。 +- [x] 7.9 完成 Runtime、Loader、Coordinator、Command、Selector 与 Hook 的接口注入测试。 ## 8. 建立 UI Parity 基线并无损接入现有 UI diff --git a/package.json b/package.json index d59886c2..4ffaab04 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "start": "next start", "lint": "eslint", "format": "prettier --write \"**/*.{ts,tsx}\"", - "test": "pnpm test:unit && pnpm test:integration && pnpm test:api", + "test": "pnpm test:unit && pnpm test:client && pnpm test:integration && pnpm test:api", "test:unit": "vitest run --config vitest.config.ts", + "test:client": "vitest run --config vitest.client.config.ts", "test:integration": "pnpm test:db:reset && vitest run --config vitest.integration.config.ts", "test:api": "pnpm test:db:reset && vitest run --config vitest.api.config.ts", "test:watch": "vitest --config vitest.config.ts", @@ -89,6 +90,8 @@ "@fission-ai/openspec": "1.5.0", "@shikijs/types": "4.3.1", "@tailwindcss/postcss": "^4", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.6", "@types/node": "^20", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", @@ -96,6 +99,7 @@ "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.3.1", + "jsdom": "^30.0.1", "playwright-core": "^1.61.1", "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddb38c98..ee98165f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 1.1.3 better-auth: specifier: ^1.6.23 - version: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))) + version: 1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(jsdom@30.0.1(@noble/hashes@2.2.0))(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -195,6 +195,12 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.3.2 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@testing-library/user-event': + specifier: ^14.6.6 + version: 14.6.6(@testing-library/dom@10.4.1) '@types/node': specifier: ^20 version: 20.19.43 @@ -216,6 +222,9 @@ importers: eslint-config-next: specifier: 16.3.1 version: 16.3.1(@typescript-eslint/parser@8.62.1(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + jsdom: + specifier: ^30.0.1 + version: 30.0.1(@noble/hashes@2.2.0) playwright-core: specifier: ^1.61.1 version: 1.61.1 @@ -233,7 +242,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(jsdom@30.0.1(@noble/hashes@2.2.0))(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) packages: @@ -319,6 +328,14 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} + '@assistant-ui/core@0.2.19': resolution: {integrity: sha512-QYwIy+l21rvVsyuc19doblCJ3bfhBzqbuNa/xdts8CIDlw+5LWg1uVGCiVhTRpkysHuJyONDF4TVBAxdMXR7yw==} peerDependencies: @@ -745,6 +762,46 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.0': + resolution: {integrity: sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.8': + resolution: {integrity: sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dagrejs/dagre@3.0.0': resolution: {integrity: sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==} @@ -1266,6 +1323,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fission-ai/openspec@1.5.0': resolution: {integrity: sha512-SLZkyF51gFYkISufZKaka0X04z4y/WCjPOcCB+EC7tALd0TC+7V76BOIzWOSIOhdhBWwh5EMIBhrgLnugIh1DA==} engines: {node: '>=20.19.0'} @@ -2817,12 +2883,40 @@ packages: '@tailwindcss/postcss@4.3.2': resolution: {integrity: sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.6': + resolution: {integrity: sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -3216,6 +3310,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3223,6 +3321,9 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -3403,6 +3504,9 @@ packages: zod: optional: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -3588,6 +3692,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3665,6 +3773,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3704,6 +3816,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -3779,6 +3894,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -3934,6 +4052,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -4410,6 +4532,10 @@ packages: resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -4607,6 +4733,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -4705,6 +4834,15 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -4957,6 +5095,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4965,6 +5107,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -5020,6 +5166,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -5353,6 +5502,9 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5513,6 +5665,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -5586,6 +5742,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: @@ -5775,6 +5934,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -6009,6 +6172,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + systeminformation@5.31.11: resolution: {integrity: sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==} engines: {node: '>=8.0.0'} @@ -6050,6 +6216,13 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.11: + resolution: {integrity: sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==} + + tldts@7.4.11: + resolution: {integrity: sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6058,6 +6231,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -6143,6 +6324,10 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -6354,6 +6539,26 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6400,6 +6605,13 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -6583,6 +6795,21 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + '@assistant-ui/core@0.2.19(@assistant-ui/store@0.2.19(@assistant-ui/tap@0.9.3(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8))(@assistant-ui/tap@0.9.3(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(assistant-cloud@0.1.34)(react@19.2.8)(zustand@5.0.14(@types/react@19.2.18)(immer@11.1.9)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))': dependencies: '@assistant-ui/store': 0.2.19(@assistant-ui/tap@0.9.3(@types/react@19.2.18)(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) @@ -7172,6 +7399,34 @@ snapshots: '@better-fetch/fetch@1.3.1': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.8(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@dagrejs/dagre@3.0.0': dependencies: '@dagrejs/graphlib': 4.0.1 @@ -7502,6 +7757,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.15.1(@noble/hashes@2.2.0)': + optionalDependencies: + '@noble/hashes': 2.2.0 + '@fission-ai/openspec@1.5.0(@types/node@20.19.43)': dependencies: '@inquirer/core': 10.3.2(@types/node@20.19.43) @@ -9084,6 +9343,31 @@ snapshots: postcss: 8.5.16 tailwindcss: 4.3.2 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) + + '@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -9095,6 +9379,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -9478,12 +9764,18 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + argparse@2.0.1: {} aria-hidden@1.2.6: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -9605,7 +9897,7 @@ snapshots: elkjs: 0.11.1 entities: 7.0.1 - better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))): + better-auth@1.6.23(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9))(next@16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(jsdom@30.0.1(@noble/hashes@2.2.0))(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))): dependencies: '@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.3)(nanostores@1.4.0) '@better-auth/drizzle-adapter': 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.29.3)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.3)(postgres@3.4.9)) @@ -9630,7 +9922,7 @@ snapshots: next: 16.3.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(jsdom@30.0.1(@noble/hashes@2.2.0))(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -9644,6 +9936,10 @@ snapshots: optionalDependencies: zod: 4.4.3 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -9824,6 +10120,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -9894,6 +10195,13 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-urls@7.0.0(@noble/hashes@2.2.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -9928,6 +10236,8 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -9983,6 +10293,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dot-prop@6.0.1: dependencies: is-obj: 2.0.0 @@ -10046,6 +10358,8 @@ snapshots: entities@7.0.1: {} + entities@8.0.0: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -10802,6 +11116,12 @@ snapshots: hono@4.12.27: {} + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - '@noble/hashes' + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -10968,6 +11288,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -11054,6 +11376,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@30.0.1(@noble/hashes@2.2.0): + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.8(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.2.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.0(@noble/hashes@2.2.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -11242,6 +11590,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -11250,6 +11600,8 @@ snapshots: dependencies: react: 19.2.8 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -11411,6 +11763,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -11859,6 +12213,10 @@ snapshots: parse-ms@4.0.0: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -11934,6 +12292,12 @@ snapshots: prettier@3.9.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -12056,6 +12420,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.2.18)(react@19.2.8): dependencies: '@types/hast': 3.0.4 @@ -12317,6 +12683,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -12656,6 +13026,8 @@ snapshots: react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) + symbol-tree@3.2.4: {} + systeminformation@5.31.11: {} tabbable@6.5.0: {} @@ -12681,12 +13053,26 @@ snapshots: tinyrainbow@3.1.1: {} + tldts-core@7.4.11: {} + + tldts@7.4.11: + dependencies: + tldts-core: 7.4.11 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.11 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -12794,6 +13180,8 @@ snapshots: undici@7.28.0: {} + undici@8.10.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -12962,7 +13350,7 @@ snapshots: tsx: 4.23.0 yaml: 2.9.0 - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(jsdom@30.0.1(@noble/hashes@2.2.0))(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) @@ -12987,9 +13375,34 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 20.19.43 + jsdom: 30.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.0(@noble/hashes@2.2.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -13059,6 +13472,10 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@2.9.0: {} diff --git a/tests/client/providers.test.tsx b/tests/client/providers.test.tsx new file mode 100644 index 00000000..2f66928c --- /dev/null +++ b/tests/client/providers.test.tsx @@ -0,0 +1,179 @@ +import { StrictMode, useState } from "react" +import { render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { beforeEach, describe, expect, it, vi } from "vitest" + +const navigationMocks = vi.hoisted(() => ({ + replace: vi.fn(), + pathname: "/thread-chat/new", +})) + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: navigationMocks.replace }), + usePathname: () => navigationMocks.pathname, +})) + +import { + useAppShellCommands, + useNewProjectDraftStore, + useProject, + useSubmitNewProjectDraft, +} from "@/lib/thread-chat/client/hooks" +import { + NewProjectDraftProvider, + ThreadChatAppProvider, + ThreadChatProjectProvider, + useThreadChatProjectRuntime, +} from "@/lib/thread-chat/client/providers" +import { + assistantMessageDTOFixture, + assistantRunDTOFixture, + creationBundleDTOFixture, + projectDTOFixture, + rootThreadDTOFixture, + userMessageDTOFixture, +} from "../fixtures/thread-chat-api-fixtures" +import { createTestApi } from "./test-api" + +const bootstrap = { + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, +} + +beforeEach(() => { + navigationMocks.replace.mockReset() + navigationMocks.pathname = "/thread-chat/new" +}) + +describe("ThreadChat Providers and hooks", () => { + it("ProjectProvider 每个 projectId 复用唯一 Runtime,并由 Hook 细粒度读取", async () => { + const bootstrapProject = vi.fn().mockResolvedValue(bootstrap) + const user = userEvent.setup() + let firstRuntime: unknown + + function Probe() { + const project = useProject() + const runtime = useThreadChatProjectRuntime() + const shell = useAppShellCommands() + const [renders, setRenders] = useState(0) + firstRuntime ??= runtime + return ( +
+ {project?.id ?? "loading"} + + {runtime === firstRuntime ? "same" : "changed"} + + {renders} + +
+ ) + } + + render( + + + + + + + + ) + await waitFor(() => + expect(screen.getByTestId("project").textContent).toBe( + projectDTOFixture.id + ) + ) + expect(screen.getByTestId("runtime").textContent).toBe("same") + expect(bootstrapProject).toHaveBeenCalledTimes(1) + await user.click(screen.getByRole("button", { name: "toggle shell" })) + expect(screen.getByTestId("renders").textContent).toBe("1") + expect(bootstrapProject).toHaveBeenCalledTimes(1) + }) + + it("/new 先 seed Runtime、订阅生成,再根据服务端 projectId replace", async () => { + const createProject = vi.fn().mockResolvedValue(creationBundleDTOFixture) + const subscribeAssistantEvents = vi.fn(async function* () { + yield { + type: "run.failed" as const, + eventSequence: 1, + run: { + ...assistantRunDTOFixture, + status: "failed" as const, + eventSequence: 1, + error: { code: "fake_failed", message: "fake" }, + finishedAt: "2026-08-25T00:01:00.000Z", + }, + } + }) + const user = userEvent.setup() + + function DraftProbe() { + const setDraftParts = useNewProjectDraftStore( + (state) => state.setDraftParts + ) + const status = useNewProjectDraftStore((state) => state.status) + const submit = useSubmitNewProjectDraft() + return ( + <> + {status} + + + ) + } + + render( + + + + + + ) + await user.click(screen.getByRole("button", { name: "submit draft" })) + await waitFor(() => expect(createProject).toHaveBeenCalledTimes(1)) + expect(createProject).toHaveBeenCalledWith({ + parts: [{ type: "text", text: "hello" }], + requestedModelId: undefined, + }) + expect(JSON.stringify(createProject.mock.calls[0][0])).not.toContain("id") + await waitFor(() => + expect(navigationMocks.replace).toHaveBeenCalledWith( + `/thread-chat/${projectDTOFixture.id}` + ) + ) + expect(subscribeAssistantEvents).toHaveBeenCalledWith( + expect.objectContaining({ + assistantMessageId: assistantMessageDTOFixture.id, + afterEventSequence: 0, + }) + ) + }) +}) diff --git a/tests/client/runtime.test.ts b/tests/client/runtime.test.ts new file mode 100644 index 00000000..91000c0b --- /dev/null +++ b/tests/client/runtime.test.ts @@ -0,0 +1,365 @@ +import { describe, expect, it, vi } from "vitest" +import { createThreadChatProjectCommands } from "@/lib/thread-chat/client/commands" +import { createGenerationCoordinator } from "@/lib/thread-chat/client/generation-coordinator" +import { + createArtifactLoader, + createThreadMessageLoader, +} from "@/lib/thread-chat/client/loaders" +import { createThreadChatProjectStore } from "@/lib/thread-chat/client/project-store" +import { + createProjectRuntimeRegistry, + createThreadChatProjectRuntime, +} from "@/lib/thread-chat/client/runtime" +import type { + AssistantMessageEvent, + GenerationCoordinator, +} from "@/lib/thread-chat/client/types" +import { + assistantMessageDTOFixture, + assistantRunDTOFixture, + creationBundleDTOFixture, + projectDTOFixture, + rootThreadDTOFixture, + userMessageDTOFixture, +} from "../fixtures/thread-chat-api-fixtures" +import { createTestApi } from "./test-api" + +function deferred() { + let resolve!: (value: T) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} + +const branch = { + ...rootThreadDTOFixture, + id: "00000000-0000-4000-8000-000000000301", + parentThreadId: rootThreadDTOFixture.id, + sourceMessageId: userMessageDTOFixture.id, + forkSourceSnapshot: { + schemaVersion: 1 as const, + sourceRole: "user" as const, + sourceSequence: 1, + }, +} + +describe("ThreadChat client runtime", () => { + it("ThreadMessageLoader 同 Thread 去重、不同 Thread 并行且 destroy 统一 Abort", async () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeBootstrap({ + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture, branch], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }) + const secondBranch = { + ...branch, + id: "00000000-0000-4000-8000-000000000302", + } + store.getState().applyThreadCreated(secondBranch) + const pendingByThread = new Map< + string, + ReturnType> + >() + const signals: AbortSignal[] = [] + const loadThreadMessages = vi.fn( + (input: { threadId: string; signal?: AbortSignal }) => { + const pending = deferred() + pendingByThread.set(input.threadId, pending) + if (input.signal) signals.push(input.signal) + return pending.promise + } + ) + const coordinator = { + resumeLoadedRuns: vi.fn(), + subscribeAssistant: vi.fn(), + unsubscribeAssistant: vi.fn(), + destroy: vi.fn(), + } satisfies GenerationCoordinator + const loader = createThreadMessageLoader({ + projectId: projectDTOFixture.id, + api: createTestApi({ loadThreadMessages }), + store, + generationCoordinator: coordinator, + }) + + const first = loader.ensure(branch.id) + const duplicate = loader.ensure(branch.id) + const parallel = loader.ensure(secondBranch.id) + expect(first).toBe(duplicate) + expect(parallel).not.toBe(first) + expect(loadThreadMessages).toHaveBeenCalledTimes(2) + loader.destroy() + expect(signals.every((signal) => signal.aborted)).toBe(true) + pendingByThread.forEach((pending) => + pending.reject(new DOMException("Aborted", "AbortError")) + ) + await Promise.all([first, duplicate, parallel]) + expect( + store.getState().requests.threadMessagesById[branch.id] + ).toMatchObject({ + loadState: { status: "loading" }, + }) + }) + + it("GenerationCoordinator 按 assistantMessageId 去重、合帧并接收终态", async () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + const terminal = deferred() + const subscribeAssistantEvents = vi.fn(async function* () { + yield { + type: "run.snapshot" as const, + cursor: 0, + run: { ...assistantRunDTOFixture, status: "running" as const }, + message: assistantMessageDTOFixture, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + } + yield { + type: "run.delta" as const, + eventSequence: 1, + chunk: { + type: "data-run-checkpoint", + data: { checkpointParts: [{ type: "text", text: "partial" }] }, + }, + } + await terminal.promise + yield { + type: "run.completed" as const, + eventSequence: 2, + run: { + ...assistantRunDTOFixture, + status: "completed" as const, + checkpointParts: [{ type: "text", text: "final" }], + eventSequence: 2, + finishedAt: "2026-08-25T00:01:00.000Z", + }, + message: { + ...assistantMessageDTOFixture, + parts: [{ type: "text", text: "final" }], + finalizedAt: "2026-08-25T00:01:00.000Z", + }, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + } + }) + const scheduled: Array<() => void> = [] + const coordinator = createGenerationCoordinator({ + api: createTestApi({ subscribeAssistantEvents }), + store, + scheduleFlush: (callback) => { + scheduled.push(callback) + return vi.fn() + }, + waitForReconnect: () => Promise.resolve(), + }) + coordinator.subscribeAssistant(assistantMessageDTOFixture.id) + coordinator.subscribeAssistant(assistantMessageDTOFixture.id) + await vi.waitFor(() => expect(scheduled).toHaveLength(1)) + expect(subscribeAssistantEvents).toHaveBeenCalledTimes(1) + scheduled[0]() + expect( + store.getState().runs.byAssistantMessageId[assistantMessageDTOFixture.id] + .checkpointParts + ).toEqual([{ type: "text", text: "partial" }]) + terminal.resolve() + await vi.waitFor(() => + expect( + store.getState().runs.byAssistantMessageId[ + assistantMessageDTOFixture.id + ].status + ).toBe("completed") + ) + expect( + store.getState().entities.messagesById[assistantMessageDTOFixture.id] + .parts + ).toEqual([{ type: "text", text: "final" }]) + coordinator.destroy() + }) + + it("ArtifactLoader 只按 artifactId 请求、缓存并拒绝跨 Project 响应", async () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + const artifactId = "00000000-0000-4000-8000-000000000305" + const loadArtifact = vi.fn().mockResolvedValue({ + id: artifactId, + projectId: projectDTOFixture.id, + sourceMessageId: assistantMessageDTOFixture.id, + kind: "markdown", + title: "Result", + content: "# body", + createdAt: "2026-08-25T00:01:00.000Z", + }) + const loader = createArtifactLoader({ + projectId: projectDTOFixture.id, + api: createTestApi({ loadArtifact }), + store, + }) + const first = loader.ensure(artifactId) + const duplicate = loader.ensure(artifactId) + expect(first).toBe(duplicate) + await first + expect(loadArtifact).toHaveBeenCalledTimes(1) + await loader.ensure(artifactId) + expect(loadArtifact).toHaveBeenCalledTimes(1) + expect(store.getState().entities.artifactsById[artifactId].content).toBe( + "# body" + ) + + const foreignId = "00000000-0000-4000-8000-000000000306" + loadArtifact.mockResolvedValueOnce({ + id: foreignId, + projectId: "00000000-0000-4000-8000-000000000999", + sourceMessageId: assistantMessageDTOFixture.id, + kind: "markdown", + title: "Foreign", + content: "hidden", + createdAt: "2026-08-25T00:01:00.000Z", + }) + await loader.ensure(foreignId) + expect(store.getState().requests.artifactById[foreignId]).toMatchObject({ + status: "error", + error: { code: "validation_error" }, + }) + expect(store.getState().entities.artifactsById[foreignId]).toBeUndefined() + }) + + it("GenerationCoordinator 网络失败后按持久游标重连,取消订阅不调用 Stop", async () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + let attempt = 0 + const subscribeAssistantEvents = vi.fn(async function* (_input: { + assistantMessageId: string + afterEventSequence?: number + signal?: AbortSignal + }): AsyncGenerator { + void _input + attempt++ + if (attempt === 1) throw new Error("disconnected") + yield { + type: "run.failed", + eventSequence: 1, + run: { + ...assistantRunDTOFixture, + status: "failed", + eventSequence: 1, + error: { code: "provider_failed", message: "failed" }, + finishedAt: "2026-08-25T00:01:00.000Z", + }, + } + }) + const stopAssistant = vi.fn() + const coordinator = createGenerationCoordinator({ + api: createTestApi({ subscribeAssistantEvents, stopAssistant }), + store, + waitForReconnect: () => Promise.resolve(), + }) + coordinator.subscribeAssistant(assistantMessageDTOFixture.id) + await vi.waitFor(() => + expect(subscribeAssistantEvents).toHaveBeenCalledTimes(2) + ) + expect(subscribeAssistantEvents.mock.calls[1][0]).toMatchObject({ + assistantMessageId: assistantMessageDTOFixture.id, + afterEventSequence: 0, + }) + coordinator.unsubscribeAssistant(assistantMessageDTOFixture.id) + expect(stopAssistant).not.toHaveBeenCalled() + }) + + it("Application Commands 只提交既有 ID 并用语义 Action 合并响应", async () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + const nextUser = { + ...userMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000303", + sequence: 3, + parts: [{ type: "text", text: "next" }], + } + const nextAssistant = { + ...assistantMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000304", + sequence: 4, + } + const sendMessage = vi.fn().mockResolvedValue({ + userMessage: nextUser, + assistantMessage: nextAssistant, + assistantRun: { + ...assistantRunDTOFixture, + assistantMessageId: nextAssistant.id, + }, + }) + const coordinator = { + resumeLoadedRuns: vi.fn(), + subscribeAssistant: vi.fn(), + unsubscribeAssistant: vi.fn(), + destroy: vi.fn(), + } satisfies GenerationCoordinator + const api = createTestApi({ sendMessage }) + const result = createThreadChatProjectCommands({ + projectId: projectDTOFixture.id, + api, + store, + messageLoader: { ensure: vi.fn(), destroy: vi.fn() }, + artifactLoader: { ensure: vi.fn(), destroy: vi.fn() }, + generationCoordinator: coordinator, + }) + await result.commands.sendMessage(rootThreadDTOFixture.id, [ + { type: "text", text: "next" }, + ]) + expect(sendMessage).toHaveBeenCalledWith({ + threadId: rootThreadDTOFixture.id, + parts: [{ type: "text", text: "next" }], + requestedModelId: undefined, + }) + expect(JSON.stringify(sendMessage.mock.calls[0][0])).not.toContain( + nextAssistant.id + ) + expect(store.getState().entities.messagesById[nextAssistant.id]).toEqual( + nextAssistant + ) + expect(coordinator.subscribeAssistant).toHaveBeenCalledWith( + nextAssistant.id + ) + }) + + it("Registry 用一次 seed handoff 交付同一 Runtime并抵抗同 tick lease 重挂载", async () => { + const api = createTestApi() + const registry = createProjectRuntimeRegistry({ + createRuntime: (projectId) => + createThreadChatProjectRuntime({ + projectId, + api, + waitForReconnect: () => new Promise(() => {}), + }), + }) + const seeded = registry.seedFromCreation(creationBundleDTOFixture) + const acquired = registry.acquire(projectDTOFixture.id) + expect(acquired).toBe(seeded) + expect(acquired.store.getState().requests.bootstrap.status).toBe("ready") + registry.release(projectDTOFixture.id) + expect(registry.acquire(projectDTOFixture.id)).toBe(seeded) + await Promise.resolve() + expect(registry.peek(projectDTOFixture.id)).toBe(seeded) + registry.release(projectDTOFixture.id) + await Promise.resolve() + expect(registry.peek(projectDTOFixture.id)).toBeNull() + }) +}) diff --git a/tests/client/store.test.ts b/tests/client/store.test.ts new file mode 100644 index 00000000..2f86c240 --- /dev/null +++ b/tests/client/store.test.ts @@ -0,0 +1,262 @@ +import { describe, expect, it } from "vitest" +import { createThreadChatAppStore } from "@/lib/thread-chat/client/app-store" +import { createThreadChatProjectStore } from "@/lib/thread-chat/client/project-store" +import { + selectForkAvailability, + selectThreadColumnView, + selectThreadMessages, +} from "@/lib/thread-chat/client/selectors" +import { + assistantMessageDTOFixture, + assistantRunDTOFixture, + creationBundleDTOFixture, + projectDTOFixture, + rootThreadDTOFixture, + userMessageDTOFixture, +} from "../fixtures/thread-chat-api-fixtures" + +const branchId = "00000000-0000-4000-8000-000000000201" + +const branch = { + ...rootThreadDTOFixture, + id: branchId, + parentThreadId: rootThreadDTOFixture.id, + sourceMessageId: userMessageDTOFixture.id, + forkSourceSnapshot: { + schemaVersion: 1 as const, + sourceRole: "user" as const, + sourceSequence: 1, + }, +} + +describe("ThreadChat client stores", () => { + it("App Store 合并稳定排序的摘要且不保存 selectedProjectId", () => { + const store = createThreadChatAppStore() + expect(store.getState().catalog.loadState.status).toBe("idle") + expect("selectedProjectId" in store.getState()).toBe(false) + store.getState().mergeProjectPage({ + items: [ + { + id: projectDTOFixture.id, + displayTitle: "First", + archivedAt: null, + updatedAt: "2026-08-25T00:00:00.000Z", + threadCount: 1, + messageCount: 2, + }, + { + id: "00000000-0000-4000-8000-000000000200", + displayTitle: "Second", + archivedAt: null, + updatedAt: "2026-08-26T00:00:00.000Z", + threadCount: 1, + messageCount: 0, + }, + ], + nextCursor: "cursor", + }) + expect(store.getState().catalog.orderedProjectIds).toEqual([ + "00000000-0000-4000-8000-000000000200", + projectDTOFixture.id, + ]) + store.getState().setCatalogFilter("archived") + expect(store.getState().catalog).toMatchObject({ + activeFilter: "archived", + orderedProjectIds: [], + nextCursor: null, + loadState: { status: "idle" }, + }) + }) + + it("Creation、Message 与 replacement 由 normalizer 原子合并", () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + expect(store.getState()).toMatchObject({ + requests: { + bootstrap: { status: "ready" }, + threadMessagesById: { + [rootThreadDTOFixture.id]: { loadState: { status: "ready" } }, + }, + }, + ui: { focusedSlotId: "root" }, + }) + expect( + selectThreadMessages(store.getState(), rootThreadDTOFixture.id) + ).toEqual([userMessageDTOFixture, assistantMessageDTOFixture]) + + store.getState().applyMessageBundle({ + threadId: rootThreadDTOFixture.id, + messages: [assistantMessageDTOFixture, userMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }) + expect( + store.getState().entities.messageIdsByThreadId[rootThreadDTOFixture.id] + ).toEqual([userMessageDTOFixture.id, assistantMessageDTOFixture.id]) + + const replacement = { + ...assistantMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000203", + sequence: 3, + replacesMessageId: assistantMessageDTOFixture.id, + } + store.getState().applyReplacementBundle({ + supersededMessageIds: [assistantMessageDTOFixture.id], + createdMessages: [replacement], + assistantRun: { + ...assistantRunDTOFixture, + assistantMessageId: replacement.id, + }, + }) + expect( + selectThreadMessages(store.getState(), rootThreadDTOFixture.id).map( + (message) => message.id + ) + ).toEqual([userMessageDTOFixture.id, replacement.id]) + }) + + it("拒绝跨 Project 归属与 duplicate sequence", () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + expect(() => + store.getState().applyThreadCreated({ + ...branch, + projectId: "00000000-0000-4000-8000-000000000999", + }) + ).toThrow(/another Project/) + expect(() => + store.getState().applyMessageBundle({ + threadId: rootThreadDTOFixture.id, + messages: [ + { + ...assistantMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000204", + sequence: 1, + }, + ], + assistantRuns: [ + { + ...assistantRunDTOFixture, + assistantMessageId: "00000000-0000-4000-8000-000000000204", + }, + ], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 1, + }) + ).toThrow(/duplicate Message sequence/) + }) + + it("稳定 Column Slot、宽度、折叠和 Snapshot 过滤非法视图", () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + generateSlotId: () => "slot-1", + }) + store.getState().mergeBootstrap({ + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture, branch], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }) + store.getState().openThread(branch.id, "root") + store.getState().commitColumnWidths({ "slot-1": 420 }) + store.getState().switchColumnThread("slot-1", branch.id) + expect(store.getState().ui.columnSlots[0]).toMatchObject({ + slotId: "slot-1", + threadId: branch.id, + widthPx: 420, + }) + store.getState().setColumnFolded("slot-1", true) + expect(store.getState().ui.focusedSlotId).toBe("root") + store.getState().restoreWorkbenchSnapshot({ + schemaVersion: 1, + columnSlots: [ + { slotId: "slot-1", threadId: branch.id, folded: false, widthPx: 360 }, + { + slotId: "duplicate", + threadId: branch.id, + folded: false, + widthPx: 360, + }, + { + slotId: "foreign", + threadId: "00000000-0000-4000-8000-000000000999", + folded: false, + widthPx: 360, + }, + ], + focusedSlotId: "duplicate", + rootColumnWidthPx: 500, + forceColumnCount: 3, + placementMode: "fold", + viewMode: "canvas", + canvasPins: { + [branch.id]: { x: 12, y: 20 }, + "00000000-0000-4000-8000-000000000999": { x: 2, y: 3 }, + }, + }) + expect(store.getState().ui).toMatchObject({ + columnSlots: [{ slotId: "slot-1", threadId: branch.id, widthPx: 360 }], + focusedSlotId: "slot-1", + rootColumnWidthPx: 500, + placementMode: "fold", + viewMode: "canvas", + canvasPins: { [branch.id]: { x: 12, y: 20 } }, + }) + }) + + it("局部 Run、Artifact Summary 与 Column View 不依赖 Artifact 正文", () => { + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + store.getState().mergeCreationBundle(creationBundleDTOFixture) + store.getState().applyRunEvent({ + type: "run.snapshot", + cursor: 0, + run: assistantRunDTOFixture, + message: assistantMessageDTOFixture, + artifactSummary: { changeSequence: 2, total: 1, byKind: { markdown: 1 } }, + }) + store.getState().applyRunEvent({ + type: "run.snapshot", + cursor: 0, + run: assistantRunDTOFixture, + message: assistantMessageDTOFixture, + artifactSummary: { changeSequence: 1, total: 0, byKind: {} }, + }) + expect(store.getState().readModels.artifactSummary?.changeSequence).toBe(2) + expect(() => + store.getState().applyRunEvent({ + type: "run.snapshot", + cursor: 0, + run: assistantRunDTOFixture, + message: assistantMessageDTOFixture, + artifactSummary: { + changeSequence: 2, + total: 2, + byKind: { markdown: 2 }, + }, + }) + ).toThrow(/without advancing changeSequence/) + expect( + selectForkAvailability(store.getState(), assistantMessageDTOFixture.id) + ).toEqual({ allowed: false, reason: "message_not_finalized" }) + expect(selectThreadColumnView(store.getState(), "root")).toMatchObject({ + status: "ready", + artifactIds: [], + }) + }) +}) diff --git a/tests/client/test-api.ts b/tests/client/test-api.ts new file mode 100644 index 00000000..abc50375 --- /dev/null +++ b/tests/client/test-api.ts @@ -0,0 +1,31 @@ +import type { ThreadChatApiCapabilities } from "@/lib/thread-chat/api/capabilities" + +const notImplemented = () => + Promise.reject(new Error("Not implemented in test.")) + +export function createTestApi( + overrides: Partial = {} +): ThreadChatApiCapabilities { + return { + listProjects: notImplemented, + createProject: notImplemented, + bootstrapProject: notImplemented, + patchProject: notImplemented, + setProjectArchived: notImplemented, + deleteProject: notImplemented, + loadThreadMessages: notImplemented, + patchThread: notImplemented, + setThreadArchived: notImplemented, + sendMessage: notImplemented, + forkThread: notImplemented, + editMessage: notImplemented, + regenerateMessage: notImplemented, + setFeedback: notImplemented, + loadArtifact: notImplemented, + async *subscribeAssistantEvents() { + throw new Error("Not implemented in test.") + }, + stopAssistant: notImplemented, + ...overrides, + } +} diff --git a/vitest.client.config.ts b/vitest.client.config.ts new file mode 100644 index 00000000..9442b0e8 --- /dev/null +++ b/vitest.client.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + test: { + environment: "jsdom", + include: ["tests/client/**/*.test.{ts,tsx}"], + clearMocks: true, + restoreMocks: true, + }, +}) From a6f9f0c82eacdfff8d0e992473e612089039fe95 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 20:23:32 +0800 Subject: [PATCH 16/23] feat(thread-chat): connect normalized runtime without UI loss --- app/thread-chat/[treeId]/page.tsx | 13 +- app/thread-chat/branching/branchable-chat.tsx | 4 +- .../branching/selection/selection-bubble.tsx | 5 + .../selection/selection-placement-map.tsx | 8 +- .../selection/use-assistant-text-selection.ts | 11 +- app/thread-chat/layout.tsx | 4 +- app/thread-chat/new/page.tsx | 13 + app/thread-chat/normalized/project-list.tsx | 206 ++++++ .../normalized/project-view-model.ts | 273 +++++++ .../normalized/thread-chat-new.tsx | 210 ++++++ .../normalized/thread-chat-project.tsx | 693 ++++++++++++++++++ .../normalized/use-project-workbench.ts | 51 ++ .../artifacts/artifact-drawer.tsx | 18 +- .../orchestration/canvas/use-canvas-layout.ts | 12 +- .../orchestration/columns/thread-columns.tsx | 6 +- .../navigation/thread-switcher-panel.tsx | 2 +- app/thread-chat/tree-redirect.tsx | 21 +- lib/thread-chat/api/json-transport.ts | 2 +- lib/thread-chat/client/commands.ts | 6 +- lib/thread-chat/client/project-store.ts | 168 +++-- lib/thread-chat/client/types.ts | 13 +- .../client/workbench-persistence.ts | 50 ++ lib/thread-chat/domain/selectors.ts | 7 +- .../design-thread-chat-client-api/tasks.md | 16 +- .../ui-parity-baseline/README.md | 86 +++ .../ui-parity-baseline/artifact-drawer.png | Bin 0 -> 156822 bytes .../ui-parity-baseline/breadcrumb-back.png | Bin 0 -> 175164 bytes .../ui-parity-baseline/collapsed-column.png | Bin 0 -> 175285 bytes .../ui-parity-baseline/divider-dragged.png | Bin 0 -> 220856 bytes .../ui-parity-baseline/empty-project.png | Bin 0 -> 42766 bytes .../ui-parity-baseline/fork-composer.png | Bin 0 -> 184832 bytes .../header-child-selector.png | Bin 0 -> 124681 bytes .../ui-parity-baseline/single-column.png | Bin 0 -> 100598 bytes .../ui-parity-baseline/switched-slot.png | Bin 0 -> 170922 bytes .../ui-parity-baseline/thread-switcher.png | Bin 0 -> 209928 bytes .../three-columns-nested-fork.png | Bin 0 -> 220615 bytes .../ui-parity-baseline/two-columns.png | Bin 0 -> 175158 bytes .../ui-parity-implementation/README.md | 38 + .../artifact-drawer-normalized.png | Bin 0 -> 113984 bytes .../empty-new-normalized.png | Bin 0 -> 42763 bytes .../three-columns-normalized.png | Bin 0 -> 179624 bytes tests/api/transport.test.ts | 17 + tests/client/store.test.ts | 90 +++ 43 files changed, 1960 insertions(+), 83 deletions(-) create mode 100644 app/thread-chat/new/page.tsx create mode 100644 app/thread-chat/normalized/project-list.tsx create mode 100644 app/thread-chat/normalized/project-view-model.ts create mode 100644 app/thread-chat/normalized/thread-chat-new.tsx create mode 100644 app/thread-chat/normalized/thread-chat-project.tsx create mode 100644 app/thread-chat/normalized/use-project-workbench.ts create mode 100644 lib/thread-chat/client/workbench-persistence.ts create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/empty-project.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/fork-composer.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/header-child-selector.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/single-column.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/switched-slot.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/three-columns-nested-fork.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-baseline/two-columns.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-implementation/empty-new-normalized.png create mode 100644 openspec/changes/design-thread-chat-client-api/ui-parity-implementation/three-columns-normalized.png diff --git a/app/thread-chat/[treeId]/page.tsx b/app/thread-chat/[treeId]/page.tsx index bad9041d..1477ab10 100644 --- a/app/thread-chat/[treeId]/page.tsx +++ b/app/thread-chat/[treeId]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from "next/navigation" import { isValidTreeId } from "@/lib/chat/tree-id" -import { ThreadChatDemo } from "../thread-chat-demo" +import { ThreadChatProjectProvider } from "@/lib/thread-chat/client/providers" +import { ThreadChatProject } from "../normalized/thread-chat-project" import { threadChatMetadata } from "../page-metadata" export const metadata = threadChatMetadata @@ -15,7 +16,11 @@ export default async function ThreadChatTreePage({ }: { params: Promise<{ treeId: string }> }) { - const { treeId } = await params - if (!isValidTreeId(treeId)) notFound() - return + const { treeId: projectId } = await params + if (!isValidTreeId(projectId)) notFound() + return ( + + + + ) } diff --git a/app/thread-chat/branching/branchable-chat.tsx b/app/thread-chat/branching/branchable-chat.tsx index 59716f7d..cfb80679 100644 --- a/app/thread-chat/branching/branchable-chat.tsx +++ b/app/thread-chat/branching/branchable-chat.tsx @@ -81,7 +81,7 @@ export function BranchableChat({ }: BranchableChatProps) { const thread = state.threads[threadId] if (!thread) return null - const isMain = threadId === "main" + const isMain = thread.parentId === null const chain = isMain ? [] : lineage(state, threadId) const inherited = isMain ? [] : collectInherited(state, thread) const childCount = thread.children.length @@ -193,7 +193,7 @@ export function BranchableChat({
讨论焦点 · 划选自 - {thread.parentId === "main" + {state.threads[thread.parentId ?? ""]?.parentId === null ? "主线" : `「${threadTitle(state, thread.parentId!)}」`} diff --git a/app/thread-chat/branching/selection/selection-bubble.tsx b/app/thread-chat/branching/selection/selection-bubble.tsx index 7bea8836..7c5927e5 100644 --- a/app/thread-chat/branching/selection/selection-bubble.tsx +++ b/app/thread-chat/branching/selection/selection-bubble.tsx @@ -291,6 +291,11 @@ export function SelectionBubble({
{preview && ( thread.parentId === null + )?.id ?? "main" + } sourceThreadId={sel.threadId} slots={slots} preview={preview} diff --git a/app/thread-chat/branching/selection/selection-placement-map.tsx b/app/thread-chat/branching/selection/selection-placement-map.tsx index b212b76c..8968c513 100644 --- a/app/thread-chat/branching/selection/selection-placement-map.tsx +++ b/app/thread-chat/branching/selection/selection-placement-map.tsx @@ -1,6 +1,7 @@ import type { PlacePreview, Slot } from "../../orchestration/columns/placement" export function SelectionPlacementMap({ + rootThreadId, sourceThreadId, slots, preview, @@ -8,6 +9,7 @@ export function SelectionPlacementMap({ titleOf, onToggleOverride, }: { + rootThreadId: string sourceThreadId: string slots: Slot[] preview: PlacePreview @@ -17,12 +19,12 @@ export function SelectionPlacementMap({ }) { const cells: React.ReactNode[] = [ , ] const ghost = (key: string) => ( diff --git a/app/thread-chat/branching/selection/use-assistant-text-selection.ts b/app/thread-chat/branching/selection/use-assistant-text-selection.ts index ef48a871..8d994231 100644 --- a/app/thread-chat/branching/selection/use-assistant-text-selection.ts +++ b/app/thread-chat/branching/selection/use-assistant-text-selection.ts @@ -59,10 +59,15 @@ export function useAssistantTextSelection({ const threadId = list?.dataset.list const msgId = messageElement?.dataset.msgId if (!threadId || !msgId) return + const sourceMessage = state.threads[threadId]?.messages.find( + (message) => message.id === msgId + ) if ( - !state.threads[threadId]?.messages.some( - (message) => message.id === msgId - ) + !sourceMessage || + sourceMessage.role !== "assistant" || + sourceMessage.status === "pending" || + sourceMessage.status === "streaming" || + sourceMessage.status === "error" ) { onSelectionChange(null) return diff --git a/app/thread-chat/layout.tsx b/app/thread-chat/layout.tsx index 79dd8484..117a2c98 100644 --- a/app/thread-chat/layout.tsx +++ b/app/thread-chat/layout.tsx @@ -1,6 +1,8 @@ import { redirect } from "next/navigation" import { getSession } from "@/lib/auth/server" import { ROUTES, signInWithRedirect } from "@/constants/routes" +import { ThreadChatAppProvider } from "@/lib/thread-chat/client/providers" +import "./thread-chat.css" // 旗舰访问门禁:一处服务端 layout 同时包住 /thread-chat 跳板与 /thread-chat/[treeId], // 用「真会话」判定(getSession),未登录即 302 到带回跳的登录页。 @@ -14,5 +16,5 @@ export default async function ThreadChatLayout({ }) { const session = await getSession() if (!session) redirect(signInWithRedirect(ROUTES.flagship)) - return <>{children} + return {children} } diff --git a/app/thread-chat/new/page.tsx b/app/thread-chat/new/page.tsx new file mode 100644 index 00000000..7991b878 --- /dev/null +++ b/app/thread-chat/new/page.tsx @@ -0,0 +1,13 @@ +import { NewProjectDraftProvider } from "@/lib/thread-chat/client/providers" +import { threadChatMetadata } from "../page-metadata" +import { ThreadChatNew } from "../normalized/thread-chat-new" + +export const metadata = threadChatMetadata + +export default function NewThreadChatProjectPage() { + return ( + + + + ) +} diff --git a/app/thread-chat/normalized/project-list.tsx b/app/thread-chat/normalized/project-list.tsx new file mode 100644 index 00000000..3b8dee9e --- /dev/null +++ b/app/thread-chat/normalized/project-list.tsx @@ -0,0 +1,206 @@ +"use client" + +import { useEffect, useMemo, useState, type RefObject } from "react" +import { useRouter } from "next/navigation" +import { ListTodo } from "lucide-react" +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" +import { Dialog, DialogPortal } from "@/components/ui/dialog" +import { + CUSTOM_TITLE_MAX_LEN, + THREAD_CHAT_SHORTCUTS, +} from "@/constants/thread-chat" +import { + useThreadChatAppRuntime, +} from "@/lib/thread-chat/client/providers" +import { useThreadChatAppStore } from "@/lib/thread-chat/client/hooks" +import { threadChatRoutes } from "@/lib/thread-chat/api/routes" +import { dialogCloseToShell } from "../orchestration/overlays/dialog-close-to-shell" +import { ShortcutHint } from "../orchestration/overlays/shortcut-hint" +import { TreeListRow } from "../orchestration/navigation/tree-list-row" + +export function ProjectList({ + currentProjectId, + closing = false, + container, + onClose, + onToast, +}: { + currentProjectId: string | null + closing?: boolean + container?: RefObject + onClose(): void + onToast(message: string): void +}) { + const router = useRouter() + const runtime = useThreadChatAppRuntime() + const catalog = useThreadChatAppStore((state) => state.catalog) + const [editingId, setEditingId] = useState(null) + const [draft, setDraft] = useState("") + const [confirmId, setConfirmId] = useState(null) + const [deletingId, setDeletingId] = useState(null) + + useEffect(() => { + void runtime.commands.loadProjectCatalog({ reset: true }) + }, [runtime]) + + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + if (editingId !== null) { + event.stopPropagation() + setEditingId(null) + } else if (confirmId !== null) { + event.stopPropagation() + setConfirmId(null) + } + } + document.addEventListener("keydown", onKey, true) + return () => document.removeEventListener("keydown", onKey, true) + }, [confirmId, editingId]) + + const rows = useMemo(() => { + const items = catalog.orderedProjectIds + .map((id) => catalog.projectsById[id]) + .filter(Boolean) + return [ + ...items.filter((item) => item.id === currentProjectId), + ...items.filter((item) => item.id !== currentProjectId), + ] + }, [catalog.orderedProjectIds, catalog.projectsById, currentProjectId]) + + async function rename(projectId: string) { + const previous = catalog.projectsById[projectId] + const title = draft.trim() + setEditingId(null) + if (!previous || !title || title === previous.displayTitle) return + if (title.length > CUSTOM_TITLE_MAX_LEN) { + onToast(`标题最长 ${CUSTOM_TITLE_MAX_LEN} 字,未保存`) + return + } + runtime.appStore.getState().upsertProjectSummary({ + ...previous, + displayTitle: title, + }) + try { + const project = await runtime.api.patchProject({ + projectId, + customTitle: title, + }) + runtime.appStore.getState().upsertProjectSummary({ + ...previous, + displayTitle: project.customTitle ?? project.autoTitle ?? "新对话", + updatedAt: project.updatedAt, + }) + runtime.projectRuntimeRegistry + .peek(projectId) + ?.store.getState() + .applyProject(project) + } catch { + runtime.appStore.getState().upsertProjectSummary(previous) + onToast("重命名失败,已恢复原名") + } + } + + async function remove(projectId: string) { + setConfirmId(null) + setDeletingId(projectId) + try { + await runtime.commands.deleteProject(projectId) + onClose() + if (projectId === currentProjectId) + runtime.navigation.replace(threadChatRoutes.newProject()) + else onToast("对话已删除") + } catch { + onToast("删除失败,请重试") + } finally { + setDeletingId(null) + } + } + + const loading = catalog.loadState.status === "idle" || catalog.loadState.status === "loading" + + return ( + + + + setConfirmId(null)} + > +
+ + 对话列表 + +
+
+ {loading &&
加载中…
} + {catalog.loadState.status === "error" && ( + + )} + {!loading && + rows.map((project) => ( + { + onClose() + if (project.id !== currentProjectId) + router.push(threadChatRoutes.project(project.id)) + }} + onDraftChange={setDraft} + onCancelEdit={() => setEditingId(null)} + onCommitEdit={() => void rename(project.id)} + onStartEdit={() => { + setConfirmId(null) + setEditingId(project.id) + setDraft(project.displayTitle) + }} + onRequestDelete={() => { + setEditingId(null) + setConfirmId(project.id) + }} + onConfirmDelete={() => void remove(project.id)} + onCancelDelete={() => setConfirmId(null)} + /> + ))} + {!loading && rows.length === 0 && ( +
还没有对话——发出第一条消息即可创建
+ )} +
+
+ 点击切换 + 悬停条目可重命名 / 删除 + + 关闭 + +
+
+
+
+ ) +} diff --git a/app/thread-chat/normalized/project-view-model.ts b/app/thread-chat/normalized/project-view-model.ts new file mode 100644 index 00000000..894670d0 --- /dev/null +++ b/app/thread-chat/normalized/project-view-model.ts @@ -0,0 +1,273 @@ +import { DEFAULT_THREAD_CHAT_MODEL_ID } from "@/constants/model" +import type { TextAnchor } from "@/lib/thread-chat/domain/text-anchor" +import type { ThreadChatProjectStore } from "@/lib/thread-chat/client/types" +import type { + Artifact, + Message, + MessageStatus, + Thread, + ThreadTreeState, +} from "../core/types" + +type LoosePart = { + type?: unknown + text?: unknown + input?: unknown + output?: unknown +} + +export interface ArtifactHint { + id: string + title: string + kind: "markdown" + sourceThreadId?: string + sourceMessageId?: string +} + +export function textFromParts(parts: readonly unknown[] | null | undefined) { + return (parts ?? []) + .flatMap((part) => { + const candidate = part as LoosePart + return candidate.type === "text" && typeof candidate.text === "string" + ? [candidate.text] + : [] + }) + .join("") +} + +export function artifactHintsFromParts( + parts: readonly unknown[] | null | undefined +): ArtifactHint[] { + const hints = new Map() + for (const part of parts ?? []) { + const candidate = part as LoosePart + if (candidate.type !== "dynamic-tool") continue + const output = candidate.output as { artifactId?: unknown } | undefined + if (typeof output?.artifactId !== "string") continue + const input = candidate.input as { title?: unknown } | undefined + hints.set(output.artifactId, { + id: output.artifactId, + title: + typeof input?.title === "string" && input.title.trim() + ? input.title.trim() + : "Markdown", + kind: "markdown", + }) + } + return [...hints.values()] +} + +function displayTitle(value: string | null | undefined, fallback: string) { + const title = value?.trim() + return title ? title : fallback +} + +function artifactContent(content: unknown): string { + if (typeof content === "string") return content + return JSON.stringify(content, null, 2) +} + +function statusOf( + state: ThreadChatProjectStore, + messageId: string, + text: string +): { status?: MessageStatus; error?: string; backgroundGeneration?: boolean } { + const run = state.runs.byAssistantMessageId[messageId] + if (!run) + return state.entities.messagesById[messageId]?.finalizedAt + ? { status: "done" } + : { status: "pending" } + if (run.status === "queued") return { status: "pending" } + if (run.status === "running") + return { status: "streaming", backgroundGeneration: true } + if (run.status === "completed" || (run.status === "stopped" && text)) + return { status: "done" } + return { + status: "error", + error: + run.error?.message ?? + (run.status === "stopped" ? "生成已停止" : "生成失败"), + } +} + +function threadDepths(state: ThreadChatProjectStore) { + const memo = new Map() + const visit = (threadId: string, path = new Set()): number => { + const known = memo.get(threadId) + if (known !== undefined) return known + if (path.has(threadId)) return 0 + const thread = state.entities.threadsById[threadId] + if (!thread?.parentThreadId) { + memo.set(threadId, 0) + return 0 + } + const nextPath = new Set(path).add(threadId) + const depth = visit(thread.parentThreadId, nextPath) + 1 + memo.set(threadId, depth) + return depth + } + for (const threadId of Object.keys(state.entities.threadsById)) visit(threadId) + return memo +} + +function branchFootnotes(state: ThreadChatProjectStore) { + const ordered = Object.values(state.entities.threadsById) + .filter((thread) => thread.parentThreadId !== null) + .toSorted( + (left, right) => + left.createdAt.localeCompare(right.createdAt) || + left.id.localeCompare(right.id) + ) + return new Map(ordered.map((thread, index) => [thread.id, index + 1])) +} + +function modelForThread(state: ThreadChatProjectStore, threadId: string) { + const ids = state.entities.messageIdsByThreadId[threadId] ?? [] + for (let index = ids.length - 1; index >= 0; index--) { + const run = state.runs.byAssistantMessageId[ids[index]] + if (run?.modelId) return run.modelId + } + return DEFAULT_THREAD_CHAT_MODEL_ID +} + +function sourceAnchor(quote: string | undefined): TextAnchor | undefined { + return quote + ? { quote: { exact: quote, prefix: "", suffix: "" } } + : undefined +} + +export function projectLegacyTreeView( + state: ThreadChatProjectStore +): ThreadTreeState { + const depths = threadDepths(state) + const footnotes = branchFootnotes(state) + const childrenByParent = new Map() + for (const thread of Object.values(state.entities.threadsById)) { + if (!thread.parentThreadId) continue + const children = childrenByParent.get(thread.parentThreadId) ?? [] + children.push(thread.id) + childrenByParent.set(thread.parentThreadId, children) + } + for (const children of childrenByParent.values()) + children.sort((left, right) => { + const leftThread = state.entities.threadsById[left] + const rightThread = state.entities.threadsById[right] + return ( + leftThread.createdAt.localeCompare(rightThread.createdAt) || + left.localeCompare(right) + ) + }) + + const artifactHints = new Map() + const threads: Record = {} + for (const entity of Object.values(state.entities.threadsById)) { + const ids = state.entities.messageIdsByThreadId[entity.id] ?? [] + const visible = ids + .map((id) => state.entities.messagesById[id]) + .filter( + (message) => + Boolean(message) && + message.supersededAt === null && + !state.readModels.replacementSupersededMessageIds[message.id] + ) + const childThreads = (childrenByParent.get(entity.id) ?? []).map( + (id) => state.entities.threadsById[id] + ) + const messages: Message[] = visible.map((entityMessage, index) => { + const run = state.runs.byAssistantMessageId[entityMessage.id] + const persistedText = textFromParts(entityMessage.parts) + const checkpointText = textFromParts(run?.checkpointParts) + const text = persistedText || checkpointText + const hints = artifactHintsFromParts( + entityMessage.parts ?? run?.checkpointParts + ) + for (const hint of hints) + artifactHints.set(hint.id, { + ...hint, + sourceThreadId: entityMessage.threadId, + sourceMessageId: entityMessage.id, + }) + const forks = childThreads + .filter((child) => child.sourceMessageId === entityMessage.id) + .map((child) => ({ + text: child.forkSourceSnapshot?.quote ?? "", + num: footnotes.get(child.id) ?? 0, + threadId: child.id, + depth: depths.get(child.id) ?? 1, + anchor: sourceAnchor(child.forkSourceSnapshot?.quote), + })) + return { + id: entityMessage.id, + parentMessageId: index === 0 ? null : visible[index - 1].id, + role: entityMessage.role, + text, + forks, + ...(entityMessage.role === "assistant" + ? { + generationId: entityMessage.id, + artifactIds: hints.map((hint) => hint.id), + ...statusOf(state, entityMessage.id, text), + } + : index === 0 && entity.parentThreadId + ? { + quote: entity.forkSourceSnapshot?.quote + ? { text: entity.forkSourceSnapshot.quote } + : undefined, + } + : null), + } + }) + const quote = entity.forkSourceSnapshot?.quote ?? null + threads[entity.id] = { + id: entity.id, + modelId: modelForThread(state, entity.id), + parentId: entity.parentThreadId, + depth: depths.get(entity.id) ?? 0, + title: + entity.parentThreadId === null + ? "主线" + : displayTitle( + entity.customTitle ?? entity.autoTitle, + quote ?? "新分支" + ), + anchorText: quote, + forkFromMsgId: entity.sourceMessageId, + footnote: footnotes.get(entity.id) ?? null, + children: childrenByParent.get(entity.id) ?? [], + messages, + activeLeafMessageId: messages.at(-1)?.id ?? null, + lastActive: + state.ui.lastActivatedOrderBySlotId[ + state.ui.columnSlots.find((slot) => slot.threadId === entity.id) + ?.slotId ?? "root" + ] ?? 0, + } + } + + const artifacts: Record = {} + for (const hint of artifactHints.values()) { + const loaded = state.entities.artifactsById[hint.id] + const sourceMessage = loaded + ? state.entities.messagesById[loaded.sourceMessageId] + : undefined + artifacts[hint.id] = { + id: hint.id, + title: loaded?.title ?? hint.title, + kind: loaded?.kind === "code" || loaded?.kind === "note" ? loaded.kind : "markdown", + content: loaded ? artifactContent(loaded.content) : "", + sourceThreadId: sourceMessage?.threadId ?? hint.sourceThreadId ?? "", + sourceMessageId: loaded?.sourceMessageId ?? hint.sourceMessageId ?? "", + } + } + + return { + schemaVersion: 2, + threads, + artifacts, + artifactOrder: [...artifactHints.keys()], + recents: state.ui.columnSlots.map((slot) => slot.threadId).toReversed(), + footnoteCounter: footnotes.size, + seq: 1, + tick: state.ui.activationClock, + } +} diff --git a/app/thread-chat/normalized/thread-chat-new.tsx b/app/thread-chat/normalized/thread-chat-new.tsx new file mode 100644 index 00000000..c4afcb1a --- /dev/null +++ b/app/thread-chat/normalized/thread-chat-new.tsx @@ -0,0 +1,210 @@ +"use client" + +import dynamic from "next/dynamic" +import { useMemo, useRef, useState } from "react" +import { DEFAULT_THREAD_CHAT_MODEL_ID } from "@/constants/model" +import { + useNewProjectDraftStore, + useSubmitNewProjectDraft, +} from "@/lib/thread-chat/client/hooks" +import { emptySeedState } from "../core/seed" +import { createThreadStore } from "../core/store" +import { ThreadColumns } from "../orchestration/columns/thread-columns" +import { BranchableChat } from "../branching/branchable-chat" +import { ThreadChatTopbar } from "../orchestration/navigation/thread-chat-topbar" +import { useColumnViewport } from "../orchestration/columns/use-column-viewport" +import { useWorkspaceOverlays } from "../orchestration/overlays/use-workspace-overlays" +import { HelpPanel, UsageHint } from "../orchestration/overlays/help-panel" +import { + useWorkspaceToast, + WorkspaceToast, +} from "../orchestration/overlays/workspace-toast" +import { ArtifactDrawer } from "../orchestration/artifacts/artifact-drawer" +import { ProjectList } from "./project-list" + +const NEW_PROJECT_VIEW = emptySeedState() +const EMPTY_ACTION_VIEW = { + recoverableByUserMessageId: new Map(), + feedbackByMessageId: new Map(), + activePathByThreadId: new Map(), + presentationByThreadId: new Map(), +} + +const ThreadCanvas = dynamic( + () => + import("../orchestration/canvas/thread-canvas").then( + (module) => module.ThreadCanvas + ), + { + ssr: false, + loading: () =>
画布加载中…
, + } +) + +export function ThreadChatNew() { + const submit = useSubmitNewProjectDraft() + const status = useNewProjectDraftStore((state) => state.status) + const error = useNewProjectDraftStore((state) => state.error) + const setDraftParts = useNewProjectDraftStore( + (state) => state.setDraftParts + ) + const setRequestedModelId = useNewProjectDraftStore( + (state) => state.setRequestedModelId + ) + const [modelId, setModelId] = useState( + DEFAULT_THREAD_CHAT_MODEL_ID + ) + const [hintDismissed, setHintDismissed] = useState(false) + const [viewMode, setViewMode] = useState<"columns" | "canvas">("columns") + const [forceCols, setForceCols] = useState(null) + const [placementMode, setPlacementMode] = useState<"replace" | "fold">( + "replace" + ) + const [canvasStore] = useState(() => createThreadStore(emptySeedState())) + const canvasViewState = useMemo(() => ({ pins: new Map() }), []) + const { windowWidth } = useColumnViewport() + const { + rootRef, + treeList, + closeTreeList, + toggleTreeList, + helpPanel, + closeHelpPanel, + openHelpPanel, + drawerOpen, + toggleGlobalSwitcher, + toggleDrawer, + closeDrawer, + } = useWorkspaceOverlays() + const colsRef = useRef(null) + const { toast, showToast, dismissToast } = useWorkspaceToast() + const hint = !hintDismissed ? ( + setHintDismissed(true)} /> + ) : null + + const create = (text: string) => { + setDraftParts([{ type: "text", text }]) + setRequestedModelId(modelId) + void submit() + } + + return ( +
+ showToast("当前就是全新对话,直接开聊吧")} + onToggleTreeList={toggleTreeList} + onOpenHelp={openHelpPanel} + onShowColumns={() => setViewMode("columns")} + onShowCanvas={() => setViewMode("canvas")} + onForceCols={setForceCols} + onPlacementModeChange={setPlacementMode} + onToggleThreadTree={toggleGlobalSwitcher} + onToggleMarkdown={toggleDrawer} + /> + {viewMode === "columns" ? ( + ( + undefined} + onOpenArtifact={() => undefined} + onCrumbNav={() => undefined} + onOpenSwitcher={() => undefined} + onOpenSubtree={() => undefined} + onCollapse={() => undefined} + busy={status === "submitting"} + onStop={() => undefined} + onModelChange={setModelId} + onSend={create} + /> + )} + onExpandStrip={() => undefined} + onCommitWidths={() => undefined} + onResetWidths={() => undefined} + /> + ) : ( + setViewMode("columns")} + onOpenArtifact={() => undefined} + chat={{ + send: (_threadId, text) => create(text), + stop: () => undefined, + retry: () => undefined, + retryAssistant: async () => ({ + ok: false, + code: "invalid_turn", + message: "还没有可重试的回复", + }), + retryUserTurn: async () => ({ + ok: false, + code: "invalid_turn", + message: "还没有可重试的提问", + }), + editAndRegenerate: async () => ({ + ok: false, + code: "invalid_turn", + message: "还没有可编辑的提问", + }), + switchTurnVariant: async () => ({ + ok: false, + code: "invalid_turn", + message: "还没有可切换的回复", + }), + submitFeedback: async () => null, + }} + messageActionState={EMPTY_ACTION_VIEW} + /> + )} + {error && ( +
+ {error.message} +
+ )} + {treeList && ( + + )} + {helpPanel && ( + + )} + undefined} + onLocate={() => undefined} + /> + +
+ ) +} diff --git a/app/thread-chat/normalized/thread-chat-project.tsx b/app/thread-chat/normalized/thread-chat-project.tsx new file mode 100644 index 00000000..7e72cab5 --- /dev/null +++ b/app/thread-chat/normalized/thread-chat-project.tsx @@ -0,0 +1,693 @@ +"use client" + +import dynamic from "next/dynamic" +import { useRouter } from "next/navigation" +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react" +import { DEFAULT_THREAD_CHAT_MODEL_ID } from "@/constants/model" +import { + selectThreadColumnHeaderView, + selectThreadColumnView, +} from "@/lib/thread-chat/client/selectors" +import { + useThreadChatProjectRuntime, +} from "@/lib/thread-chat/client/providers" +import { useThreadChatStore } from "@/lib/thread-chat/client/hooks" +import type { ThreadChatProjectStore } from "@/lib/thread-chat/client/types" +import type { ThreadStore } from "../core/store" +import type { MessageActionViewState } from "../chat/actions/message-action-types" +import type { ThreadMessageActionCommands } from "../chat/actions/message-action-commands" +import type { SelectionInfo } from "../branching/selection/selection-bubble" +import type { PlacementHint, Slot } from "../orchestration/columns/placement" +import type { SwitcherMode } from "../orchestration/navigation/thread-switcher" +import { useColumnViewport } from "../orchestration/columns/use-column-viewport" +import { ThreadColumns } from "../orchestration/columns/thread-columns" +import { ThreadChatTopbar } from "../orchestration/navigation/thread-chat-topbar" +import { ThreadSwitcher } from "../orchestration/navigation/thread-switcher" +import { useWorkspaceOverlays } from "../orchestration/overlays/use-workspace-overlays" +import { HelpPanel, UsageHint } from "../orchestration/overlays/help-panel" +import { + useWorkspaceToast, + WorkspaceToast, +} from "../orchestration/overlays/workspace-toast" +import { BranchableChat } from "../branching/branchable-chat" +import { SelectionBubble } from "../branching/selection/selection-bubble" +import { ArtifactDrawer } from "../orchestration/artifacts/artifact-drawer" +import { ProjectList } from "./project-list" +import { projectLegacyTreeView } from "./project-view-model" +import { useProjectWorkbench } from "./use-project-workbench" +import { kickoffQuestion } from "../net/prompt/prompt-pure" + +const ThreadCanvas = dynamic( + () => + import("../orchestration/canvas/thread-canvas").then( + (module) => module.ThreadCanvas + ), + { + ssr: false, + loading: () =>
画布加载中…
, + } +) + +const EMPTY_SLOTS: Slot[] = [] + +function useProjectedThreadStore( + state: ThreadChatProjectStore, + projected: ReturnType, + onModelChange: (modelId: string) => void +): ThreadStore { + const rootThreadId = Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + )?.id + return useMemo( + () => + ({ + getState: () => projected, + getVersion: () => 0, + subscribe: () => () => undefined, + setThreadModel: (threadId: string, modelId: string) => { + if (rootThreadId === threadId) onModelChange(modelId) + }, + }) as unknown as ThreadStore, + [onModelChange, projected, rootThreadId] + ) +} + +function messageActionView( + state: ThreadChatProjectStore, + projected: ReturnType +): MessageActionViewState { + const activePathByThreadId = new Map() + const presentationByThreadId = new Map() + for (const thread of Object.values(projected.threads)) { + const ids = thread.messages.map((message) => message.id) + activePathByThreadId.set(thread.id, ids) + const latestUser = thread.messages.findLast((message) => message.role === "user") + const latestAssistant = thread.messages.findLast( + (message) => message.role === "assistant" + ) + presentationByThreadId.set(thread.id, { + latestUserMessageId: latestUser?.id, + latestAssistantMessageId: latestAssistant?.id, + alternatives: latestAssistant + ? [{ assistantMessageId: latestAssistant.id, derivedThreadCount: 0 }] + : [], + sourceProvenance: null, + }) + } + return { + recoverableByUserMessageId: new Map(), + feedbackByMessageId: new Map( + Object.values(state.entities.feedbackByMessageId) + .filter((feedback) => feedback.value !== null) + .map((feedback) => [feedback.messageId, feedback.value!]) + ), + activePathByThreadId, + presentationByThreadId, + } +} + +function scopeError(state: ThreadChatProjectStore, scope: string) { + const command = state.requests.commandByScope[scope] + return command?.status === "error" ? command.error.message : null +} + +function currentSlotIdForThread( + state: ThreadChatProjectStore, + threadId: string +) { + return state.ui.columnSlots.find((slot) => slot.threadId === threadId)?.slotId +} + +export function ThreadChatProject({ projectId }: { projectId: string }) { + const router = useRouter() + const runtime = useThreadChatProjectRuntime() + const state = useThreadChatStore((snapshot) => snapshot) + const bootstrapReady = state.requests.bootstrap.status === "ready" + useProjectWorkbench(runtime, bootstrapReady) + const projected = useMemo(() => projectLegacyTreeView(state), [state]) + const [rootModelOverride, setRootModelId] = useState(null) + const root = Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + ) + const latestRootModelId = root + ? (state.entities.messageIdsByThreadId[root.id] ?? []) + .toReversed() + .map((messageId) => state.runs.byAssistantMessageId[messageId]?.modelId) + .find(Boolean) + : undefined + const rootModelId = + rootModelOverride ?? latestRootModelId ?? DEFAULT_THREAD_CHAT_MODEL_ID + const projectedStore = useProjectedThreadStore( + state, + projected, + setRootModelId + ) + const actionView = useMemo( + () => messageActionView(state, projected), + [projected, state] + ) + const { windowWidth, autoColumnCount } = useColumnViewport() + const totalColumns = state.ui.forceColumnCount ?? autoColumnCount + const maxExpanded = Math.max(0, totalColumns - 1) + const colsRef = useRef(null) + const [flashId, setFlashId] = useState(null) + const flashTimer = useRef(null) + const [hintDismissed, setHintDismissed] = useState(false) + const [focusNode, setFocusNode] = useState<{ id: string; n: number } | null>( + null + ) + const focusSequence = useRef(0) + const canvasViewState = useMemo( + () => ({ + pins: new Map(Object.entries(state.ui.canvasPins)), + onPinsChange: (pins: ReadonlyMap) => { + const current = runtime.store.getState().ui.canvasPins + for (const threadId of Object.keys(current)) + if (!pins.has(threadId)) runtime.store.getState().setCanvasPin(threadId, null) + for (const [threadId, point] of pins) + runtime.store.getState().setCanvasPin(threadId, point) + }, + }), + [runtime, state.ui.canvasPins] + ) + const { toast, showToast, dismissToast } = useWorkspaceToast() + const { + rootRef, + selection, + setSelection, + switcher, + closeSwitcher, + toggleGlobalSwitcher, + openColumnSwitcher, + openSubtree, + treeList, + closeTreeList, + toggleTreeList, + helpPanel, + closeHelpPanel, + openHelpPanel, + drawerOpen, + activeArtifactId, + setActiveArtifactId, + openArtifact, + toggleDrawer, + closeDrawer, + } = useWorkspaceOverlays() + + useEffect(() => { + if (!bootstrapReady) return + const visible = [ + Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + )?.id, + ...state.ui.columnSlots.map((slot) => slot.threadId), + ].filter((threadId): threadId is string => Boolean(threadId)) + for (const threadId of visible) + void runtime.commands.ensureThreadMessages(threadId) + }, [bootstrapReady, runtime, state.entities.threadsById, state.ui.columnSlots]) + + useEffect(() => { + const expanded = state.ui.columnSlots.filter((slot) => !slot.folded) + const excess = expanded.length - maxExpanded + if (excess <= 0) return + for (const slot of expanded.slice(0, excess)) + runtime.store.getState().closeColumn(slot.slotId) + }, [maxExpanded, runtime, state.ui.columnSlots]) + + useEffect( + () => () => { + if (flashTimer.current !== null) window.clearTimeout(flashTimer.current) + }, + [] + ) + + const flash = useCallback((threadId: string) => { + setFlashId(threadId) + if (flashTimer.current !== null) window.clearTimeout(flashTimer.current) + flashTimer.current = window.setTimeout(() => setFlashId(null), 950) + }, []) + + const rootThread = Object.values(state.entities.threadsById).find( + (thread) => thread.parentThreadId === null + ) + const slots: Slot[] = state.ui.columnSlots.map((slot) => ({ + id: slot.threadId, + folded: slot.folded, + })) + const widths: Record = {} + if (rootThread && state.ui.rootColumnWidthPx !== null) + widths[rootThread.id] = state.ui.rootColumnWidthPx + for (const slot of state.ui.columnSlots) + if (slot.widthPx !== null) widths[slot.threadId] = slot.widthPx + + const slotIdForThread = useCallback( + (threadId: string): "root" | string => + rootThread?.id === threadId + ? "root" + : (state.ui.columnSlots.find((slot) => slot.threadId === threadId) + ?.slotId ?? "root"), + [rootThread?.id, state.ui.columnSlots] + ) + + const openThread = useCallback( + (threadId: string, sourceThreadId: string | null = null) => { + if (rootThread?.id === threadId) { + flash(threadId) + return + } + runtime.store + .getState() + .openThread( + threadId, + sourceThreadId ? slotIdForThread(sourceThreadId) : "root", + { maxExpanded } + ) + runtime.store.getState().setViewMode("columns") + flash(threadId) + }, + [flash, maxExpanded, rootThread?.id, runtime, slotIdForThread] + ) + + const activeAssistantId = (threadId: string) => + projected.threads[threadId]?.messages + .toReversed() + .find( + (message) => + message.role === "assistant" && + (message.status === "pending" || message.status === "streaming") + )?.id + + const messageCommands = useMemo( + () => ({ + async retryAssistant(threadId, assistantMessageId) { + await runtime.commands.regenerateMessage(assistantMessageId, rootModelId) + const error = scopeError( + runtime.store.getState(), + `regenerate:${assistantMessageId}` + ) + return error + ? { ok: false, code: "network_error", message: error } + : { + ok: true, + generationId: assistantMessageId, + userMessageId: "", + assistantMessageId, + } + }, + async retryUserTurn() { + return { + ok: false, + code: "invalid_turn", + message: "请编辑消息后重试", + } + }, + async editAndRegenerate(threadId, userMessageId, text) { + await runtime.commands.editMessage( + userMessageId, + [{ type: "text", text }], + rootModelId + ) + const error = scopeError(runtime.store.getState(), `edit:${userMessageId}`) + return error + ? { ok: false, code: "network_error", message: error } + : { + ok: true, + generationId: userMessageId, + userMessageId, + assistantMessageId: userMessageId, + } + }, + async switchTurnVariant() { + return { + ok: false, + code: "invalid_turn", + message: "该回复版本已被替换", + } + }, + async submitFeedback(threadId, messageId, feedback) { + await runtime.commands.setFeedback(messageId, feedback) + const value = runtime.store.getState().entities.feedbackByMessageId[messageId] + return value?.value + ? { + treeId: projectId, + threadId, + messageId, + feedback: value.value, + updatedAt: value.updatedAt, + } + : null + }, + }), + [projectId, rootModelId, runtime] + ) + + const send = useCallback( + async (threadId: string, text: string) => { + await runtime.commands.sendMessage( + threadId, + [{ type: "text", text }], + rootModelId + ) + const error = scopeError(runtime.store.getState(), `send:${threadId}`) + if (error) showToast(error) + }, + [rootModelId, runtime, showToast] + ) + + const handleFork = useCallback( + async ( + selection: SelectionInfo, + hint?: PlacementHint, + question?: string + ) => { + const before = new Set(Object.keys(runtime.store.getState().entities.threadsById)) + const sourceSlotId = slotIdForThread(selection.threadId) + await runtime.commands.forkThread({ + sourceSlotId, + placement: { + maxExpanded, + keepSource: hint?.keepSource, + targetSlotId: hint?.targetId + ? currentSlotIdForThread(runtime.store.getState(), hint.targetId) + : undefined, + }, + sourceThreadId: selection.threadId, + sourceMessageId: selection.msgId, + anchor: { + exactQuote: selection.text, + ...(selection.anchor.position + ? { textPosition: selection.anchor.position } + : {}), + }, + }) + const current = runtime.store.getState() + const created = Object.values(current.entities.threadsById).find( + (thread) => + !before.has(thread.id) && thread.sourceMessageId === selection.msgId + ) + if (!created) { + const error = scopeError(current, `fork:${selection.msgId}`) + showToast(error ?? "开启分支失败,请重试") + return + } + if (question?.trim()) await send(created.id, question.trim()) + if (state.ui.viewMode === "canvas") { + setFocusNode({ id: created.id, n: ++focusSequence.current }) + showToast(`已开启分支 · ${projected.threads[created.id]?.title ?? "新分支"}`) + } else { + flash(created.id) + } + }, + [flash, maxExpanded, projected.threads, runtime, send, showToast, slotIdForThread, state.ui.viewMode] + ) + + if (state.requests.bootstrap.status === "idle" || state.requests.bootstrap.status === "loading") + return ( +
+
对话加载中…
+
+ ) + + if (state.requests.bootstrap.status === "error") + return ( +
+
+ 对话加载失败:{state.requests.bootstrap.error.message} ·{" "} + +
+
+ ) + + const mainHasMessage = rootThread + ? (state.entities.messageIdsByThreadId[rootThread.id]?.length ?? 0) > 0 + : false + const mainSubtitle = + state.entities.project?.customTitle ?? + state.entities.project?.autoTitle ?? + "新对话" + const hintVisible = !hintDismissed && !mainHasMessage + const hintNode = hintVisible ? ( + setHintDismissed(true)} /> + ) : null + const branchCount = Math.max(0, Object.keys(state.entities.threadsById).length - 1) + const markdownCount = state.readModels.artifactSummary?.byKind.markdown ?? 0 + const activeArtifactLoadState = activeArtifactId + ? state.requests.artifactById[activeArtifactId] + : undefined + + const pickSwitcherRow = (row: { id: string }, mode: SwitcherMode) => { + closeSwitcher() + if (mode.kind === "column") { + const slot = state.ui.columnSlots[mode.vpIndex] + if (!slot) return + if (row.id === rootThread?.id) runtime.store.getState().closeColumn(slot.slotId) + else runtime.store.getState().switchColumnThread(slot.slotId, row.id) + flash(row.id) + return + } + openThread(row.id, mode.kind === "subtree" ? mode.rootId : null) + } + + const canvasChat = { + send: (threadId: string, text: string) => void send(threadId, text), + stop: (threadId: string) => { + const id = activeAssistantId(threadId) + if (id) void runtime.commands.stopAssistant(id) + }, + retry: (threadId: string, messageId: string) => + void runtime.commands.regenerateMessage(messageId, rootModelId), + ...messageCommands, + } + + return ( +
+ router.push("/thread-chat/new")} + onToggleTreeList={toggleTreeList} + onOpenHelp={openHelpPanel} + onShowColumns={() => runtime.store.getState().setViewMode("columns")} + onShowCanvas={() => runtime.store.getState().setViewMode("canvas")} + onForceCols={(count) => runtime.store.getState().setForceColumnCount(count)} + onPlacementModeChange={(mode) => { + runtime.store.getState().setPlacementMode(mode) + if (mode === "replace") + for (const slot of runtime.store.getState().ui.columnSlots) + if (slot.folded) + runtime.store.getState().setColumnFolded(slot.slotId, false) + }} + onToggleThreadTree={toggleGlobalSwitcher} + onToggleMarkdown={toggleDrawer} + /> + + {state.ui.viewMode === "columns" ? ( + { + const slot = runtime.store.getState().ui.columnSlots.find( + (candidate) => candidate.threadId === threadId + ) + if (slot) runtime.store.getState().setColumnFolded(slot.slotId, false) + }} + onCommitWidths={(patch) => { + const widthsBySlot: Partial> = {} + for (const [threadId, width] of Object.entries(patch)) + widthsBySlot[slotIdForThread(threadId)] = width + runtime.store.getState().commitColumnWidths(widthsBySlot) + }} + onResetWidths={() => { + const reset: Partial> = { + root: null, + } + for (const slot of runtime.store.getState().ui.columnSlots) + reset[slot.slotId] = null + runtime.store.getState().commitColumnWidths(reset) + }} + renderThread={(threadId, viewportIndex) => { + const slotId = viewportIndex < 0 ? "root" : state.ui.columnSlots[viewportIndex]?.slotId + if (!slotId) return null + const columnView = selectThreadColumnView(state, slotId) + const headerView = selectThreadColumnHeaderView(state, slotId) + const loadingIntro = + columnView.status === "loading" ? ( +
对话加载中…
+ ) : columnView.status === "error" ? ( + + ) : undefined + const busy = Boolean(activeAssistantId(threadId)) + return ( +
+ openThread(target, threadId)} + onOpenArtifact={(artifactId) => { + void runtime.commands.ensureArtifact(artifactId) + openArtifact(artifactId) + }} + onCrumbNav={(target) => { + if (target === rootThread?.id) { + runtime.store.getState().closeColumn(slotId) + } else { + const existing = runtime.store + .getState() + .ui.columnSlots.find( + (candidate) => candidate.threadId === target + ) + if (existing && existing.slotId !== slotId) { + runtime.store.getState().closeColumn(slotId) + runtime.store.getState().focusColumn(existing.slotId) + } else { + runtime.store + .getState() + .switchColumnThread(slotId, target) + } + } + flash(target) + }} + onOpenSwitcher={(button) => + openColumnSwitcher(viewportIndex, button) + } + onOpenSubtree={(button) => openSubtree(threadId, button)} + onCollapse={() => runtime.store.getState().closeColumn(slotId)} + busy={busy} + onRetry={(message) => + void runtime.commands.regenerateMessage(message.id, rootModelId) + } + onStop={() => { + const id = activeAssistantId(threadId) + if (id) void runtime.commands.stopAssistant(id) + }} + composerPrefill={ + projected.threads[threadId]?.messages.length === 0 + ? kickoffQuestion(projected.threads[threadId]?.anchorText ?? "") + : undefined + } + onModelChange={setRootModelId} + onSend={(text) => void send(threadId, text)} + messageActionState={actionView} + messageCommands={messageCommands} + /> +
+ ) + }} + /> + ) : ( + { + runtime.store.getState().setViewMode("columns") + openThread(threadId) + }} + onOpenArtifact={(artifactId) => { + void runtime.commands.ensureArtifact(artifactId) + openArtifact(artifactId) + }} + /> + )} + + + void handleFork(selection, hint, question) + } + slots={state.ui.viewMode === "canvas" ? EMPTY_SLOTS : slots} + mode={state.ui.placementMode} + maxExpanded={maxExpanded} + lastActiveOf={(threadId) => projected.threads[threadId]?.lastActive ?? 0} + /> + + {treeList && ( + + )} + {switcher && ( + + )} + {helpPanel && ( + + )} + { + void runtime.commands.ensureArtifact(artifactId) + setActiveArtifactId(artifactId) + }} + onLocate={(threadId) => openThread(threadId)} + loadState={ + activeArtifactLoadState?.status === "error" + ? { + status: "error", + message: activeArtifactLoadState.error.message, + onRetry: () => { + if (activeArtifactId) + void runtime.commands.ensureArtifact(activeArtifactId) + }, + } + : activeArtifactLoadState + } + /> + +
+ ) +} diff --git a/app/thread-chat/normalized/use-project-workbench.ts b/app/thread-chat/normalized/use-project-workbench.ts new file mode 100644 index 00000000..aea02151 --- /dev/null +++ b/app/thread-chat/normalized/use-project-workbench.ts @@ -0,0 +1,51 @@ +"use client" + +import { useEffect, useRef } from "react" +import { + createWorkbenchSnapshot, + parseWorkbenchSnapshot, + projectWorkbenchStorageKey, +} from "@/lib/thread-chat/client/workbench-persistence" +import type { ThreadChatProjectRuntime } from "@/lib/thread-chat/client/types" + +const SAVE_DELAY_MS = 180 + +export function useProjectWorkbench( + runtime: ThreadChatProjectRuntime, + bootstrapReady: boolean +) { + const restored = useRef(false) + + useEffect(() => { + if (!bootstrapReady || restored.current) return + restored.current = true + const snapshot = parseWorkbenchSnapshot( + window.localStorage.getItem(projectWorkbenchStorageKey(runtime.projectId)) + ) + if (snapshot) runtime.store.getState().restoreWorkbenchSnapshot(snapshot) + else runtime.store.getState().resetWorkbenchToDefault() + }, [bootstrapReady, runtime]) + + useEffect(() => { + if (!bootstrapReady || !restored.current) return + let timer: number | null = null + const save = () => { + timer = null + window.localStorage.setItem( + projectWorkbenchStorageKey(runtime.projectId), + JSON.stringify(createWorkbenchSnapshot(runtime.store.getState())) + ) + } + const unsubscribe = runtime.store.subscribe(() => { + if (timer !== null) window.clearTimeout(timer) + timer = window.setTimeout(save, SAVE_DELAY_MS) + }) + return () => { + unsubscribe() + if (timer !== null) { + window.clearTimeout(timer) + save() + } + } + }, [bootstrapReady, runtime]) +} diff --git a/app/thread-chat/orchestration/artifacts/artifact-drawer.tsx b/app/thread-chat/orchestration/artifacts/artifact-drawer.tsx index ed2e51bc..4b53debc 100644 --- a/app/thread-chat/orchestration/artifacts/artifact-drawer.tsx +++ b/app/thread-chat/orchestration/artifacts/artifact-drawer.tsx @@ -24,6 +24,9 @@ export interface ArtifactDrawerProps { onSelect: (id: string) => void /** 定位来源会话(壳层用 openBranchUI 打开) */ onLocate: (threadId: string, sourceMessageId: string) => void + loadState?: + | { status: "idle" | "loading" | "ready" } + | { status: "error"; message: string; onRetry: () => void } } export function ArtifactDrawer({ @@ -33,6 +36,7 @@ export function ArtifactDrawer({ onClose, onSelect, onLocate, + loadState, }: ArtifactDrawerProps) { const titleId = useId() const closeButtonRef = useRef(null) @@ -123,20 +127,28 @@ export function ArtifactDrawer({ )}
+ {a && loadState?.status === "loading" && ( +
Markdown 加载中…
+ )} + {a && loadState?.status === "error" && ( + + )} {!a && (
还没有 Markdown——在主线或分支里生成后会出现在这里。
)} - {a && a.kind === "code" &&
{a.content}
} - {a && a.kind === "note" && ( + {a && loadState?.status !== "loading" && loadState?.status !== "error" && a.kind === "code" &&
{a.content}
} + {a && loadState?.status !== "loading" && loadState?.status !== "error" && a.kind === "note" && (
{a.content.split("\n\n").map((p, i) => (

{p}

))}
)} - {a && a.kind === "markdown" && ( + {a && loadState?.status !== "loading" && loadState?.status !== "error" && a.kind === "markdown" && (
diff --git a/app/thread-chat/orchestration/canvas/use-canvas-layout.ts b/app/thread-chat/orchestration/canvas/use-canvas-layout.ts index e7829360..76ed5de8 100644 --- a/app/thread-chat/orchestration/canvas/use-canvas-layout.ts +++ b/app/thread-chat/orchestration/canvas/use-canvas-layout.ts @@ -48,6 +48,8 @@ import { canvasLayoutPositions } from "./canvas-layout" */ export interface CanvasViewState { pins: ReadonlyMap + /** 新 Runtime 可把 pin 变更提交到 Project Store;旧壳层不传时仍保留原行为。 */ + onPinsChange?(pins: ReadonlyMap): void } /** 写宿主镜像:对长寿对象的突变收敛在非 React 代码里(与 core/store 同一约定) */ @@ -56,6 +58,7 @@ function persistPins( pins: ReadonlyMap ): void { host.pins = pins + host.onPinsChange?.(pins) } /* ---------------- 卡片尺寸估算(精确尺寸与 CSS 共用 canvas-card-dimensions) ---------------- */ @@ -166,9 +169,9 @@ function buildBaseGraph( ) : undefined const data: CanvasCardData = { - isMain: t.id === "main", + isMain: t.parentId === null, title: t.title, - subtitle: t.id === "main" ? mainSubtitle : null, + subtitle: t.parentId === null ? mainSubtitle : null, depth: t.depth, footnote: t.footnote, anchor: t.anchorText ? clip(t.anchorText, 40) : null, @@ -229,7 +232,10 @@ function buildBaseGraph( } t.children.forEach(walk) } - walk("main") + const root = Object.values(state.threads).find( + (thread) => thread.parentId === null + ) + if (root) walk(root.id) return { nodes, edges, sizes } } diff --git a/app/thread-chat/orchestration/columns/thread-columns.tsx b/app/thread-chat/orchestration/columns/thread-columns.tsx index 5016621e..c46ed743 100644 --- a/app/thread-chat/orchestration/columns/thread-columns.tsx +++ b/app/thread-chat/orchestration/columns/thread-columns.tsx @@ -33,7 +33,7 @@ function ColumnShell({ width?: number children: React.ReactNode }) { - const isMain = thread.id === "main" + const isMain = thread.parentId === null return (
thread.parentId === null + ) // 展平为渲染单元(主线 + 各槽位),在相邻两个「展开列」之间插入分割线 const cells: { thread: Thread; folded: boolean; vpIndex: number }[] = [] diff --git a/app/thread-chat/orchestration/navigation/thread-switcher-panel.tsx b/app/thread-chat/orchestration/navigation/thread-switcher-panel.tsx index e1cb70d7..ebd580d4 100644 --- a/app/thread-chat/orchestration/navigation/thread-switcher-panel.tsx +++ b/app/thread-chat/orchestration/navigation/thread-switcher-panel.tsx @@ -61,7 +61,7 @@ export function ThreadSwitcherPanel({ }, [hi]) const statusOf = (id: string): { label: string } | null => { - if (id === "main") return { label: "锚定" } + if (state.threads[id]?.parentId === null) return { label: "锚定" } const index = slots.findIndex((slot) => slot.id === id) if (index < 0) return null return { label: slots[index].folded ? "细条" : `第 ${index + 2} 列` } diff --git a/app/thread-chat/tree-redirect.tsx b/app/thread-chat/tree-redirect.tsx index 9a913c71..db4d2933 100644 --- a/app/thread-chat/tree-redirect.tsx +++ b/app/thread-chat/tree-redirect.tsx @@ -9,14 +9,27 @@ import { useEffect } from "react" import { useRouter } from "next/navigation" import "./thread-chat.css" -import { getLastTreeId } from "./net/persistence/persist" +import { useThreadChatAppRuntime } from "@/lib/thread-chat/client/providers" +import { threadChatRoutes } from "@/lib/thread-chat/api/routes" export function TreeRedirect() { const router = useRouter() + const runtime = useThreadChatAppRuntime() useEffect(() => { - const id = getLastTreeId() ?? crypto.randomUUID() - router.replace(`/thread-chat/${id}`) - }, [router]) + let cancelled = false + void runtime.commands.loadProjectCatalog({ reset: true }).then(() => { + if (cancelled) return + const projectId = runtime.appStore.getState().catalog.orderedProjectIds[0] + router.replace( + projectId + ? threadChatRoutes.project(projectId) + : threadChatRoutes.newProject() + ) + }) + return () => { + cancelled = true + } + }, [router, runtime]) return (
正在打开对话…
diff --git a/lib/thread-chat/api/json-transport.ts b/lib/thread-chat/api/json-transport.ts index 7b56eddc..5814bdae 100644 --- a/lib/thread-chat/api/json-transport.ts +++ b/lib/thread-chat/api/json-transport.ts @@ -28,7 +28,7 @@ type RequestOptions = { export class JsonThreadChatTransport implements ThreadChatApiCapabilities { constructor( - private readonly fetcher: typeof fetch = fetch, + private readonly fetcher: typeof fetch = globalThis.fetch.bind(globalThis), private readonly baseUrl = "" ) {} diff --git a/lib/thread-chat/client/commands.ts b/lib/thread-chat/client/commands.ts index bc22f2da..a867aa92 100644 --- a/lib/thread-chat/client/commands.ts +++ b/lib/thread-chat/client/commands.ts @@ -200,7 +200,11 @@ export function createThreadChatProjectCommands(input: { input.store.getState().applyThreadCreated(result.thread) input.store .getState() - .openThread(result.thread.id, command.sourceSlotId) + .openThread( + result.thread.id, + command.sourceSlotId, + command.placement + ) }, }) }, diff --git a/lib/thread-chat/client/project-store.ts b/lib/thread-chat/client/project-store.ts index df61981e..0ec00321 100644 --- a/lib/thread-chat/client/project-store.ts +++ b/lib/thread-chat/client/project-store.ts @@ -683,7 +683,7 @@ export function createThreadChatProjectStore(input: { }, })) }, - openThread(threadId, sourceSlotId) { + openThread(threadId, sourceSlotId, placement) { set((state) => { const thread = state.entities.threadsById[threadId] clientInvariant( @@ -694,12 +694,46 @@ export function createThreadChatProjectStore(input: { const existingIndex = state.ui.columnSlots.findIndex( (slot) => slot.threadId === threadId ) + const maxExpanded = Math.max( + 0, + placement?.maxExpanded ?? + (state.ui.forceColumnCount === null + ? Number.POSITIVE_INFINITY + : state.ui.forceColumnCount - 1) + ) const clock = state.ui.activationClock + 1 if (existingIndex >= 0) { - const slots = state.ui.columnSlots.map((slot, index) => + let slots = state.ui.columnSlots.map((slot, index) => index === existingIndex ? { ...slot, folded: false } : slot ) const slotId = slots[existingIndex].slotId + if ( + state.ui.placementMode === "fold" && + slots.filter((slot) => !slot.folded).length > maxExpanded + ) { + const candidates = slots + .filter( + (slot) => + !slot.folded && + slot.slotId !== slotId && + slot.slotId !== sourceSlotId + ) + .toSorted( + (left, right) => + (state.ui.lastActivatedOrderBySlotId[left.slotId] ?? 0) - + (state.ui.lastActivatedOrderBySlotId[right.slotId] ?? 0) + ) + const fallback = slots.find( + (slot) => !slot.folded && slot.slotId !== slotId + ) + const foldTarget = candidates[0] ?? fallback + if (foldTarget) + slots = slots.map((slot) => + slot.slotId === foldTarget.slotId + ? { ...slot, folded: true } + : slot + ) + } return { ui: { ...state.ui, @@ -715,50 +749,93 @@ export function createThreadChatProjectStore(input: { } let slots = [...state.ui.columnSlots] - const expanded = slots.filter((slot) => !slot.folded) - const limit = state.ui.forceColumnCount - if (limit !== null && expanded.length + 1 >= limit) { - const candidate = expanded.toSorted( + const sourceIndex = + sourceSlotId === "root" + ? -1 + : slots.findIndex((slot) => slot.slotId === sourceSlotId) + const expandedSlots = () => slots.filter((slot) => !slot.folded) + const lru = (pool: readonly ThreadColumnSlot[]) => + pool.toSorted( (left, right) => (state.ui.lastActivatedOrderBySlotId[left.slotId] ?? 0) - (state.ui.lastActivatedOrderBySlotId[right.slotId] ?? 0) )[0] - if (candidate && state.ui.placementMode === "replace") { - const candidateIndex = slots.findIndex( - (slot) => slot.slotId === candidate.slotId - ) - slots[candidateIndex] = { ...candidate, threadId, folded: false } - return { - ui: { - ...state.ui, - columnSlots: slots, - focusedSlotId: candidate.slotId, - activationClock: clock, - lastActivatedOrderBySlotId: { - ...state.ui.lastActivatedOrderBySlotId, - [candidate.slotId]: clock, - }, - }, - } + const finish = (slotId: string) => ({ + ui: { + ...state.ui, + columnSlots: slots, + focusedSlotId: slotId, + activationClock: clock, + lastActivatedOrderBySlotId: { + ...state.ui.lastActivatedOrderBySlotId, + [slotId]: clock, + }, + }, + }) + const replaceSlot = (slotId: string) => { + slots = slots.map((slot) => + slot.slotId === slotId + ? { ...slot, threadId, folded: false } + : slot + ) + return finish(slotId) + } + + const target = placement?.targetSlotId + ? slots.find((slot) => slot.slotId === placement.targetSlotId) + : undefined + if (target && state.ui.placementMode === "replace") + return replaceSlot(target.slotId) + + if ( + state.ui.placementMode === "replace" && + expandedSlots().length >= maxExpanded + ) { + let candidate = target + if (!candidate && placement?.keepSource) { + candidate = slots[sourceIndex + 1] + if (!candidate) + candidate = lru( + expandedSlots().filter( + (slot) => slot.slotId !== sourceSlotId + ) + ) } - if (candidate) - slots = slots.map((slot) => - slot.slotId === candidate.slotId - ? { ...slot, folded: true } - : slot - ) + if (!candidate && sourceSlotId !== "root") + candidate = slots.find((slot) => slot.slotId === sourceSlotId) + candidate ??= lru(expandedSlots()) + if (candidate) return replaceSlot(candidate.slotId) } + const slotId = generateSlotId() - const sourceIndex = - sourceSlotId === "root" - ? -1 - : slots.findIndex((slot) => slot.slotId === sourceSlotId) - slots.splice(sourceIndex + 1, 0, { + const insertAt = placement?.keepSource ? sourceIndex + 1 : slots.length + slots.splice(insertAt, 0, { slotId, threadId, folded: false, widthPx: null, }) + + if (state.ui.placementMode === "fold") { + let foldTarget = target + if (!foldTarget && expandedSlots().length > maxExpanded) { + const preferred = expandedSlots().filter( + (slot) => + slot.slotId !== slotId && slot.slotId !== sourceSlotId + ) + foldTarget = lru( + preferred.length + ? preferred + : expandedSlots().filter((slot) => slot.slotId !== slotId) + ) + } + if (foldTarget && foldTarget.slotId !== slotId) + slots = slots.map((slot) => + slot.slotId === foldTarget.slotId + ? { ...slot, folded: true } + : slot + ) + } return { ui: { ...state.ui, @@ -781,21 +858,24 @@ export function createThreadChatProjectStore(input: { thread.parentThreadId !== null, "Column target must be a Branch in this Project." ) - clientInvariant( - !state.ui.columnSlots.some( - (slot) => slot.slotId !== slotId && slot.threadId === threadId - ), - "Thread is already open in another column." + const source = state.ui.columnSlots.find( + (slot) => slot.slotId === slotId + ) + clientInvariant(source, "Column Slot does not exist.") + const duplicate = state.ui.columnSlots.find( + (slot) => slot.slotId !== slotId && slot.threadId === threadId ) const clock = state.ui.activationClock + 1 return { ui: { ...state.ui, - columnSlots: state.ui.columnSlots.map((slot) => - slot.slotId === slotId - ? { ...slot, threadId, folded: false } - : slot - ), + columnSlots: state.ui.columnSlots.map((slot) => { + if (slot.slotId === slotId) + return { ...slot, threadId, folded: false } + if (duplicate?.slotId === slot.slotId) + return { ...slot, threadId: source.threadId } + return slot + }), focusedSlotId: slotId, activationClock: clock, lastActivatedOrderBySlotId: { diff --git a/lib/thread-chat/client/types.ts b/lib/thread-chat/client/types.ts index e173745a..c4810b3d 100644 --- a/lib/thread-chat/client/types.ts +++ b/lib/thread-chat/client/types.ts @@ -194,6 +194,12 @@ export interface ThreadWorkbenchSnapshotV1 { canvasPins: Record } +export interface ThreadPlacementOptions { + maxExpanded?: number + keepSource?: boolean + targetSlotId?: ColumnSlotId +} + export interface ThreadChatProjectActions { mergeCreationBundle(bundle: CreationBundle): void mergeBootstrap(bootstrap: ProjectBootstrap): void @@ -217,7 +223,11 @@ export interface ThreadChatProjectActions { setArtifactLoadState(artifactId: ArtifactId, state: LoadState): void restoreWorkbenchSnapshot(snapshot: ThreadWorkbenchSnapshotV1): void resetWorkbenchToDefault(): void - openThread(threadId: ThreadId, sourceSlotId: "root" | ColumnSlotId): void + openThread( + threadId: ThreadId, + sourceSlotId: "root" | ColumnSlotId, + placement?: ThreadPlacementOptions + ): void switchColumnThread(slotId: ColumnSlotId, threadId: ThreadId): void closeColumn(slotId: ColumnSlotId): void setColumnFolded(slotId: ColumnSlotId, folded: boolean): void @@ -275,6 +285,7 @@ export interface ThreadChatProjectCommands { ): Promise forkThread(input: { sourceSlotId: "root" | ColumnSlotId + placement?: ThreadPlacementOptions sourceThreadId: ThreadId sourceMessageId: MessageId anchor?: { diff --git a/lib/thread-chat/client/workbench-persistence.ts b/lib/thread-chat/client/workbench-persistence.ts new file mode 100644 index 00000000..677e246f --- /dev/null +++ b/lib/thread-chat/client/workbench-persistence.ts @@ -0,0 +1,50 @@ +import type { + ThreadChatProjectStore, + ThreadWorkbenchSnapshotV1, +} from "./types" + +const WORKBENCH_STORAGE_PREFIX = "thread-chat:project-workbench:v1:" + +export function projectWorkbenchStorageKey(projectId: string) { + return `${WORKBENCH_STORAGE_PREFIX}${projectId}` +} + +export function createWorkbenchSnapshot( + state: ThreadChatProjectStore +): ThreadWorkbenchSnapshotV1 { + return { + schemaVersion: 1, + columnSlots: structuredClone(state.ui.columnSlots), + focusedSlotId: state.ui.focusedSlotId ?? "root", + rootColumnWidthPx: state.ui.rootColumnWidthPx, + forceColumnCount: state.ui.forceColumnCount, + placementMode: state.ui.placementMode, + viewMode: state.ui.viewMode, + canvasPins: structuredClone(state.ui.canvasPins), + } +} + +export function parseWorkbenchSnapshot( + value: string | null +): ThreadWorkbenchSnapshotV1 | null { + if (!value) return null + try { + const parsed = JSON.parse(value) as Partial + if ( + parsed.schemaVersion !== 1 || + !Array.isArray(parsed.columnSlots) || + (parsed.focusedSlotId !== "root" && + typeof parsed.focusedSlotId !== "string") || + !Object.prototype.hasOwnProperty.call(parsed, "rootColumnWidthPx") || + !Object.prototype.hasOwnProperty.call(parsed, "forceColumnCount") || + (parsed.placementMode !== "replace" && parsed.placementMode !== "fold") || + (parsed.viewMode !== "columns" && parsed.viewMode !== "canvas") || + typeof parsed.canvasPins !== "object" || + parsed.canvasPins === null + ) + return null + return parsed as ThreadWorkbenchSnapshotV1 + } catch { + return null + } +} diff --git a/lib/thread-chat/domain/selectors.ts b/lib/thread-chat/domain/selectors.ts index 7c2c61ee..e57655f4 100644 --- a/lib/thread-chat/domain/selectors.ts +++ b/lib/thread-chat/domain/selectors.ts @@ -111,7 +111,7 @@ function rowOf(t: Thread, relDepth: number): TreeRow { id: t.id, depth: t.depth, relDepth, - isMain: t.id === "main", + isMain: t.parentId === null, title: t.title, footnote: t.footnote, anchor: t.anchorText, @@ -120,7 +120,10 @@ function rowOf(t: Thread, relDepth: number): TreeRow { /** 整棵树的先序遍历 rows(⌘K / 每列 ⇄ 切换器用) */ export function allTreeRows(state: ThreadTreeState): TreeRow[] { - return subtreeRowsInner(state, "main", 0, true) + const root = Object.values(state.threads).find( + (thread) => thread.parentId === null + ) + return root ? subtreeRowsInner(state, root.id, 0, true) : [] } /** 以 rootId 为根的整棵子树 rows(不含根自身,relDepth 从 0 起),子树弹层用 */ diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md index e1d802e0..7db4681a 100644 --- a/openspec/changes/design-thread-chat-client-api/tasks.md +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -79,14 +79,14 @@ ## 8. 建立 UI Parity 基线并无损接入现有 UI -- [ ] 8.1 在修改 UI 数据接缝前,使用 Ego Browser 和专用本地测试账号记录现有空白页、单列、多栏、Header、Fork、Artifact Drawer、折叠/切换、breadcrumb 与分割线拖拽的参考截图和交互清单。 -- [ ] 8.2 保持现有组件、CSS 类名、布局与交互输出,先只把 Project 列表、`/new` 和已有 Project 页面接到新的 Provider/Runtime。 -- [ ] 8.3 将 Root/Branch Column 改为消费 ThreadColumnView 和 ThreadColumnHeaderView;允许内部拆分复用,但最终呈现不得变化。 -- [ ] 8.4 接入现有 Header 的 Child 选择、Thread 切换、收起和 breadcrumb,保持稳定物理 Slot 与列宽。 -- [ ] 8.5 保留相邻列分割线拖拽;Pointer Move 使用组件瞬时状态,Pointer Up/键盘/双击复位只提交一次 Store Action。 -- [ ] 8.6 实现按 projectId 的 Workbench Snapshot 防抖保存和刷新恢复;恢复多栏、折叠、焦点和列宽,不恢复滚动条或 Composer 草稿。 -- [ ] 8.7 接入 Artifact Drawer 按 `artifactId` 加载及独立 loading/error;生成期间禁用 Fork,服务端仍作最终校验。 -- [ ] 8.8 逐项对照阶段 8.1 的截图和交互清单;任何必须产生用户可见差异的实现立即停止,记录影响并向用户确认后才能继续。 +- [x] 8.1 在修改 UI 数据接缝前,使用 Ego Browser 和专用本地测试账号记录现有空白页、单列、多栏、Header、Fork、Artifact Drawer、折叠/切换、breadcrumb 与分割线拖拽的参考截图和交互清单。 +- [x] 8.2 保持现有组件、CSS 类名、布局与交互输出,先只把 Project 列表、`/new` 和已有 Project 页面接到新的 Provider/Runtime。 +- [x] 8.3 将 Root/Branch Column 改为消费 ThreadColumnView 和 ThreadColumnHeaderView;允许内部拆分复用,但最终呈现不得变化。 +- [x] 8.4 接入现有 Header 的 Child 选择、Thread 切换、收起和 breadcrumb,保持稳定物理 Slot 与列宽。 +- [x] 8.5 保留相邻列分割线拖拽;Pointer Move 使用组件瞬时状态,Pointer Up/键盘/双击复位只提交一次 Store Action。 +- [x] 8.6 实现按 projectId 的 Workbench Snapshot 防抖保存和刷新恢复;恢复多栏、折叠、焦点和列宽,不恢复滚动条或 Composer 草稿。 +- [x] 8.7 接入 Artifact Drawer 按 `artifactId` 加载及独立 loading/error;生成期间禁用 Fork,服务端仍作最终校验。 +- [x] 8.8 逐项对照阶段 8.1 的截图和交互清单;任何必须产生用户可见差异的实现立即停止,记录影响并向用户确认后才能继续。 ## 9. 前后端集成与 Ego Browser E2E diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md new file mode 100644 index 00000000..c8e1cf0e --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md @@ -0,0 +1,86 @@ +# Thread Chat UI parity baseline + +本目录是在客户端数据接缝修改前,通过 Ego Browser 任务空间 `11` +(`thread-chat-ui-parity`)记录的旧 UI 参考。后续阶段 8.8 与 9.7 必须以这里的截图和 +交互清单为准;除已批准的 Project loading 外,不得把实现差异解释为新的设计。 + +## 采集环境 + +- 日期:2026-08-25 +- viewport:`1674 × 963` CSS px +- 专用本地账号:`thread-chat-e2e-20260825@example.com` +- 数据库:allowlist 保护的独立 `thread-chat-test` PostgreSQL +- Fixture:一条 Main、两层嵌套 Fork、一个 Markdown Artifact;不调用真实模型 +- 旧路由:`/thread-chat/{treeId}` +- 旧 UI 的历史 Message 省略 `status`,沿用“缺省即完成”的既有语义;没有伪造 + generation sidecar,也没有建立新旧双写 + +## 参考截图 + +| 文件 | 状态与必须保持的输出 | +| --- | --- | +| `empty-project.png` | 空白 Project:单个 Main 列、固定 Header、底部居中 Composer | +| `single-column.png` | Main 消息、Artifact 卡片、Header 计数,正文宽度与留白 | +| `header-child-selector.png` | Header Child 选择器;列出直接和嵌套后代,显示脚注号 | +| `two-columns.png` | Main + 第一层 Fork;相邻列、breadcrumb、引用与继承上下文 | +| `three-columns-nested-fork.png` | Main + Fork + 嵌套 Fork;两个相邻分割线 | +| `artifact-drawer.png` | Markdown Drawer 打开态;右半屏预览、来源与定位操作 | +| `divider-dragged.png` | 第一条分割线向右拖动 110px 后的局部列宽变化 | +| `collapsed-column.png` | 收起末端 Fork 后保留 Main + 上游 Fork,并重新均分空间 | +| `thread-switcher.png` | 当前物理 Slot 的 Thread 切换器、搜索框和“本列”标记 | +| `switched-slot.png` | 同一第二 Slot 从第一层 Fork 切换到嵌套 Fork | +| `breadcrumb-back.png` | 点击上游 breadcrumb 后,同一 Slot 回到第一层 Fork | +| `fork-composer.png` | 划选 assistant 原文后的 Fork Composer 与放置目标 | + +## 固定交互清单 + +### Header 与基本布局 + +1. 顶栏顺序保持为:新对话、对话列表、产品名、使用提示、列/画布、列数、列满放置 + 策略、会话树、Markdown、账号。 +2. Main Header 显示“锚定”、标题、副标题和 Child 数量;Branch Header 显示层级、标题、 + Child、`⇄ 切换`、`收起`,其上方保留 breadcrumb。 +3. Composer 固定在每列底部;Main placeholder 为“继续在主线提问…”,Branch 为 + “在这个分支里追问…”。 +4. 1674px viewport 下三栏初始宽度约为 `558.33 / 558.34 / 557.34px`。 + +### Fork 与多栏 + +1. 划选 assistant 文本后,原文保持浏览器选择高亮,并打开“在新分支中讨论这段” + Composer;问题可留空,放置目标显示现有物理 Slot 与右侧新增 `+`。 +2. Main Header 的 Child 选择器同时显示第一层和嵌套后代;点击第一层 Fork 在来源列紧邻 + 右侧打开,不改变 Main Slot。 +3. 从第一层 Branch 的 Child 选择器打开嵌套 Fork 后形成三栏;嵌套列的 breadcrumb 为 + `Main › 第一层 › 当前 Thread`。 +4. 点击 Branch 的“收起”会移除该物理 Slot,并让剩余列重新分配可用宽度。 + +### 切换与 breadcrumb + +1. `⇄ 切换`打开当前 Slot 的 Thread 选择器;选择器列出 Main 和全部 Branch,当前内容显示 + “本列”,点击目标只替换该 Slot。 +2. 第二 Slot 从第一层切到嵌套 Fork 后仍保持两栏,各约半宽;不会额外打开第三栏。 +3. 点击嵌套 Fork breadcrumb 中的第一层标题,会在同一 Slot 回退到第一层并短暂使用 + `flash` 状态提示定位。 + +### 分割线 + +1. 分割线使用 `role="separator"`,可由鼠标、键盘操作,且 aria-label 明确相邻 Thread。 +2. 第一条分割线从 `x≈558` 拖到 `x≈668` 后,列宽变为 + `668.33 / 448.34 / 557.34px`:只改变相邻两列,第三列不变。 +3. 双击该分割线恢复三栏均分。 + +### Artifact Drawer + +1. Header 的 Markdown 按钮显示 Artifact 数量;消息中的 Artifact 卡片可打开同一 Drawer。 +2. 在本 viewport 下 Drawer 从 `x=837` 开始,宽 `837px`,覆盖右半屏;左侧工作区保持原布局 + 状态而不是销毁。 +3. Drawer 显示 Artifact 标签、Markdown 正文、来源 Thread 和“定位来源会话”;关闭后恢复 + 原多栏视图。 + +## Parity 验收规则 + +- 后续截图使用相同 viewport、相同 Fixture 语义和相同交互顺序。 +- 对比 Header 控件顺序、列数与宽度、稳定 Slot、breadcrumb、引用卡、Composer、Drawer + 尺寸以及拖拽前后数值。 +- 文案由 Project/Thread 新实体提供时可以改变数据内容,但 CSS 类、空间关系和交互输出必须 + 保持;任何必须造成用户可见差异的实现先停止并向用户确认。 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf66ca8d0f18de6d146d7142ad7800927f88dba GIT binary patch literal 156822 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
1TJfwL@~jMs!ZMwcYi! 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ℜ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 literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png new file mode 100644 index 0000000000000000000000000000000000000000..aebd9c596ca94d71ad0463e966a333b372fb676c GIT binary patch literal 175164 zcmZUa1yEbxy7r+IDPG)NiWe*Hw79z$DDLi19Euh#?h>@PyIXK~cY*~7`sIJlJ#+8< z?o4J+W@pdJ&R$us{GMk;Dl1B%Arm3Pz`&r%NK2~1z##mBfr0BodIvp&0s_jwz+l42 zNQ$X@W}aoie^=AOA2JfOJGr6j5=CcQ_u4ZoTDn{@1Ag z@n+>wTh@~^{#%~?`7LxSAzc3SHH9%An1>xAYq!_M@yxD~5Zw`cqF zRY#@%=N_kd=lDvpE)hpK)p4qfv#X?c~lT5{P=$l{<9EV zp+iQ$Ga)Vb8fk6)BsCygvUEsDf~c+_SJ1>4cJ8{rweEY43jZGdRSpOD;8HG1QoK`~ zmEMri6+w+M_2UFFe{c^sJ7?YB#zrTaldm^?Ti#Y5E%H>l)gY>#`xpYM)DLrzoFT_9 zzTSu`m+uYxd_*b$X~O-eulo~n<1+zT1Z_rMd{$;fp^bMJc$#-4tZ|}-ANr}ptegzR z4AWaeV>$|agsNQr^SHVOAkVcsYZW-0Hd0lgl|i|Sx(@cLd%ajpa(ZfNDEZ6Oi89DH zoX9S-q?tk!WJRsuibf};xHy;>F`_Q6My6EKbS(vk5ubm8CfhEzj8OINe9 zGP8K@*xe&!R>26GBe8nr!Esx%J2x0-v5SFRFtRWVk14v{+nd>2DN(SHoB(E*C#qSy zbaKD{FcHb4%ikbU$?{Ep)ifoN4VdjuMm7VxS=B#wuBE4Ccbhn8)cq-%Hi?$WOildu z$*OdsEwP;^JN1EJ8D!dTg2@S^hu2?l?;hRF&l2rSx3Gf zS7~#BqzbT|G;#U!bia#X@}noC5tf_KnXp)vf>+A>Ygy%z65s(_Wjn{540Rb((tC?N z)$_Py;ZLr(9fwJStIunO+{o)7jpY!5vIwH>1xLZdZ!(Ib+x~lQ3KY1EO$AWRUt1D>OSQtioM*4b3ftPqis*%#__A{CFEh$k9ld{Zs zqckRXdTxSXQWtmQ3?wNSHw zCbhiG=t?ex4H3Rv+S<$b2SxMXTB&~mb757)*ZpO}$e8Ovyicx%ujkk~>!vR9s9g-2 zhKAW<{!-H%I{K% zrp15er=ZBnR3E$~NK5j*QiaMhVkPZIe)Wz23HDDE#l#bPCyfxPCo8L*(~!1<2q?fj0JlL!p;RPkyY)KY zQ^GK{CdTcj)T@dgq3ni6@+Ke-P~f>6Y1m}yUtDtb+w>&G_Nt^uJ$Rfx^@QBl9p3Ii3E zmR5egJvnrh6qPi5u;Vm+MD6qHlnz*|GKeD=ad&ewXmtf_-}(xRFfuX<2(0$>e`%ZGMLL*HZ&Hp zUrfRp96R%E-D=4zByrfdmP^B)!OLK}e)6{{&E8*a`FX+5SZO#|m(7n)`^m{D9LP76 z9d!+r1r0G9*ai;uh;AIHf=M+?wx0E0;=_zI)3cHpFfU27GS5jfQRx>X>L8s7V`lPn zQKk4;k$*niJ2cF`M~t_*7H>0b`~#h^u4J}j4ojz`{gN>9=dw>sh9FhIU*?n1h4G~f z1A1R$`$I8;;Je?|+ax2nvd|S~Z*N#at{`n|ZLO=RIcV~SgMbSTV&f~DQSe>WNKd&u zmq8*O)cRfY%`NLG>K(=oF!XQaClsL3^@9|j?ZONA9>S~BTc>T z*PTay>-;9O*Bkc9#Kx0{!~Ve!3}U{j`g$5_mamdj%*?}bN^-_EOLd1VUk);+Fa19% zC>5Lm{^~pKO_813t_!vEyPn9Zk!~dCynx0WG_lL9S$!Q;KlC~nz^yj;IGj+EK+>%{ zRBJ>5GuJ2YE8a2HwwO=>(eQ3@B!EEqAxl(b3&sYz8MSI_SrF6VWD1mndZz zmOPcp>|&4$3%WeYD?3gl6vvz6l}z@%-Ti~2nf7A#H{mAYhgX`D2=Tm!tJTT6I!+Cr z?b_$r>X=3JEy}A+mH=Fwy%-WshC+?fyZrX{Ow;gt;GYH^O z_}m^zlxZlJd;A3W-7gNvF)NPY#n)YaT-Khs36GGAUJV8_)$5Z!g0jtyJ%M(m?mq

mWI!DpLiYz*G-5hDyKW1uqt^u?Fu?m|GCHB}8F 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

mWI!DpLiYz*G-5hDyKW1uqt^u?Fu?m|GCHB}8F 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ℜ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,J7YOu5erTa{7eG(7Y4d1aE{$ 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 z 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%0DoX!^=}6!|SnxuewHc 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 literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/single-column.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/single-column.png new file mode 100644 index 0000000000000000000000000000000000000000..13c916d495f082bcbf23aa3c6046d7ebe464015c GIT binary patch literal 100598 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%0A%WK}b-uu`bOC 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

JQk+%(Pm z)s>(G@CL4HM=_=xYsa$s4|5^1U->r)@h9!KJo6{C6=MeQ)xv==`{jTz`G}3Nr@b0EI`wA=*<}8@mdhmH%7oO1BBjx3 zOo*MGQX#>I9Z9P1euS{4QIM0bObrD5Fpy8Twy}{2WA%BKsRYEj{G}zcC5oJ!bd^#n z4*k+7jUL<8)I-bu=Fk3qX~<;W5W;9f3Dky*ii-@wb6KR?;ibfG~( zn*-gHI;F)K!cU);uNiriY_Fs%=MhV@d-1MQkjInW*gJj2acWQ2i-cyC#nRs^tBSS(n1ZV@glW@AY zHu~PN-`MDBB9N~CEdBU01E*0Yw8T^#96^pQKn;ynxBI<<{zN$92W6lg2SP@MPEMNN8`9E)?@?asy7G#Of^D zja&B$_sc7^yo^-JZgEh5_-Ny_eGPH}5MB**uPSsX#Z}m zn}GsmztaNManRx($TFzQKji~Wm7*rAE2~viRe@!E&z@CIJmIgZtn^P;ksj(D59(_l zYrNul?aszmzmWYGQa39qr+=aKJU0(GIS-KlhiUl*7ewCRs3M*VKF6LQg*<7lb{xr>hIyxp=2B-0$=(KU_ zzp(W`NEdY9XLrbqTSySQ9P8oYlzw6@)A_kDMm06_@fI)wWr=!kOLtF*1+s4zr89CU z+Q@#$f4*n9ol9?^r1V0!`{R@#!BWSSrU?Fu9|FspYH>u0ii#m@MmN%%GX=%hb&SoH z%)mEwtB>JwniEq{pprM-a_gy+42_C3$RlPm=1}EwvK*zdGchqS2suHol*`JNPS-mf zc++|731R{@uuHIB)3-jyYdu=K+nJz8!C{d1S<>F2cr#Dv9LZG{vy;0}{qa8nmbM&K z!Nf7w@Hgsac}qPH4gCH6)vOy!ui`gdQDy^0I`Sj8#E}uU<7+R*KF0Ze@bK8$8?Z_q zsclg$grJO$oZPy)l@BTpFrAsMl(Utcc!`+zA}cmXgttZ=+dgsbTIP#Q8k)PtGrA(S zc1|JegEzUj46}E?LADjhRO6u|+v}AT-<5I=t8{9=dD`vH`2+>osL3uZSo7+vttjP8 zr>K@G%FA0##V%G<@X030O;g#1^8-*h|6D9rUmgdnteJcSwu3=$hj3WY3u*c8+cfff zuyVGjSXgpmb6Mrh&>lvyIua4Z7Ry_{dUsh^Sb)bvM@ji&tbXKD{i*fjoBZ!kK}9!s z7#PU7owwzJn4mU_DBz`_pg=16WLU4%-`tzc982bPSc@u)w`@Ml8WO>v$Wa2kKaX$q zmi=x{IOle~Kltzaj*X24Ii1-wsB-aq_E7TC#>Vim2=%gxPX_pwOVt^`K^?He^=Dzq zOp@K6>1l`MruQuYxo{z7((^PBow)6v?}H5d=;?(H2~Y+N;d&*F`9-RjKX=4?ne zlR1QHPh@16sbr_n$ucE{V`#=9f+s%QOsAnzHnX_rklbaIVq}O*{7t^B)WRp;(p&oK zJwq*L7k7Q7PEwNQL5*Ul&Vh7cf$eT?lUu!5F|3Mse>sS{V4iQIdUmXOaqap&3&mpH zz3(@xU}F37Wmo~)!6NJCN6wb^ml)M zk}}=rnG$^xP0~U6$noeNamve2GQ+77NB(g2|EB%|_WT=Q&ov7+%8XGN$63P8oL)F{ zc7q`(QKvm{^SX2kC~7r6=j+K7gl5*U~-@o5jYH`dI0i-RhKfU8ZIR%#( zml|=iD>4L5<}e}YtJBr6bn>pwySFYpHcJx? zIepCOjR6c-uljli1Pr9RiHors_GVf%j!*gfgJq21=1o<~8tji|QopaPtSl?*eu3K; zlsOFAT`X4W96EAUu@Bq~-dFcm8Z7!R{n9)b)>|a8V;~7HDp0#; z$Ex+{6VG#psl~-JyAF$MycXj}5#vq4d%H#RZ$-O#oHlu$jq0X7hco9<+4ctq!M&ao zZy9{<+Tq&OWc%^@Vdv)AYqp|+&P-mOmcx}j7Sn7tj*C56^N~%TIdDWYHGOKugTF?& z8y(BM@D|nT<+HxJ+Sb)JytGpH{=_(;5wHKY2$T)-*p0n>e5SjLw)^tZ)76W-y}jw$ zP5rnkCK3|H>6MNjKLH5MXy~q@5Y>mQh5OOcvv@F05c`twm2scPfK@1QtGa^$;o+rY zH;(4h{+%>M8CB~*d|ULX_g5-- zrWn95P%9Z48XWu}SN@sQ*Abz#04J{6Y>gN}!mh^VFmBD6{0}huWnpgTZT=b_V502j zmMx29A0KZhLGJ_hhTemao{mO7L17ZsOhMO8777YlSMf(;J2R67cHQTAOk|*It}{7+ zIzer~8>QU$?JLQq14#7hcO(eIA(d-1P;9b4{E5t^^~YHv<~P&lxDZokuU}W)Qx*?q z%`$5J;^+|K-P_w9FLo9OM^@Hv%A-(!j7hoA1^6G(e)T2g$#TUpdz?r8m`(}7fVaY-D30F zm`-yrXaiN@r&q8ag^^r%ugNN8vsT8tro!{r@r|)yO8J)Ckvr;E6Et#6eYu+J4P&DB zat$cFE@jWRelVbA)9mU`^r^L)|5_1k<_9LOB?6D$uiW;Pq`ZE!t7oy*wLtgx@ZJWtl3D}u|^&08)qHFRB;mhy}>64KJ{%j}N)_@NtYW>)Y* ztEHjAyQzrVAyW&}p7Pdx;OY}}53GOn;)_a#TK=hOIb3j#qXwP(<$H7V26I*43Hy9r z^vO&#!6PAIr1au6+qt~%=K3u#uqO*>x*3HA=rxC}wTRoMrzuEyZP}k~d7J3IT=za0o6?qx-yX4p|33Mb+3AlT)B8yRMMF@(D(x!WAad8s%o#-|h9PtsK4PNw&iu_2go}u)$g$zI4 zmya4L9L8?LbBi?|@WA$-9TzD?=b%aW^<6Xrh>(tX;}EjgtnCngFL-Cu)6*~va~tcCk8j`tazsGIup>ayq?x8WMWy5%MGR>bjbg{+ z3*4G-Qkb{E;7}V;%{0(6GDdv*)KcTtn&n~XWab&l$uttqYjKN_@y^Y;PmQY|ulSFN zH*FMD*FKz_@=Mav*jyP-P4ZuL3}Azt(c3ytbvxdu)te0p}oygZ(4&PM*#EoR!gji|cD;*WUi78D3S zNfI?=Ba*^3{FazypFE?d+^ij1$$cY#BH{gQhI2SeXK-ZSeDl1-Fc-FA-n6dhO!ll6V&RmtyxQC=jwN?ox<`<6Aa9HlLH? z8SY-FGQ*SdzBDrOj6O+ebJG!-d@C`Ldgm^cu163x5lK zERJJcK31=8(3(9kvd!k}w`@pBNVK6^CeveoOR6RRBH0xDeL}gnRpAEZ<4>%Ur8YM8 zNaXa?hdKq=PhJ55+&UnH62;4~x3O1XgP;Z9Vh;+_|E?YoufTP!vh!WZ#LZv1fX(J) z9TmI#f?AKD;xW|G;dTV;vVp?l6UJM#w630HRs5xnj@|h-pN~>XRiyNuVMF(>hLk$Z zYPaXJ1Y{@ZkL`}Z*%$5M;b<#(P4U&D>75AP(lmp1x(H#}oRm6+WjEsW955y7YHorg;PTl3dOC~I>sXTre5%t6-hiF6)vNDm@n(g^GBq1^h zVou%>T_&wkOQ~s&dRrqyN^JTy#|3Ns+}vu$OLIMVc1k7fvRhmhx#a&b)4k<}oV%&N9g@GBSR?DE4f%zaj%G?95?-k3J>%z?uE4Yn(Su z?A2yyDw@3EvXNbaDC)>2*U{HIoZ~}F4ZD0VBdpNjRaB*x!;kg&HI17Rap(Jai!J8c zhJbSkl$F{3#p^UQ`VBNmweeRv^}8g+g-cB*b@NJN5@WkRv+*4%Wp!6p=gN^6R#a3~ zZA>zb`RY~UFG7g} z@-=NwhL7GfoAJDfc0N`%P2hX#3=W;9u9yiPo*&XpRLrf+YB$^T_!-CN$=~k}rK=IA z_O7=yG=V2J+i#GM>Xwuw1~qD%mr21}2J7!EjDiEG4eaN~6@w#Pv_HgFTRV0kg-?kz z&_Iur8p%*chMpjc2E~bDu-aHy^{z9V8B%3C-)4&P|H!TGPyavUNWwvmgxX(ouMhU$ z-M9R&q#2Y;A4HZ}MMhZlnBnD%+1e_ZNj-Zj()s#Wsi?n8Epd*0rhE5Jw0+X}bm8XC`=zLgy{!{2 zC*s^+A9E-;TM%KI@#Ki(5GmOMYRMq5*YDODLgZ4Cq7)x!zYbQSxOC}lNoz&A|C*sKtYch1{Yxf&zmQvqikkLrSx7{1 z{QbzJc)oMjhHzNf7mD}@$XG1gQ&PmzIB9}R!7br&no>y>pPFjjh z>y=O9`n$TvOM?GOh#%1IG&I`Yvo$-nbj_HXBd8Tiv>ldeNvrtt^D17I@*UwJ87lZp zPWeezey}4nUb8#NKwrOMmX`9YyBM?%LqiI!d~#UOr^Trr=M&<6!V>O8+$7uF}l2W3RKC8QoAMSvizd+sQ71fpB(pr0D ze~Rm<{H27t0MkXIiFeA51)me2r91=D$;lrJgD)j8;AraHni_*j4!m~$`g8swTlbr}Tm4ZoM+DKmEe7#nbO{GRz2`_c`o1rXGz0Uu z2)_wSC1mlv0u%czcBZANUS_+!4i2)rkV4v=DsaX)uCiqx;{912l#efwE%BUm{aKg8{4H*Dm5ptcG7-Z@zZO*3ExkZ`A^cE7RP}A4_6S2o6PKu`a{>%zvku zsI+n~@1WB2QUz0BI%y~S$11sdNBL>^g*rozr5SZSZpw`d8&2!m&@jJ$5hEy8Mk2Rz z@tj8GA1QFAMfES}%YMFab2oThTFQ?WknX=H#42|>e`I;YnEsbg;KT&{SVw0E6jL^M zZ-M8}N33}U3(JxB2j_2H$vCIst;gc3C9$~@VlRA$tW5Uj2glrKG56Q$n2oteL(HL+^>-d-C9w_QF zpI}bCp;bmv(GNbNlFuGj?uIGv?_Rz|C!W0!&vVSobbTh}-y`pu*ZS|d$jgDGCPHz?QBet(q;U-k_Me4Vd3`iIX$4j{&q7vVA>Zzdbvq+a@1 z=80_T<3tgC5vd3EVumjsDZFP4@q_9n=%7kSs0k|y2#8nJe|w-P;EuWae{GJD%`N9T zkTg@Q7|KPnPbA^tr(u>XR@uqMf7^<7Vj$(<-#?!EzrlU{@w9Zv8~5dvP?+R643<%D zOQdu#^0IC-F#PeH_wW6yUS=v9^fJd8d43Irsr{pviS>_SCT0cI{4ZC`{8#nb*wcSS zmtOkA{lk~~U0VMw-U@mA6-HjUgg<_8|5jg-(%D~1r>uYW zC;TUZzW?S#(8&B#J8!RU(4o9eR?UXQNw8#Hn?)38&t~%j`N!f5U%DTmjEeWs%IdKa z?Tt6IWP|@ACqMt_##J%K6Y~9b^8=Zqw-mio2m2gQP1hxT1j(YFH7{%nZaI0hsJIJ~ zZZ3@W+eIuA1v5oB=cMgG1L)zas7&r02q&O@eM-gKk^UuU+19fp{lrO$OY>y3}#*)j1e^ZDzcW|`}u1>@s#@I$y7 zw;g_7)SRCi8s6c0Gq9;}_|{i(?+{Ek7uTCrqLfT==ZY8ee_^ih;8J&urwkajMY86& zb)2{kzxcUE4ux&M2yL-nKjW|!He(>+DIe^ho;4q${k&ngkfYhY_}M%Me30jV{LxH< zvV1Voe()N&n=_u7?)&H6)orjp*HyR_19}Muuem)Xj+N`{;nEVMqT-&vasg}Mce{T+ zwX^Y(KQFMvdyxnZgLhvzNkk9dDik&|2Mt(wE8JN&SMPSx`R@@Xx1Kv2w6l$%PB$ znJ}#v3+FA-nm^xS6E-$Jj(Mxg{7VBX&WZMtOFthhJmI3m^nw}M=EC*R-NnN%&OwKM zIxFuOeCP1tAFT7tbPjGcy)}pCQj_2c>UY!Q%*%@(OAgkM%YXdQ*7C>yUZ1Fr;Kov^ zKksf7^)iRTbb}|12pqf$w++Txt@`aGIC=oFXny#lZU^%Gyr^}NTFlBPuAH-{JA7;M z7P2|@;4Ry8ay{`CPYbPHAM8@h8g^t`#q1fy_3$j-@WU_uZj2~pw?Xpmt>5chVvXf< z9c^=Ide6G}UG}l(1EJbHY8mU8byN2re>A~-Qsm$3Ge`*xM8ltVml?r>ZJ@gO1lr{f zURF-2L|YzqC~8VORFJ{XCv~Eo^5;dVA+0fXh5uWaC&l4g1LOr57p0tE0aZcM$9z?6 zU8423v57B*J;OXbXuejLb@0W}UqYR8)Hn5NV}Gx0Wa;>dbzILn%XX>WiO)o7A?;0d zv}*_J=IVd^(IcvOzm{YNw=~8;{L9^qZ$dv!rRDwGXw9Yj@ei5KbBt-tbC@^(d{R%h z4y`Qt04>3RK|EQsoN@S8YzX;J^E}O=fCxF^JUMvkx3L-M=!SiwJz&?eXMOm^iTusK ztkLo|2mW5$oITy4#Oci2rP@Pm&EI{iEX;#9t!oY}CUMn&{L#h9um4_?I{IJ~sQ8r2TwSSH&WJUX&fh2*x1dYFP33AHMa%=jWNj zzAScNmnJ=aKJw?|K?gtb^}0`<3;a)q8#w>ioj-O_F=k(2x&7Nkb?|TH_&)#f2K~o2 z{9_x2{;>`Jn{31APuGnbJK3=P>FohdzTeJA$QGQTU;lIitIW=IIgWiM@cDi2QEmsf zkN9a1bt7`t=&C(wq?PewW_}6*0E|v-o1L9B1`;U|T{}U(s zAIJJ1$NC?~`X9&of4O6Ql*V`ZS1#a}ll}8F13bQC%t8{ReeSchGN*>&Dwcjlo1AEO zPl?w&yY=Z}-f7LB-uIg~f)wNkD)6pfID5FEnTw+R7DKhApC1IGly|%if3dOuVJ79h z;Ln_b;$cn!`%XU%^WU+LnsjomhsHr_iSPHjeX5W)XlC|opW$S!JN&)a|NZaH?)u97 zI}XarO7Fclh!z}{g$N&O{W8%tjrE+arStB;JplX_7IYna(mRtj9m`-NA;zY*&oCi5F)e|r%#>qY=J_M*7h>M(_n+pa2goRU9_Bl4 z^cvM?dRI={^elcLaJVZ-12OxcxCE(-OQVq3vxdmZozT!TLKiP?zZ0spm`_5scE$XB za#1<{@fPa)+HE->GeZcRDgQe0mKwf$-o0btGinT z)hHoMGS$cmiHq_uB*DGPG5?{@b`QwdP#dYXQTRTS_ouui0@;5q(JdS zDy6)%|6!cScQ%iwu)yEE5$=rQ7fHErOgzS0palK%5|HOk!@P?7h= z7kXHO2@-Kh8$&*3KGW=!HlQh)6PWL(aV<`2@a~;z*xFJ`El`B)-wM+SMn>MUmoFo| zzI)ZTexd3-6Et`w1v@jokc;)EDkfv;)%qncdYVIo!eaNdNY1Km`c;(Q75vLX^~<;? zPrJT%oo+{=c1$Oa2f^iw z&~|GGH=T+_QG4nN^uEARvHN zz9o!XT2?mk-sdqB8CBJ=Fo&p4XP_V!nZ`VVTG(KDYm7~waWz$OIN?>IDMs6BRYvXv* z*){d`ZD-+3Ung!RVy0rrNe5HP9n;t?QSyAgpf|Vy_E1n&JnTNK^=?Xv<(b9n8pC&v zyt+BHmZ{PmZ(%;bgNTcxlpyXlAdvU-_g5+Or6r^3(*%a-E%9K|yNTHNbbBj4p2;+{ zh|7K`hdG(8ODz>`cVR zooZ<17Y||**LSYCZLrplGu_R-_03FOU48c@$l3B^ulG5M+Sv2}9Sjx9ftaj+VbU>i zM~c~?nJHI!ZDPcBf52++8m~q)4=1Pjc=lM{^73-KQ_NIS60~+34P;WYu?d~6vrD4Ivw(Y~ z;H&rRz>U&kZrG3uXET#N<+!Ijzi7N|EiqkDcwH;S-`kr;z+8G6fc${EyMTumCl+%r zNNuvtnxLsK6;MTtN;{ABgcJD)w3I%x=`@Fv+5FhZNJyA)b35X%GZf-gk@Xp2&8K$QG9%Fq1AVL6|b}LG^40`YxCak)zT)FaXZ{o|BFEzOI)U>p+ z@qJ2&Iqd>(23|(M|AhII$rtZUEsd!M-7~UPRZX=I3gW2Wt5J~#Gfrt?RubE@u3Tg* zafPclwFGV4Xe~j@Sn?^5NdaS9KGsiEzb>A6Q|%bxq?ya0@kMdp!XQ`j{{5GZ&E0FW z%G>dA?nE3SLAkHCclUSK`cMw2wMPV?q@43s=;6l3Mu&`ifWQCX9nE;LA}#+s&F$Rn zIlFSykUHk51Uw(hKF@n>X=i}umb&|D*QbN&(- z`0ZT)m)XWbMQ22st^H;ICLavETY|efcvmjx?tD0gtt}`Zn5w)zN!Cala5IujQ2Z=lBt!1gdp9OW1jqI1+0?n>p4Dlu%GJjGfB&z zp4;@F9;k-F$aK}BsRikVh6Xu@ohUW@q2dC&^%gQAxAj7xQVG^oPdL~FEoGRaEgP+n zk}K=WdG;Xi>hA8obnkk2F>s+Bwyu7*hB-DVxLh=WVN#ls5Owo+i)rJoD|JIIm;=z+ zGrbH9>jGV7RH9f7TY|1` zg12R82uO%`u-E2`(1Y8n+5!TAaKa{5%WPUh^7B{GnM_8C#S~_*yl9NGzj;#v#jm5D z|D2n5XlV@iC0FPj*>ByFgY!&HAS<0&-od~j8B=%EF13{~&o`c1o8{KoYRj{byaMN_ zu}14X_L3==fY#3KSlzH*ySd8E<&j!RLw!R-T|+O9j;}(*vUzLa?bEFF_h;ddiG!xN z4($1|EZs$K)?4ZfV`wy>pr*1oET*Rl{NEvP2+V#|198g(wn9pL;g*jq3BcE>xw!h* zu5Jo8a&orV8cWIA!y_ibdcbssvQy-YyhZ7%Iy9j#95JKL3!6^a;7HY}UC$uf{# zFLU(O?zRlJD%hI!w6Nf=o$s#&_V)HxaaOn9v@oepC-7tAp37B~>+I}2aUyp~1{i-! zmxh2)-jLJ)V|xvGXG!jS9lmOloxc&^EZv!^R?=YU$8I&Jsy8`FAEDxrl9-rbPhA$K z-@H9vlA2BohXP!7K7FD6?2G9zWp|Trz0a@Mtz;0tad&e&jf3MSbmq+I(fT^#{mc%)!%)#v`~Ucs z5d%>8Eo<0q!spPB5Rv{NA$KkNtvk;#P5PisS>=49`f5>K)9KQAR`cPbzP^By4du16 zZ0YBucTj9XQp@eyjM}HzQzzfwjRL+|V*6Zu6H%-aH80@q$eM%w{a8jT-!60DGxqL1 z+*H+W3XKDUX{gvGt;E(n5EES-4csKo?aEqEGXBI0#=;?v8SttWot<@_Bw@ETyr@d)F1sSxlB=zC_FIvT6|lu^%l7DT^Ow^$o142TdZr|K2{`m$ zJbM-$r@76L;fIH}+#tUpH8Kt-%youdz*9aRF+MQBa-oDq{dWUshT^^V&eG*eQHFWjLm2so3E1R|T3A()PGA)gb8?9i58fly6 zWu2(m;wh#2jwJKtjywr8NW(r`jZv_BsR@ay{;vp!`c_D^Wt%k@x>qtvUk&=>1ZLj6n zYdWY7q^WXV#T4pYl#t5DN3|#Z4-9Ofb6zEM{rr0`qQW1GlfWS z?}WY13FMqlTkBtJ{R%#Eo=G1Zx^MmCf>ZJt)C-wy8f4y)B_}6?Fq757_g8R=3;A=0 zM_A2P;SP$5h+JdRQjjvsEG0G4*EdsG5>~%=Vnu}a$pf7nc zTOy)O`uXmb8$s6Ed>?HVhhpReifsDx&JGtzyIN(2>0(=3o9W`>@P!LYaiKsO?0}P~ zBb5-l*u;o~PDg|SP7=#IoPA5)AbfG=^perMkDtvDxauNL)YKgJc6xx|h)`!&H<415 zrKWAU-j_F(%r-flCt$zUv-NSwj6)fu8-kHO?zEL4Eu}D+oAM>{#+^ID=^*IwL`sUw ze0=5|vC=gn(&YBreMZg(Ny!MV&#-HVRx(S_h7McEI*1x<>0JxE)pnEB?Ck8gH#@Q* zC;$+KomP(~pk6_MOSQo4ZF+h{Sy|cS+2lo|7OioL46VtSK~|GX!`^g-r%#_IR{8Si z%B##TT#hq=T*ENp39M=<=~Oh&^70Z0|H>&T^_Dr+a<31P;Op2&y!-a;Tgpv-U>P_p zeBWa+;F;}CADZD*&Sy4^i-b|T_aRJZFD-knYD0I;?eb2;mwU15Yb z8gJ9`+|)qLg@Ks3;xpT-6sYLFEH5uEHeh-T7+-IFMjc4zmf7F2gW(D{5surb+mVVP-^_ z5fuS}v7*uhq&FQ4B2B4M1JY}x6H20@FiNkXBO*b14WT7bks7M>8U#WF3?&dCq$LRpHUA3cHOTFGtOd-L zohhza4w;BNsR=7v**H;kt?O!~qs5|{E?!PfAK=mDzM?9<`=+-)pXZCI|B;cgC2$4x z;e(pjO{rp+XkMW!;?##uAri8;xPzz#Q|DDxNoNxj}lj`SY@F9 z{#^9{{jaG ztvM|)mdbxQ-il5FGTkMO)sI7kv`HI@?x$D({`;M!Cjh!eo*&Jutxe^3Zr(t*me=EO z>&xIFgwEx_J7d$!y20dTmWos+PVFdPbbJmk9W!@!zUOfJ1$@iacdJhlDJypfLKzBK zd3M)Uh%hQw$U3%f-`0xMblXdgu-WFWRuSUdxfB*pt;SmR*g;b5a_VqdPtz@d(ZA$Z zhM`l{n~Lr%hrFsEA1x8VGc|5lkH^0iQ?|M6Uc36!e+)@@>yU z_o!ca_H^~n-LU_3XjB4v@c_3qLK0}?tt}sK&9(oOL%m34+Nc&>C)*B)K4BPGiRRNf!1HhpeUy>e`5}M_TgT#SYpJur=7t$6kKck! z)~*MYAFH2%O9`094&9fN7pRXVJ1y)25F%b!6IEV6^4+4pDdh3;8$QXV#KFPusNB0U zwms6XHM-Yig7RDk2h@(k@Jt^MBHIRA_tyd+WLQtCJD613+b_$jrMhQn zB=jyjRbSPvY;1hepQ@y&xIW#oH`AmL7iS=F#ZbPVq0g}`FRASA*U}D{*yLlqK6+$- z;bVD)Weu#aG(vq$PJJo`lJn6whwWwLpQ}Z)c}O*SQK5YPpb~i2VKCIdD;cy4;K)&PiIx z5UR^@xVe3!3uq~P@bXb@#KYyU-?d8#fdxirz8LhbNiczKuvjW=+It^}qF+V!>azv#Be_xg+rnGbw z44A;08C2N8FE6%>5iIsvdm3p~DJgS4tVIc`6|~7XE-s>OU%g7I7t;rIcvp3^yGmB< zNs*7Z(DC)=@b|t82@!9yj%-~Ev=a5q!tUlBF0MmxfmJWu$K??t6$ zipt9TB1#ujFFViW2E&NW^vjCqjd+Uncojt5{+0`f%t~`dHcz53QHlKYTHvK4L4L2# z@2p{AY|I45{Gr16^S2SoPppA?(7~cOyjY)eN(xH(;-^VRfzMB0-xNq%MXz)U_^Lxjq-2DB?Fk)q{Ita9MgxvwF9y5l{-hEX> zge~m~HIz-A!P8b-Y|vumpk206MN`mr^4Y`PF~2A14P@bF+I@3vWZfx0JVl#AD>ds> zRbAReBoQno6!`W?N|rs0d8oMftklt|A&?b%8&vzy&;x$>W+bph0LYIGE_}tY*%;I8);#i3(LD|bhS{1fNVZE2&JEiM!}cBj9=N6&=g)$ zUhXyc?zUV_4QLLvVGAL~ets}mrbZL*iOzV%giR?#re;fUU!KAS@FZJd9dvJh&GaF7 z>%vLtP=T?*3AY;=gkv!#_n-Otda5%?(do zhiOtv^RQtBtLXm-Rd$3&Nv|c8u;rmbFkSbH>wo-_;JNlL&&s45akHrD>_Aga-wIE; zU7fj$KX71@H<6TOFbQr#wrQsXl@%3ToKLVvtKhWFJ2B!moAX^fXYSs;-&3d_?1i8~ z5rO;Z<>lv1iiL>k3-Lk{OVbCv?lxvsf zF55&~8=KM^?M_*y{N5Rtf5T(shq8I=+~9m8r@rlY#tnysf0GLNL^Tozixb$t2(PX| z5UkPqdN@HWLgUiSfQ`J_s#fOHCr?81zut8Y<#_`PjeCx4Ewp<tDdcgc79ILs(PF zJK*#Mj|`49dJy?W8C_+ePv=g=8~+4<8??-htWeJ@N;!n}L(>+!Gp9zS`RyYpC`3VaoZr zHQUzFZw5x4e(!L)h;r3T9>&AlI-qB8(uiF!8XYe_k*L~BUC#|#6VJY#xfr@Kc4rw! zdf*gZ}R`RIc(AK3{JC|u2Urx0_+aLS;hd%nd_2e%^pe;OC?TuX{o6@y$S>6 zwk>0wojRO1-n@XZvMoxSOmFit+|Pc3`-z*(VgKTVukZc!o=pyo&wP3^N8bCoYnXti zzJ9q!93Jr;HzWqs`xN8HFIFp-&}g;1wbTEB`anNgUs?XQHSM8{U4{$9z4Bn}z#QUP z2W@gQlvNx(5V=zCuOwkdUn?6m8?~FWD=I1~)$0}rg$aSF-Y#^d$Q%(cEemQ=BYJg8 zc+{G@PLGY>?cHwtT^SjPoyTUKV?h6x7zMy(#9rydNS3(k( zd;C>*-{)~v}Rw_ESr z97IvoGwL%zgYfpDaE_W+yj^huPQq~}vh;YM57w#Gt@2`L7@G4MeJ%`3+g0!G);)dt zeL2&SGP zs)oL&@yMqQW)BOE=th|Mgwu;nR)L8irw(vQp{{_c zah_A#=x>3ZI*r}xAW9hKXuw|v`h^~hbe|9zn++y%fqQc;7j${+6FA!EV==}ieVkuu z&=F7A6r05$$~mTMkdrP(1B>ovrXD1T4KTpqpA%yre3VVj&2H9<$r%X5G zY5H3kiUCN+)%ipwyi*^x4E*uYW!kI`OrA;pILNEs$U{J_teOcI^&Y-ehBp+<9|Xck zfBlu@*cmU9qj)!vi!R?qDp!1Mwym;Xvndo2Ddh*zj zTjWblu_@O^V`W)aZrn6a7Y5zlgN-o8e)RofeVd&^%?(zId>ln9Y%8<0cI{iNp|DMc z|5cfYREYf(cGK+n$GS()50#b$bWW8_hRo)`x=@y$k@EYEaLtOo?IN@NXvPbNVhOXc${Qc31(v-hdMrA0J1vD8Qr&w2UCv%)H{n?Af-XM>da*PmJvJ zM41`$>aA^1NNDz^-O7a6v%2?Ep9Y}I)iuTzrfHxBsI00%sz1@vprB;Y=AOS zWQ2_Yx)7-Wt5LSpa~Zs}=wt3|sgrmswc!;RE1MyB_p+b_TE3uap*# zjF=*JzVp=ng4o$20eBNEbIdgQ-m!o5C~P@Y6b&Wj0e8{To}P4m%2ZQfYx&PD5`^!M zKddl8eai+S0EGTY{hefFYxg)WHFc&|R!H~^kP<+=#s@Yedo-^Lc$Xuqo=D|C@A-85|Yw~$zP{uSc z@cM--vOixN3Oh>4>6B~YIx-(KA3k@Qp7_*bOgr7weCDbwTEo;Df+sclz7sety!#_o zor}eF-P5z*wuNaFgVY-y2>3jryb)~N7EG~vYMgvZ zey$0&9Yt~Wk-yXo;(ypYXSx#I#(t{W*^PNy0~itEnxG;f{B2b`EXjM^v#8UDuT z%Tnx|o(^L)R3sl9R|(qrSD$yIdAf1s`=mlKh>(|+C@nT@X`}LjH4tlzSATbR`Z!40 z_?2+)W%F_JfU=TOe{L``Ki{`4_iX{ zCed!WjaIB*Z-y`HKqTLl)vO30WDxUFSy3@tzf}ijV;*j9!;Bkp{)6_>#QO5`ogd%Z z^pKhdfUdP-i#pxEhJTq8fjn5JZ%@<51uUwBIl&(41ua38(wiwMCWrWaKLw^!JMc-@ zP78ioxpKtZvWmOK7wpEw5l}i3#eBJ^&z@=7XyZH(SJamA{&4fody^&N&$@hOSjC9G*j^LJ6Rg^-VW|HP+ z*;!tBN43D!S6X@M5eG|&=i4f6nw(TY3XH$}+RC)D!7@ZX(!Y_%#KPL7kiSj}wE2K| zXywdJw?!xD>rzC-#0sVgYHN*AR=RJ-hK%ob9HCch-oNq&P)XUUo(;W!j#t=5{G9kJ z^WEr(X;@e`a)!#AhFTl)nQ5$XXj98fE!w3>ZYvVbgiJU1$Vf>w0%jGI{R-Qb(3Oh` zz%9ix4-X>NwY27T$8`;LXPX?eG{Flo$=oCvp{)O&AJ!lMN|TOwmH|35g$? zEb{k;7+Y9cUY6j_M!Ssi0lsWNV>`B-KM!2Ya*mxp41@+qq!z#n!|&d?XxG4Z`-{!;ik5FELtMT2giG;B85gaxFUk3G>fKwlUJLF`+^? z0AiA*8!h9H?9R9#(lWy>O-vju4M8fG92UX>X^H$|8kf}7rzef7N-7j_=+~%j=6ZK= zUSLLF_n?%v?7-wKjw*aTQ-lbUEsn%^9jG8rv zQkKxH!D{@lunfr7yq7eFz1SCBzy{AME88%=EUm)qJ zS}Rqwa6U(0yif>&x3OX5v3va5XU?6wkgz_9rWmic7bU>&X@^jX#g}bc7Rcm$)sU|0 zzY4jyBI89>L+pt%tf9vJS7G8I58e(-Bv7VX>gqqJIL^NdTetq8MxIFGqRKnHzUlL2 zsoTWWt|@2xdzhrZe=VMopsjDXQl?f3${dGH)@D;AIFoGXne_(cLO?#6O>U{`ey|>`m+$4_ zlWyo=w>!2w#=|4PbAq+bm9TVbDWVq9iCMR%GED|p>x-B%(Wyd>=cHz(itM$@;f`kG z4kJ*FCk6YXqoQC!yl>vTNsoT0r{~1J%vGM8ya0kRyC>XsbDHMn<_-!)w5R_(dGbPv zO6G+VhkU*J;-bKM3+Hy`fvpO{hukk}tpsfjV`Ce5K(tYv(Np7mtub-Izua3>K{Sp2 za%jJux2^5$m~=K^N=F8-4$R(oW{;}7sHgYv{rg^I`|D_(kT4-gkXhQ+(q3tT_RhAV zowx<@k!+dy^hWadBaf_;Q_Q~}I(L)FWD9HSi;(-X%_B1M*H_C~ahL{>NZIPZn!>Xv zq<{bIdR%y)UqtH*H4)fsLy6Zf{u8aoC;bao93GBmvuK{DC5$o~w(4jUJo27+$IAGi z9MGpuP^nv@nw%!82vUhn@(k8sGkaxOyEA#gbtqZwniW}eT16}61;qZx$Ci9OAvOO! zb?u5W83R!z^$X`6$=2HCWqvHN-znb=)~-f2DTN5_AYOR%p|IpB*sLXCGDj*TJR$fqv=}lp&fBd;Q&S!PWbx7k)=?Y@I*CI>)TFDCWU|pND zjAz>h{^M^Fwnu%05+dd0S|hb~mnucGAz`G~Rq$;_X_@T+zQK7oRH#%S89ZGpsJ}xkeTZ2=PUv=e2KtxK;dzpog{zi`chegXy1VnONF>zO#AJ5F%ZA#$ z9TvurvZC5rU~(wgqQvpPUn7y94~9j0wQZyF z0LKdgJ_DC~GyAfYJ387gU)mWG2T`ojQc_oOFq5c&gX3(cQ5_SMp}1yfZ-s@}GKu8e zL^5Yb$@6Rs&YO#W7V*peA1t8z_3Nk_B7h*rCc^OIxf)J{GNSv(imC0nfpz8qL}!*o zp}wD~YauphO8tGQXIyqr`-YT1_2TcWPVWdx*lX1e%*ojusxnHOz4n)X{_NiODpTbC zInPUdIO2u4y&9_y&)v?jk^xCTOr80Yn$H(8z75Ed&yhTbChgxDL}i@f-P^Y#B4f64 zFbN4rWbm^`i6fwLIbFAaSb#qwPQty{hDlbn)gsyst2Gy+rHH3R1oP$W2N$Bn)q_SF zb9F-QI$7a)#l<0+>ohrUVL#@Ng3p$1PF{kiMHMLVlcoM+RrHYl+`Tv8V<^_z>dsjwfn?qL$zC^twGge|0C%C!wejp} zamw<$KFh-bc7^$VP6v`T`FGc}?YnDb^R-~D4X9;s4+Ikr7GCt`rm@u|1n2BbpMvV{tWB5So*me&#FE0{auZ6wLODh zTHt2t{Ig>zK)B@n$MHYdd!2a|l@$?e)@guDu$N0Vdd1c0Px|RU*DN@`LyLGgwQ1#! z4lH|4ZXO^@XACm0Z8svUsb8(9$MM+3x_{>fGpc{1@4F3*iDTJMP5i?KRCYb0#Ci?4 z_x6yV2Z6v8T<*?qFO_`-LYNP`=oZS~j-NgO0&$xR@*;Dbr;$SwA9TSZl`SJ4+?%0j z|LK!IxxbzqJvKI0X~%Lm>~9B&Z=h?;>_eZ6?sgm4)w~+8*lCn{bf(GQ6}Y%=Q>jYO zgI7E7n+hIe^D z*sUO>Djh$X-Y4Q5a-9bN2%6zP-szuqM#JnmxTPhh$O1=B{y$d&dY6wJX^@xD%u%9N z4o`zk16yG&a%R&&>%`2&GtyVoe3#Z<#0K`SZQPaD=a$#RHP+q`$4667X2n3y5WW-@ z;BE(gO#r-+ASB!u5V{~S*s3lRO;{x9m|B?KyC;c=z%F-Y&0 zV{~m4FVb?FX9KGmHWyA%?vY=Zf1z1Y!U_+!qAN!8iqe?~_FePGK#$mcR^=mQyxMtK zjs9jx&ai5!2Op$}h1{PuT5#TMUd!V#F&=jMb+;^^wI<8n=KS&t*Fdgnk+#({O$c7p zaXem{4zs}1XE_peH&_KdZ>eVDxNS5v0@4v9%Vdue*>m_;uj+xwW2;Z(@#CSMeZlSH zfAGmjOO{lYrdqJE;>@czZtU}O7-d6_5?u=Q%s11Rh%1W(9Wg|0Yin;%#Iikk*B~=8 zDhk~c;pgyT3)J03@hHz}yTEOkiJNl{Tr6N}72-3rJZi8@ufan^qt{!h0f9PivS zpve;1)_z_&y(@;c{q#-?_J&)$uSl&gSDjd1hq)>qznp_SXh(}zPGKVGy>z|e zfv~6REIuRj!KZ#AyxzD;y@IDsa>jonA|FoOT05eSZ!tU9u^lHj&VE_Q~VN zm3Xs{>!_hPIBTOO;9zfHqR}^!M6Flap=2us%`}eYmwSw5IZJ0|WqB}gmELFN{7ivn zvbSMuY@{n$H_9BO6l1jQcRIzf9I`g+yqX#y_jRFsqDuNGKWKS~bnnjGZ$(8N!DkHh zA7-ieeoODm7S0w1xSABfmbE+x3!bBOmnLwi3;9cEXBR%dkhE+Bcg1*#^g z0Ow5!gbXV}K??ConHY{OD7gJL>7tIV>z7x$W#$n(R0umKIoX5W#&F$#s2r z8sE{;VVQTE+j~nXcmyEsKpq$Q@}<)!3B6z;I*<<;O*11$+Acg3~w)ipTtK)X&0oep&~C|1C;tVFfxOaD7w(TOm_$$=H%9M9gJ z-sn2}vAMMQj?vS}S3ZDL0x`zdt_ArX{~&eut=QMr3)dLfglVwM?u@-?;h_h;W7zh-srAq*k25)Bl|4nY&zU zB$iTQ{^-%>hwfmTR(mEw5O~9H?`~OQ4jc{KYfq^M*bnRo`CG#s?>I)1B0f9aADgg0 zmwn}O(e4^_hEG*hk+Sf5n~TR8&Ft9Re2}?t@BTf05xM5k&-wD+uQ0CTN|`$(o7Q(Z zDJj@v$LQM3ZbUbt34Q?O+;I37z!KM)-Z`9~hL5}WA9)^BT2aLr%V)QgG|^Y%ULr#1 zhy=0~M-$DNxkmp%lu%XYl!nt;NX}lUdkr&thjc=mpz|&t;bxzRL1O4%QN{p!=;1nR z<>_t}ysLh1OoLyO`5+=9!rQ>$Nl0UnxPSU1G-=v z@@p(q~yK;Z7!8HaTKppkGYRN?QxpKaaIbHZKmyhqrc zPIj1V(~CvO{Ppc=TH0I{=i`v$5`HN?Wmvv7YA(f+jD<4#ydp_XVW>MyHm!nR)x;`TPx(M<|fuZiX10Mc!Op+`@72tCBjtk-r zraVu0x5CP9kKH^9qzoHKrH6>iT-OEGf&z z)z&WDfBc=XQJx67o9V;v?%~0U3oNY38C%a!T5+{XzEb=FXh9Xim!6FBH3+C3KE!-A zGpQcx*>!`i{uinydp(g}}hiLs4*{{*kEYq$U;i-)sMUHSs69isH3iTd6(i|J)scK=DI!zsl zYQ9dtAzExU)9S&nMW<1wXt+h$uKz%Ql(7WmkJcMA9(BLx21+)Y6PwVv&@)MjAwv$$HN!+A0gP$OB~`)$Pu8dF7AKM-4?_AcXUcVd?Hd??D9SlKk=M!n)b4TQZMiPV+g1&hoB5t2~=_ict{zz+40%uh}8*Dv03WxdqGy<{shf#-zItJ>a#M-znyYaAj zOy`YcL$YDCajrLWchh?->=b@Gazv|m-CAc-8}Iv|_@yHY|-0unh zK2yr`oOTsaayrpnT2Th-s*i^kOZp*L;cyB;W+gu(GkJXGu6ocz6kjVLoj)8!=rSG>XjIpUUt}Vec@=&KZ}t(OQ_fd+^nr zNzOJz`#8ui#|H`rF~64JSsVGHGkEp+v5Km#-y;rCG|sqqxa4I;1NLu5S`6>Z19bR_ zI`SNg!C?*LGuQUOp~_e4lT7oiKFfjN_X}dlSLT`r>Pbe-AFMQ_zyB|hFIQJr*RcrS zGQ9l*w~X+s!`U(P1G*~1zk99soW@QuXBYeY)4SgVieEj5AC6B#2SRr5QHlId*i8zV}!m@Q5AcdyjX2=TS9kc1M;{Dsaf!%B z8~Qb!`Id#=-GLg+_eQi;luT9NKJS>DS%ndQ#ge=X-wqBA7Uzd9^pGTz9w%{E#bfiE zD{BfqPkI^VJ<8h!8O9LS>iCzoHtUF^rT(p0EC3mS%%V8fI_}q3O=`JwRIDKAvaNmo zI{9s(SpfpkhN0~$+5O;xFJa#zpxq)Bjd9}67yGR#~qJ)BE&FKt(yFJYN(T&yaE zl4Y7V?4~Lb#6AMq!c?k;Q`Bmj4ZeEaQpa+xbyVfYusig~k>CAw@7^{osVfn6)fCNz zMbPyKT#>(kbxJ7+R%crb_ygv)a&sC@)Wq@eXIVhTsfT5sfv( z&msMstb6?Uv8{YTibPUzvIKL*2$7YSH<#C9+4K|`%_Z#2aOt`7i4=zNz%Elmr!?*= z%Cn2QZH3?9$j z#xZ6vM!9OI#l*yfPSfbS1t!a^<@aCOE&|Aj#o9n1*i1UuhYGJ@ZMQ+#)SqFalq*%Y z?R2L*Zz_$z!iej$7HF_vAG{M0&Rp6%6!KJFaxu(g^y*<)G8V9-RQaX$df!+ns8iA~ zKo8g8{?s9_;PB3Mk-(s&tR19Ggvtf^1y^lOLMll2d9|OskQ|RgdfvKq%gNNq-+vo~ z$RoC5AYI@AY#811es-9YE9O2Kt4@v=#(&?$vB*ZYt?&vIE*kaE3pkr~GP&bKS8~3X zh}a5sAFMggC(6vlVcK|6MRt3|h+B^g0`Ivs9ObC4zU8i1|H)9+?e-h~a zV)feXzqV>h+ESu>pJ9nP#NuY}wu}*qjcfv;Q;CW51Z35($BtFlG}S`zbNhzgV3|HB zX(fYqW$6yhJnjUoHV=%IW*Fqy9ig+Q%pb!q-psM}IeO$)SoiaCO@-R&2kFYSU2^8O z7!Ucgyji4DH4~2>AH(q;bxe@4l@)Q17!kJeE!rqw&DPv1%k8Al>C>mq{agb&O;Qoj zPV#m5H3L{zzyo7T%gL2tSFwnlA+s{`qT=F1uK|p)pMtE^h2>pnw@dZp*-G`b!Rn9S z7V^|LP5=cGmlI4tSj@S+8D>D*WLMa7P#*ykW~8-6iasM7PM!s?yum8%DV=}!E@NWi z-TXUL2P&%LzG}yKr;eSDxA$z^g+jnF7U>~nENd@mXxM;L-I%?+j1oD9Og2^+^)9A8 zqL9VUo;zokkuNYVacEr7sBJmXMeP`n9o)YsF|G%^Sp6>N%0qOEC&h2 zr6h3F%XF29<)2_rKS4}AXzQt=gM)*!bDD} za2lnlRXHb4B=G&l`xs+uu^~5p|9&~q^iU>p7<>qN%I2unHCSXk13bqq`a6+t}rh}XR+MOH>N0+Stn21Tzl5^{ z+C2XO@p!$T;^$7TZvv6nn8skqITV?3 zc#!tMXdNspOajGwRaUw=E9?tV`>NChC6!BGWdA-ip9owy1~xIC`wY&^m_jUU%^q9X zSv|3^7CoJE_OH`bp=p&9(nW>W2$P@Y2eV&~8;Z_eM;sbk080vSpJ2d7VvQZ|*n%^C zj~@4J;pKed=1y!9{OAi!{7d>hOSBTB|#_tDx9DG5DqlUGZV7v7iA zPM}LC4=9*FM&CQCM=4Bb6!`5%0toY##M#zXCi9nHUjDO`^VIE0>gwzI=@dBpMMSc~ z>YB&(o|u?Qa@S9=2WS_#nj?E?P?Knxk&q9Hi~XY~9#-kTb*iIx)-JCx?oEtzG+EY- z*(yI((f^UU*bX1Vy)O5aCd{NOG%CGST5SNx_L01f+NB|%JJPOsg{+j+^Cdj+Gym-y z87N!T2YxVNNFN=uWm7hei@>cdPl+X>)gYc>m&W^Dxq-&-m!bdlq^%xP^J{gf@D6&X zP*3fz=ttR7a;lnI6DugLKIE`3aNME7Z~i;)e>uoFL3JaMpBo&{S)*0%D|`gvLcS@S zeILoG@0nhi(Zc2+vzgv&o*I+h%VwNY#+1qk zcguYNssPBT3hCU~?5zLea~g$X{aT*3Xiz6Vy1QMkmFL;yZBxe8XpYJB9LWAhg(>9< zY2bWmHpl<^AHiZy{r_=ohCn>GL;5PAlYxd-2WR_eeHGqrSH9M5AB~d)~~l~19w!%9|tcp?Dqc+S22ZSYHTd#8*nrACf-{0!>f+^ zP>9j}B(U~&1Y*PeUv5NHr4POuqswyA!SCtx@|=S)QXy^%jTo;ljjsvz<*jcW*E`a6Z^luLcx0{xMeGv zn&K9%dHZ4`!0)_s`5Ldc&Em4(Kdzjg75pnxgLzHX(Jub66!7K+la@c+`(N`y_4uIwEnNTJ>#>5(56_i0(bZyOsagzx;p9W}-a+ literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cbd97980e17e9e3d727a1a3e271732561a67aa76 GIT binary patch literal 209928 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 z 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%0AIh8?sm=_#S$XF_*jV5G)VYN=B=49{&7m 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 z zKHq%OA|-8hbg;c5jF?^Z31evvW*(_{@_{W$QeKdgKk-`z*HHWw5}y=d1pc@5h^K6F@%e%hpyU zOJh)?$3T7DmyGSz5!Cn+L<1s(ZAJ$j7uw?Z&ymb3ppzhG??gi061X8;JYfk523~ku zQQQ{W=_@S%I@HRjz^GWd=gRe9<-6tf`_+m_21@x;2 zkjXfLl(tgvci9yo8X?3fV_VQ2Ln(Njl;`6-;UY%Zu5~1mW>~9kvoT!t_xAVqm^d(Q31^jil^bjDYEiSmbS)VynUFdM z<uO9qLxF?#r1OhPB!ct+}6V{AaxAF-8p&rjt(z8!uvWIC7aeRt6v|*l)bp8 zoR^rmwz+wRKxT&l(gXOGW8gbO>Y@1Qa4!b$GG#aTf?S+x{_x=)NFUiMgSx;$Cnh@j z5R0V|^w7#M?EqjG6%~Oi*-#mBmp9KO4oc(xj&^C_%X%V=1MQBVpAT-4Mb)Vtm_l!q zVOfKmgGs&o1wr&j;frcTXYolMco$>dA};UlFAi(~LsRv;7Fh+8LSh5dzyQ?U6f~MI zfO5AhpxBwboB{#6btM28Tvnr3cLi;CVznW5_M&`cXxS0plND12h8$8h+N0$z$MvJ? zr??!8tahcTG=@juMQ2%s2R*4DSQ;2y#I3XCLRdEF{lKe~xK+q5F==o( zIXImpk|2+F>C=`@0=;)UJ$d$bXNM=g9S$m1^yt&4PMzZU%atZ0PsOX6!)ZKZ27*>l zq8yDkvuJB;fqIGGwL2WjlU%0#db_JOT`95{AQ5_A$8KO#<$aF!OnNio;2uKkax_PQ zz#KVaPfuHz0_;Jcz5GCgMNa?vrT?H`5Z_Cn6Zl%fN6ue^@?ECIu=|dRNfM6REfESy zgV&g~W@mE^YQ0H@V9CYs6{1k6+LJU`|C#|f26fBgGb*L)w04SV1N{T?eLiLOQ}$}6 zYLGq!yQn1Y<~SBiuhq~cybUjix105x>lQxWQ>=S2*VTHsXW|v~O(LWTHt3d+1*?H0 zb`!MF1<&>AC>u3vPUlsV{S864ZK;s&%3uVYI5Xfdc_Mj$DY6y%gTkgfOE_!z*diOn z<<=Vw9&91Z+UCbcxPq=PD;*Dt%vf}Y>E)`9zg1>u%K~|jXcJ3I_xlAEcOULBDx4=T zo{z*9&cGm_qQq}CjzcGEykJ>oY@KP1isO1qJD~-MujuEv*)}L|nXW8Xmoe zM>u;{aqp&`cC6*}%03urlP>gCH;!>Q zOt(A}KI$*F5mr_0gZOBB4y*s`mPnQklA$%{_i~9|7svq^-S$(BY6zkW znA0%0q58}oZV(Qlk9&^G0(<$RKbpg`4bh>3k1)FSf4>g2dTya{UkTeC^{Bq`ES%#Pe%LEp90Ut;mLV61NJA@gTI&2uFmp zxpIYZ$Of@2;zP+pih37-i%YmDOnQ~vd>|HFQFnzA!8|Mcu;Y2hU-;%5iwlEedqc`S zzwX%l!or9zJKw^`3l}f)A$G;}^z=a9JJS-ev2T&xlcotF*>IdEt8@ETSuL$rx2WFY z!ciHY0B>K8pA}dd#OhH!jb|WuUR-aD!jwBYjQ^y81*Y;1!E5$wE~3ZT!7zkp2K|8RN99@wc-%an^*w3Vz!62`@^_B zmG>r=vbN&3kiG_^zGC3DF=u7@kg%*tOemMDv8rNX7^pw6#cAcM_yy6)U{@y+xmF6E z1#$UV9v`gikaNs-Q4ZvDn|1PxtxbSZS=e>c6s#!rx63gLULbyXW+WF|VV6{X#sjnF zEI=JlFWhCZHZ38NEod_IpB+vsfNf=uHoVexX>gF!VuyyucE8)~9wC@a0}W^Ey-LEj^g#(=D;Ij(o^0 zKyM(`V>dZkoSB)Kn;NLlF53}y)?J6yDIEjiYeM0d3k*GcPX^eMQe>h6IVDw^2OXe{ z8ZBVdRAM)w2$GZbuhk^Fh%6g@u%CR7*l3NzzkaycC1n)dqUjD^m3VY?$mQ-i_Vw}R zmypj;kA1wPQFwLVy?)|kV2`D;7wr!UXO!++pd(N>>Bq8gVBWsE)+|`5iJC?ecU7pM zwhI?DmniOj%~D4hFO@oSepk&_LAe51@cu-10sRuF2Pk@Nc2_E;nVFdtmgX_paa&eE zcEP~o)#KE4^|mbjjCr{14xVX*Mv{pAJN z0)HJhV4Ay{dFEHi0;X7s*KK!l?z|4b1%FE}#BPq&P;qmpXLqsec=^EzBH2GyG!mvW zLKD9KlM5)a*gCGYG;3=;=2n@zFJqH%AyDD>0v7M+4~9y->|M7U`7d7}3?aO92}s>$ zmR89!5rdEggI(m#0c!1S&xCBV1Dxj09YfTD_J(`jAtDz^%tw;_!ctOjD!|H$E4SXici#IP zQSL%P=A!=~Bsx0J#{WI$OL$v{b#k|Vf6tS(Tj|kX|Gv}4NB91LsV`A^|J;0@>c7`W zlfl;Zz3TVc38<&@Klxs79k;e3L5siNbv;?Ae^yZVWXpm-Li}(u3LDB6OnI*E*0vZi zd=c(o?BT3n?mr*=|2?Yz-29_s_P@toLh+xmKZ8a8{ly zaCuof&KHbZ+5%hAAAS#IReDX9>h+U9kiRCq9mx@-iAA82xWX8e=TlQtKY!L^J$Wvt zbf{5;Dr6?gV}GAG6GZj@9yvmdy|Eh`$cY8_g&#=8Z{Kc?ZxGXGoV;mligl}6bY<_? zpEr#%mJNF>@e;#6Z5($dND~(q7X~J>qPG<6bQx6A8rOfXi0n%!x<8l18+m4mSD0qy zzIy(GkVELA6BfZK?PhztoP3#4gpj7+(H28+^5jk(`>JLPX9iSLt1<3uICoVn+y)tW@cQ;x1!Uon>;voSBfOzj%?)L;lhna z?K=m-h4W<|f1bc3OW41UK*5_m+`YhjdYj?d-^=UO+c!O$a=*u1G4k)N()PxptI^+& zp~Em+>i1N$y|F2zW)!Xb`tM~QAWNBD8^xI4_q>2a)`b9WsOt5D|MTdfCsj^v`rPA{ zPk%o<5k>$1+jnK+d6_Mr`MtB9Y;HJH8s*5&|J{lH|4cQ;e|8ai9m5c!-?u5x!An2& zdpAAzdLHd+?9$a}=nV4}V6&`M=Ix!640(sZ4qPzovx5 zKjV|L5c1E>2cg)0FYT(=!1)L%B)^o5B>MMZT?jszt3Lm;6GgTP#XaLp2n_n?RcV-2 zJE&t;gfxzdGPiBA9*#D)&Q^2#{T#kIe^HkC`+;^$ehvORggjrSqyEp$4>10FjsK@L z@-Hk?q_NzLrTKdU6cGA_%4=Tgi&f0c5H#6-CV+HA#|plF{W{bKa1SUph?QBSm6U?5 zfi=pc(_iM4U0m!`UObZPkP;9eJKPEpQDhHFLBujoq8@O*A9_j z?NjJ#;$G9LSwzWee`lEs8000+tfw*AtE;QTjH=!mAJ*sgXX0wX_cVt9tkx#nMSB~Z zo?2fWDg-VE)UiFLfhla^CHCIZmz5K<0ns+0^x7e zJ#Yc8fxobN8Qywz(UAfrFsOQ&Hfqb9R-mG$Roe)DaW=!{WKI;Go}POz)#xDW1VL-1 zD**vD-;4Q8v#qO?{2l`Sx^r6#+&{4t%r=TGD~@1ES$TYP(6T)`DzxrpgQT~?)cGB! z@qBSR0D{0$#o(#N|DG6;!3p3oR>rMYRquJU=LS6Atvi6Bzud0O)~$>M+DoXXHy`pB;@fpE!)3p7H7FeK^4q#ZkQL?t#-o! zK|x*J_Y}c<4U?^l;fZ9@&3#OrYTOzbY6yfC$XwjlDERnB^(Vkn;BXJ7DT-m^m1ru+ z*b>2JXlbbO#77tJ@_hZvnh17JFfVxme!deW8FMYB%Or45Q7aF})eL=0zY7D>3o+fPZ%pZ(%3_4H?uMxl9{>g!-$ zuEPTxDDXo*DO=~P0=q_`>y{>yX0l|M42lw>C%3L-X&++7?L%&$qi!@RrWj+_^N$u@ z0E3&QVs}2f@_IjKnd_E$Sk_1#&_~K%GTk$C?=Hr$uVgx;$UuUDxT{$A7{H*%_CU&d zeu6G-+*_zDFVIx78q{-I3>H95v$r?~>?IHR=D4{FF-%iH7px$u1SLd`m_M|ilBks6 z6Ncix-cI}a8of%nT3a-4W(~-6iY@n&r^e$&ju!_}We}DqSZr)};NmFC! z%Ja^ArnC?ogALw6juXwjLzfcpu^GO%%yfZ}5PHAl%0zOQ<$2WLl~G81djM&KP@jYC z<%MBn{!~2WP2CFn+ER@jsmWJJl}tWjhF~D^=(JdX4U7Hw&qioHkSGx%L2EErWMxB! zn4F*1h<9EKwJG#nvfUT?^zq|tH?n!x&H!pJ6dMe5UfrzG6-WDwX-hz1ONZz(5S;d{ z=kkntnQCf&z~bQq*5JbG#A{sG6joc#p`2DKx|N0ZCJol|jnUg%8;UG`mNUgQ?_VzT zVwZ|Bq3A?5_fC&U-i?}38pD?Gj#48iqgZ2*8#*8S6_cex89p68Yqt4td8Pjs3aT;i zD5i@s$E18OKI%jfC64Y)h3P_^>oT}$hIE+9qj*L_MogAu2EL0|Y|>r`6E=r3;S&&e zC{G~|c2+!v?Um93lXR3_maATS2}>Qa+QgPCR0kwP1fM0O z1SIVm@tJ!B=dNlX?L)SdsnY}!Wxl(mwJ^g$|AivK#-oZX`KnnlD4Ov33~rkq&c-Y<;P?<|m{qm1yj5g~a3<)YT3*z+tMX8_d`J z&8zN$F1Xa}?uDt_3-E#uJPn<6puq?nFrYck?_S}1>76x;D;&hAYvwEOITjY=h3{q{T3y>Qfq48>L_SPVf&IFZo`5BH_|?8f@K zfm(GbI3bdr(tfCSby=<<6rSzRO-LF5LEvQ7Xjil3=$i@^r$8apngj9m>&b+&K~W#x zzi+Bf0Jk=Kd5QRax+eBQ6X%jj*9$LKMzI*rX<5gJ&Lk*)rMsnFY^ytAwaH< zP;CUx8+z1d?iUR$5yH)MMd4--(xRe<;1G~7sa($#k+T(ir-a=;S-Q(_+RYi97Df_t-0HKp2t{lOI5#z zZhX>AxCnR(%modRA(lBD0E9l%MsQhucl4}+i2%9zFF0{*{!jVAf38qV2^;$N^o-r=ufiEWS!MGC5FsaTD3>nwditF~oYETWPq z`+>ZvzAW9Ht+kM&DA4q)Wb3#O4{sL@%HHTwuy|fVkTe?`HQW5zu%% z^S@F4>`9bQ`)d{ngvxVxcul{8uH8L>6d;y2qGZ(9`mnsaHW3@E^Sm>0El;S;vfaP*8-Biui?g@B1HD*=)Mz}LY_IO|nt?Ri|2zZA@ zr~Iiot?L1>2Gm7Gf4&xKRUs3rI%|Xum6os{2Ny<AuqcIHF0t1_8^uNKl!Ce?RL;A(E16nSf6Y6GJMZIiu$1*Cxi+~ z6CWwG;1;}qPZ-3u49lF)X*u54>-vy}=y4;-Oxcy1klahEY2dEVyK{Sr6xjnwNbA9? z^%44Xx(`w02Z0SNvf{Q44J)E*LhNg7^9L&yeO!bLmR-|ez_qWIOy{)N&ue_JKGoD1 zr{oiCmIo^e_eagtx%(imG46FVq}n45ps_j1oE=E`U=6MXOk14pk>8UJT>%@yi6QB} z3bw~Un+uuO?9EV*iN0h{xNZyRpfQLJ6q%a$o6pWR6w?{h`6~SAWdRI%1F4C+v$S#y zo_4XlPf(LJTvq#+?lm@^ld6zdH#01BPfn%L#Y}&JWndxn zu>tK6?T`pS@a(E8Co`Vm& z#YCT)CwC-#`69o*7ZxU-7042ESeD|3562)D$At*Z-zqVmVwgx4UKl`r7-I zzW_>@jg}*mPepzR#p~-RR;CGgi`_^Zx|fKb8I|$+!pl z#Retxg1V-x6gl6_gk`e`rQB{fov8C%v*LLY7=Q+k7Nn+vT1(BkP{jY3%OTG)8ieDg^XhX#FVewNyVwH-u_72_!p=3A`UP2XqkaZ_MEi3cOq2|j zhKg3M-&eA&+l;$!AeH#M1Q0iL?*M`7v!;tLiN-B;4)01QOUF?&i+rS$<$JNnVc5XE zzmOnRdzQB;hRrB%w>J|rH&l#Xo1lXrBD(<6zl|wgc*p^Ngi^A;5kQeqwki`KS$iI> zrG(3+dA)~|hU>A!ReM&aSb@s67z6hwF47fLT#K2-n%5bW#F_mmxQ(l-0xGHalyuZ2 zDO|WAmwj7oVvB+!*zfRgqxveffU_KmFVQfW1$)<~{YNFq)-v{+(VLl3K{x2qdTXn` z-%Q-c z@E9y~SJlPo^aoy2TO2o3Z^W8RdUjc7cRG3)RY{ok>l+8sJg~0V{q4dQkr1}7#Q}#C zHJ_?-w$4%wp-_AABM)vo_x%A`_GC%*$szq3c9SNhuMb~iJPulihEg8aUVQQaaT`;O zIo_SfuJm;nJ?FlkhDb|#aLr-$>)~JnOAEd~lT2%|NQlc^W}LMTfwA0Xu8x$yGt)dH5(0YM6U1VqeTV+?HCW`T*S$EDAp` zsFO0{yYiYEXzROxs#n*xkxs|4h!i1e9=RWYwi|4eAgHT_n-!f9=S%-$7wRTtznQw6 zWOyHYeVMwh)Cz>@+LC*xkxo0Y9!uF9)0GEp{JNglkX_Fs^>Q%sgcYN?84+ogpT zlPPt1db2ejjsSYyLXOd%ll9UqaBX*r=}1b!8utG9Vl zNu`o1_h=#ioX6_XK{l%Qu{jx#Y9iTO0S?pMsadsD34{fV7huZx=?7d}J)cM;t zjP5EcwxWoXiios=beVL>&@CvSq%aag3^qzgcZ^C&cgGlXjdU}TBMk!s%nwfP0y5B#?voH79C4T(o`#sN3;@3bgi$PP~I)8km;TTSc5gjacj+KhAkS4N7x-^*~ zVFl-#6z7s#WxI0yT{)IQY=-nO#j>X1;aesgWi$1Pp&)(}-6+6;46fqAbUs|;)O9=- z{J`5w-$t^tvk|Qa4$;L(+Z1c1>}+g{K9XG_V3Ot5Mt#^OnxZv*1u$rljS}+8q($($ z`O=Rs!0XNS<-O42>&?_>=eXO$Z%mq7N1nkeo& zC%Fn0UoAklOpgs&8cbtrohOZ&;?-ki5+Ts}g$!BGTHUEdVDdxMErv;b1&zAaRw-hJ zGKj2Hnzy#egy*o^zB@~*8aB{vy7o}bS{m_3i0>eTTniLd=@mXa&ZWJz@vX)I%fjz` zlIwK@zjjjx8lH;!R}7fXGa;k`rhT0QFJuU9h`zr0YBz~3Cn*X@@CYOx(ap71t!6YB?5UK6RMarGUM`0aD=)DycN%X^CEdx0&( zutehVj~}A9UN;pKGZ{c>g-DuW0E>`Z4tsO`bs0>WwDhEEFyXR8#yX1w*N-M$xuz7a zO_D3pIlR@+{~9EFc~kd58iaByz~l5F;XOJI2ZiOMrDsyEQj&lkG+AUgqmML zd~9G&vVI+`I}?q_X`%(0oF7Jb$13eU*9T5<^^sDvag!YJPC;R0PTzV;>ypA`p4Xrb zN`Hb$+U)uAgCqCr145r`gw{>*(|YB7)wx7jB`e4Zzm)O62J(60VUkc*#ASydPvN;H zB&dWO9i6M3XN%FxiZ!921~cSuI`|M34%b>IxWba_(p6Xv0Uv2lWu5FUGo-Rr_4vcq zbFUSWE^(09Z}nobZ62*Q2?tKjD?~~c_A60M+T-o@3^v?4o_f8>FTyY7S>yc_mH2d1 z7#1{I^p{RwQe)2XzBl(>J{(kD&4PQgohG@7F>?cD87^4{;LwV|TcNgxhXPFLroGC% zsnwp`69?92x-}gHup$WB!iqr#iAMt0_nObdYNu35|K_X4++dhU`&mS=9pr5NSq5{; ztJg2l+az75Lh4P?Fm@NO`70I%`1&6Ya*ec#PaUh`ciGxFkfxu6UO@7SiHmE0(ldkh zm!A4iCoLC9iG%RvKH)X*jivX~XY?@kP#z@;plHxH$kpVWrbQ$4gHBwx4V;{w?oN6X z3NCm(m>b3)j$}rYhzOYTB0IVPf|;C#aA2b&^z{L3ISjX57W&9jOb$PHouW)4TTJxy z%%@q=3qxd3KTa!eMsgWbK7V=P^pLxLgu^#7N_aMsIO*Tq0a}CG#k-OFh=jp1WNMe@ z&c?WCT4bX(4Ar3$Q6}X9Z{D0CZ?7YyL9iH$*3Ma%jgs-IJ1VPE=)sRr5AN0XWyl&q z8%ooHuc+P10t!Zqjs!wR6s0A)mPMkHK}gE8rgSyD>sVGZ62Zv4I&FnGpQTeYV|ws( z7_a`=y6g^Qo75*M%4%g9Xs0^>Koc!3YgJc2pKDT-(h81-u*|#DWa^sy5dn`1Lb$SahIQX~at=h$ zQ3n1kWz#KC$riFbn0mMC<=&qj&!{yT)d?&d4eJ7SOGpm93 z#^;*-p?ujwo~wNN)}T@(|MnMJHYLRj07~;wN7Kq@#4S4j0OucEs9XCC<@0}k| z#H(&oWlHO8eWq1%s#R5a*xGJAmf|e7q!!QoV_}faU^|evHg5^fsbz}96OmN1r&g}0Yxv|-OZBt z3m zVs(;wq=~n_MBRq?Oz&c16yaI;^J&m8U4D8|e(|lgj&#Xy8tTurCjJsr54GC-wYYxH z8z_E=2Ntqa@mTt783Xc3We5OgJAIZLJ+yHE0`b72sV(q~`7wICRTONX`EhN|`0DLP z9VrLgm>(|;iRWB1=~ZD5nfnACt57orzXD%k5F|+aoSxLrf4S|r1ED~x-PiQ@pS=VGd~`|3k7755!i~M+9)V|N`9ur1D;rW z#tFnZ?>$tX9T^!Jr_V;YcfY{SK+mo92|NV|iFPX`NM%V@#@J8cWSOjrb<*Mhbt}QU zD`V}xy*?Z^{n8&{7shb$hsINx9!Mi9KnQ)eK{j36x#Q~!P4QYO?Fh3EW`H<_)5W0e zppXKF6U+X}TIGnJoXX+#;pzz37G_eV19o%M^gYAV!*#O^SPYwKkFpw-xoa!gQtqgB zf-|%ue=>Hk*nWGaS%9~8FDiu(@2N?5OWY|+`GB#6tJ?TvxJ(u5Yf4yjHB2!wRhx5t zdAD*}VWO1uShHPVT|j&cDaVzQW|QcN>1`2kg013Y{hYm_NHvXl+Un!?;G=Ju=bJS< z1*!oz>zDH4&bBxfsJPx7c)#KUr5f8dPEml04*D26Z zt@?|J7K(F1BD?aly@5aL&3q@LXt8Ww%gbrmp3wm=Hwar&vBk)dFUW#2HKlv{2Ces6 z8tGfSnBz@4)P1?Ye91gZfY^9D)sp*SuHKg;n=7Ffz%58DjW};fd?aA{q0;m2eVJew zl=VjPxwX?<5~SsQZHP~_Jn2Q3TQVk2IxUYSN9`WSt>Ky|?-wsD*h!AsGvfPI6w8L1 zdEB4sK{|r>mSf05XH>41!{n{Y93KFVP@G3JZFhv8mpvGwEUw+Od=+a7J&E`tWW5kb zk@W==e7>w5Is?6-Bd!Z#vV3yQR=o0S%{X+$za9=cC7w2O|oC2o7Lz}%k;n4)f0^^d&s zt=&Z z4yI*UTb?YkXd^5%qg15g;?hjA1|S>2QUy~CouMFVQ`*)zyLBefa4gp0HF|UYam5uu zZX!zN8O?!rUrMPyzl1I+DGfyg5H*<@CxkxtW7g&A{OdTE3Vj2{v>4)B;uy1fzR~^l zS6?Mc8LEFseoifnC(fFeeNwrlb@rO=`%NZMhqdVy4M=#Mhyr!*!K8Pg0;bib=x7dI z)TUdGeuWFzoyTT-3QWa60Z|6xYly86nVbuvJ;E9i6R+v-s10Vds0RjeO6jmX+iRwb zkHR}pWJ5KHFL#Eh$1~@JWl~F$)-wOI#O^`q5ua zNFM`i5?;WF6W>BP)srr9F@U7S-VoNzVn{IRK=7bmFpoM*z=I2j|qh2DCiI6=yj5uRy2 z7ibY}eEC#FS3+7AC)0kPKTRv#g)jD1PDRoeo2xB!ek@dr_rISHF49u!kLm>nd>Ksf zvwf^uGphK2GpxugN|aHROBU#`^Neja7tS=xX=oTQW9od41_(GYa~$B(n^mvjk3{&9 zYKKrxvjyqN=WI$)C?`Th%sXGCX7cOoRi)+TG-B1qKdmxlIyl14Pc?a zLKk~^(V-FpJvkK-SHd@d7M4MEB=yBO&-LBdNfH6(_;W+Un|1tR^3j`d(EQq2@Y+R* z_H!k9CAr5oQu!Gv5)3;*Xc!YmLrI8H>*U_;3rD#t_V2E@yr9 z%5dMoS%=C;WtZ`qTz7S{PZ%pn=6Yjn0lA!b-O+k4dMiU#mAurD6Tq4vhQ5h0Fo7;F z*o_rZZ~9HOR`0ksKv5*@s1B~>udOb0KOhsq`LmpgNtqR>P3uhcjuy6A%QVgOxB)HM z>!e0_<;-z5}Shd!{f zFPhrO8wUrg4G2dey9h+dn;+)CB)tn0pICwr_2Q{mX4$LbshC}wIQfe!JGNv4Sd{8z z$}GYqSkMTve|&uE)3v8UKPn|B0$uD-xlH-+&4-V`pL@>dJ4pB>9LHXoB>4IJ59Onp zQ!z&nI3MP^qs&+C!8A>p?e=pc)^xWDFRVGuRhg5OxNuo|WgXeX3-yf%^UZ~d<2{6H z$5!uSA*&WvE{Tc350ENE28F8(JC2Q3S&rMoSg^uoNf)QD#y6oo>l?6pL@f88^C_-@L_w=%5h zU}=C|0A`nW=y&_#IY&ovtGKt!TF^A2KH|7dpCJJ&PljeiF|z@7hcXND+9DGLHAbFu z!xOcU5{u8F)tS%W=lHCUZ)T$a?ACxF75| z=rp?N9pGPd9nHOb8^s#pc}gHT+Wq+0#p)}+09gV(qup;^vd_&bHW`A|*dgOv!r=1AGitp;Ly2qyB!kN4NETHBrE?Bjj@ zNVH7&$jzI#G*gwbHeY#DIVfg}ZkB8w9-E#*58iBf>h~0%%r?fdu4M8;W2#*MUMmUhvjvJGmCe`Dl zNDCEp$?NNoq|JO{2(}T@kiylE5S`^D>-2;$x^Uce9MGxzP%7rVQ}zR5OZl>n8dO(N-Ksq0%Pq>fAhm|W$ zBI`fF#1zQ7E0Zt{Rc7=tm5z~I@6D;EAiRg6-S5&q)nLk9c)5KYeM;Mb@jK>_{0v#~{~+P|ftZX~_V^s*J?rWSA-+j^ZPt3y6~;>$(EV z5D(N{KPb;O4Yf>;X@BbPf3$z!J{UlW!>)J@TtIcH;oZk~TQZ>H!r**FYBXn{Bh7m( zmA&~i_jp{75Y!gu+f2KzFMv1fNMFK?sAiqN%QoF$iNn#q+~T&UU(UJ(3#4?FGVpF> zz)S%rS8^mFDM8GmXAYbV`jL4P%^XbD{=t`=sz3@GR)lS;|^oTVn-@c^6kG$+h$O zmedipWn&0SRtmfPA>k;QWpg@px@v^_-U?>|{VYsU8wF7>6oq1fh;bajy3S2qY0w`2 zuq)SWXkB*MhYiS4j#E|ewl(5`LTC!-LU@y*2! z@9a*=lJede>PqXxdn__GpH*s9M>}OPDDP9y`$zlSURC<9K9Gw1@7llBUF}~;CWzmQ5_LFftNp#%3kb@oHF5ylMJXcr zO_Hw$)}+cRdsbjy5j1A{=Eb^ju4e{Tg6c#uULv8WCFz|`4q95AH|$&2>eb?vZIdcs z0Um*GE!mTCX=6EwjZ>D+tWWauPb=PWT{&#or2jR=0D+XXAyO&4ATOu&l3QT zTnDS!_wBWi2(*KBNG^ai>yZZNxgvdW&~iD`ox@Zh#rb5u!(tSF(5huakuEVG1--#0 z754E%sA$mjWEs0kl>7Ak#sWsS$N=)D5#&sK!D!USHv+6{7^u#=GOXg-jB@XN0VUR9 z)~#9xY-{K2UE*a#PhLU{XKZ_p1HL{m0LjfH`l#HqKObKFZ5U+`SwXQ1T%DA*Wcm|l z&mvO%w||O{zZEuWxpfXs?8{!SSMS&R>)={z*wA4o^j`HfoAdyh0^$$qq;h0Iy<66< z-l3Mkw9&QS{m3G?hm#ceUd3{2w-ES(-SvCzhlh-YlTxIdCS3ER?^d|jB~Pr98BEbj z=$y-{v65EVp0BZ6#Dk|e^SsU`S6_OimG)GDlw@8@2bVFEu)F`oo|148c!XXhb`=h< z55V%#p6 zQbkHhGls240SiB?X1KbXllxzLxl z>wt!+0|&BBGt8G0ZSjWHQ%*c-r}gcL4S4GG`rsRSWx%O&21rTN(hqD5tm$$ zt_MMU*7bP&90=QPf$0W(vB^CV;%kB*1+(&&C^l2sf1zTbBJXlH4xGbnapVGeMZWGT z$Q3&q7YFsmmmGk0LT|!h*Swgk#RpcfP25;5qzFWM-4q3DgmY3 z>`#3$CezRxY!0?yY?fa2&2&DTkmwpXVS!?@fqE2}6q3&DifW~Ej5Bm#ph>C$|E)Dw zl)-}15fThp-0F{a9I#{x*?F-?+d2k}d!1HyEt|r`-#JPU^Wr3&n`#c61obf&+a|+bO$W2sD#Nz-iC~>RP{x;k*51T zbV9mJ0@a@lu{>06G5g~SMn41uYE2m90sv7<9(7)N*O&$4`x94W?A7~}+)?%s>ql|5 zm1f)S%|%f8qE})cKRE_>58wj$bkd--N42cy#+9l=oY$=T&UruEORT{cOPtTIY(`MR ze2U}9Hy#coy>%X50}HK}jIW8HpxwZ=h_oIU+E(Yg(h^M$b?3!#PTUFBZCT7V1)y!- zNjWdS^1y5c6b8Y0duxJW0~UjVoi4@mhBEidBfa;=9csU$oo37`_V_xw2RP2}*6NG7nIbbHGtgOq$h*gQaT7pIm}-V{Z~Wo) zWQ`N!Nu1O*os5#yl|8=nbZ47yA0CXl`gHNI{Dm%du~X#{u{@?DJ2kwi>X=~sG4j5M z#nP<^M8b>PU#(ewf3_X9YGdn}K{H!-jD88hsj!U+ei31^h{d|%7RsS!!48PM#h0{F;eCuT4N!fA0pdJ zKA0iY4j5P4rkNZMooT6gE2|Ja{(bZtoF8DLqw>STSbDkiOOUvxW!j%g)9LpgAoB2PJu94a=z&*wVzZgO{N5hj^tKwPYod9 zVnZaSL(m9yXM2K$QP_R8E`SL+)ey~LbDHzTm&Sg5)~kXSZbF#!D~JSBqtlzm&)!XS zfnIvrKlkn1b-+T3Xo@n1-=lc0jlT`i``AhFNNhky;Zkm{*|+KdaqhdEVq$tRRX>^N zL4grn$J|IejWWT2NKBb4-J)4^yzlif&Ew?gqpNXJxS!l9AgM z%csUzBfxcDJnd4m$LfzSFHEXr`br&dS!bZuZF;h+K-bfe+?>?G*S!SeWYu7C)>cRt zWVX#TfN+5XIQs-`Q!`IMjXTOh1D?1vQL<4mI0s#BXH5RaXp|kGz1mJdw?`+9Q%_ zSDNIF(JCOmkA-yUv%=|sBkArO7%XwcNZmlqu1w-|iqSx-hIDID#*zje} z3gg~N71}-}X_V{}KQmbF97}(hTQ_@43OZz=@HC-vhr)#Lu?F)CXq=Nj7MhcN!D(+P z>c)3KAHpvZq@Y199%w$6f`=Dgol2AL?|v~+CF8M{X_0Lf!5cllzgI8l{wAbi>wdaB zilvGO+gT%v?{eK7E*m!ndCB%I4uLKp?ZLNS<9;e#492!yaRnbq#%4kc3vx3Tp`zra zcG%*ZPlkPr&B@8Z;Ehp3mpi$H`1qjKmIt#2CxNnKz2HkFnV{+vrkAf?D{d-egpR_( z$v$#Id+a_JE-h&aK9;akM;?vZ5>qg%bw@w3RY?#`c1)CWR1eWkIUWgYPP$H3x^pR# zkPYGNlh7iiUyuS6Yl(H|RBLODNYORWc6a$m$O6=LQhZxF(4 zSL(y|3zS+A!|hNq@?J+t%dfp-k$8|RK`MR)Iv4#MM(b9J)URc3bX^-Y$)LAUYl7Zb8T%0nzUY*y3ZRh3-5J1KY;E1puTuCtNzd~4{+~^`|C)!ra+?g`7acnAE55{u_-E6u-}-;aM%Yb%iP8917;P%p zYi^fwZP2KoRaIEbLeySOs6;nY`fu~^-O2`T&R@PAr@Q~Z&-^Bzh z%ky0op(2MPlgUuvd#eSqBaq}{_$);`?@oIbYOw3Ru-d$UlJ80FLYXLQf zJ@*#)M6dha65=y6pAM}3?d|r!TuUT?X!J1%n;=Pu8h;p43OdV4k*4TnfE}xs>zKe( zcUIJX%N2h(2Wr}O>El1-xd|C@!{sHV0^&$ZGccNBwiRFvKD^tO$BN*je3g9=kVFpX&*Mv@4M`H z$>JHPR07K3fu!uV?b4gkZ-5m;v%3JrIDFmAbbYa51z+$*5vozP@}5 zndz{LW}`Le-Y7@%l?)X-XOv7P+jN%)U~({D?p=|Ue;MB2 zCWBd5(1TBI^P(hj7QQ*vDEIy24>#dEg~hb>ntF2x3pWlAOM?#-jFFL0y9|T>*1&9z z7jgNzMP_^e(6Pr>$OKaW70SVIsHUcdsGWsf6B3d@WXPv$H(U%krRe|~hgF2w2&)1t z7;QIL9SiCzf8o}$)|hRvT?oA(P^B(=h8$D^BOYn)JUf3eyNsxSs%7T|Dnb}o`?3OWxg#e;`F zN8$lW4gu;IN9)}KaiSoSs058G;!i1$wV{^>#`#3UN~Rk|zYSNoGGKq?V7YbEFHV*P z1qHo5S>Fp&r2{)hdjX-13!b)ufr?k|{$Ebwwub`pPmPj>4k2O531w*@ zgP=g@7{I~&95>a~)k!B>Hk~1U-#Dqxb!+1-NW@#B%vjSe3D>}1X`!!7lRMm9@ZNQA zGQ=S^jX!+~DoyUe@4yglbEoBaZG53MM{kVEZ>!Ru21PU;B*nq(UvAvMZVAFb76MI+ zr0Q`m1Cub#RFVj2{=ot;SZ$X^~D=oRXxxt_ngBWrzowRe>*pVvPElP$1nz~&A#Gsx%5&X`kUZn7- z@85%fQU|{<0Qt?KAtA|r1uGx`fm+#O7?Do%#RP(QHaS2GDvdnx6dj`fsuHHs6OhQZ zFs{VObofhW8)B;=hHIDoe^(C{%pEurp0F*6UEe)7LlXPvR-(217HaTMTLkwrz-2eM(LkuFp;Qkq}v@&;6bL=fXZBV9FOgO^c_xdvL< zkdQd~UNFpdxS>w*8@X+5I4&$K?AjyX=oNP)PUKrCkE6dQNLMf&QiI_rO{)w62|OUZ z5?E(69EXxLkc$oGTg*mXP2^S1-NpezOaifU*dzC8NLm8JMj&Oi*#6w9>-}fAWcc-# z$F1p5J0W20;;YapW2@42600_WKUhbMl$CIXr%;C#3bx4{+5RlTwdcyIuEE1O7L z7?9h3T*T}fS3EzFHZ2;)Z3h3orYl{0#fR;Z0iFO#6)jJ&S$9#)zsVi``xWEM%k+5u zOO+D0t4is~G0ljD!!Hg1Mzq)F3ggTSN>Yz%r&@f=P;Nx`P1V{^C>ZsYmhNT7rU;)MfuJOX}15Tl+ z6#@g7QIcBx07%LG6QvlA9m@sE2GA#9qe|VyVHqI;C;`nRX7~{=_%l&mzhPx}#Fowd zeE@sA1y+a7R9r(bki;9!z?h*Y8UTM}k#@I*3oqj2ZBSP}hn4ruviDD_OGXK=s4NIQ zK&lL)p$I;Fn1M+^jBwI(GUVT(SKH-5n0dEOQ}iK)<~p>L!MDCVUXfd^lVz|gcZUO~ z2TP^73Jb1X`^Pd?xIx+7=gayF0FcXpkLtNcc?R`={*&UM%B;OSbyXgxU!)jAG_s|? z9ONB6edq&%#b%fi59p~z;lp5fCBSTAi_aUcIL7r{@GS2n<&^L(G+LMa7^l%n3b)S)kA7c7ep1JP zNGD_<+6dCU+Bn&~c{v(@PlNJEQT35Z2*D3>w}1e6!)p$wt5E6;uQlro244d$2LVs; z(9GB7Ou;bkOeR`jD_oQN7?_!Pg;n{uoB9if5`28*0ZOS+;JPakrez!VAO4*hq+sGFs>|&@FFv@YDB|806Z9h}fQbs99c4fU;0kbs zBMt&xz!AV8pkZWh@=gJTWSk)I%uyFgj<&n(!Ecp>Ms z;1P}S(N1wWxUs>@Z<^K*)Nm>K1*KwrF8Dh&mQpjsm znrj=qXR&mI_8I)T!h(*2M$qyfG=(lGjfvKl6$9KGJ-hS>TNh;{NtU2;nOb< zfo>599YxsN&1HOH)$JMpH1rZljB93lV>}6HV7TX$l%l~N7$xek14<6|3+&@!E15&y zWyDjq7gW7AMrk9I`|~a5`wM&ovzgo)JgZV(`S>We-^Fgk3H-HtMew?-XXek8{W~Y- z>bm^9cE~&erS&;UBj&5!PX`6@$b{o}O$tJsVFbZ-O6U zZNW}P{tF1(vKD8+{IBl`hXj;Gn#&jgR*<5SkhIAv?NQb9@fG?+2Nj&82H3kEFs3P^ z+MY{!HCx`6lamk^*UzSaBrO%!cgQYuZ(LyDB6$3BD?;~dZxR|qJ%A5|smOygqpb+1 zYc_Vxg(A4@dfO$OPui@+@b#~kUwm*-gfp~|$s525xr^z=Y75tZtRoI$Y(7+iYV|7r-5183~0zg?Si2YyhZ(me@k;_oqpzGdCe!fo}(NRFEH;1MoFin1z9m8T}@|we;$}t3q#&_J#0QI#BeGN}uwdSo)@U*Vw!v|oRT^Zwu%W5)S*UI&9q!qp z%rYx~QEPetZg`RSJ7rwk=3opu#f9iw@Cv3KE<~*)v3Va`oFA7)bCEAF~srXmY3jaeP1=DAU5&C!-D8;;jQn_sT&TCv4kyZ?4Y~ z#`nUPKxMnpK z9hQ+Xn^5JEg^N1RRBS&?0lf=6B0kgV%H0sQ6SNu5D!+25YJQ(mOuI>E;q;Kov<=WE zY=!_0JSQnC^D)b!FF+|H<-i4y^qo3;c1_CFF!$awSdfMmaADyx-)%c=Y;LN@ZG3~wejo`$&)=fJ^n1CJ&K{)Oim{heK}2a}^iet4 z#me_sn`vn-+yV*G?s`7J1N9_MBpqU)ryojEygw@f69X*47<%T(X^7$An|j?SqR?Jo zUh5lin=S?}@I!QT5S#Q6DmySFBi49vWI3!(lsjC4P5YIF3*gSB+;qNq_ij*S2%H$f zF}upPUHtLlne{DuQjDyw%BBHnpf}q=Ex^N*?$<0tO|LwiT-zg;Q=sqEdhL<0gecBT z*u<(Z#mM14B{1T_KT&0F9Dm1G_O74$b@1zEXiS}&NN~(D3G(ZQ2+H1s&d69Hf_v=c zaf?<(SdwU-e!FMT|9e=y@jHd=OQ@#S8O!j0btL&Ej4 z(+=7GNPWJy&YMY6$lb_Bno9S-1xYhJ2|@p6ks1A$9;r9MPc5u0Am!g6NP+yO-`|b- z|CR*#|7qM+xJ<>q`x@VvqT2(lmzHOHXlZ`Xz+hm-$-tm13ZaGP9CxGO9Ch~n4qJ1q zgr~2yI)E&m53LvgspDuqxqrtpjBU?Yq2eyNf&3?6vN~7IupLq z4M--S{{8^}P9~^|D73-Nj}#eOBb%OVDA^sMVLn5C%cEJQ`0(-f?TV4z|1a-9g6=N$ z|17&Nf0RwyQ$Z!GJiw;gE(zlC;-A-M7>T=nr+#3&|C8q2o~!@Y`?`p*mrZu0=zi=% zTI-(ARJQy{ZqLcB^Xj<5%v-cH2VVUh1vvh;+ej}1M~l8 zs$#BL*&jZ9xLuVMp!y~1iJU@4_QVVHG}mvwkB+#Kw^~2;i**dGQul4l($Juz`9T%l z4-ux`GtCN$%jsL9ozkQ2A$KR-2e3((0^3S{hPC(xk&#{IMI#H|B6}V|F4+U z#@{Kaf2RxvbWR`sADHS1GSc5D?)Cam zh7Ny;iLfl}V%q6Cn(*T+Nkw@){PiqqG*3C$zq#t(magi{g#%h&21UGG^(UhD;Nip0 z&H+zc*WZ*lK6o%eveCUig<6>ek#~>(wB|tZ>|H3o48Nk_1-VcxiS;Uv9Z_{mPqryV z1y*WMGM#z-%gy~SZm63yUIMwq<62_!V2K@RX-Uj^=H*2Ifm3Y=>(8I>CGCe8b@E0i z?g`e0-~zjp1@mCqwjffjzRmdl$Y^C*0fT_aHRl0!Xoy`Vk>bV7Y=Jxn!?f^p{c^WW zSI403aDEzIo^3`3@}ebO7O9>~oQPDUqp*xCsyRl)?#6D!-+>Drz=zOMHIPb-h*`ri}joivF^6MXoLbXobX>m#SE8bc=Uh)%Cghhp0xpU49T;DA0sZ9k_jL$7?`RsrGiamM3i z8<2Ck))3bbFF5K82yB6nbF)v=AzGt%R z%^S{~bdgl{^{Kfz=5^j);N)B4*L|X3UTve0Q>(XWLE5L!Z`l?yxnL|CP7ksVP+W@2 z%3>Gg12YV=C_7#rST^yk?F3_;Oatmi*U9nuD(gvNN^8tuI00VTFxc;8(HJTSL@jZ_ zkfD;M>7itj{~(P*Vrq*8>>6BaA^W^{ST{D7u}nQIV!7b$eO=~6V5Q9ewLJj(wuO-* z&2_kpZ@oSOAx08jGZPhdBZ!n|&nViY*{+|mOQC!Ipq*ym(av@n{fcUifVLmA-ZmC< zRvEV-ZY%!3AH&=KG^1HBPc_XIotdvqeX4ourO&H0UDe9<>6saW5)&^n%!y$}yxQsh z==N{lq~;dd*{Q6<6570`>6$*p)zvZ~EffpO@%_#~L-!|XR0ZP?Yo^?=u5x#Qol|Ad zb;O{+J)HdE+c!>sw$Aq^t#lU3g${$1p?)FvYUkN0m&M|AljN->2kcw-p0~Ur+a=3m zuMV1eNhcX^9@$4nZ%Z|)-}ytP8a*GQm2WaOC|f13*YH}fUFdCVYls%% zu-wQ!AEBQtv0F95xqYsvSkR)Gr4Ng1?B{~vu}Le@sCB%?)1qE8TD@(Hjdcxp=#C{Z?NmG(Tr=KW#pX{o8Op9! zS9xR`SDBY>Y&u}8@fjIgplLUPbMEuX{r`xvR$Z7ZYiMYol8ufzo?9n}5S-$rX9*I{ zvx;8U)at54c`wdgGjg^mR>ZC;kPq)(0pe*HnGxv<=Oahx&I-AhH-;q3vUGW`^N&By zm8C|>QhUYYC0w@`Hm`Q1EXuPf0FOAiXPYuqHM@ReF>vqRy+CU*uN+rTxW>P~x71Nc znP|3HEw3I21v3wAW1oQFFQ7DPNZC6$;hF#X5IowHRHu2b2_1c^E z3P3Cv!Y!J*FcUhMBWL@FSvruNRkkuQT!i#7f{3;VnOpm-CR;Ememey}7Z1<&qKVUl z-B$3ccKM;9dEA`BGA}}xk=;dxfjQNz6tQ#X#uQ(oGa$Pa+`7tCkf0Z!A9C@6BF(e8 zHW(i$?Xi=-)t!Og#qU3_uvb0UFDabG@n6%X(aMBOnK1WEvhPDjJdivE6w_de!8+?A zPbVX=nKE{boI6+UuraKG(`E$gO^xhdahhy1uD)3dz;l*X5N3++)`eHs<=l`iw?85uS`w*3j-p&*1o?u)^2QQIQ9Jbh7!hz{a@ia!d*=BD&V`W0Mw&lfk?9>t`h7 zfD2!?5q5Dt`|myeTMMw8Q_Tk&glof`>%_Rz`0r(sCH6zr&chOEh^lw)r`DN8!P5O* z6<`VA0UtE3FkJo=dys&8>oWIU2YZI~v`hCjl%};IySyhDCr6>)o3E+#Fap<+!~G~m zfd}3mK}%Hj-7hx>OYE1|s@aPQ2t!H0Kxwf1K#(pe8-uEnLh+uGobAohFI*C6zLORV z+<4(5QVOt<%{r}=$y5M!Ov|6#X!68(?IdGz7J)|5$s+5r+D!Ud0QP}&DW)1tHe;wh zikhja@kZq;(ZDvX5Zc}`isn7c8;jTvz9xlm?jphXFm`T1>yD!*C)Ba&D8Jraf9Ss!N`UBeK( zcWiGiI*UiA_J(BAJ2=kA4xHqSuW)nji9@;c*74q*YL3LJ4Z9O{A+vys{VeXo=!EC| zK=&cw{>rwvI=_0dt?npb6+v~Esz(0O*f?EPoCSbJxl3#M_wUCl-ew?9dT#u<@l8&3 zVW_%0%MfJc%1P>&50i#jHF^UIFMj+uca=i-q|=*XF+Md<)Gab_{^#!dk63@4IdB`$q9JLiY9^W0MD!oYyu$8X4G>b-?eMvNH_B{{ zj_wH5eG!^cQ?F)eSF9a98DG8Qv?T9E8z+VG#8*pST?EY2@)Wmr42&xZj}<1lwbkRq zWSpA47&^52f2z?T1fFI%NLBhA*@I z;Au{dL%Nnorv@}ZLy%w=-6KY!uE*IJGb=$Ztn1Fnva!e+cXa>F-4EFv znfq<74%jsAzFnor**YO)TGM1@xM(BKlUH;M(L0?2iCDH}IloK$IlqBYJuNVtX{)yP z#r`#$?Vj0M-^fq8*e@Nmb(-&?fOx7R`2HpJm_EX{1Yw(QfKP@w{22O&KY-m7RFpDo zTaCboU$Y<{1%>J1vm&gL3qE4SA8ghWz}BQIe<*D}*(7>bK%~2|b`R zTu1DzmJ6m@7JHIkSUjtQY-Qhau?nEOEx_EM;M@V6-wX=~ScoK2f4TF@VAjA-KJR#H zs$Yhmrx7r{DT)U#=xJJ0Ltm^w>CcDS_h&a=jtwYID3{ zFDn@zhzh-VN2#o|e116Z239Xup^=9HrT~~qDPJ%J$Uq} z_mnmw!7y(^y`mk-6Ulo;wkc~CrY?`!_;-231+l?3XO6?qYi6ykUO`SkVBltmH&#xQ z+qZ8YyjZnfK0p9K>Z|ogC3$EVW3&0k-n|nt@o@=PgT=YInF!@mSnT8NYBq?G=ciVWannBkX7sG*cfH08k4F!uw6mT z&`2#yxpPPE4r^aknq0i){uiNs8MDEE4EjZ6KAx)i(lMF7Fj6h!I&#xMD@;DUYr4a# zDU74S{nW;A+1T7z7ky0d%Jczdn(JT9r$79v6*#G^U0GIUV3iml z^6qW|k7=I4&ep4V)mN-(5X>nUkR|ykiAQ47Vu>t(#~D?fUTynAV_ z(@8?hEfEnbcOD3Tph>tBQ#hT`YhUg$SgzDPQ{j>1{$X(c{{65vfbGev%0WV0(&zP` zqP9Jhwk#WvGcA&bgZC#lYeR&CYN(vdM}vjRSFAFnh993KFD>P|Es|#@3&;mgM@m%e zrBS$<*&~Wd|3CISVDh1F=J9M@xBSl0+M!YxeRV;LwwdMU@^dsn`9Y?+5P1NH!vkC} zr^BAu<)kk+p4A^+M5K&5Py~@5K72vE_SpJqRtJxYoyh4N((g1;<~8n1zlca)E362OL(#o{{ipO1H?7PEWcbzUiaFrAjzHdRV3&LS`ZCSa zq}qE!?6>~`K2B-^)MT+{;JTJCikzx5Zm9sBPxW9U?!$+ z3UMAi&1VA5FBkcAW}|*hUvCq+N2I2Hif4!%akPFDZ}^U-KPQk@E9LovGGFb3^p`)< zeE!hhf_E2!E$*~8H5-?_!Z~xzQ)onzOE0PGgO?$Sn5O)a7W_0ui9%yEF%#=(V-q2-|AjBsA%!M_{j; z{XguzWmJ~!w=ar>NQi(6f`CYOcXxwyhje%IP!b{{B_$2gE!`kp(%m54-L)_H|9#iH z##(EiaXy?q&KUdkgC87^&wW4leO+_TYtCP65lPn^hrYR>gtE~*-MMCEVw&ik6}&E| zT(g?5LdU~1W0!7S3Cc2Kb!izLm2O;LUbZ-BzSX*`j<<5zy3B|lr&TJNr7F)-**F}P z!JXDgl}Pm9WbM{kp;VRXwtTjGh09yl)7uN`d;wlwDkwg?|DA+W*7@N~q6X`I_~K}< zkpngzIPu^ItGVC$w5{}CPUSMc*3kI{g6FjwjdI?s5G#n%eTn^=GyP%oLDw7O>U|Ay z^a^8-r8Ovl$CuyV&-u4^S`e9BJCM~tp6j2$R?N{BCDtb(Akfsr(LE;c_SFR~RH$?@ zro0Btevb@7C6EAhx?cQG%yjD?f7}mOuGdu!x(5xbM$II3a z!yV7cHU_3ACs(>6_KA@^5BJ4(cEc%jKImn1D?LSes#f_sRC$LtEE=;9uhxD`{e(k0 zhBjemb$53ow!(d7YSJE?bz@Fzubz{olRLftTLwrc_t8%MyJiKB>v6zbovvwgK6_sh z$Mw{7;!t|;{z3y_GrZP;iz8YW(=w+5p3%mK-JJ2>n|UBW6V&gAWIaNKicPI3)CZB#)Tt^l>L(VRn4 zR1RXs@xIvT;VhmCZQ^XHzF<%^u0qBsm)m>MmCdL2%iWcxH+t2AMNAt$zIYmV97Jpb zy|XQ#oNtcRkIH2YqaMu1sJjPefBMVEljaYw{o*d+FK3yx_4fy*sYLf11OPN;@0-kIpj06@1$y`J=H9U#}t|A+^%&U)0{roz;5vca(guD?00? zQnNSeO`5(sW#$gwH2#?eUumV;;MK2UCr&^}=wzX;mLe+-9?teIo%GZ($Z5Kn_R^dB zXI%0bC{Ga%r6Oc|dDy6y;=nEVi>STdzaNK9CfDb(b8FeFQQ^Ec-jb35981kgp^ zR65c;Go?$H*dcz&)Wf8@H$7gu+L<^tOi4i@pBzHbRz(Q!gfml{p*TsQt!)}*>o*%g zK->)?ygZ^M{cgXw)vK2;4XXv|<N*I^{D))IxEdkFw}DpL{HAj z!X$QEd?q4Q2}f6?7g1L^^UFJcpys7=6UFHK2zb3d3sr2BRQmGe6I+QFq}Po)e@~ji zEr)wp%y<8XEH}b4q$9AUHeyMbh{%-8`tv94b*fz`q-(xhmha-_Fc^2e4b29(2bJ_( z;Y~h&?2A9hDzd-%vEAp4^1;19IU8a;B8SR}NrYz9>^7(U}{gK3& zMK!XFOn5ljp=xZ2!C8X`Ven}FgkA=PW{u6mIDm^Z`d0Pd@x|Yyo2;K)0YSl6=M?@YO5w`G0cj%F~PVxXMzAwU?AErGL37RnF$iPN!S*ir5uV71D=d zytb@lay1|3_O|@qkjmv`{mP?T{X~oRFC1`8&tIN?3}qPM@tXt2S5J*!bt$0W-6?#V3c+D>H?01 zE#q8a28xeC_-|%jB>b*@jOAI-Q0HD><`XGt`tqqk#pIj8`^OmVZ5(Vz08o-0HtqzY zfu`Us6)Ww=ucSI){J4MjIH$L|I?K+2N>OQq1gEq}5kXb^q?eCX~CIEtVz?8VRs zVt{ib-{0vn0{}sV@D!Wb){(hyM-K`Y>YA6|%--3f(5Nq36qGnPLx0hF$fQdSH32+h z-n}+BD{CD58jM?d;QYnLBp+Yvb!JqN05>zM0IuiP@?LX_sD>Bg&-UE*kWb>E15>I! z$8Ec{Q)X(0BCg9d|g}fuQh$g{+|g{R&|ckN;K(e)ybaF>7VWdA?0ll^Ct{wGV?e>Ul#`QJZW;Q#-4KL1~{N&l;r^`Cru%$xSW z&&Xt2`4=z^#qa(ELW%lso-;6Hw*Pmz_xZotiT}&@df|@t9~<%erx^F2%-O-w=%3&fAkXz#ir@i3Ucx_x+UM_G+W)jX{>h~OYUU>I|7~LP|1y*MqT;-;>EFBOc7g^t-*DMl zUALWhQ6rYrb2Vv8WH-t1tag)s7v)d1O&N^(l~e(g6p9aepJ0W(x1B4ED@zUQ-yJdZ zKe{gd*ohLE@tdM45dkzmnuN$|Dna__wi3>+N@}DH7*A) zYm!9S^4kNuY=Q69zga{tGQpf1{K0wh^xql^y4}kUU0?X-{d}?ipr=GIsMwY}SFbrs zKHP10E2Feqe2@AslE0e!7nGgl6VrgRIxBq~muQr@-aa!&3@L2BXC6YylgP=T@rT}| z(QZLJ9}Xg*ojSyJHmv%u_T`6xzi#N?jYYm!N!Wjh6Ij%J&U^aj5lBkB!NWLYTi_S7 z;ZrrS5fZABh5?uGUiKbo)|{E)md}z%WRafwVzry|y~xe~N;uB4aD0hrx;6LDS2Vvb zzl-^VjfZD9zvm=7kamn!YS^WXue8ZTQwVT|fUkUkc0fc$1ni5wA*_Xo2gedI}B#GCCeX&_p$!DW^ zF(HQiYGhg;^AT#t4L)tD|9L@+YT=p0nx1#NI~}3Bm^5<9J&aD{b^P<4nLXTR`VIB2K0T9VA>-MD%BwRDbA*1L*Je zu4en9OX-$Y!RLNH3e&=#EUxxCbJE+DR{N`m>T((LQ~NU7*9XtASMDA{4#fsrqEA@M zy14kZ9VxK=?dTvCi#IE%u%j#2`x-17Y~-Occg!o=^K$#0|PISQ!_;d{-BF2 zV#WKR&4LS2pJ$|HQsfHzUOXnFL0=TL@!N-A#1PJnHJKbC70FhL5*JSgeAtRHm$DvP_140WxH~>hd z_3capWeqU%g9^i;Bg!S26ha1Ls~oOjqpl>K`&S26jTcxjs;8DEBmuausnJf_Cad)! zH;?;{e!ftw(gOKFh`z@fc6PaPY0)F;b5CaG7ScpfY9TJ4 z&VX4CkS80foCUz#rH;^f*UFvV*Q_c9nuEDY@eUiw#Ri|beg|)jn~&xa&b_B&vKU!z z48C*P9%&mLRkB{#+w)r9=iiElh2`P^aPw+=W2!*K2ihHpM9|j~t$>ik6Zfv)!b*8c z3fwF+`1K&Mc4gWywlSkIXG?BDx-(9Nps>KBx_x;eG?Qw|;;T20s zKm>PJkG(p<p8%TQ)pIwT1m>FsMNn21A~6nId@6yHCow}B(on58@zznL8g>j zHnKoPU8GeH%$55pHOh;*^Z-lZXpQ#A!TgDIeE9&S0}$?rMvOa&p0Bp&=azT>(+d#Y zG*l}wDkqALM|%7AEzl$ytXCYP(Wy!AFFZLqD)(Fm>Lm?FR<(c|7iFrRH!@2_xKXz^ zGg<3gnX%%->$NqM0ombj1PF%}y<)vKOV-s25NEtwmx<3}n5M7Q($)+DL?Jb&nqMQ= z%AoEb>_?IJ0tHYZ0w1N=J@z#Fl1dlu8tA4eo9;7A%B4GSQ&`t;aN383sxM^2 z3^qW}36IVu%u$lOY=g2K81BshB^@HRiTU|P?VHE)J73l33KKc!fJrf{vvd6l0PO>n zCe;ef3qX63wA`J4y45PVLfNZPz+!#5w;Z@+*OI_)-vzTXS-&+_5MQ8PDGqQqmy4TY zd5!uV75-kdE;}czjE3Dr8I{0fL4!4{B=H68jsOx3O`ly}7PVG7a+VG<3JUr$SF<0( z)7s$(ebG>{x9`q|7*=Py?b&UASDg&GDCH{lguQ(b4iX}Hdzz@%$b!&q-^3pZj4PLZ zZ)V;<1su4^%5ZL+$$s&jSY_h3>I9qi_JEifgxTl3JwhD9GV~shfaKjM6$&+hyP9Ma zO{wn9loDl3rOQq*AVFi$XsQC0I{4GQdpChCmOY~`J-n$?kc2;xwhO4P{!*K&fb6Xg zwrq_R?#^UP1N#;b(^MMy1$lKEzl8#m-{LP=47atqI}ltBNi*_iuUi zTs^~{L=M;AzuS=PMnQZ9Vv}2|MpQ>#9^HasyX-(KF23`V>ITp<0^t ziy#e|Z}8~Zl>@ZXGPhC0S>7ryuf4h1ju5Q18UQIa>P}GWH)LF~KHE)<*61Rvsjb$a zF_TjVur-UU*+fhVMIuoY{g+_hFy5y^^f$gy5Tz?Q2}U;V#9r()5A;ZoFp~is>p`HP zL*UfLfLD~5TAfM`deznzTnN1LEa`X=iT95}VVw3D#&VBw6=snDkHl?NTycd2a4XaS zWhuv%on5p@&4N8y5NDm4R;wqPlGf`mrPR3BeQSqB9#JtfLKuhLy>m#!iw)>$0(`ko|aiARjtNx&OYjIPki{$z;k@1Y#?TnF^S3m1q64N%!G z?d7xt)YtFer{JrlPI6E;a82>{?g06QUqG_1*4AFU@-$&1HNrs}1AN3Awkf6l{Rzb* zmPMtai0}0yQ}!`5+W8>q835;mPozNO`A%E(#9@bhw&TN#5kRbzlLCz8Xj+Y#)kG&- z#fIN)(^nxeD%kZbSxF&DL6i9gz_Lsxk;`M#AZN%>z{htyUt6s}Js2qIs8xo)R`1)# z$Ki3H6Fp71Tpp{+kuQ%sc?w?UeaXdl*t$5L1d#(GX^mW!!H77s*h9yNJHLGs^vQTB8TAs-gX@!ct9%e^Xv^*1Q2Ni7NBeurZ1X-PWQ?n zb=EP5{iWg}fv};dEX&;GZN?Qat>N9`E}aOgZ>Nl*eMu`Pe_sl4k=}1`SwN*46oh}4 zjkyeh59DlntSlRZY+GmNXI|)qD(h4y<(Ml#$&xtVDX+O%Kq14eU5lNwwv&)X>+s!s z*#-ZjwG|S>o~TN5!f_rKoB7Sc=CJ||%lep!bn&BWIK|r zGdy69dJl^vLMU>d2hL6!!K4>IM~Hyba1s>_7@p6)E|1CY0-m)du-RTOw!pi& z7Mhh~J9(tb1DBT_Gq4Ru?eU{{s*@+W&Ey@X>rB%c5;?BU_IeUY`+-0ijW*kSl{lj$ z4X6E3Tkd@5aYwiCB+^I`;?t)&T3Om4pPU38+K|fN1PzsaC6)v#W(S*5lw+Y}{TZIV zAMbb4PwbxC$mpLux-K~TY)nkYhbxltchh8?rYqes<7bgTn(5BkX1^<<1IS9IY6CaZ zlY{EvGBtb2!~I17ckRauBCRg~8ZwiO*498d4Cw{ci&B1&`h|ssG5dh$jsoCjf?T1F zzE#@#n8!1}h?SJn8t~(bYbkx38n!Bxwpirk2CuqycTdfm0l{6(i6Q6(xf>`6ec?~L zJToo!0RgmMt#vYyo+gBi@RD^;Z~uM0^g#+Ok32CP-zELVMS88v*Bd zn^di6JTHRR>jO}11Twi?^oh_?qPa#fv|8QagjtS~Jr}^VoJ%fKioIumz30eerfLHu zP2jw45Fp;hmm~u?dUk7lX*x4?7me9u_N1G@?9!mJ2t>dP2n(oHcCU`u?jSRIq6 z!s;di8QscXcP6{S0agaUXtVWG}>JAg<1h1@P3R3rgM?peOjEA zTM+B7Tf+o^nt#rQ&*x^&3Koo!^b+1;>~I!U7WL852~cm2*?pJHbF#na`}iq#{goBu z9qlBiw0XUSjhM}k$hCVpA8*_)iye00!=UB}g4Jw~%q*~^9Ih`dRp{4#bII%Oaz60k zjKcvK%KiWWdM~d_pA_BsJSQL=&od)5`^{6JYAl-}vqH%_{iuOFXM(WV7ZP%`6foIYq4w z@QkETY4Dk$0E(uKeTfAzEkHS<@C)gpoBPF3hH}AtxKAR(&RHjlG?2Y0c%yYXk?%aU zoOHJHG?4whwDyNP7OT!gPN1Jh3OI-}nbOnV9gpM%>)ci(qH;$Ki~4GSV!pH??at3z zkNblC`J`lNKIHHhcWKzb05>Gf`4OYhFGY|R;qE%-Tz|6K8hE)A_XvL_u0ETid>?0N zAw^Gptd0EEKUzf0u6pLq%+6VyCdTg=2Z20&Km!q>9!rX~?Xkto>)qMSwxK(5^CCmX zC7%yxhP+3vS5Dy1Qy~7rc=VVb6_y{>H~aCAvR691ykXzbpGQcI*xEQGuzI0YQ#l;3 z!?L|VglqN0eww%BiuwcVON>Ye&d&0-!H>yBLJ%k|?_&xSS{`)tWTnpO|UWuS9G{ z)tzxa#(8_=7ppRnkjJWjPNi;jmTZqx|WNw9B>26P|U9XtQ{$x)? zM5L7YwIg}dj=QIZk|D95IbE1o+Hhx}^&Ah6Ec;gbN&4~&b93_?#kj;oCDpM0MLR5I zT;Hr4t}{}NY?-M{&w|V#J$Dz zv%Fbnea6mXhC!NTh5q`9M#AovMzF5Uo5$!oC-A6A7x1qN5$H*gi_qJ3&G6lz$vl?7 zKSeZu`k3-{#5Kqt2;i;2@;`o6Q2c`@DD8>D$)0f1RydtACn+i6(sl4_HGg5@a^`aP z4^RoG78O}BDkdf-nwpvt5)x8h7HyV`HyGjEZySDpN5kevOhSUmrQ3BoAK=%}(2$*Z z&lZ`FuoNsT?2m#HvbcyA9Q<^Rp-(1>D%Ql89v(-l!Bje*O zP$a2piHH=ht2-l`eq=V|2MP(S+`zp~=L;7TbL<}*3sIag`jtS9`UF_jVkL(`wU?X6 z;&+XXkK(f1bq)@8sRbjM_(XESf1cje;{5$F)Wp(s&~@eOZ@q!%js)xw6yr}M5n^g; zruGWYKj4Fh7)o^LB&=-fspe52MnLiyHX3($e$>N6_FMTV%#TSAcnZQo&tt;-r|qLE zp{6CRZKx(_E?H}b&q7xDU88y*r@8*-W|j(@YMXC#RzCK~c{1^Kx6_rTZ-q&UfCOqFz9nu&Bt(3yO)UtgYp&p7beaZt4Ckr=wE|gzI|aSzD@1 z)YYeec~-S%q0GHcD^NN4P^?faCgf(N*@%IKrKYC#Xh?(KQ`qYLxEbS7~OyY+jlkPL*31C*Ajl>psB;a5>S6rQ_y-UlPXJ%&BB~p&b5x* zX=$+165vUCG=mn$?u_W~z7@(V>k-efLvI#&B6XRQo?bjQ#v(18JlzYO{J1<-;rm3ii`kXUeRHvj~1Nl!c;MM&a!^4F| zKd4X$K+)_ABVTUy)^GWd#LVG(;n%Uf_@8Vz^9dx~5%3qy-4RjEFt4)_BK#He(OOP*N*&s%)3sl$2g3%Dj8WD{K@JAwk^M z2JHK&KlBlW;#OMk(a?mh?e0F}h*PDyU0J(Hff6W>j>Z5QH=yoTQjGLfC5h;I;`o@u znD-89U}2S3^?Zj_$JhA!JVeOuv3>s|`!(tm3+dhJqkWSbl?Er~(vm}(GAKPg14X7u zC1D6ePWOW%1^4;Y2YPY+QETZ+^gzGx@WGW8!}LP<7?MmG3`Bos=Fx+LK-l}mhwo;9 zvE0z;Wgd|dT=mYIq-yUvM`2P$C$Uk_z=VjtQ|{^e!26Fq(ZhOkS8{ar%f9sn#(sfJgbp_JpXMj+Dvzb=}%DE>eA9Fu{n^5J|xg%G2LnvGW$Wht6)ipC;n48Ve@WI0RezVnMOjo4zPjCdG!lACpoPDfIONF3&GzVmW$q z?GJ*IlfBv}kJCh-1$_Jn4+r-=j><77uHfy0w?eh1w!A#@=G#vD-d<@StO}JC5CC8O zI-dnZz`LvK=K@CxDHTq_U}c4gJ{nlPJ#4xA#1FOZI}lN+!oxc8lEX|pJ~!F&A-}|uv%do$Op zoX%Ko%TI{vgYUf2+UDkW#oX2Y-})(p8&zFHeL1d;3V(Xiw1Cpq`3yg! zCS?JYOqf8<)D(eC6VTK@4*&j5P!-gs*Y@;HYAJ%syZ}$gKz^}5CVXPZfQi%ohp3$$ zgg|{OBjdq>d_L8YhP;}ZTrKXF!hylAz?Q|Y3Ktf~PTSD2)+ces*$Ow9IfaD{wm^#? z*(iZ~Y5O-i78ab_{DnOh5Pkm6(#m)LG~iJ_2wmUa7PP*HzC1el*!Xl)sWA6Z8QQ~> z_*Hy!9M1o}H=NJ-dX{Ur{T;JHQOv>DZs^8b($Y1m!PS($iXG1R`E!0CQp$&Kkz*~F z)0UI;Xi8aHy1>_0@csJ*^P>8(k&(!R1VwFaWo30lbj|NZZP`&#QSvR@lSh$p(i{9q z`gE^iRcJ6@zC66JyuE$I!(--1#ml>Qobw#G>;a9h`D*=XzrYBG(|Vuj6+0|b$`^wyFKVp=2Qfl?&C8V(5ty|vDi6btk#h6M!$;1EMoSg68S1f*Mrr#rF=3SMHv z585nuq9BA`GGNjsyh~&vrrdISFN<2Xl&Kb-_pGkumPOaj-$!I?V1qBvSF|Ucfh6eB zW3sRK{pIE5`~gCGXx;ajztgt|1g71kIAvcxuN0Lny!dy>>U3u~4?EBIK+w3P%Sgw8fETYP6F6(qA_JFdRl2cwj2YK~$NXcG70a8mq zL2;PqJf{XsfoELp1A1fE7P4;*Ko<18o5e`f`2PKSL`Uvf$SYP)X!h8`%*@M}%=_VG90n(tw}Onb8_YFol>?EI&IGe`XcA2@x7Wf{lh&$1Kap=nwrSa zdbYOV<>ihJ4#?|)8(HrVVXyZp-1W`Pk>(i*80>)juOFJ3xj7KUo*bE2&x=hH^?Wq2 z)T!pzGo&2$nCa?QX@3OE{UhS~)H(eUp2ir}_u2aBu!7OExq3|{7*&FYwE1m$;-sfF z1(w6e2wE#yL^{{%XjSpj(L|m;nQrWPPg(WXuR}lt2WW^@R8*Xb#1DV$SvqtqXwPy>aD&%)nEmOPk)F$F#G*xG?Y}cjhLkZwJwV zU2jI8B(5hoJnQ5Tt>b3lLMKYtXVQG>Phu?Sl~Z|%*k(^#x4yIk3Y;s6c$gIOfzrDA zdN4u&hZ$XjcbYWW0WuPG?_)ziSH_M2=)wj6$w) z!UOZQzF96B6?{|)BJ}TXyp2+AX<}+Er&*rQ(#?uLggHF78QcUeT&1pf*sDqhc(9g}`=fy;$hYP_2tNw`n2i$wsuZl+Ni? zIV3bRlr?$Yn0)Q{L%T;^z4P`2|F(B$|c@VRnk9~7tKI@zbAb9A%=o0QF@v2>k(CABb~=K0>E z2uCmQ6>!{EcA9ks!OX+$PVU>ELdP4>7k0(RKA=tV{?fR;EE#1%b{x8U)rkKIDwXGn z6D6sP^CAbThnQEp+$L0cZDTx_vB1$#qXQ?Y=ugZ~#OTiz*0$Lb%D2Xv=slZVZu0aM zn1>hb+Uf00MMFkaN{6|PN}GJ0Yey$zWZY10 zj@0VpS%`bZV98On_p4ybOYQo_bVw{Jo|e1{GY6BIX?LW*cuqOBv_b$W>NEJVM^tYN z@HPFOD5T1wa*N`ll9eCUd};%6Qs6u03(S{+LfojIQm6N2e>XsTpa%Lq)JDf>gn|S@ z^5QbW;@L57mHpSunp$Ajpu z67=RsS5@*3sti0X+n%Z}7d69lVPNPhoun#Owa`0{Y+20Ls0@*P=d)B)pdOJNQ{=%+ zLqo&Nd;-Qt%%jXA@v{7_gu11Zph^!62^pKLj+!bMeV;6CarF6qC3Ol4R#hf*M?`Tr zjI94yhhn9zpgy`$dfh3B+ES2QZXkLYp6Bn3&oXg7%tA|wnJTosw>ddCr#-29cf73T z>fEF5cm0V6d3BdI_YoCD+QVZO2)3ydsdeZTi}x=IHu}zS7;fr6Jvo`Ky!#eLXXM}> zV;2{1e>G7|SZdUZfQ(4O4MImRTRae*f#z)tNyc|2G!-muYH9+S*?ufwVjGhs8 zf$=S){Q&m*z@A}72C5Aw8;49X{c|^g`==$g_X-UgfuYYUS$(g0rHLFl=c}j2$MVq9 z>10gqi)8M3H0;S;f9bu^3YHIy%^i0lwzmT*bTko;K1sSHLN@u&_|Nx7SDm$`Zw)1_*kqR#?P(_h(&$vSM^}l)*mUP%J}U zMI{CpUp@aQ#2UGHzp9tfL{E{j2NhyougPfY| zN(1M?>xn*`xsdU>8TaM2_>B6vOQxz6EIa=FvCv!3<=<^bQ-?U8Y&l*Y9_^ZRb4er- zs+BN3EvwGW?PlD_vasA~Cn8peQCE)x67}@@tJ^6l?>#l9`+0Y2ZEvQs2M_$%3pE=Y zwv<`cO@I(TFt5VzqJK5krLXYmg8&uA&Ee5I)DN^??iq==jI3v;z?SM8RWxyp_Pc8L zi|_UI^|JT_jSjb(A{ypfOO(k5#j2txZ#WzeTk9^6dRd+%*96b6?(C%P1$=GUslAiF z?<_nciG{14B=&c}rnbB;+a3~N8*M;r$&+orguklF#uPD#!iXFnzxLRQdEdxn99vmR zX?ljmMBP(Tm91dTsGgjuynIaSIIwPI#LA|Js={&8MvO$3?Oy?!~EMl|S>qrU#r!=ajejnZ7}*MR9Uo(JT3sh{фH5so)^?-J( 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 literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/two-columns.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/two-columns.png new file mode 100644 index 0000000000000000000000000000000000000000..10b744a6054d38c68f722e693b99920204ef28be GIT binary patch literal 175158 zcmZUa1yEbxy7r+IDPG)NiWe*Hw79z$DDLi19Euh#?h>@PyIXK~cY*~7`sIJlJ#+8< z?o4J+W@pdJ&R$us{GMk;Dl1B%Arm3Pz`&r%NK2~1z##mBfr0BodIvp&0s_jwz+l42 zNQ$X@W}aoie^=AOA2JfOJGr6j5=CcQ_u4ZoTDn{@1Ag z@n+>wTh@~^{#%~?`7LxSAzc3SHH9%An1>xAYq!_M@yxD~5Zw`cqF zRY#@%=N_kd=lDvpE)hpK)p4qfv#X?c~lT5{P=$l{<9EV zp+iQ$Ga)Vb8fk6)BsCygvUEsDf~c+_SJ1>4cJ8{rweEY43jZGdRSpOD;8HG1QoK`~ zmEMri6+w+M_2UFFe{c^sJ7?YB#zrTaldm^?Ti#Y5E%H>l)gY>#`xpYM)DLrzoFT_9 zzTSu`m+uYxd_*b$X~O-eulo~n<1+zT1Z_rMd{$;fp^bMJc$#-4tZ|}-ANr}ptegzR z4AWaeV>$|agsNQr^SHVOAkVcsYZW-0Hd0lgl|i|Sx(@cLd%ajpa(ZfNDEZ6Oi89DH zoX9S-q?tk!WJRsuibf};xHy;>F`_Q6My6EKbS(vk5ubm8CfhEzj8OINe9 zGP8K@*xe&!R>26GBe8nr!Esx%J2x0-v5SFRFtRWVk14v{+nd>2DN(SHoB(E*C#qSy zbaKD{FcHb4%ikbU$?{Ep)ifoN4VdjuMm7VxS=B#wuBE4Ccbhn8)cq-%Hi?$WOildu z$*OdsEwP;^JN1EJ8D!dTg2@S^hu2?l?;hRF&l2rSx3Gf zS7~#BqzbT|G;#U!bia#X@}noC5tf_KnXp)vf>+A>Ygy%z65s(_Wjn{540Rb((tC?N z)$_Py;ZLr(9fwJStIunO+{o)7jpY!5vIwH>1xLZdZ!(Ib+x~lQ3KY1EO$AWRUt1D>OSQtioM*4b3ftPqis*%#__A{CFEh$k9ld{Zs zqckRXdTxSXQWtmQ3?wNSHw zCbhiG=t?ex4H3Rv+S<$b2SxMXTB&~mb757)*ZpO}$e8Ovyicx%ujkk~>!vR9s9g-2 zhKAW<{!-H%I{K% zrp15er=ZBnR3E$~NK5j*QiaMhVkPZIe)Wz23HDDE#l#bPCyfxPCo8L*(~!1<2q?fj0JlL!p;RPkyY)KY zQ^GK{CdTcj)T@dgq3ni6@+Ke-P~f>6Y1m}yUtDtb+w>&G_Nt^uJ$Rfx^@QBl9p3Ii3E zmR5egJvnrh6qPi5u;Vm+MD6qHlnz*|GKeD=ad&ewXmtf_-}(xRFfuX<2(0$>e`%ZGMLL*HZ&Hp zUrfRp96R%E-D=4zByrfdmP^B)!OLK}e)6{{&E8*a`FX+5SZO#|m(7n)`^m{D9LP76 z9d!+r1r0G9*ai;uh;AIHf=M+?wx0E0;=_zI)3cHpFfU27GS5jfQRx>X>L8s7V`lPn zQKk4;k$*niJ2cF`M~t_*7H>0b`~#h^u4J}j4ojz`{gN>9=dw>sh9FhIU*?n1h4G~f z1A1R$`$I8;;Je?|+ax2nvd|S~Z*N#at{`n|ZLO=RIcV~SgMbSTV&f~DQSe>WNKd&u zmq8*O)cRfY%`NLG>K(=oF!XQaClsL3^@9|j?ZONA9>S~BTc>T z*PTay>-;9O*Bkc9#Kx0{!~Ve!3}U{j`g$5_mamdj%*?}bN^-_EOLd1VUk);+Fa19% zC>5Lm{^~pKO_813t_!vEyPn9Zk!~dCynx0WG_lL9S$!Q;KlC~nz^yj;IGj+EK+>%{ zRBJ>5GuJ2YE8a2HwwO=>(eQ3@B!EEqAxl(b3&sYz8MSI_SrF6VWD1mndZz zmOPcp>|&4$3%WeYD?3gl6vvz6l}z@%-Ti~2nf7A#H{mAYhgX`D2=Tm!tJTT6I!+Cr z?b_$r>X=3JEy}A+mH=Fwy%-WshC+?fyZrX{Ow;gt;GYH^O z_}m^zlxZlJd;A3W-7gNvF)NPY#n)YaT-Khs36GGAUJV8_)$5Z!g0jtyJ%M(m?mq

mWI!DpLiYz*G-5hDyKW1uqt^u?Fu?m|GCHB}8F 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ℜ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 KelF{r5}E)CbEJm= literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md b/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md new file mode 100644 index 00000000..fa43a523 --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md @@ -0,0 +1,38 @@ +# Normalized UI parity verification + +阶段 8 的实现态使用与基线相同的 Ego Browser task space、专用测试账号和 +`1674 × 963` viewport 验证。测试数据位于隔离的 `thread-chat-test` PostgreSQL, +Project、Thread、Message、MessageRun 与 Artifact 全部来自新规范化表;页面没有读取或 +写入旧 `branch_trees`。 + +## Screenshots + +| Screenshot | State | +| --- | --- | +| `empty-new-normalized.png` | `/thread-chat/new` 无实体草稿;关闭一次性帮助后与空白基线同态 | +| `three-columns-normalized.png` | Root、第一层 Branch、嵌套 Branch 三栏完成独立 Message Query | +| `artifact-drawer-normalized.png` | Artifact 按 `artifactId` 加载完成后的 50% Drawer | + +## Ego Browser interaction results + +- `/new` 保持单个 Main Column、Header、Composer、Project List,并可在空实体状态切换 + Columns/Canvas;首次提交前没有伪造 Project、Thread 或 Message ID。 +- Root 脚注打开第一层 Branch,Branch 脚注打开嵌套 Branch;三栏在精确基线 viewport + 下恢复为 `558.33 / 558.34 / 557.34px` 的原组件布局。 +- Header Child 选择器同时显示直接与嵌套后代;Thread 切换到已在另一 Slot 打开的目标时 + 交换两个 Slot 的 Thread 内容,Slot 宽度和物理位置不变。 +- Breadcrumb 回到已经打开的上游 Thread 时关闭当前重复 Slot;“收起”继续删除对应 Slot。 +- 第一条分割线向右拖动 `110px` 时只改变相邻两列,第三列不变;双击后三个列宽恢复自动 + 均分,Snapshot 中 Root/Branch 宽度恢复 `null`。 +- 强制两列并选择 `fold` 后打开嵌套 Branch,来源 Branch 原地折叠为细条;Snapshot 保存 + `folded=true`,刷新后恢复。 +- Project-scoped Snapshot 恢复多栏、稳定 Slot、焦点、列宽、placement 和 view mode; + Drawer、滚动位置和 Composer 草稿不进入 Snapshot。 +- Artifact 卡片先建立 `artifactId` 引用,再由独立 loader 请求正文;完成态 Drawer 的 + `x=837px`、`width=837px` 与基线一致,并正确显示来源 Thread 和 Markdown 正文。 +- Root assistant 文本选择可打开既有 Fork Composer;queued/running/failed assistant 仍禁止 + Fork,服务端命令继续执行最终校验。 +- Project Catalog 来自 `/api/v1/projects`,显示当前 Project、Branch 数、重命名与删除入口。 + +未发现需要批准的最终样式、页面布局、多栏、Header、Fork、breadcrumb、Drawer 或分割线 +交互变化。新增的 Project loading 与 Artifact loading/error 仅出现在规范要求的异步状态。 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png b/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png new file mode 100644 index 0000000000000000000000000000000000000000..ec3d931aac9c9440b8932b868ab8c6fec8d0aad7 GIT binary patch literal 113984 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

-xlt8K4T9ZVD@ay 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

^K;uPuuEGDM*t|qkFtu=i< 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�Q^0Ck9Npf7-UJ=M4(xeC;(JBr5Bmfk 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 z¥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 z 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 z%0A+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 InU|mb51U>7O#lD@ literal 0 HcmV?d00001 diff --git a/tests/api/transport.test.ts b/tests/api/transport.test.ts index 586a60cf..d71137f8 100644 --- a/tests/api/transport.test.ts +++ b/tests/api/transport.test.ts @@ -7,6 +7,23 @@ import { } from "../fixtures/thread-chat-api-fixtures" describe("JsonThreadChatTransport", () => { + it("默认 fetch 绑定 globalThis,浏览器调用不会 Illegal invocation", async () => { + const nativeLikeFetch = vi.fn(function (this: unknown) { + if (this !== globalThis) throw new TypeError("Illegal invocation") + return Promise.resolve( + Response.json({ data: { items: [], nextCursor: null } }) + ) + }) + vi.stubGlobal("fetch", nativeLikeFetch) + try { + await expect(new JsonThreadChatTransport().listProjects()).resolves.toEqual( + { items: [], nextCursor: null } + ) + } finally { + vi.unstubAllGlobals() + } + }) + it("编码请求并严格校验成功响应", async () => { const fetcher = vi.fn().mockResolvedValue( Response.json({ diff --git a/tests/client/store.test.ts b/tests/client/store.test.ts index 2f86c240..2d0345b3 100644 --- a/tests/client/store.test.ts +++ b/tests/client/store.test.ts @@ -29,6 +29,16 @@ const branch = { }, } +const branchTwo = { + ...branch, + id: "00000000-0000-4000-8000-000000000202", +} + +const branchThree = { + ...branch, + id: "00000000-0000-4000-8000-000000000205", +} + describe("ThreadChat client stores", () => { it("App Store 合并稳定排序的摘要且不保存 selectedProjectId", () => { const store = createThreadChatAppStore() @@ -218,6 +228,86 @@ describe("ThreadChat client stores", () => { }) }) + it("列满时保留物理 Slot、来源替换、指定保留和重复 Thread swap", () => { + let nextSlot = 0 + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + generateSlotId: () => `slot-${++nextSlot}`, + }) + store.getState().mergeBootstrap({ + project: projectDTOFixture, + threadTopology: [ + rootThreadDTOFixture, + branch, + branchTwo, + branchThree, + ], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }) + + store.getState().openThread(branch.id, "root", { maxExpanded: 2 }) + store.getState().openThread(branchTwo.id, "slot-1", { maxExpanded: 2 }) + store.getState().commitColumnWidths({ "slot-1": 400, "slot-2": 500 }) + store.getState().switchColumnThread("slot-1", branchTwo.id) + expect(store.getState().ui.columnSlots).toMatchObject([ + { slotId: "slot-1", threadId: branchTwo.id, widthPx: 400 }, + { slotId: "slot-2", threadId: branch.id, widthPx: 500 }, + ]) + + store.getState().openThread(branchThree.id, "slot-1", { + maxExpanded: 2, + keepSource: true, + }) + expect(store.getState().ui.columnSlots).toMatchObject([ + { slotId: "slot-1", threadId: branchTwo.id, widthPx: 400 }, + { slotId: "slot-2", threadId: branchThree.id, widthPx: 500 }, + ]) + }) + + it("fold placement 展开新列时折叠非来源 LRU Slot", () => { + let nextSlot = 0 + const store = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + generateSlotId: () => `slot-${++nextSlot}`, + }) + store.getState().mergeBootstrap({ + project: projectDTOFixture, + threadTopology: [ + rootThreadDTOFixture, + branch, + branchTwo, + branchThree, + ], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }) + store.getState().setPlacementMode("fold") + store.getState().openThread(branch.id, "root", { maxExpanded: 2 }) + store.getState().openThread(branchTwo.id, "slot-1", { maxExpanded: 2 }) + store.getState().openThread(branchThree.id, "slot-2", { maxExpanded: 2 }) + + expect(store.getState().ui.columnSlots).toMatchObject([ + { slotId: "slot-1", threadId: branch.id, folded: true }, + { slotId: "slot-2", threadId: branchTwo.id, folded: false }, + { slotId: "slot-3", threadId: branchThree.id, folded: false }, + ]) + }) + it("局部 Run、Artifact Summary 与 Column View 不依赖 Artifact 正文", () => { const store = createThreadChatProjectStore({ projectId: projectDTOFixture.id, From 4dd510acad9a88c67a7bd91c08e46c47a258819c Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 20:48:37 +0800 Subject: [PATCH 17/23] test(thread-chat): verify normalized end-to-end flows --- lib/thread-chat/api/server/runtime.ts | 12 +- .../isolated-test-ai-runtime.ts | 108 +++++++++++ .../e2e-evidence.md | 67 +++++++ .../design-thread-chat-client-api/tasks.md | 14 +- tests/client/runtime.test.ts | 175 ++++++++++++++++++ tests/unit/isolated-test-ai-runtime.test.ts | 87 +++++++++ 6 files changed, 455 insertions(+), 8 deletions(-) create mode 100644 lib/thread-chat/infrastructure/isolated-test-ai-runtime.ts create mode 100644 openspec/changes/design-thread-chat-client-api/e2e-evidence.md create mode 100644 tests/unit/isolated-test-ai-runtime.test.ts diff --git a/lib/thread-chat/api/server/runtime.ts b/lib/thread-chat/api/server/runtime.ts index bebe3f99..05c60602 100644 --- a/lib/thread-chat/api/server/runtime.ts +++ b/lib/thread-chat/api/server/runtime.ts @@ -9,11 +9,21 @@ import { MessageRunner } from "../../application/message-runner" import { ThreadChatCommands } from "../../application/thread-chat-commands" import { ThreadChatQueries } from "../../application/thread-chat-queries" import { AiSdkRuntime } from "../../infrastructure/ai-sdk-runtime" +import { + IsolatedTestAiRuntime, + usesIsolatedTestAiRuntime, +} from "../../infrastructure/isolated-test-ai-runtime" import { ThreadChatUnitOfWork } from "../../infrastructure/repositories" import { ThreadChatApiError } from "./errors" const unitOfWork = new ThreadChatUnitOfWork(dbClient) -const runner = new MessageRunner(dbClient, unitOfWork, new AiSdkRuntime(), { +const aiRuntime = usesIsolatedTestAiRuntime({ + databaseUrl: process.env.DATABASE_URL, + nodeEnv: process.env.NODE_ENV, +}) + ? new IsolatedTestAiRuntime() + : new AiSdkRuntime() +const runner = new MessageRunner(dbClient, unitOfWork, aiRuntime, { generateId: randomUUID, now: () => new Date(), }) diff --git a/lib/thread-chat/infrastructure/isolated-test-ai-runtime.ts b/lib/thread-chat/infrastructure/isolated-test-ai-runtime.ts new file mode 100644 index 00000000..3bbc3084 --- /dev/null +++ b/lib/thread-chat/infrastructure/isolated-test-ai-runtime.ts @@ -0,0 +1,108 @@ +import { isExplicitMarkdownArtifactRequest } from "@/lib/chat/markdown-artifact" +import type { + AiRuntime, + AiRuntimeEvent, + AiRuntimeRequest, +} from "../application/ports/ai-runtime" + +const ISOLATED_TEST_DATABASE_NAME = "thread-chat-test" + +export function usesIsolatedTestAiRuntime(input: { + databaseUrl: string | undefined + nodeEnv: string | undefined +}): boolean { + if (input.nodeEnv === "production" || !input.databaseUrl) return false + try { + const url = new URL(input.databaseUrl) + return ( + (url.protocol === "postgres:" || url.protocol === "postgresql:") && + decodeURIComponent(url.pathname.slice(1)) === ISOLATED_TEST_DATABASE_NAME + ) + } catch { + return false + } +} + +function waitForDelayOrAbort( + delayMs: number, + signal: AbortSignal | undefined +): Promise<"elapsed" | "aborted"> { + if (signal?.aborted) return Promise.resolve("aborted") + return new Promise((resolve) => { + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort) + resolve("elapsed") + }, delayMs) + const onAbort = () => { + clearTimeout(timer) + signal?.removeEventListener("abort", onAbort) + resolve("aborted") + } + signal?.addEventListener("abort", onAbort, { once: true }) + }) +} + +/** + * 只在 allowlisted `thread-chat-test` 数据库 + 非 production 进程启用。 + * 它不是产品 feature flag:无法通过任意环境变量切换,也不会在正式数据库上运行。 + */ +export class IsolatedTestAiRuntime implements AiRuntime { + constructor( + private readonly delays: { + normalMs: number + slowMs: number + stopTimeoutMs: number + } = { normalMs: 120, slowMs: 2_500, stopTimeoutMs: 30_000 } + ) {} + + async *execute( + request: AiRuntimeRequest, + options: { signal?: AbortSignal } = {} + ): AsyncIterable { + const prompt = request.prompt + .findLast((message) => message.role === "user") + ?.parts.filter((part) => part.type === "text") + .map((part) => part.text) + .join("\n") + .trim() ?? "" + const response = `测试回复 ${request.assistantMessageId.slice(-6)}:已收到「${prompt}」。` + + yield { + type: "delta", + partsDelta: [{ type: "text", text: "测试回复生成中…" }], + } + + const stopScenario = prompt.includes("直到我停止") + const slowScenario = prompt.includes("刷新恢复") + const waitResult = await waitForDelayOrAbort( + stopScenario + ? this.delays.stopTimeoutMs + : slowScenario + ? this.delays.slowMs + : this.delays.normalMs, + options.signal + ) + if (waitResult === "aborted") { + yield { type: "stopped" } + return + } + + if (isExplicitMarkdownArtifactRequest(prompt)) { + yield { + type: "artifact", + output: { + kind: "markdown", + title: "E2E Markdown", + content: + "# E2E Markdown\n\n- Artifact 按 ID 加载\n- 来源 Message 可定位\n- 刷新后可恢复", + toolCallId: `e2e-${request.messageRunId}`, + }, + } + } + + yield { + type: "completed", + parts: [{ type: "text", text: response }], + } + } +} diff --git a/openspec/changes/design-thread-chat-client-api/e2e-evidence.md b/openspec/changes/design-thread-chat-client-api/e2e-evidence.md new file mode 100644 index 00000000..be49952f --- /dev/null +++ b/openspec/changes/design-thread-chat-client-api/e2e-evidence.md @@ -0,0 +1,67 @@ +# 阶段 9 前后端集成与 E2E 证据 + +日期:2026-08-25 +环境:`thread-chat-test` PostgreSQL、Next.js `http://localhost:4040`、Ego Browser task space `11`、`1674 × 963` viewport。 + +## 确定性 AI Runtime + +- 本地 E2E 没有调用真实模型。服务端仅在 `NODE_ENV !== production` 且 + `DATABASE_URL` 的数据库名严格等于 `thread-chat-test` 时自动使用 + `IsolatedTestAiRuntime`。 +- 该选择没有 feature flag,也不接受任意环境变量覆盖;production 或任何非 allowlist + 数据库始终走真实 `AiSdkRuntime`。 +- Runtime 用自然测试输入提供确定性完成、慢生成、显式 Stop 和 Markdown Artifact,单元测试 + 覆盖选择边界、delta/terminal、Artifact 与 abort。 + +## 自动集成测试 + +- `/new`:无实体草稿只提交 user parts;CreationBundle 先 seed 唯一 Project Runtime、先订阅 + assistant SSE,再执行 route replace。测试让 terminal event 早于目标 Provider 挂载,确认 + handoff 不丢事件且不二次 Bootstrap。 +- Project 冷启动:Bootstrap 完成前保持 loading;无 Snapshot 使用默认 Root 视图,有合法 + Snapshot 时在 Bootstrap 后恢复稳定 Slot、焦点、Root/Branch 宽度和列数。 +- Branch 并行加载:两个不同 Thread 同时发起 Message Query;一个成功、一个失败,失败状态只 + 落在目标 Thread,Root 和成功 Branch 仍为 ready。 +- running Run 刷新恢复:Bootstrap 返回既有 running Run 后,Coordinator 以同一 + `assistantMessageId` 和持久 `eventSequence` 订阅并进入 completed,没有创建第二个 Run。 + +## Ego Browser E2E + +- 通过 UI 注册专用本地账号 `thread-chat-e2e-20260825@example.com`;没有真实邮箱验证。 +- `/new` 首次发送创建服务端 Project 并 replace 到 + `/thread-chat/da6e6e77-83f3-4bd9-88ea-7c29ff32ed0d`。逐帧探针记录 212 帧,其中 + 172 帧位于目标路由,`blankProjectFrames=0`、`loadingProjectFrames=0`。 +- 首条和后续消息均得到确定性回复。输入“E2E 刷新恢复”时先观察持久 checkpoint 和 Stop, + 生成中刷新后立即恢复同一 partial,随后进入 completed。 +- 输入“请持续生成,直到我停止”后点击显式 Stop;最终保留 checkpoint,后台状态和 Stop + 控件消失,没有把 SSE 断开当作 Stop。 +- Edit 把最后一轮有效 user Message 替换为“E2E 编辑后的消息”并生成新 assistant; + Regenerate 再次生成不同 assistant ID 后缀的回复,旧 finalized 内容没有原地改写。 +- 从 completed assistant 划选原文创建 L1 Fork,再从 L1 assistant 创建 L2 嵌套 Fork; + 页面显示 Root/L1/L2 三栏,三个 Message Query 和生成互不阻塞。 +- Header Child 选择器可在收起 L2 后重新打开既有 Thread;重复 Thread 的 `⇄ 切换`交换两个 + 物理 Slot,再次切换可恢复原顺序;L2 breadcrumb 回到已打开 L1 时只收起重复后代列。 +- 三栏初始宽度约 `558 / 558 / 557px`。第一条分割线向右拖 `80px` 后为 + `638 / 478 / 557px`,只改变相邻列;刷新后恢复相同 Thread ID、L1/L2 层级与列宽。 +- 嵌套 Branch 请求 Markdown 后,消息 API 的 tool output 只含 + `artifactId=0516d587-5d46-45af-9f15-520bb078627f`,没有正文;消息卡片和 Header 计数为 1, + Drawer 按 ID 加载标题与正文。 +- 在隔离测试库临时建立另一个 owner 的 Project。当前浏览器 session 请求其 Bootstrap 得到 + HTTP 404,Project list 不泄露该 ID;断言完成后临时 owner 已级联删除,残留计数为 0。 + +## UI parity + +阶段 9 沿用阶段 8 的同一 Ego Browser task space、账号、viewport、组件和 CSS 类,并逐项重放 +空白页、三栏、Header Child、Switcher、收起、breadcrumb、Fork Composer、Artifact Drawer +和分割线交互。对照 `ui-parity-baseline/` 与 `ui-parity-implementation/`,除规范批准的 +Project/Artifact 异步 loading/error 外,没有发现最终样式、布局或交互输出变化。 + +## 阶段验收 + +- `pnpm test`:87 项通过(17 unit、17 client、20 PostgreSQL integration、33 API)。 +- `pnpm typecheck`:通过。 +- `pnpm lint`:0 errors;3 个既有 warnings。 +- `pnpm openspec:validate`:27 个 change/spec 严格校验全部通过。 +- Next.js production build:默认 Turbopack 在受限网络请求 Google Fonts 时持续等待且 0% CPU; + 停止该单一构建进程后,使用 Next.js 16.3.1 官方 `next build --webpack` 完成同一生产构建, + 编译、TypeScript、23 个静态页面、page data 与 build traces 全部成功。 diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/design-thread-chat-client-api/tasks.md index 7db4681a..60f6a8cf 100644 --- a/openspec/changes/design-thread-chat-client-api/tasks.md +++ b/openspec/changes/design-thread-chat-client-api/tasks.md @@ -90,13 +90,13 @@ ## 9. 前后端集成与 Ego Browser E2E -- [ ] 9.1 完成 `/new` 无实体草稿、首次发送、seeded Runtime 无空白帧切换和 AI 事件早于目标 Provider 挂载的集成测试。 -- [ ] 9.2 完成已有 Project 冷启动、有/无 Workbench Snapshot、多 Branch 并行加载、单列失败和刷新恢复 running Run 的集成测试。 -- [ ] 9.3 使用 Ego Browser 通过邮箱注册专用本地测试账号;不得依赖真实邮箱验证或真实模型随机输出作为断言。 -- [ ] 9.4 E2E 验证 Project 创建/列表、首条与后续消息、生成中刷新、Stop、Edit、Regenerate、Fork 与嵌套 Fork。 -- [ ] 9.5 E2E 验证多栏异步加载、Header Child 选择、Thread 切换、收起、breadcrumb、分割线拖拽与刷新视图恢复。 -- [ ] 9.6 E2E 验证 Markdown Artifact 创建、消息 `artifactId` 引用、Drawer 按 ID 加载和其他 Project 访问隔离。 -- [ ] 9.7 对比 UI parity 基线,确认除已批准的 Project loading 外,最终样式和交互行为没有变化。 +- [x] 9.1 完成 `/new` 无实体草稿、首次发送、seeded Runtime 无空白帧切换和 AI 事件早于目标 Provider 挂载的集成测试。 +- [x] 9.2 完成已有 Project 冷启动、有/无 Workbench Snapshot、多 Branch 并行加载、单列失败和刷新恢复 running Run 的集成测试。 +- [x] 9.3 使用 Ego Browser 通过邮箱注册专用本地测试账号;不得依赖真实邮箱验证或真实模型随机输出作为断言。 +- [x] 9.4 E2E 验证 Project 创建/列表、首条与后续消息、生成中刷新、Stop、Edit、Regenerate、Fork 与嵌套 Fork。 +- [x] 9.5 E2E 验证多栏异步加载、Header Child 选择、Thread 切换、收起、breadcrumb、分割线拖拽与刷新视图恢复。 +- [x] 9.6 E2E 验证 Markdown Artifact 创建、消息 `artifactId` 引用、Drawer 按 ID 加载和其他 Project 访问隔离。 +- [x] 9.7 对比 UI parity 基线,确认除已批准的 Project loading 外,最终样式和交互行为没有变化。 ## 10. 旧客户端退役与归档 diff --git a/tests/client/runtime.test.ts b/tests/client/runtime.test.ts index 91000c0b..fd9793c8 100644 --- a/tests/client/runtime.test.ts +++ b/tests/client/runtime.test.ts @@ -47,6 +47,181 @@ const branch = { } describe("ThreadChat client runtime", () => { + it("Project 冷启动先落 Bootstrap,再恢复 Workbench,且 Branch 失败彼此隔离", async () => { + const secondBranch = { + ...branch, + id: "00000000-0000-4000-8000-000000000302", + } + const branchUser = { + ...userMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000307", + threadId: branch.id, + sequence: 1, + } + const branchAssistant = { + ...assistantMessageDTOFixture, + id: "00000000-0000-4000-8000-000000000308", + threadId: branch.id, + sequence: 2, + } + const loadThreadMessages = vi.fn( + async ({ threadId }: { threadId: string }) => { + if (threadId === secondBranch.id) throw new Error("branch unavailable") + return { + threadId, + messages: [branchUser, branchAssistant], + assistantRuns: [ + { + ...assistantRunDTOFixture, + assistantMessageId: branchAssistant.id, + }, + ], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + } + } + ) + const runtime = createThreadChatProjectRuntime({ + projectId: projectDTOFixture.id, + api: createTestApi({ + bootstrapProject: vi.fn().mockResolvedValue({ + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture, branch, secondBranch], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [assistantRunDTOFixture], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }), + loadThreadMessages, + }), + generateSlotId: () => "slot-branch", + }) + + await runtime.commands.loadProjectBootstrap() + expect(runtime.store.getState().ui).toMatchObject({ + columnSlots: [], + focusedSlotId: "root", + viewMode: "columns", + }) + runtime.store.getState().restoreWorkbenchSnapshot({ + schemaVersion: 1, + columnSlots: [ + { + slotId: "saved-branch", + threadId: branch.id, + folded: false, + widthPx: 420, + }, + ], + focusedSlotId: "saved-branch", + rootColumnWidthPx: 560, + forceColumnCount: 3, + placementMode: "replace", + viewMode: "columns", + canvasPins: {}, + }) + expect(runtime.store.getState().ui).toMatchObject({ + columnSlots: [ + { + slotId: "saved-branch", + threadId: branch.id, + folded: false, + widthPx: 420, + }, + ], + focusedSlotId: "saved-branch", + rootColumnWidthPx: 560, + forceColumnCount: 3, + }) + + await Promise.all([ + runtime.commands.ensureThreadMessages(branch.id), + runtime.commands.ensureThreadMessages(secondBranch.id), + ]) + expect( + runtime.store.getState().requests.threadMessagesById[branch.id] + .loadState.status + ).toBe("ready") + expect( + runtime.store.getState().requests.threadMessagesById[secondBranch.id] + .loadState.status + ).toBe("error") + expect( + runtime.store.getState().requests.threadMessagesById[ + rootThreadDTOFixture.id + ].loadState.status + ).toBe("ready") + runtime.destroy() + }) + + it("刷新冷启动复用 running Run,并从同一 assistantMessageId 恢复到终态", async () => { + const runningRun = { + ...assistantRunDTOFixture, + status: "running" as const, + finishedAt: null, + } + const subscribeAssistantEvents = vi.fn(async function* () { + yield { + type: "run.completed" as const, + eventSequence: 1, + run: { + ...assistantRunDTOFixture, + status: "completed" as const, + eventSequence: 1, + finishedAt: "2026-08-25T00:01:00.000Z", + }, + message: { + ...assistantMessageDTOFixture, + parts: [{ type: "text" as const, text: "recovered" }], + finalizedAt: "2026-08-25T00:01:00.000Z", + }, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + } + }) + const runtime = createThreadChatProjectRuntime({ + projectId: projectDTOFixture.id, + api: createTestApi({ + bootstrapProject: vi.fn().mockResolvedValue({ + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [runningRun], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }), + subscribeAssistantEvents, + }), + }) + + await runtime.commands.loadProjectBootstrap() + await vi.waitFor(() => + expect( + runtime.store.getState().runs.byAssistantMessageId[ + assistantMessageDTOFixture.id + ].status + ).toBe("completed") + ) + expect(subscribeAssistantEvents).toHaveBeenCalledTimes(1) + expect(subscribeAssistantEvents).toHaveBeenCalledWith( + expect.objectContaining({ + assistantMessageId: assistantMessageDTOFixture.id, + afterEventSequence: 0, + }) + ) + runtime.destroy() + }) + it("ThreadMessageLoader 同 Thread 去重、不同 Thread 并行且 destroy 统一 Abort", async () => { const store = createThreadChatProjectStore({ projectId: projectDTOFixture.id, diff --git a/tests/unit/isolated-test-ai-runtime.test.ts b/tests/unit/isolated-test-ai-runtime.test.ts new file mode 100644 index 00000000..6270645a --- /dev/null +++ b/tests/unit/isolated-test-ai-runtime.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest" +import { + IsolatedTestAiRuntime, + usesIsolatedTestAiRuntime, +} from "@/lib/thread-chat/infrastructure/isolated-test-ai-runtime" +import type { AiRuntimeRequest } from "@/lib/thread-chat/application/ports/ai-runtime" + +function request(text: string): AiRuntimeRequest { + return { + messageRunId: "00000000-0000-4000-8000-000000000001", + assistantMessageId: "00000000-0000-4000-8000-000000000002", + modelId: "fake/model", + prompt: [ + { + id: "00000000-0000-4000-8000-000000000003", + role: "user", + parts: [{ type: "text", text }], + }, + ], + } +} + +describe("IsolatedTestAiRuntime", () => { + it("仅允许非 production 的 thread-chat-test 数据库启用", () => { + expect( + usesIsolatedTestAiRuntime({ + databaseUrl: "postgres://localhost:5432/thread-chat-test", + nodeEnv: "development", + }) + ).toBe(true) + expect( + usesIsolatedTestAiRuntime({ + databaseUrl: "postgres://localhost:5432/thread-chat-test", + nodeEnv: "production", + }) + ).toBe(false) + expect( + usesIsolatedTestAiRuntime({ + databaseUrl: "postgres://localhost:5432/thread-chat", + nodeEnv: "development", + }) + ).toBe(false) + }) + + it("稳定产生 delta、Artifact 与 completed", async () => { + const events = [] + for await (const event of new IsolatedTestAiRuntime({ + normalMs: 0, + slowMs: 0, + stopTimeoutMs: 0, + }).execute(request("请生成 Markdown 文档"))) + events.push(event) + + expect(events.map((event) => event.type)).toEqual([ + "delta", + "artifact", + "completed", + ]) + expect(events[1]).toMatchObject({ + type: "artifact", + output: { title: "E2E Markdown" }, + }) + }) + + it("显式 Stop 会终止等待中的运行", async () => { + const controller = new AbortController() + const iterator = new IsolatedTestAiRuntime({ + normalMs: 0, + slowMs: 0, + stopTimeoutMs: 30_000, + }) + .execute(request("请持续生成,直到我停止"), { + signal: controller.signal, + }) + [Symbol.asyncIterator]() + + await expect(iterator.next()).resolves.toMatchObject({ + value: { type: "delta" }, + }) + const terminal = iterator.next() + controller.abort() + await expect(terminal).resolves.toMatchObject({ + value: { type: "stopped" }, + done: false, + }) + }) +}) From 4df327fc1035f0127df025f49606dea6e87e70de Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 21:21:12 +0800 Subject: [PATCH 18/23] refactor(thread-chat): retire legacy authority and archive redesign --- .../[generationId]/route.ts | 30 - .../[generationId]/stop/route.ts | 34 - .../[treeId]/active-leaf/route.ts | 69 -- .../messages/[messageId]/feedback/route.ts | 50 -- app/api/branch-trees/[treeId]/route.ts | 248 ------- app/api/branch-trees/route.ts | 24 - app/api/chat/conversation-text.ts | 27 - app/api/chat/generation-settlement.ts | 123 ---- app/api/chat/generation-start-error.ts | 34 - app/api/chat/request-context.ts | 196 ------ app/api/chat/research-context.ts | 75 --- app/api/chat/research-tool-capabilities.ts | 17 - app/api/chat/route.ts | 246 ------- app/api/chat/stream-lifecycle.ts | 131 ---- app/api/chat/surface-tools.ts | 70 -- app/api/chat/system-prompt.ts | 52 -- app/api/chat/thread-generation-context.ts | 146 ---- app/api/chat/tool-set.ts | 42 -- app/api/chat/tool-step-policy.ts | 61 -- app/api/title/route.ts | 99 --- app/thread-chat/[projectId]/page.tsx | 22 + app/thread-chat/[treeId]/page.tsx | 26 - .../chat/actions/message-action-commands.ts | 9 +- .../actions/message-action-presentation.ts | 57 -- .../actions/message-action-session-logic.ts | 41 -- .../chat/actions/message-action-types.ts | 17 +- .../chat/actions/use-message-actions.ts | 103 --- app/thread-chat/core/regeneration.ts | 4 - app/thread-chat/core/store.ts | 417 +----------- .../generation-reconciliation-logic.ts | 72 -- app/thread-chat/generation/merge-result.ts | 4 - app/thread-chat/generation/project-result.ts | 4 - app/thread-chat/generation/reconcile-turns.ts | 4 - app/thread-chat/generation/types.ts | 4 - .../use-generation-reconciliation.ts | 97 --- app/thread-chat/layout.tsx | 2 +- app/thread-chat/net/boot/thread-chat-boot.ts | 33 - .../net/boot/use-thread-chat-boot.ts | 49 -- app/thread-chat/net/chat-controller.ts | 402 ----------- .../net/commands/chat-generation-command.ts | 78 --- .../net/commands/message-feedback-command.ts | 50 -- .../net/commands/regeneration-command.ts | 169 ----- .../net/commands/stop-generation-command.ts | 35 - .../commands/switch-active-leaf-command.ts | 69 -- app/thread-chat/net/persistence/persist.ts | 335 ---------- .../net/persistence/sanitize-loaded-state.ts | 149 ----- .../net/persistence/save-tree-response.ts | 40 -- .../net/persistence/transient-state.ts | 27 - .../net/persistence/tree-save-gate.ts | 34 - .../net/persistence/use-tree-persistence.ts | 61 -- app/thread-chat/net/prompt/message-context.ts | 4 - .../net/prompt/message-serialization.ts | 4 - app/thread-chat/net/prompt/prompt-pure.ts | 4 - app/thread-chat/net/prompt/prompt.ts | 86 --- .../net/stream/assistant-delta-buffer.ts | 94 --- .../net/stream/assistant-output.ts | 9 - .../net/stream/assistant-stream-runtime.ts | 128 ---- .../net/stream/local-generation-executions.ts | 43 -- app/thread-chat/net/stream/ui-stream.ts | 198 ------ .../net/titles/thread-title-candidate.ts | 59 -- app/thread-chat/net/titles/thread-title.ts | 24 - .../net/titles/use-thread-titles.ts | 64 -- .../normalized/thread-chat-project.tsx | 6 +- .../orchestration/canvas/canvas-actions.ts | 2 +- .../orchestration/canvas/canvas-node.tsx | 2 +- .../orchestration/canvas/thread-canvas.tsx | 6 +- .../orchestration/canvas/use-canvas-layout.ts | 2 +- .../orchestration/columns/use-column-slots.ts | 219 ------ .../navigation/thread-chat-topbar.tsx | 3 +- .../navigation/tree-list-row.tsx | 8 +- .../orchestration/navigation/tree-list.tsx | 274 -------- .../workspace/branch-workspace-actions.ts | 153 ----- .../workspace/ui-state-snapshot.ts | 11 - .../workspace/use-thread-chat-runtime.ts | 99 --- .../workspace/use-thread-chat-workspace.ts | 104 --- .../workspace/use-ui-state-persistence.ts | 31 - app/thread-chat/page-metadata.ts | 2 +- app/thread-chat/page.tsx | 6 +- ...tree-redirect.tsx => project-redirect.tsx} | 9 +- app/thread-chat/thread-chat-demo.tsx | 406 ----------- e2e/thread-chat/README.md | 265 -------- .../artifact-card-css-ownership.test.mjs | 39 -- .../artifact-drawer-accessibility.test.mjs | 20 - e2e/thread-chat/artifact-drawer-css.test.mjs | 22 - .../assistant-delta-buffer.test.mjs | 89 --- .../assistant-retry-command.test.mjs | 115 ---- .../assistant-stream-runtime.test.mjs | 126 ---- .../boot-loading-css-ownership.test.mjs | 25 - e2e/thread-chat/branch-source-css.test.mjs | 29 - .../branch-workspace-actions.test.mjs | 91 --- .../canvas-card-dimensions.test.mjs | 46 -- e2e/thread-chat/canvas-dimensions.test.mjs | 31 - .../canvas-expand-ownership.test.mjs | 48 -- e2e/thread-chat/canvas-layout-cache.test.mjs | 51 -- .../chat-conversation-text.test.mjs | 36 - .../chat-generation-command.test.mjs | 147 ---- e2e/thread-chat/chat-request-context.test.mjs | 213 ------ e2e/thread-chat/chat-surface-tools.test.mjs | 26 - e2e/thread-chat/chat-system-prompt.test.mjs | 78 --- e2e/thread-chat/chat-tool-set.test.mjs | 62 -- .../column-control-css-ownership.test.mjs | 18 - .../column-slot-ownership.test.mjs | 45 -- e2e/thread-chat/column-viewport.test.mjs | 50 -- e2e/thread-chat/composer-css-source.test.mjs | 28 - .../conversation-composer.test.mjs | 34 - e2e/thread-chat/conversation-message.test.mjs | 63 -- .../dialog-close-to-shell.test.mjs | 72 -- e2e/thread-chat/finalize-with-retry.test.mjs | 56 -- .../generation-actions-db.test.mjs | 241 ------- e2e/thread-chat/generation-db.test.mjs | 469 ------------- .../generation-identity-contract.test.mjs | 31 - .../generation-intent-contract.test.mjs | 43 -- .../generation-persistence.test.mjs | 282 -------- .../generation-reconciliation.test.mjs | 147 ---- .../generation-result-contract.test.mjs | 62 -- .../generation-settlement.test.mjs | 150 ----- .../generation-start-error.test.mjs | 39 -- .../help-panel-css-ownership.test.mjs | 28 - .../local-generation-executions.test.mjs | 76 --- .../markdown-artifact-state.test.mjs | 331 --------- e2e/thread-chat/markdown-artifact.test.mjs | 123 ---- e2e/thread-chat/markdown-settlement.test.mjs | 107 --- .../message-action-availability.test.mjs | 51 -- ...message-action-contract-ownership.test.mjs | 39 -- .../message-action-presentation.test.mjs | 88 --- .../message-action-session.test.mjs | 32 - .../message-actions-controller.test.mjs | 278 -------- e2e/thread-chat/message-artifacts.test.mjs | 27 - .../message-feedback-command.test.mjs | 81 --- .../message-feedback-contract.test.mjs | 71 -- e2e/thread-chat/message-feedback-db.test.mjs | 184 ----- e2e/thread-chat/message-graph.test.mjs | 350 ---------- .../message-id-order-selector.test.mjs | 69 -- .../message-quote-css-ownership.test.mjs | 22 - .../model-pricing-registry.test.mjs | 27 - e2e/thread-chat/model-selection.test.mjs | 133 ---- e2e/thread-chat/openrouter-models.test.mjs | 88 --- e2e/thread-chat/prompt-budget.test.mjs | 93 --- e2e/thread-chat/reconcile-turns.test.mjs | 266 -------- e2e/thread-chat/regeneration-patch.test.mjs | 182 ----- e2e/thread-chat/research-context.test.mjs | 109 --- e2e/thread-chat/research-events.test.mjs | 74 -- .../research-router-context.test.mjs | 49 -- .../runtime-composition-ownership.test.mjs | 30 - e2e/thread-chat/save-tree-contract.test.mjs | 75 --- e2e/thread-chat/search-abort.test.mjs | 37 - .../selection-composer-dimensions.test.mjs | 33 - .../selection-css-ownership.test.mjs | 21 - .../selection-observer-ownership.test.mjs | 26 - ...selection-placement-map-ownership.test.mjs | 23 - .../stop-generation-command.test.mjs | 48 -- e2e/thread-chat/stream-lifecycle.test.mjs | 120 ---- .../style-extension-order.test.mjs | 25 - .../switch-active-leaf-command.test.mjs | 86 --- .../switch-active-leaf-contract.test.mjs | 59 -- e2e/thread-chat/switcher-dimensions.test.mjs | 34 - e2e/thread-chat/syntax-highlighting.test.mjs | 121 ---- e2e/thread-chat/text-anchor.test.mjs | 105 --- e2e/thread-chat/thread-chat-boot.test.mjs | 32 - e2e/thread-chat/thread-chat-topbar.test.mjs | 41 -- .../thread-generation-context.test.mjs | 170 ----- .../thread-switcher-panel.test.mjs | 93 --- .../thread-title-candidate.test.mjs | 114 ---- .../thread-tree-schema-source.test.mjs | 31 - e2e/thread-chat/tool-step-policy.test.mjs | 62 -- e2e/thread-chat/topbar-css-ownership.test.mjs | 32 - e2e/thread-chat/tree-deletion-db.test.mjs | 127 ---- e2e/thread-chat/tree-list-db.test.mjs | 87 --- .../tree-list-repository-ownership.test.mjs | 22 - e2e/thread-chat/tree-list-row.test.mjs | 80 --- e2e/thread-chat/tree-ownership-db.test.mjs | 92 --- .../tree-persistence-write.test.mjs | 55 -- e2e/thread-chat/tree-rename-db.test.mjs | 108 --- e2e/thread-chat/tree-revision-db.test.mjs | 197 ------ e2e/thread-chat/tree-save-db.test.mjs | 124 ---- e2e/thread-chat/tree-save-gate.test.mjs | 25 - e2e/thread-chat/ui-state-snapshot.test.mjs | 29 - e2e/thread-chat/umapis-models.test.mjs | 87 --- e2e/thread-chat/usage-cost-evidence.test.mjs | 55 -- e2e/thread-chat/user-edit-command.test.mjs | 106 --- .../user-turn-retry-command.test.mjs | 98 --- e2e/thread-chat/verify-bubble-composer.mjs | 631 ------------------ e2e/thread-chat/verify-canvas-chat.mjs | 610 ----------------- e2e/thread-chat/verify-live.mjs | 328 --------- e2e/thread-chat/verify-markdown-artifact.mjs | 362 ---------- e2e/thread-chat/verify-markdown-model.mjs | 87 --- e2e/thread-chat/verify-persist.mjs | 263 -------- .../verify-syntax-highlighting.mjs | 288 -------- e2e/thread-chat/verify-tree-list.mjs | 351 ---------- .../web-research-placement.test.mjs | 16 - e2e/thread-chat/web-research-sources.test.mjs | 60 -- .../workspace-composition-ownership.test.mjs | 35 - e2e/thread-chat/workspace-overlay.test.mjs | 64 -- e2e/thread-chat/workspace-toast.test.mjs | 7 - lib/ai/model-call-logger.ts | 2 +- lib/chat/thread-chat-prompt.ts | 33 - lib/chat/tree-id.ts | 15 - lib/db/schema.ts | 132 ---- .../execution-state-repository.ts | 65 -- lib/thread-chat-generation/execution.ts | 95 --- .../finalize-with-retry.ts | 38 -- lib/thread-chat-generation/finalize.ts | 148 ---- .../message-feedback-repository.ts | 132 ---- .../query-repository.ts | 68 -- .../stale-generation-repository.ts | 95 --- .../start-generation-repository.ts | 406 ----------- lib/thread-chat-generation/tree-repository.ts | 307 --------- .../compile-thread-chat-messages.ts | 70 -- .../application/merge-generation-result.ts | 134 ---- .../application/project-generation-result.ts | 189 ------ .../application/reconcile-turns.ts | 180 ----- .../serialize-message-for-model.ts | 28 - .../contracts/generation-identity.ts | 17 - .../contracts/generation-intent.ts | 23 - .../contracts/generation-result.ts | 54 -- .../contracts/message-action-failure.ts | 33 - lib/thread-chat/contracts/message-feedback.ts | 117 ---- lib/thread-chat/contracts/save-tree.ts | 61 -- .../contracts/switch-active-leaf.ts | 68 -- lib/thread-chat/contracts/title-request.ts | 44 -- lib/thread-chat/contracts/tree-revision.ts | 6 - lib/thread-chat/domain/generation.ts | 80 --- lib/thread-chat/domain/regeneration.ts | 140 ---- lib/thread-chat/domain/types.ts | 11 +- .../.openspec.yaml | 0 .../backend-verification.md | 0 .../cleanup-evidence.md | 7 + .../design.md | 0 .../design/enter-thread-chat-from-home.md | 0 .../load-thread-messages-by-sequence.md | 0 ...egenerate-replacement-assistant-message.md | 0 .../resume-running-message-after-refresh.md | 0 .../proposal.md | 0 .../specs/domain/spec.md | 0 .../tasks.md | 18 +- .../.openspec.yaml | 0 .../backend-api-verification.md | 0 .../design.md | 0 .../design/api-contracts.md | 0 .../new-project-first-message-lifecycle.md | 0 .../design/open-existing-project-lifecycle.md | 0 .../design/thread-message-loading.md | 0 .../e2e-evidence.md | 6 + .../frontend-state-verification.md | 0 .../proposal.md | 0 .../specs/thread-chat-client-state/spec.md | 0 .../specs/thread-chat-command-api/spec.md | 0 .../tasks.md | 16 +- .../ui-parity-baseline/README.md | 0 .../ui-parity-baseline/artifact-drawer.png | Bin .../ui-parity-baseline/breadcrumb-back.png | Bin .../ui-parity-baseline/collapsed-column.png | Bin .../ui-parity-baseline/divider-dragged.png | Bin .../ui-parity-baseline/empty-project.png | Bin .../ui-parity-baseline/fork-composer.png | Bin .../header-child-selector.png | Bin .../ui-parity-baseline/single-column.png | Bin .../ui-parity-baseline/switched-slot.png | Bin .../ui-parity-baseline/thread-switcher.png | Bin .../three-columns-nested-fork.png | Bin .../ui-parity-baseline/two-columns.png | Bin .../ui-parity-implementation/README.md | 0 .../artifact-drawer-normalized.png | Bin .../empty-new-normalized.png | Bin .../three-columns-normalized.png | Bin openspec/specs/domain/architecture.md | 62 ++ openspec/specs/domain/spec.md | 221 +++++- .../thread-chat-client-state/architecture.md | 53 ++ .../specs/thread-chat-client-state/spec.md | 298 +++++++++ .../thread-chat-command-api/api-contracts.md | 57 ++ .../specs/thread-chat-command-api/spec.md | 215 ++++++ 271 files changed, 1004 insertions(+), 21167 deletions(-) delete mode 100644 app/api/branch-generations/[generationId]/route.ts delete mode 100644 app/api/branch-generations/[generationId]/stop/route.ts delete mode 100644 app/api/branch-trees/[treeId]/active-leaf/route.ts delete mode 100644 app/api/branch-trees/[treeId]/messages/[messageId]/feedback/route.ts delete mode 100644 app/api/branch-trees/[treeId]/route.ts delete mode 100644 app/api/branch-trees/route.ts delete mode 100644 app/api/chat/conversation-text.ts delete mode 100644 app/api/chat/generation-settlement.ts delete mode 100644 app/api/chat/generation-start-error.ts delete mode 100644 app/api/chat/request-context.ts delete mode 100644 app/api/chat/research-context.ts delete mode 100644 app/api/chat/research-tool-capabilities.ts delete mode 100644 app/api/chat/route.ts delete mode 100644 app/api/chat/stream-lifecycle.ts delete mode 100644 app/api/chat/surface-tools.ts delete mode 100644 app/api/chat/system-prompt.ts delete mode 100644 app/api/chat/thread-generation-context.ts delete mode 100644 app/api/chat/tool-set.ts delete mode 100644 app/api/chat/tool-step-policy.ts delete mode 100644 app/api/title/route.ts create mode 100644 app/thread-chat/[projectId]/page.tsx delete mode 100644 app/thread-chat/[treeId]/page.tsx delete mode 100644 app/thread-chat/chat/actions/message-action-presentation.ts delete mode 100644 app/thread-chat/chat/actions/message-action-session-logic.ts delete mode 100644 app/thread-chat/chat/actions/use-message-actions.ts delete mode 100644 app/thread-chat/core/regeneration.ts delete mode 100644 app/thread-chat/generation/generation-reconciliation-logic.ts delete mode 100644 app/thread-chat/generation/merge-result.ts delete mode 100644 app/thread-chat/generation/project-result.ts delete mode 100644 app/thread-chat/generation/reconcile-turns.ts delete mode 100644 app/thread-chat/generation/types.ts delete mode 100644 app/thread-chat/generation/use-generation-reconciliation.ts delete mode 100644 app/thread-chat/net/boot/thread-chat-boot.ts delete mode 100644 app/thread-chat/net/boot/use-thread-chat-boot.ts delete mode 100644 app/thread-chat/net/chat-controller.ts delete mode 100644 app/thread-chat/net/commands/chat-generation-command.ts delete mode 100644 app/thread-chat/net/commands/message-feedback-command.ts delete mode 100644 app/thread-chat/net/commands/regeneration-command.ts delete mode 100644 app/thread-chat/net/commands/stop-generation-command.ts delete mode 100644 app/thread-chat/net/commands/switch-active-leaf-command.ts delete mode 100644 app/thread-chat/net/persistence/persist.ts delete mode 100644 app/thread-chat/net/persistence/sanitize-loaded-state.ts delete mode 100644 app/thread-chat/net/persistence/save-tree-response.ts delete mode 100644 app/thread-chat/net/persistence/transient-state.ts delete mode 100644 app/thread-chat/net/persistence/tree-save-gate.ts delete mode 100644 app/thread-chat/net/persistence/use-tree-persistence.ts delete mode 100644 app/thread-chat/net/prompt/message-context.ts delete mode 100644 app/thread-chat/net/prompt/message-serialization.ts delete mode 100644 app/thread-chat/net/prompt/prompt-pure.ts delete mode 100644 app/thread-chat/net/prompt/prompt.ts delete mode 100644 app/thread-chat/net/stream/assistant-delta-buffer.ts delete mode 100644 app/thread-chat/net/stream/assistant-output.ts delete mode 100644 app/thread-chat/net/stream/assistant-stream-runtime.ts delete mode 100644 app/thread-chat/net/stream/local-generation-executions.ts delete mode 100644 app/thread-chat/net/stream/ui-stream.ts delete mode 100644 app/thread-chat/net/titles/thread-title-candidate.ts delete mode 100644 app/thread-chat/net/titles/thread-title.ts delete mode 100644 app/thread-chat/net/titles/use-thread-titles.ts delete mode 100644 app/thread-chat/orchestration/columns/use-column-slots.ts delete mode 100644 app/thread-chat/orchestration/navigation/tree-list.tsx delete mode 100644 app/thread-chat/orchestration/workspace/branch-workspace-actions.ts delete mode 100644 app/thread-chat/orchestration/workspace/ui-state-snapshot.ts delete mode 100644 app/thread-chat/orchestration/workspace/use-thread-chat-runtime.ts delete mode 100644 app/thread-chat/orchestration/workspace/use-thread-chat-workspace.ts delete mode 100644 app/thread-chat/orchestration/workspace/use-ui-state-persistence.ts rename app/thread-chat/{tree-redirect.tsx => project-redirect.tsx} (68%) delete mode 100644 app/thread-chat/thread-chat-demo.tsx delete mode 100644 e2e/thread-chat/README.md delete mode 100644 e2e/thread-chat/artifact-card-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/artifact-drawer-accessibility.test.mjs delete mode 100644 e2e/thread-chat/artifact-drawer-css.test.mjs delete mode 100644 e2e/thread-chat/assistant-delta-buffer.test.mjs delete mode 100644 e2e/thread-chat/assistant-retry-command.test.mjs delete mode 100644 e2e/thread-chat/assistant-stream-runtime.test.mjs delete mode 100644 e2e/thread-chat/boot-loading-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/branch-source-css.test.mjs delete mode 100644 e2e/thread-chat/branch-workspace-actions.test.mjs delete mode 100644 e2e/thread-chat/canvas-card-dimensions.test.mjs delete mode 100644 e2e/thread-chat/canvas-dimensions.test.mjs delete mode 100644 e2e/thread-chat/canvas-expand-ownership.test.mjs delete mode 100644 e2e/thread-chat/canvas-layout-cache.test.mjs delete mode 100644 e2e/thread-chat/chat-conversation-text.test.mjs delete mode 100644 e2e/thread-chat/chat-generation-command.test.mjs delete mode 100644 e2e/thread-chat/chat-request-context.test.mjs delete mode 100644 e2e/thread-chat/chat-surface-tools.test.mjs delete mode 100644 e2e/thread-chat/chat-system-prompt.test.mjs delete mode 100644 e2e/thread-chat/chat-tool-set.test.mjs delete mode 100644 e2e/thread-chat/column-control-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/column-slot-ownership.test.mjs delete mode 100644 e2e/thread-chat/column-viewport.test.mjs delete mode 100644 e2e/thread-chat/composer-css-source.test.mjs delete mode 100644 e2e/thread-chat/conversation-composer.test.mjs delete mode 100644 e2e/thread-chat/conversation-message.test.mjs delete mode 100644 e2e/thread-chat/dialog-close-to-shell.test.mjs delete mode 100644 e2e/thread-chat/finalize-with-retry.test.mjs delete mode 100644 e2e/thread-chat/generation-actions-db.test.mjs delete mode 100644 e2e/thread-chat/generation-db.test.mjs delete mode 100644 e2e/thread-chat/generation-identity-contract.test.mjs delete mode 100644 e2e/thread-chat/generation-intent-contract.test.mjs delete mode 100644 e2e/thread-chat/generation-persistence.test.mjs delete mode 100644 e2e/thread-chat/generation-reconciliation.test.mjs delete mode 100644 e2e/thread-chat/generation-result-contract.test.mjs delete mode 100644 e2e/thread-chat/generation-settlement.test.mjs delete mode 100644 e2e/thread-chat/generation-start-error.test.mjs delete mode 100644 e2e/thread-chat/help-panel-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/local-generation-executions.test.mjs delete mode 100644 e2e/thread-chat/markdown-artifact-state.test.mjs delete mode 100644 e2e/thread-chat/markdown-artifact.test.mjs delete mode 100644 e2e/thread-chat/markdown-settlement.test.mjs delete mode 100644 e2e/thread-chat/message-action-availability.test.mjs delete mode 100644 e2e/thread-chat/message-action-contract-ownership.test.mjs delete mode 100644 e2e/thread-chat/message-action-presentation.test.mjs delete mode 100644 e2e/thread-chat/message-action-session.test.mjs delete mode 100644 e2e/thread-chat/message-actions-controller.test.mjs delete mode 100644 e2e/thread-chat/message-artifacts.test.mjs delete mode 100644 e2e/thread-chat/message-feedback-command.test.mjs delete mode 100644 e2e/thread-chat/message-feedback-contract.test.mjs delete mode 100644 e2e/thread-chat/message-feedback-db.test.mjs delete mode 100644 e2e/thread-chat/message-graph.test.mjs delete mode 100644 e2e/thread-chat/message-id-order-selector.test.mjs delete mode 100644 e2e/thread-chat/message-quote-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/model-pricing-registry.test.mjs delete mode 100644 e2e/thread-chat/model-selection.test.mjs delete mode 100644 e2e/thread-chat/openrouter-models.test.mjs delete mode 100644 e2e/thread-chat/prompt-budget.test.mjs delete mode 100644 e2e/thread-chat/reconcile-turns.test.mjs delete mode 100644 e2e/thread-chat/regeneration-patch.test.mjs delete mode 100644 e2e/thread-chat/research-context.test.mjs delete mode 100644 e2e/thread-chat/research-events.test.mjs delete mode 100644 e2e/thread-chat/research-router-context.test.mjs delete mode 100644 e2e/thread-chat/runtime-composition-ownership.test.mjs delete mode 100644 e2e/thread-chat/save-tree-contract.test.mjs delete mode 100644 e2e/thread-chat/search-abort.test.mjs delete mode 100644 e2e/thread-chat/selection-composer-dimensions.test.mjs delete mode 100644 e2e/thread-chat/selection-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/selection-observer-ownership.test.mjs delete mode 100644 e2e/thread-chat/selection-placement-map-ownership.test.mjs delete mode 100644 e2e/thread-chat/stop-generation-command.test.mjs delete mode 100644 e2e/thread-chat/stream-lifecycle.test.mjs delete mode 100644 e2e/thread-chat/style-extension-order.test.mjs delete mode 100644 e2e/thread-chat/switch-active-leaf-command.test.mjs delete mode 100644 e2e/thread-chat/switch-active-leaf-contract.test.mjs delete mode 100644 e2e/thread-chat/switcher-dimensions.test.mjs delete mode 100644 e2e/thread-chat/syntax-highlighting.test.mjs delete mode 100644 e2e/thread-chat/text-anchor.test.mjs delete mode 100644 e2e/thread-chat/thread-chat-boot.test.mjs delete mode 100644 e2e/thread-chat/thread-chat-topbar.test.mjs delete mode 100644 e2e/thread-chat/thread-generation-context.test.mjs delete mode 100644 e2e/thread-chat/thread-switcher-panel.test.mjs delete mode 100644 e2e/thread-chat/thread-title-candidate.test.mjs delete mode 100644 e2e/thread-chat/thread-tree-schema-source.test.mjs delete mode 100644 e2e/thread-chat/tool-step-policy.test.mjs delete mode 100644 e2e/thread-chat/topbar-css-ownership.test.mjs delete mode 100644 e2e/thread-chat/tree-deletion-db.test.mjs delete mode 100644 e2e/thread-chat/tree-list-db.test.mjs delete mode 100644 e2e/thread-chat/tree-list-repository-ownership.test.mjs delete mode 100644 e2e/thread-chat/tree-list-row.test.mjs delete mode 100644 e2e/thread-chat/tree-ownership-db.test.mjs delete mode 100644 e2e/thread-chat/tree-persistence-write.test.mjs delete mode 100644 e2e/thread-chat/tree-rename-db.test.mjs delete mode 100644 e2e/thread-chat/tree-revision-db.test.mjs delete mode 100644 e2e/thread-chat/tree-save-db.test.mjs delete mode 100644 e2e/thread-chat/tree-save-gate.test.mjs delete mode 100644 e2e/thread-chat/ui-state-snapshot.test.mjs delete mode 100644 e2e/thread-chat/umapis-models.test.mjs delete mode 100644 e2e/thread-chat/usage-cost-evidence.test.mjs delete mode 100644 e2e/thread-chat/user-edit-command.test.mjs delete mode 100644 e2e/thread-chat/user-turn-retry-command.test.mjs delete mode 100644 e2e/thread-chat/verify-bubble-composer.mjs delete mode 100644 e2e/thread-chat/verify-canvas-chat.mjs delete mode 100644 e2e/thread-chat/verify-live.mjs delete mode 100644 e2e/thread-chat/verify-markdown-artifact.mjs delete mode 100644 e2e/thread-chat/verify-markdown-model.mjs delete mode 100644 e2e/thread-chat/verify-persist.mjs delete mode 100644 e2e/thread-chat/verify-syntax-highlighting.mjs delete mode 100644 e2e/thread-chat/verify-tree-list.mjs delete mode 100644 e2e/thread-chat/web-research-placement.test.mjs delete mode 100644 e2e/thread-chat/web-research-sources.test.mjs delete mode 100644 e2e/thread-chat/workspace-composition-ownership.test.mjs delete mode 100644 e2e/thread-chat/workspace-overlay.test.mjs delete mode 100644 e2e/thread-chat/workspace-toast.test.mjs delete mode 100644 lib/chat/thread-chat-prompt.ts delete mode 100644 lib/chat/tree-id.ts delete mode 100644 lib/thread-chat-generation/execution-state-repository.ts delete mode 100644 lib/thread-chat-generation/execution.ts delete mode 100644 lib/thread-chat-generation/finalize-with-retry.ts delete mode 100644 lib/thread-chat-generation/finalize.ts delete mode 100644 lib/thread-chat-generation/message-feedback-repository.ts delete mode 100644 lib/thread-chat-generation/query-repository.ts delete mode 100644 lib/thread-chat-generation/stale-generation-repository.ts delete mode 100644 lib/thread-chat-generation/start-generation-repository.ts delete mode 100644 lib/thread-chat-generation/tree-repository.ts delete mode 100644 lib/thread-chat/application/compile-thread-chat-messages.ts delete mode 100644 lib/thread-chat/application/merge-generation-result.ts delete mode 100644 lib/thread-chat/application/project-generation-result.ts delete mode 100644 lib/thread-chat/application/reconcile-turns.ts delete mode 100644 lib/thread-chat/application/serialize-message-for-model.ts delete mode 100644 lib/thread-chat/contracts/generation-identity.ts delete mode 100644 lib/thread-chat/contracts/generation-intent.ts delete mode 100644 lib/thread-chat/contracts/generation-result.ts delete mode 100644 lib/thread-chat/contracts/message-action-failure.ts delete mode 100644 lib/thread-chat/contracts/message-feedback.ts delete mode 100644 lib/thread-chat/contracts/save-tree.ts delete mode 100644 lib/thread-chat/contracts/switch-active-leaf.ts delete mode 100644 lib/thread-chat/contracts/title-request.ts delete mode 100644 lib/thread-chat/contracts/tree-revision.ts delete mode 100644 lib/thread-chat/domain/generation.ts delete mode 100644 lib/thread-chat/domain/regeneration.ts rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/.openspec.yaml (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/backend-verification.md (100%) create mode 100644 openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/cleanup-evidence.md rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/design.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/design/enter-thread-chat-from-home.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/design/load-thread-messages-by-sequence.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/design/regenerate-replacement-assistant-message.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/design/resume-running-message-after-refresh.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/proposal.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/specs/domain/spec.md (100%) rename openspec/changes/{define-thread-chat-domain-model => archive/2026-08-25-define-thread-chat-domain-model}/tasks.md (93%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/.openspec.yaml (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/backend-api-verification.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/design.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/design/api-contracts.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/design/new-project-first-message-lifecycle.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/design/open-existing-project-lifecycle.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/design/thread-message-loading.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/e2e-evidence.md (89%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/frontend-state-verification.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/proposal.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/specs/thread-chat-client-state/spec.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/specs/thread-chat-command-api/spec.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/tasks.md (95%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/README.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/artifact-drawer.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/breadcrumb-back.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/collapsed-column.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/divider-dragged.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/empty-project.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/fork-composer.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/header-child-selector.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/single-column.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/switched-slot.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/thread-switcher.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/three-columns-nested-fork.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-baseline/two-columns.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-implementation/README.md (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-implementation/artifact-drawer-normalized.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-implementation/empty-new-normalized.png (100%) rename openspec/changes/{design-thread-chat-client-api => archive/2026-08-25-design-thread-chat-client-api}/ui-parity-implementation/three-columns-normalized.png (100%) create mode 100644 openspec/specs/domain/architecture.md create mode 100644 openspec/specs/thread-chat-client-state/architecture.md create mode 100644 openspec/specs/thread-chat-client-state/spec.md create mode 100644 openspec/specs/thread-chat-command-api/api-contracts.md create mode 100644 openspec/specs/thread-chat-command-api/spec.md diff --git a/app/api/branch-generations/[generationId]/route.ts b/app/api/branch-generations/[generationId]/route.ts deleted file mode 100644 index a6a455f2..00000000 --- a/app/api/branch-generations/[generationId]/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getCurrentUserId } from "@/lib/auth/server" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { failStaleGenerationForOwner } from "@/lib/thread-chat-generation/stale-generation-repository" -import { toGenerationSummary } from "@/lib/thread-chat-generation/query-repository" - -type RouteContext = { params: Promise<{ generationId: string }> } - -export async function GET(_req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) - return Response.json( - { error: { code: "unauthorized", message: "请先登录" } }, - { status: 401 } - ) - - const { generationId } = await params - if (!isValidTreeId(generationId)) - return Response.json( - { error: { code: "invalid_id", message: "generationId 必须是 UUID" } }, - { status: 400 } - ) - - const generation = await failStaleGenerationForOwner(userId, generationId) - if (!generation) - return Response.json( - { error: { code: "not_found", message: "generation 不存在" } }, - { status: 404 } - ) - return Response.json({ generation: toGenerationSummary(generation) }) -} diff --git a/app/api/branch-generations/[generationId]/stop/route.ts b/app/api/branch-generations/[generationId]/stop/route.ts deleted file mode 100644 index 385b63d6..00000000 --- a/app/api/branch-generations/[generationId]/stop/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getCurrentUserId } from "@/lib/auth/server" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { requestGenerationStop } from "@/lib/thread-chat-generation/execution-state-repository" -import { toGenerationSummary } from "@/lib/thread-chat-generation/query-repository" -import { abortGenerationLocally } from "@/lib/thread-chat-generation/execution" - -type RouteContext = { params: Promise<{ generationId: string }> } - -export async function POST(_req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) - return Response.json( - { error: { code: "unauthorized", message: "请先登录" } }, - { status: 401 } - ) - - const { generationId } = await params - if (!isValidTreeId(generationId)) - return Response.json( - { error: { code: "invalid_id", message: "generationId 必须是 UUID" } }, - { status: 400 } - ) - - const generation = await requestGenerationStop(userId, generationId) - if (!generation) - return Response.json( - { error: { code: "not_found", message: "generation 不存在" } }, - { status: 404 } - ) - if (generation.status === "stop_requested") { - abortGenerationLocally(generationId) - } - return Response.json({ generation: toGenerationSummary(generation) }) -} diff --git a/app/api/branch-trees/[treeId]/active-leaf/route.ts b/app/api/branch-trees/[treeId]/active-leaf/route.ts deleted file mode 100644 index c7af8f3e..00000000 --- a/app/api/branch-trees/[treeId]/active-leaf/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { getCurrentUserId } from "@/lib/auth/server" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { - SWITCH_ACTIVE_LEAF_ERROR_STATUS, - SWITCH_ACTIVE_LEAF_ROUTE_ERRORS, - switchActiveLeafErrorResponseSchema, - switchActiveLeafRequestSchema, - switchActiveLeafSuccessResponseSchema, - type SwitchActiveLeafErrorCode, -} from "@/lib/thread-chat/contracts/switch-active-leaf" -import { - switchActiveLeafForOwner, - TreeCommandError, -} from "@/lib/thread-chat-generation/tree-repository" - -type RouteContext = { params: Promise<{ treeId: string }> } - -function activeLeafErrorResponse( - code: SwitchActiveLeafErrorCode, - message: string, - currentRevision?: number -) { - return Response.json( - switchActiveLeafErrorResponseSchema.parse({ - error: { - code, - message, - ...(currentRevision !== undefined ? { currentRevision } : {}), - }, - }), - { status: SWITCH_ACTIVE_LEAF_ERROR_STATUS[code] } - ) -} - -export async function PATCH(req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) { - const error = SWITCH_ACTIVE_LEAF_ROUTE_ERRORS.unauthorized - return activeLeafErrorResponse(error.code, error.message) - } - - const { treeId } = await params - if (!isValidTreeId(treeId)) { - const error = SWITCH_ACTIVE_LEAF_ROUTE_ERRORS.invalid_id - return activeLeafErrorResponse(error.code, error.message) - } - const body = switchActiveLeafRequestSchema.safeParse( - await req.json().catch(() => null) - ) - if (!body.success) { - const error = SWITCH_ACTIVE_LEAF_ROUTE_ERRORS.invalid_request - return activeLeafErrorResponse(error.code, error.message) - } - - try { - return Response.json( - switchActiveLeafSuccessResponseSchema.parse( - await switchActiveLeafForOwner({ userId, treeId, ...body.data }) - ) - ) - } catch (error) { - if (!(error instanceof TreeCommandError)) throw error - return activeLeafErrorResponse( - error.code, - error.message, - error.currentRevision - ) - } -} diff --git a/app/api/branch-trees/[treeId]/messages/[messageId]/feedback/route.ts b/app/api/branch-trees/[treeId]/messages/[messageId]/feedback/route.ts deleted file mode 100644 index e1fee337..00000000 --- a/app/api/branch-trees/[treeId]/messages/[messageId]/feedback/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getCurrentUserId } from "@/lib/auth/server" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { - MESSAGE_FEEDBACK_HTTP_ERRORS, - setMessageFeedbackErrorResponseSchema, - setMessageFeedbackRequestSchema, - setMessageFeedbackSuccessResponseSchema, -} from "@/lib/thread-chat/contracts/message-feedback" -import { setMessageFeedbackForOwner } from "@/lib/thread-chat-generation/message-feedback-repository" - -type RouteContext = { - params: Promise<{ treeId: string; messageId: string }> -} - -function feedbackErrorResponse(key: keyof typeof MESSAGE_FEEDBACK_HTTP_ERRORS) { - const definition = MESSAGE_FEEDBACK_HTTP_ERRORS[key] - return Response.json( - setMessageFeedbackErrorResponseSchema.parse({ error: definition.error }), - { status: definition.status } - ) -} - -export async function PUT(req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) return feedbackErrorResponse("unauthorized") - - const { treeId, messageId } = await params - if (!isValidTreeId(treeId) || messageId.trim() === "") - return feedbackErrorResponse("invalid_id") - - const body = setMessageFeedbackRequestSchema.safeParse( - await req.json().catch(() => null) - ) - if (!body.success) return feedbackErrorResponse("invalid_feedback") - - const result = await setMessageFeedbackForOwner({ - userId, - treeId, - threadId: body.data.threadId, - messageId, - feedback: body.data.feedback, - }) - if (!result.ok) return feedbackErrorResponse(result.reason) - - return Response.json( - setMessageFeedbackSuccessResponseSchema.parse({ - feedback: result.feedback, - }) - ) -} diff --git a/app/api/branch-trees/[treeId]/route.ts b/app/api/branch-trees/[treeId]/route.ts deleted file mode 100644 index ec3fd4d5..00000000 --- a/app/api/branch-trees/[treeId]/route.ts +++ /dev/null @@ -1,248 +0,0 @@ -/** - * /api/branch-trees/[treeId] —— 分支对话树(app/thread-chat)的整树读写。 - * - * 一棵树一行(branch_trees.state = 完整 ThreadTreeState JSON): - * · GET 命中返回 { state, customTitle }(customTitle = 用户重命名过的标题,未改过为 null, - * 供主线列头副标题优先展示);未命中返回 200 + { state: null, customTitle: null }—— - * 首次访问是正常路径不是错误,客户端一个分支判断即可,无需在 fetch 层区分 - * 「404 = 正常」与「404 = 路由不存在」。 - * · PUT { state, title?, baseRevision } 严格校验 schema-v2 消息图,并按 owner/revision - * 做 CAS upsert。只写 state / 派生 title / updatedAt,不触碰 custom_title(双轨标题)。 - * · PATCH { title } 重命名:trim 后非空且 ≤ CUSTOM_TITLE_MAX_LEN,只写 custom_title 列; - * 树不存在 404——与 PUT 的派生轨互不踩踏。 - * · DELETE 删除该行,幂等(不存在也返回 { ok: true })。 - * treeId 做 UUID 形状校验(安全阀),不合法一律 400。 - */ - -import { isValidTreeId } from "@/lib/chat/tree-id" -import { - CUSTOM_TITLE_MAX_LEN, - THREAD_TREE_SCHEMA_VERSION, -} from "@/constants/thread-chat" -import { getCurrentUserId } from "@/lib/auth/server" -import type { ThreadTreeState } from "@/lib/thread-chat/domain/types" -import { parseThreadTreeState } from "@/lib/thread-chat/domain/message-graph" -import { - assertCompletedMessageGenerationLinks, - reconcileThreadChatTurns, -} from "@/lib/thread-chat/application/reconcile-turns" -import { failStaleGenerationsForTree } from "@/lib/thread-chat-generation/stale-generation-repository" -import { - listCurrentGenerationsForTree, - toGenerationSummary, -} from "@/lib/thread-chat-generation/query-repository" -import { listMessageFeedbackForTree } from "@/lib/thread-chat-generation/message-feedback-repository" -import { - deleteOwnedTreeIfIdle, - loadOwnedOrClaimLegacyTree, - renameOwnedTree, - saveOwnedTree, -} from "@/lib/thread-chat-generation/tree-repository" -import { - SAVE_TREE_ERROR_STATUS, - SAVE_TREE_REVISION_ERRORS, - saveTreeErrorResponseSchema, - saveTreeRequestSchema, - saveTreeSuccessResponseSchema, - type SaveTreeErrorCode, -} from "@/lib/thread-chat/contracts/save-tree" - -type RouteContext = { params: Promise<{ treeId: string }> } - -function unauthorized() { - return Response.json( - { error: { code: "unauthorized", message: "请先登录" } }, - { status: 401 } - ) -} - -function notFound() { - return Response.json( - { error: { code: "not_found", message: "分支树不存在" } }, - { status: 404 } - ) -} - -function saveTreeErrorResponse( - code: SaveTreeErrorCode, - message: string, - currentRevision?: number -) { - return Response.json( - saveTreeErrorResponseSchema.parse({ - error: { - code, - message, - ...(currentRevision !== undefined ? { currentRevision } : {}), - }, - }), - { status: SAVE_TREE_ERROR_STATUS[code] } - ) -} - -export async function GET(_req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) return unauthorized() - const { treeId } = await params - if (!isValidTreeId(treeId)) - return new Response("treeId 必须是 UUID", { status: 400 }) - - const row = await loadOwnedOrClaimLegacyTree({ userId, treeId }) - if (!row) return notFound() - - await failStaleGenerationsForTree(userId, treeId) - const [generations, messageFeedbacks] = await Promise.all([ - listCurrentGenerationsForTree(userId, treeId), - listMessageFeedbackForTree(userId, treeId), - ]) - const generationSummaries = generations.map(toGenerationSummary) - let reconciled - try { - reconciled = reconcileThreadChatTurns({ - state: row.state as ThreadTreeState, - generations: generations.map((generation) => ({ - ...toGenerationSummary(generation), - turnSnapshot: generation.turnSnapshot, - })), - }) - assertCompletedMessageGenerationLinks( - reconciled.state, - generationSummaries - ) - } catch (error) { - console.error("[thread-chat] 消息图读取协调失败", { treeId, error }) - return Response.json( - { - error: { - code: "invalid_tree_state", - message: "分支树消息结构或生成关联无效", - }, - }, - { status: 500 } - ) - } - return Response.json({ - state: reconciled.state, - revision: row.revision, - customTitle: row.customTitle, - generations: generationSummaries, - messageFeedbacks, - recoverableTurns: reconciled.recoverableTurns, - }) -} - -export async function PUT(req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) return unauthorized() - const { treeId } = await params - if (!isValidTreeId(treeId)) - return new Response("treeId 必须是 UUID", { status: 400 }) - - let body: { state?: unknown; title?: unknown; baseRevision?: unknown } - try { - body = await req.json() - } catch { - return new Response("body 必须是 JSON", { status: 400 }) - } - const { state } = body - if (typeof state !== "object" || state === null || Array.isArray(state)) - return new Response("state 缺失或不是对象", { status: 400 }) - // threads 必须是普通对象(codex review:数组/标量会让列表接口的 jsonb_object_keys - // 对这一行永久抛错,一行毒数据打挂整个 GET /api/branch-trees) - const threads = (state as Record).threads - if (typeof threads !== "object" || threads === null || Array.isArray(threads)) - return new Response("state.threads 必须是对象", { status: 400 }) - - const title = typeof body.title === "string" ? body.title : null - const incomingSchemaVersion = (state as Record).schemaVersion - if (incomingSchemaVersion !== THREAD_TREE_SCHEMA_VERSION) - return saveTreeErrorResponse( - "invalid_tree_state", - `只接受 schemaVersion=${THREAD_TREE_SCHEMA_VERSION} 的消息图` - ) - const command = saveTreeRequestSchema.safeParse(body) - if (!command.success) { - const error = SAVE_TREE_REVISION_ERRORS.revision_required - return saveTreeErrorResponse(error.code, error.message) - } - - let validatedState: ThreadTreeState - try { - validatedState = parseThreadTreeState(state) - } catch { - return saveTreeErrorResponse( - "invalid_tree_state", - "消息图包含无效的 parent、active leaf 或 Artifact source" - ) - } - const saved = await saveOwnedTree({ - userId, - treeId, - state: validatedState, - title, - baseRevision: command.data.baseRevision, - }) - if (saved.kind === "not_found") return notFound() - if (saved.kind === "conflict") { - const error = SAVE_TREE_REVISION_ERRORS.tree_revision_conflict - return saveTreeErrorResponse(error.code, error.message, saved.revision) - } - return Response.json( - saveTreeSuccessResponseSchema.parse({ - ok: true, - revision: saved.revision, - }) - ) -} - -export async function PATCH(req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) return unauthorized() - const { treeId } = await params - if (!isValidTreeId(treeId)) - return new Response("treeId 必须是 UUID", { status: 400 }) - - let body: { title?: unknown } - try { - body = await req.json() - } catch { - return new Response("body 必须是 JSON", { status: 400 }) - } - const title = typeof body.title === "string" ? body.title.trim() : "" - if (title === "" || title.length > CUSTOM_TITLE_MAX_LEN) - return new Response( - `title 必须为 trim 后非空且不超过 ${CUSTOM_TITLE_MAX_LEN} 字的字符串`, - { status: 400 } - ) - - // 只写 custom_title(用户意志轨)——防抖 PUT 的派生 title 与之互不踩踏(design D1) - const renamed = await renameOwnedTree({ - userId, - treeId, - customTitle: title, - }) - if (!renamed) return new Response("树不存在", { status: 404 }) - return Response.json({ ok: true }) -} - -export async function DELETE(_req: Request, { params }: RouteContext) { - const userId = await getCurrentUserId() - if (!userId) return unauthorized() - const { treeId } = await params - if (!isValidTreeId(treeId)) - return new Response("treeId 必须是 UUID", { status: 400 }) - - const outcome = await deleteOwnedTreeIfIdle({ userId, treeId }) - if (outcome === "generation_running") { - return Response.json( - { - error: { - code: "generation_running", - message: "请先停止正在运行的生成,再删除这棵对话树", - }, - }, - { status: 409 } - ) - } - return Response.json({ ok: true }) -} diff --git a/app/api/branch-trees/route.ts b/app/api/branch-trees/route.ts deleted file mode 100644 index e794a955..00000000 --- a/app/api/branch-trees/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * /api/branch-trees —— 分支树的轻量列表(会话列表 UI 的数据源)。 - * - * GET 返回 { trees: [{ id, title, updatedAt, threadCount }] }: - * · title = coalesce(custom_title, title)(双轨标题,design D1),双空回退「未命名对话」; - * · threadCount 在 SQL 内由 state->'threads' 的顶层键数派生(design D2)—— - * 不回传整树 state(可能百 KB 级),列表只要元信息; - * · updated_at 降序,limit 100 兜底(v1 不做分页/搜索)。 - */ - -import { getCurrentUserId } from "@/lib/auth/server" -import { listOwnedTreeSummaries } from "@/lib/thread-chat-generation/tree-repository" - -export async function GET() { - const userId = await getCurrentUserId() - if (!userId) - return Response.json( - { error: { code: "unauthorized", message: "请先登录" } }, - { status: 401 } - ) - - const rows = await listOwnedTreeSummaries(userId) - return Response.json({ trees: rows }) -} diff --git a/app/api/chat/conversation-text.ts b/app/api/chat/conversation-text.ts deleted file mode 100644 index eeefaa01..00000000 --- a/app/api/chat/conversation-text.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { UIMessage } from "ai" -import { RESEARCH_ROUTER_CONTEXT_MESSAGES } from "@/constants/research" - -/** 只看最后一条 user 消息的文本 part,供高置信首步强制路由。 */ -export function latestUserText(messages: UIMessage[]): string { - for (let index = messages.length - 1; index >= 0; index--) { - const message = messages[index] - if (message.role !== "user") continue - return message.parts - .flatMap((part) => (part.type === "text" ? [part.text] : [])) - .join("\n") - } - return "" -} - -/** Router 只取最近少量纯文本上下文,用于理解“这个/它”等指代。 */ -export function recentConversationText(messages: UIMessage[]): string { - return messages - .slice(-RESEARCH_ROUTER_CONTEXT_MESSAGES) - .map((message) => { - const text = message.parts - .flatMap((part) => (part.type === "text" ? [part.text] : [])) - .join("\n") - return `${message.role}: ${text}` - }) - .join("\n") -} diff --git a/app/api/chat/generation-settlement.ts b/app/api/chat/generation-settlement.ts deleted file mode 100644 index 82fa3421..00000000 --- a/app/api/chat/generation-settlement.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { UIMessage } from "ai" -import type { ResearchPlan, ResearchRoute } from "@/lib/chat/research-router" -import type { ThreadChatGenerationIdentity } from "@/lib/thread-chat/contracts/generation-identity" -import { GENERATION_ERRORS } from "@/constants/generation" -import { projectGenerationResult } from "@/lib/thread-chat/application/project-generation-result" -import { finalizeGenerationWithRetry } from "@/lib/thread-chat-generation/finalize-with-retry" -import type { StreamLifecycle } from "@/app/api/chat/stream-lifecycle" - -type SettlementDependencies = { - project: typeof projectGenerationResult - finalize: typeof finalizeGenerationWithRetry -} - -const defaultDependencies: SettlementDependencies = { - project: projectGenerationResult, - finalize: finalizeGenerationWithRetry, -} - -type GenerationSettlementInput = { - persistence: ThreadChatGenerationIdentity - researchRoute: ResearchRoute - researchPlan: ResearchPlan | null - unbilledPreview: boolean - streamLifecycle: Pick -} - -/** 将 UI stream 的结束信号投影并一次性收口到 generation 终态。 */ -export function createGenerationSettlementHandler( - { - persistence, - researchRoute, - researchPlan, - unbilledPreview, - streamLifecycle, - }: GenerationSettlementInput, - dependencies: SettlementDependencies = defaultDependencies -) { - return async ({ - responseMessage, - isAborted, - finishReason, - }: { - responseMessage: Pick - isAborted: boolean - finishReason?: string | null - }) => { - const { capturedUsage, modelStreamError, abortedUsageUnavailable } = - streamLifecycle.snapshot() - const failedWithoutFinish = - finishReason == null && modelStreamError !== undefined - const requestedTerminal = isAborted - ? "stopped" - : failedWithoutFinish - ? "failed" - : "completed" - const projected = dependencies.project({ - generationId: persistence.generationId, - threadId: persistence.threadId, - assistantMessageId: persistence.assistantMessageId, - responseMessage, - terminalStatus: requestedTerminal, - error: modelStreamError, - researchRoute, - researchPlan: researchPlan ?? undefined, - usage: capturedUsage - ? { - inputTokens: capturedUsage.inputTokens, - outputTokens: capturedUsage.outputTokens, - totalTokens: capturedUsage.inputTokens + capturedUsage.outputTokens, - } - : undefined, - }) - const outcome = - requestedTerminal === "completed" && !projected.hasDisplayableOutput - ? "failed" - : requestedTerminal - await dependencies.finalize({ - generationId: persistence.generationId, - outcome, - result: projected.result, - error: projected.result.error ?? modelStreamError, - usage: unbilledPreview ? undefined : capturedUsage, - usageUnavailable: - !unbilledPreview && (abortedUsageUnavailable || !capturedUsage), - }) - } -} - -/** stream 初始化阶段抛错时,尽力保存失败终态;结算失败不覆盖原 HTTP 错误。 */ -export async function settleGenerationInitializationFailure( - { - persistence, - usageUnavailable, - }: { - persistence: ThreadChatGenerationIdentity - error: unknown - usageUnavailable: boolean - }, - dependencies: SettlementDependencies = defaultDependencies -) { - const projected = dependencies.project({ - generationId: persistence.generationId, - threadId: persistence.threadId, - assistantMessageId: persistence.assistantMessageId, - responseMessage: { parts: [] }, - terminalStatus: "failed", - error: GENERATION_ERRORS.streamFailed, - }) - try { - await dependencies.finalize({ - generationId: persistence.generationId, - outcome: "failed", - result: projected.result, - error: projected.result.error, - usageUnavailable, - }) - } catch (finalizeError) { - console.error("[thread-chat-generation] 请求初始化失败后的终态保存失败", { - generationId: persistence.generationId, - finalizeError, - }) - } -} diff --git a/app/api/chat/generation-start-error.ts b/app/api/chat/generation-start-error.ts deleted file mode 100644 index 8de4ec82..00000000 --- a/app/api/chat/generation-start-error.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GenerationRepositoryError } from "@/lib/thread-chat-generation/start-generation-repository" -import type { MessageActionFailureResponse } from "@/lib/thread-chat/contracts/message-action-failure" - -/** 将 generation start 事务错误映射为稳定的 HTTP 响应。 */ -export function generationStartErrorResponse(error: unknown): Response { - if (error instanceof GenerationRepositoryError) { - return Response.json( - { - error: { - code: error.code, - message: error.message, - }, - } satisfies MessageActionFailureResponse, - { - status: - error.code === "not_found" - ? 404 - : error.code === "persistence_failed" - ? 503 - : 409, - } - ) - } - console.error("[thread-chat-generation] start transaction 失败", error) - return Response.json( - { - error: { - code: "persistence_failed", - message: "无法建立生成任务,尚未调用模型", - }, - } satisfies MessageActionFailureResponse, - { status: 503 } - ) -} diff --git a/app/api/chat/request-context.ts b/app/api/chat/request-context.ts deleted file mode 100644 index 1c866dfc..00000000 --- a/app/api/chat/request-context.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { safeValidateUIMessages, type UIMessage } from "ai" -import type { ToolJSONSchema } from "assistant-stream" -import { z } from "zod" -import { getCurrentUserId } from "@/lib/auth/server" -import { - DEFAULT_MODEL_ID, - getChatModel, - isLinearChatModelId, - isThreadChatModelId, - isUnbilledPreviewModel, -} from "@/constants/model" -import { isModelConfigured } from "@/lib/ai/provider" -import { hasPositiveBalance } from "@/lib/billing/credits" -import { - threadChatGenerationIdentitySchema, - type ThreadChatGenerationIdentity, -} from "@/lib/thread-chat/contracts/generation-identity" -import type { MessageActionFailureResponse } from "@/lib/thread-chat/contracts/message-action-failure" - -type ChatRequestBody = { - messages: UIMessage[] - tools?: Record - deepResearch?: boolean - /** thread-chat 分支对话页的持久化 generation identity。 */ - threadChat?: unknown - modelId?: unknown - id?: string -} - -const chatRequestEnvelopeSchema = z.object({ - messages: z.unknown(), - tools: z.record(z.string(), z.unknown()).optional(), - deepResearch: z.boolean().optional(), - threadChat: z.unknown().optional(), - modelId: z.unknown().optional(), - id: z.string().optional(), -}) - -function invalidChatRequest(message: string) { - return { - kind: "response" as const, - response: Response.json({ error: message }, { status: 400 }), - } -} - -type ChatRequestContextDependencies = { - currentUserId: typeof getCurrentUserId - getModel: typeof getChatModel - linearModelAllowed: typeof isLinearChatModelId - threadModelAllowed: typeof isThreadChatModelId - modelConfigured: typeof isModelConfigured - unbilledPreview: typeof isUnbilledPreviewModel - positiveBalance: typeof hasPositiveBalance -} - -const defaultDependencies: ChatRequestContextDependencies = { - currentUserId: getCurrentUserId, - getModel: getChatModel, - linearModelAllowed: isLinearChatModelId, - threadModelAllowed: isThreadChatModelId, - modelConfigured: isModelConfigured, - unbilledPreview: isUnbilledPreviewModel, - positiveBalance: hasPositiveBalance, -} - -/** 鉴权、解析并完成模型/余额门禁,返回可直接进入生成编排的请求上下文。 */ -export async function prepareChatRequestContext( - req: Request, - dependencies: ChatRequestContextDependencies = defaultDependencies -) { - const userId = await dependencies.currentUserId() - if (!userId) { - return { - kind: "response" as const, - response: Response.json( - { error: "请先登录后再使用对话功能。" }, - { status: 401 } - ), - } - } - - let input: unknown - try { - input = await req.json() - } catch { - return invalidChatRequest("请求体必须是有效 JSON。") - } - const envelope = chatRequestEnvelopeSchema.safeParse(input) - if (!envelope.success) - return invalidChatRequest("请求体缺少有效的 messages。") - - const validatedMessages = await safeValidateUIMessages({ - messages: envelope.data.messages, - }) - if ( - !validatedMessages.success || - validatedMessages.data.length === 0 || - validatedMessages.data.some((message) => message.role === "system") - ) - return invalidChatRequest("messages 必须是非空的 user/assistant 消息数组。") - - const body: ChatRequestBody = { - ...envelope.data, - messages: validatedMessages.data, - tools: envelope.data.tools as Record | undefined, - } - const rawModelId = body.modelId - if ( - rawModelId !== undefined && - (typeof rawModelId !== "string" || !dependencies.getModel(rawModelId)) - ) { - return { - kind: "response" as const, - response: Response.json({ error: "未知或无效的模型。" }, { status: 400 }), - } - } - - const modelId = typeof rawModelId === "string" ? rawModelId : DEFAULT_MODEL_ID - const model = dependencies.getModel(modelId)! - let threadChat: ThreadChatGenerationIdentity | undefined - if (body.threadChat != null) { - const parsedIdentity = threadChatGenerationIdentitySchema.safeParse( - body.threadChat - ) - if (!parsedIdentity.success) - return { - kind: "response" as const, - response: Response.json( - { - error: { - code: "invalid_generation_identity", - message: "thread-chat 请求缺少有效的持久化身份,请刷新页面后重试", - }, - } satisfies MessageActionFailureResponse, - { status: 400 } - ), - } - if (!dependencies.threadModelAllowed(modelId)) - return { - kind: "response" as const, - response: Response.json( - { - error: { - code: "invalid_thread_model", - message: "Thread Chat 不允许使用该模型,请刷新页面后重试", - }, - } satisfies MessageActionFailureResponse, - { status: 400 } - ), - } - threadChat = parsedIdentity.data - } else if (!dependencies.linearModelAllowed(modelId)) { - return { - kind: "response" as const, - response: Response.json( - { error: "该模型不支持线性对话,请选择可用模型后重试。" }, - { status: 400 } - ), - } - } - if (!dependencies.modelConfigured(model)) { - return { - kind: "response" as const, - response: Response.json( - { - error: `模型「${model.name}」未配置,请联系管理员在服务端配置对应 API Key 或可用网关。`, - }, - { status: 400 } - ), - } - } - - const isUnbilledPreview = dependencies.unbilledPreview(model) - if (!isUnbilledPreview && !(await dependencies.positiveBalance(userId))) { - return { - kind: "response" as const, - response: Response.json( - { error: "额度不足,请充值后再试。" }, - { status: 402 } - ), - } - } - - return { - kind: "ready" as const, - userId, - messages: body.messages, - tools: body.tools, - deepResearch: body.deepResearch, - threadChat, - linearThreadId: body.id, - modelId, - model, - isUnbilledPreview, - } -} diff --git a/app/api/chat/research-context.ts b/app/api/chat/research-context.ts deleted file mode 100644 index 7dfde842..00000000 --- a/app/api/chat/research-context.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { LanguageModel, UIMessage } from "ai" -import type { ModelCallTrace } from "@/lib/ai/model-call-logger" -import { - createResearchPlan, - resolveResearchRoute, - type ResearchRoute, -} from "@/lib/chat/research-router" -import { - latestUserText, - recentConversationText, -} from "@/app/api/chat/conversation-text" - -type ResearchContextInput = { - model: LanguageModel - messages: UIMessage[] - deepResearchRequested: boolean - searchReady: boolean - modelCallTrace?: ModelCallTrace -} - -type ResearchContextDependencies = { - resolveRoute: typeof resolveResearchRoute - createPlan: typeof createResearchPlan -} - -const defaultDependencies: ResearchContextDependencies = { - resolveRoute: resolveResearchRoute, - createPlan: createResearchPlan, -} - -/** 解析一次请求的联网路由与可选研究计划,不执行实际搜索。 */ -export async function resolveResearchContext( - { - model, - messages, - deepResearchRequested, - searchReady, - modelCallTrace, - }: ResearchContextInput, - dependencies: ResearchContextDependencies = defaultDependencies -) { - const latestText = latestUserText(messages) - const researchRoute: ResearchRoute = deepResearchRequested - ? searchReady - ? { - mode: "research", - reasonCode: "multi_source_research", - urls: [], - suggestedQueries: [], - } - : { - mode: "answer", - reasonCode: "search_unavailable", - urls: [], - suggestedQueries: [], - } - : await dependencies.resolveRoute({ - model, - latestUserText: latestText, - recentConversation: recentConversationText(messages), - searchReady, - modelCallTrace, - }) - const researchPlan = - researchRoute.mode === "research" - ? await dependencies.createPlan({ - model, - userRequest: latestText, - route: researchRoute, - modelCallTrace, - }) - : null - - return { latestText, researchRoute, researchPlan } -} diff --git a/app/api/chat/research-tool-capabilities.ts b/app/api/chat/research-tool-capabilities.ts deleted file mode 100644 index eef342d7..00000000 --- a/app/api/chat/research-tool-capabilities.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ResearchRouteMode } from "@/lib/chat/research-contract" - -export type ResearchToolName = "readUrl" | "webSearch" - -const RESEARCH_TOOL_NAMES_BY_MODE = { - answer: [], - fetch: ["readUrl"], - search: ["webSearch", "readUrl"], - research: ["webSearch", "readUrl"], -} as const satisfies Record - -/** 路由模式可暴露的联网能力;数组顺序同时定义首步强制工具。 */ -export function researchToolNames( - mode: ResearchRouteMode -): readonly ResearchToolName[] { - return RESEARCH_TOOL_NAMES_BY_MODE[mode] -} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts deleted file mode 100644 index bf256a67..00000000 --- a/app/api/chat/route.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { - convertToModelMessages, - consumeStream, - createUIMessageStream, - createUIMessageStreamResponse, - isStepCount, - streamText, -} from "ai" -import { after } from "next/server" -import { frontendTools } from "@assistant-ui/react-ai-sdk" -import { resolveAttachmentParts } from "@/lib/chat/resolve-attachments" -import { isSearchConfigured } from "@/lib/ai/search" -import { RESEARCH_MAX_STEPS } from "@/constants/research" -import { MAX_OUTPUT_TOKENS } from "@/constants/model" -import { MODEL_CALL_PURPOSE } from "@/constants/model-call" -import { resolveChatModel } from "@/lib/ai/provider" -import { - withModelCallLogging, - type ModelCallTrace, -} from "@/lib/ai/model-call-logger" -import { buildUsageMetadata } from "@/lib/billing/usage-meta" -import { isExplicitMarkdownArtifactRequest } from "@/lib/chat/markdown-artifact" -import { reasoningForResearchRoute } from "@/lib/chat/research-router" -import { unregisterGenerationController } from "@/lib/thread-chat-generation/execution" -import { createToolStepPolicy } from "@/app/api/chat/tool-step-policy" -import { buildChatSystemPrompt } from "@/app/api/chat/system-prompt" -import { resolveResearchContext } from "@/app/api/chat/research-context" -import { buildChatToolSet } from "@/app/api/chat/tool-set" -import { createStreamLifecycle } from "@/app/api/chat/stream-lifecycle" -import { - createGenerationSettlementHandler, - settleGenerationInitializationFailure, -} from "@/app/api/chat/generation-settlement" -import { prepareThreadGenerationContext } from "@/app/api/chat/thread-generation-context" -import { prepareChatRequestContext } from "@/app/api/chat/request-context" - -// AnySearch 搜索与网页深读可能形成多步循环,放宽单次请求时长上限。 -export const maxDuration = 300 - -export async function POST(req: Request) { - const requestContext = await prepareChatRequestContext(req) - if (requestContext.kind === "response") return requestContext.response - const { - userId, - messages, - tools, - deepResearch, - threadChat, - linearThreadId, - modelId, - model, - isUnbilledPreview, - } = requestContext - - const prepared = await prepareThreadGenerationContext({ - userId, - modelId, - messages, - threadChat, - unbilledPreview: isUnbilledPreview, - }) - if (prepared.kind === "response") return prepared.response - const { - persistence, - authoritativeMessages, - authoritativeAnchorText, - preparedRevision, - generationController, - generationObserver, - } = prepared - - try { - // AnySearch 是当前统一联网层:所有模型都获得相同的搜索与网页深读工具。 - // deepResearch 只控制研究提示强度,不再决定工具是否存在。 - const research = deepResearch === true - const searchReady = isSearchConfigured() - const isThreadChat = persistence != null - const chatModel = resolveChatModel(modelId) - const modelCallTrace: ModelCallTrace = { - requestId: crypto.randomUUID(), - ...(persistence - ? { - treeId: persistence.treeId, - threadId: persistence.threadId, - generationId: persistence.generationId, - assistantMessageId: persistence.assistantMessageId, - } - : linearThreadId - ? { threadId: linearThreadId } - : {}), - } - const { latestText, researchRoute, researchPlan } = - await resolveResearchContext({ - model: chatModel, - messages: authoritativeMessages, - deepResearchRequested: research, - searchReady, - modelCallTrace, - }) - const markdownArtifactRequested = - isThreadChat && isExplicitMarkdownArtifactRequest(latestText) - const { tools: allTools, webToolsEnabled } = buildChatToolSet({ - researchMode: researchRoute.mode, - searchReady, - threadChat: isThreadChat, - markdownArtifactRequested, - frontendToolSet: frontendTools(tools ?? {}), - }) - - // MiniMax 不接受 file part:先把附件(PDF→提取文本,其余→占位说明)转换为 text part - const resolvedMessages = await resolveAttachmentParts(authoritativeMessages) - - const system = buildChatSystemPrompt({ - threadChat: isThreadChat, - anchorText: authoritativeAnchorText, - markdownArtifactRequested, - researchMode: researchRoute.mode, - researchPlan, - deepResearchRequested: research, - searchReady, - }) - - const streamLifecycle = createStreamLifecycle({ - userId, - modelId, - model, - persistentGeneration: isThreadChat, - unbilledPreview: isUnbilledPreview, - linearThreadId, - }) - - const result = streamText({ - model: withModelCallLogging( - chatModel, - MODEL_CALL_PURPOSE.chatAnswer, - modelCallTrace - ), - ...(generationController - ? { abortSignal: generationController.signal } - : {}), - reasoning: reasoningForResearchRoute(researchRoute.mode, model), - system, - messages: await convertToModelMessages(resolvedMessages, { - tools: allTools, - }), - tools: allTools, - // 明确 Markdown 交付请求只强制第 0 步启动工具调用;后续步骤仍保留工具, - // 让模型在用户要求多份独立文档时,为每份文档分别创建一个 Artifact。 - prepareStep: createToolStepPolicy({ - isThreadChat, - markdownArtifactRequested, - researchMode: researchRoute.mode, - }), - maxOutputTokens: MAX_OUTPUT_TOKENS, - stopWhen: isStepCount(webToolsEnabled ? RESEARCH_MAX_STEPS : 5), - onError: streamLifecycle.onError, - onAbort: streamLifecycle.onAbort, - onEnd: streamLifecycle.onEnd, - }) - - const uiStream = createUIMessageStream({ - ...(persistence - ? { - originalMessages: resolvedMessages, - generateId: () => persistence.assistantMessageId, - } - : {}), - execute: ({ writer }) => { - writer.write({ - type: "data-research-route", - id: "research-route", - data: researchRoute, - }) - if (researchPlan) { - writer.write({ - type: "data-research-plan", - id: "research-plan", - data: researchPlan, - }) - } - writer.merge( - result.toUIMessageStream({ - onError: (error) => { - console.error("[chat] 流内错误:", error) - return "An error occurred." - }, - messageMetadata: ({ part }) => - part.type === "finish" - ? buildUsageMetadata(modelId, part.totalUsage) - : undefined, - }) - ) - }, - onEnd: persistence - ? createGenerationSettlementHandler({ - persistence, - researchRoute, - researchPlan, - unbilledPreview: isUnbilledPreview, - streamLifecycle, - }) - : undefined, - }) - - const response = createUIMessageStreamResponse({ - stream: uiStream, - consumeSseStream: ({ stream }) => { - after(async () => { - await consumeStream({ - stream, - onError: (error) => { - console.error("[chat] 服务端 UI stream 消费失败", error) - }, - }) - generationObserver?.stop() - if (generationObserver) await generationObserver.done - if (persistence && generationController) { - unregisterGenerationController( - persistence.generationId, - generationController - ) - } - }) - }, - }) - if (preparedRevision !== null) - response.headers.set("x-thread-tree-revision", String(preparedRevision)) - return response - } catch (error) { - generationController?.abort(error) - generationObserver?.stop() - if (persistence && generationController) { - unregisterGenerationController( - persistence.generationId, - generationController - ) - await settleGenerationInitializationFailure({ - persistence, - error, - usageUnavailable: !isUnbilledPreview, - }) - } - console.error("[chat] 请求初始化失败", error) - return Response.json({ error: "生成初始化失败,请重试。" }, { status: 500 }) - } -} diff --git a/app/api/chat/stream-lifecycle.ts b/app/api/chat/stream-lifecycle.ts deleted file mode 100644 index 28d9e87c..00000000 --- a/app/api/chat/stream-lifecycle.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { ProviderMetadata } from "ai" -import type { ChatModel } from "@/constants/model" -import { GENERATION_ERRORS } from "@/constants/generation" -import { chargeUsage } from "@/lib/billing/credits" -import { usageCostEvidence } from "@/lib/billing/usage-cost-evidence" -import type { OpenRouterStepLike } from "@/lib/ai/openrouter" -import type { FinalizeGenerationUsage } from "@/lib/thread-chat-generation/finalize" - -type UsageStep = OpenRouterStepLike & { - usage: { - inputTokens?: number - outputTokens?: number - } -} - -type StreamLifecycleInput = { - userId: string - modelId: string - model: Pick - persistentGeneration: boolean - unbilledPreview: boolean - linearThreadId?: string -} - -type StreamLifecycleDependencies = { - charge: typeof chargeUsage -} - -const defaultDependencies: StreamLifecycleDependencies = { - charge: chargeUsage, -} - -/** 请求级 stream usage/error 状态;handler 写入,持久化终态只读取 snapshot。 */ -export function createStreamLifecycle( - { - userId, - modelId, - model, - persistentGeneration, - unbilledPreview, - linearThreadId, - }: StreamLifecycleInput, - dependencies: StreamLifecycleDependencies = defaultDependencies -) { - let capturedUsage: FinalizeGenerationUsage | undefined - let modelStreamError: string | undefined - let abortedUsageUnavailable = false - - return { - onError({ error }: { error: unknown }) { - modelStreamError = GENERATION_ERRORS.streamFailed - console.error("[chat] 模型流错误:", error) - }, - - onAbort({ steps }: { steps: readonly UsageStep[] }) { - if (!persistentGeneration) return - const inputTokens = steps.reduce( - (total, step) => total + (step.usage.inputTokens ?? 0), - 0 - ) - const outputTokens = steps.reduce( - (total, step) => total + (step.usage.outputTokens ?? 0), - 0 - ) - const providerMetadata = steps.at(-1)?.providerMetadata - if (steps.length > 0) { - capturedUsage = { - inputTokens, - outputTokens, - costEvidence: usageCostEvidence({ - provider: model.provider, - steps, - providerMetadata, - }), - } - } - abortedUsageUnavailable = true - }, - - async onEnd({ - usage, - providerMetadata, - steps, - }: { - usage: { inputTokens?: number; outputTokens?: number } - providerMetadata?: ProviderMetadata - steps: readonly UsageStep[] - }) { - if (unbilledPreview) return - const costEvidence = usageCostEvidence({ - provider: model.provider, - steps, - providerMetadata, - }) - if ( - model.provider === "openrouter" && - costEvidence.source !== "openrouter" - ) { - console.warn( - `[chat] OpenRouter 成本元数据不完整,使用静态估值:${model.id}` - ) - } - if (persistentGeneration) { - capturedUsage = { - inputTokens: usage.inputTokens ?? 0, - outputTokens: usage.outputTokens ?? 0, - costEvidence, - } - return - } - await dependencies.charge({ - userId, - model: modelId, - inputTokens: usage.inputTokens ?? 0, - outputTokens: usage.outputTokens ?? 0, - threadId: linearThreadId ?? null, - costEvidence, - }) - }, - - snapshot() { - return { - capturedUsage, - modelStreamError, - abortedUsageUnavailable, - } - }, - } -} - -export type StreamLifecycle = ReturnType diff --git a/app/api/chat/surface-tools.ts b/app/api/chat/surface-tools.ts deleted file mode 100644 index 8bcb7d1e..00000000 --- a/app/api/chat/surface-tools.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { tool } from "ai" -import { z } from "zod" -import { - MARKDOWN_ARTIFACT_TOOL_DESCRIPTION, - MARKDOWN_ARTIFACT_TOOL_NAME, - markdownArtifactInputSchema, - type MarkdownArtifactToolResult, -} from "@/lib/chat/markdown-artifact" - -const getWeather = tool({ - description: "Get the current weather for a city.", - inputSchema: z.object({ - location: z.string().describe("City name, e.g. 'San Francisco'"), - }), - execute: async ({ location }) => { - const conditions = [ - "Sunny", - "Partly Cloudy", - "Cloudy", - "Light Rain", - "Clear", - ] - const seed = [...location].reduce((acc, c) => acc + c.charCodeAt(0), 0) - return { - location, - temperatureF: 55 + (seed % 35), - condition: conditions[seed % conditions.length], - humidity: 30 + (seed % 50), - asOf: new Date().toISOString(), - } - }, -}) - -const compareTable = tool({ - description: - "Render a comparison table for two or more items across one or more numeric metrics. Use whenever the user asks to compare things 'in a table' with real numeric data.", - inputSchema: z.object({ - title: z.string(), - unit: z.string().optional(), - columns: z - .array(z.string()) - .describe("Category labels, e.g. country names"), - series: z.array( - z.object({ - name: z.string(), - values: z - .array(z.number()) - .describe("One value per column, same order as columns"), - }) - ), - }), - execute: async (input) => input, -}) - -const createMarkdownArtifact = tool({ - description: MARKDOWN_ARTIFACT_TOOL_DESCRIPTION, - inputSchema: markdownArtifactInputSchema, - execute: async (): Promise => ({ created: true }), -}) - -/** 产品 surface 对应的基础工具;联网与前端工具由 route 在其上继续组合。 */ -export function surfaceTools(input: { - threadChat: boolean - markdownArtifactRequested: boolean -}) { - if (!input.threadChat) return { getWeather, compareTable } - return input.markdownArtifactRequested - ? { [MARKDOWN_ARTIFACT_TOOL_NAME]: createMarkdownArtifact } - : {} -} diff --git a/app/api/chat/system-prompt.ts b/app/api/chat/system-prompt.ts deleted file mode 100644 index a50eedf2..00000000 --- a/app/api/chat/system-prompt.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - DIRECT_FETCH_SYSTEM_PROMPT, - RESEARCH_SYSTEM_PROMPT, - WEB_ACCESS_SYSTEM_PROMPT, -} from "@/constants/research" -import { buildThreadChatSystem } from "@/lib/chat/thread-chat-prompt" -import { - researchPlanExecutionPrompt, - type ResearchPlan, - type ResearchRoute, -} from "@/lib/chat/research-router" - -type ChatSystemPromptInput = { - threadChat: boolean - anchorText: string | null - markdownArtifactRequested: boolean - researchMode: ResearchRoute["mode"] - researchPlan: ResearchPlan | null - deepResearchRequested: boolean - searchReady: boolean -} - -const SEARCH_UNAVAILABLE_PROMPT = - "用户开启了深度研究,但服务端未启用搜索服务,请如实告知该功能暂不可用,并基于已有知识尽力回答。" - -/** 将各能力拥有的 system 片段按既有优先顺序组合为单一服务端提示。 */ -export function buildChatSystemPrompt({ - threadChat, - anchorText, - markdownArtifactRequested, - researchMode, - researchPlan, - deepResearchRequested, - searchReady, -}: ChatSystemPromptInput): string { - return [ - threadChat - ? buildThreadChatSystem(anchorText, { - enableMarkdownArtifact: markdownArtifactRequested, - }) - : null, - researchMode === "fetch" ? DIRECT_FETCH_SYSTEM_PROMPT : null, - researchMode === "search" || researchMode === "research" - ? WEB_ACCESS_SYSTEM_PROMPT - : null, - researchMode === "research" ? RESEARCH_SYSTEM_PROMPT : null, - researchPlan ? researchPlanExecutionPrompt(researchPlan) : null, - deepResearchRequested && !searchReady ? SEARCH_UNAVAILABLE_PROMPT : null, - ] - .filter((part): part is string => part !== null) - .join("\n\n") -} diff --git a/app/api/chat/thread-generation-context.ts b/app/api/chat/thread-generation-context.ts deleted file mode 100644 index 88369e4a..00000000 --- a/app/api/chat/thread-generation-context.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { UIMessage } from "ai" -import { compileThreadChatMessages } from "@/lib/thread-chat/application/compile-thread-chat-messages" -import type { ThreadChatGenerationIdentity } from "@/lib/thread-chat/contracts/generation-identity" -import { - observeGenerationCancellation, - registerGenerationController, - unregisterGenerationController, -} from "@/lib/thread-chat-generation/execution" -import { toGenerationSummary } from "@/lib/thread-chat-generation/query-repository" -import { prepareGeneration } from "@/lib/thread-chat-generation/start-generation-repository" -import { generationStartErrorResponse } from "@/app/api/chat/generation-start-error" -import { settleGenerationInitializationFailure } from "@/app/api/chat/generation-settlement" -import type { MessageActionFailureResponse } from "@/lib/thread-chat/contracts/message-action-failure" - -type ThreadGenerationContextInput = { - userId: string - modelId: string - messages: UIMessage[] - threadChat?: ThreadChatGenerationIdentity - unbilledPreview: boolean -} - -type ThreadGenerationContextDependencies = { - prepare: typeof prepareGeneration - summarize: typeof toGenerationSummary - compile: typeof compileThreadChatMessages - createController(): AbortController - register: typeof registerGenerationController - unregister: typeof unregisterGenerationController - observe: typeof observeGenerationCancellation - startErrorResponse: typeof generationStartErrorResponse - settleInitializationFailure: typeof settleGenerationInitializationFailure -} - -const defaultDependencies: ThreadGenerationContextDependencies = { - prepare: prepareGeneration, - summarize: toGenerationSummary, - compile: compileThreadChatMessages, - createController: () => new AbortController(), - register: registerGenerationController, - unregister: unregisterGenerationController, - observe: observeGenerationCancellation, - startErrorResponse: generationStartErrorResponse, - settleInitializationFailure: settleGenerationInitializationFailure, -} - -/** 校验并准备一次线性或持久化 Thread generation 的权威请求上下文。 */ -export async function prepareThreadGenerationContext( - { - userId, - modelId, - messages, - threadChat, - unbilledPreview, - }: ThreadGenerationContextInput, - dependencies: ThreadGenerationContextDependencies = defaultDependencies -) { - if (threadChat == null) { - return { - kind: "ready" as const, - persistence: null, - authoritativeMessages: messages, - authoritativeAnchorText: null, - preparedRevision: null, - generationController: null, - generationObserver: null, - } - } - - const persistence = threadChat - - let started: Awaited> - try { - started = await dependencies.prepare({ - userId, - modelId, - ...persistence, - }) - } catch (error) { - return { - kind: "response" as const, - response: dependencies.startErrorResponse(error), - } - } - if (!started.created) { - return { - kind: "response" as const, - response: Response.json( - { generation: dependencies.summarize(started.generation) }, - { status: 202 } - ), - } - } - - let generationController: AbortController | null = null - let registered = false - try { - const committedThread = started.state.threads[persistence.threadId] - const authoritativeAnchorText = committedThread?.anchorText?.trim() - ? committedThread.anchorText - : null - const authoritativeMessages = dependencies.compile({ - state: started.state, - threadId: persistence.threadId, - excludeAssistantMessageId: persistence.assistantMessageId, - }) as UIMessage[] - generationController = dependencies.createController() - dependencies.register(persistence.generationId, generationController) - registered = true - const generationObserver = dependencies.observe( - persistence.generationId, - generationController - ) - - return { - kind: "ready" as const, - persistence, - authoritativeMessages, - authoritativeAnchorText, - preparedRevision: started.revision, - generationController, - generationObserver, - } - } catch (error) { - generationController?.abort(error) - if (registered && generationController) - dependencies.unregister(persistence.generationId, generationController) - await dependencies.settleInitializationFailure({ - persistence, - error, - usageUnavailable: !unbilledPreview, - }) - return { - kind: "response" as const, - response: Response.json( - { - error: { - code: "network_error", - message: "生成初始化失败,请重试。", - }, - } satisfies MessageActionFailureResponse, - { status: 500 } - ), - } - } -} diff --git a/app/api/chat/tool-set.ts b/app/api/chat/tool-set.ts deleted file mode 100644 index 9ec60734..00000000 --- a/app/api/chat/tool-set.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ToolSet } from "ai" -import { readUrlTool, webSearchTool } from "@/lib/chat/research-tools" -import type { ResearchRoute } from "@/lib/chat/research-router" -import { surfaceTools } from "@/app/api/chat/surface-tools" -import { researchToolNames } from "@/app/api/chat/research-tool-capabilities" - -const RESEARCH_TOOLS = { - readUrl: readUrlTool, - webSearch: webSearchTool, -} - -type ChatToolSetInput = { - researchMode: ResearchRoute["mode"] - searchReady: boolean - threadChat: boolean - markdownArtifactRequested: boolean - /** assistant-ui 使用其内嵌 AI SDK 类型;只在最终组合出口统一适配。 */ - frontendToolSet?: Record -} - -/** 将各能力的私有工具集合组合成一次模型调用唯一可见的 ToolSet。 */ -export function buildChatToolSet({ - researchMode, - searchReady, - threadChat, - markdownArtifactRequested, - frontendToolSet, -}: ChatToolSetInput): { tools: ToolSet; webToolsEnabled: boolean } { - const webToolsEnabled = searchReady && researchMode !== "answer" - const routedWebTools = Object.fromEntries( - researchToolNames(researchMode).map((name) => [name, RESEARCH_TOOLS[name]]) - ) as ToolSet - - return { - webToolsEnabled, - tools: { - ...surfaceTools({ threadChat, markdownArtifactRequested }), - ...(webToolsEnabled ? routedWebTools : {}), - ...(frontendToolSet ?? {}), - } as ToolSet, - } -} diff --git a/app/api/chat/tool-step-policy.ts b/app/api/chat/tool-step-policy.ts deleted file mode 100644 index 6d284ce2..00000000 --- a/app/api/chat/tool-step-policy.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { MARKDOWN_ARTIFACT_TOOL_NAME } from "@/lib/chat/markdown-artifact" -import type { ResearchRoute } from "@/lib/chat/research-router" -import { - researchToolNames, - type ResearchToolName, -} from "@/app/api/chat/research-tool-capabilities" - -type ToolStepPolicyInput = { - isThreadChat: boolean - markdownArtifactRequested: boolean - researchMode: ResearchRoute["mode"] -} - -type RoutedToolName = ResearchToolName | typeof MARKDOWN_ARTIFACT_TOOL_NAME - -type ToolStep = { - activeTools: RoutedToolName[] - toolChoice?: { - type: "tool" - toolName: RoutedToolName - } -} - -/** - * 生成工具的逐步暴露策略:首步强制当前路由的联网工具;明确的 Markdown - * 交付仅在没有更高优先级联网动作时首步强制,后续步骤保留全部可用工具。 - */ -export function createToolStepPolicy({ - isThreadChat, - markdownArtifactRequested, - researchMode, -}: ToolStepPolicyInput) { - const activeWebTools = [...researchToolNames(researchMode)] - const activeTools: RoutedToolName[] = isThreadChat - ? [ - ...(markdownArtifactRequested ? [MARKDOWN_ARTIFACT_TOOL_NAME] : []), - ...activeWebTools, - ] - : activeWebTools - - if (activeTools.length === 0) return undefined - - return ({ stepNumber }: { stepNumber: number }): ToolStep => { - if (stepNumber === 0 && activeWebTools.length > 0) { - return { - activeTools, - toolChoice: { type: "tool", toolName: activeWebTools[0] }, - } - } - if (stepNumber === 0 && markdownArtifactRequested) { - return { - activeTools, - toolChoice: { - type: "tool", - toolName: MARKDOWN_ARTIFACT_TOOL_NAME, - }, - } - } - return { activeTools } - } -} diff --git a/app/api/title/route.ts b/app/api/title/route.ts deleted file mode 100644 index d333b16a..00000000 --- a/app/api/title/route.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { generateText } from "ai" -import { - ARK_BRANCH_TITLE_MAX_OUTPUT_TOKENS, - ARK_BRANCH_TITLE_MODEL, -} from "@/constants/ark" -import { MODEL_CALL_PURPOSE } from "@/constants/model-call" -import { arkCodingChatModel, isArkCodingConfigured } from "@/lib/ai/ark" -import { withModelCallLogging } from "@/lib/ai/model-call-logger" -import { - parseThreadTitleInput, - type ThreadTitleInput, -} from "@/lib/thread-chat/contracts/title-request" - -/** - * POST /api/title —— 主线与分支共用的异步语义标题生成。 - * - * body: - * - { kind: "main", question } - * - { kind: "branch", anchorText, question, answer } - * - * 返回:{ title: string | null } —— null 表示生成失败、未配置模型或输出为空; - * 客户端保留各自的回退标题。 - */ - -/** 喂给标题模型的首答摘录上限(字符):标题只需主旨,控制成本与延迟 */ -const ANSWER_EXCERPT_LIMIT = 600 -/** 同理,用户问题与分支锚点原文的截断上限 */ -const INPUT_EXCERPT_LIMIT = 200 - -function buildPrompt(input: ThreadTitleInput): string { - if (input.kind === "main") { - return ( - "这是一个新对话的首条用户消息。\n" + - `用户消息:「${input.question.slice(0, INPUT_EXCERPT_LIMIT)}」\n\n` + - "请根据用户消息所用的语言,为这个对话拟一个简短、清晰、便于扫描的标题,概括对话主题。" + - "中文和英文都使用自然短语;不要为了凑长度或限制长度而删词、截断。" + - "只输出标题本身,不要引号、标点、序号或任何解释。" - ) - } - - return ( - "这是一个分支对话:用户阅读 AI 回答时划选了一段文字,就它开启了分支讨论。\n" + - `被划选的文字:「${input.anchorText.slice(0, INPUT_EXCERPT_LIMIT)}」\n` + - `用户的问题:「${input.question.slice(0, INPUT_EXCERPT_LIMIT)}」\n` + - `首答摘录:「${input.answer.slice(0, ANSWER_EXCERPT_LIMIT)}」\n\n` + - "请根据用户问题所用的语言,为这个分支拟一个简短、清晰、便于扫描的标题,概括这轮讨论的主题。" + - "中文和英文都使用自然短语;不要为了凑长度或限制长度而删词、截断。" + - "只输出标题本身,不要引号、标点、序号或任何解释。" - ) -} - -/** 清洗模型输出:剥 推理段 / 引号 / 标点,取首个非空行;空则 null。 */ -function sanitizeTitle(raw: string): string | null { - const text = raw.replace(/[\s\S]*?(<\/think>|$)/g, "") - const line = text - .split("\n") - .map((value) => value.trim()) - .find((value) => value !== "") - if (!line) return null - const cleaned = line - .replace(/^[「『"'《【\s]+/, "") - .replace(/[」』"'》】。!?!?,,.…\s]+$/, "") - .trim() - return cleaned.length >= 2 ? cleaned : null -} - -export async function POST(req: Request) { - let body: unknown - try { - body = await req.json() - } catch { - return Response.json({ error: "请求体不是合法 JSON" }, { status: 400 }) - } - - const input = parseThreadTitleInput(body) - if (!input) { - return Response.json({ error: "标题请求参数无效" }, { status: 400 }) - } - - if (!isArkCodingConfigured()) return Response.json({ title: null }) - - try { - const { text } = await generateText({ - model: withModelCallLogging( - arkCodingChatModel(ARK_BRANCH_TITLE_MODEL), - MODEL_CALL_PURPOSE.threadTitle, - { requestId: crypto.randomUUID() } - ), - maxOutputTokens: ARK_BRANCH_TITLE_MAX_OUTPUT_TOKENS, - // 标题是可选增强;配额不足、鉴权失败等确定性错误不应额外消耗请求。 - maxRetries: 0, - prompt: buildPrompt(input), - }) - return Response.json({ title: sanitizeTitle(text) }) - } catch (error) { - console.warn("[title] 标题生成失败:", error) - return Response.json({ title: null }) - } -} diff --git a/app/thread-chat/[projectId]/page.tsx b/app/thread-chat/[projectId]/page.tsx new file mode 100644 index 00000000..74dde1ba --- /dev/null +++ b/app/thread-chat/[projectId]/page.tsx @@ -0,0 +1,22 @@ +import { notFound } from "next/navigation" +import { idSchema } from "@/lib/thread-chat/api/contracts" +import { ThreadChatProjectProvider } from "@/lib/thread-chat/client/providers" +import { ThreadChatProject } from "../normalized/thread-chat-project" +import { threadChatMetadata } from "../page-metadata" + +export const metadata = threadChatMetadata + +/** Project ID 是 URL 中唯一的服务端领域身份;不存在或不属于当前 actor 时由 API 拒绝。 */ +export default async function ThreadChatProjectPage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = await params + if (!idSchema.safeParse(projectId).success) notFound() + return ( + + + + ) +} diff --git a/app/thread-chat/[treeId]/page.tsx b/app/thread-chat/[treeId]/page.tsx deleted file mode 100644 index 1477ab10..00000000 --- a/app/thread-chat/[treeId]/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { notFound } from "next/navigation" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { ThreadChatProjectProvider } from "@/lib/thread-chat/client/providers" -import { ThreadChatProject } from "../normalized/thread-chat-project" -import { threadChatMetadata } from "../page-metadata" - -export const metadata = threadChatMetadata - -/** - * URL 即树身份:/thread-chat/{treeId} 打开指定的分支树(直访新 UUID = 开新树)。 - * treeId 做 UUID 形状校验(安全阀),不合法 404。key={treeId} 保证切树(如「新对话」 - * 跳转)时 loader/store 整体重挂,不残留上一棵树的内存状态。 - */ -export default async function ThreadChatTreePage({ - params, -}: { - params: Promise<{ treeId: string }> -}) { - const { treeId: projectId } = await params - if (!isValidTreeId(projectId)) notFound() - return ( - - - - ) -} diff --git a/app/thread-chat/chat/actions/message-action-commands.ts b/app/thread-chat/chat/actions/message-action-commands.ts index 2ab08758..987ff77e 100644 --- a/app/thread-chat/chat/actions/message-action-commands.ts +++ b/app/thread-chat/chat/actions/message-action-commands.ts @@ -1,7 +1,12 @@ import type { MessageFeedback, MessageFeedbackSummary } from "../../core/types" -import type { MessageActionFailureCode } from "@/lib/thread-chat/contracts/message-action-failure" -export type { MessageActionFailureCode } +export type MessageActionFailureCode = + | "invalid_turn" + | "not_latest_turn" + | "generation_conflict" + | "model_mismatch" + | "unauthorized" + | "network_error" export type GenerationActionResult = | { diff --git a/app/thread-chat/chat/actions/message-action-presentation.ts b/app/thread-chat/chat/actions/message-action-presentation.ts deleted file mode 100644 index 0aebc39d..00000000 --- a/app/thread-chat/chat/actions/message-action-presentation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - activeLeafTurn, - activeMessagePath, - assistantTurnAlternatives, - childThreadSourceProvenance, -} from "../../core/selectors" -import type { ThreadTreeState } from "../../core/types" -import type { MessageActionViewState } from "./message-action-types" - -export function buildMessageActionViewState({ - state, - recoverableByUserMessageId, - feedbackByMessageId, -}: { - state: ThreadTreeState - recoverableByUserMessageId: MessageActionViewState["recoverableByUserMessageId"] - feedbackByMessageId: MessageActionViewState["feedbackByMessageId"] -}): MessageActionViewState { - const activePathByThreadId = new Map( - Object.values(state.threads).map((thread) => [ - thread.id, - activeMessagePath(thread).map((message) => message.id), - ]) - ) - const presentationByThreadId = new Map( - Object.values(state.threads).map((thread) => { - const latestTurn = activeLeafTurn(thread) - const alternatives = latestTurn?.assistantMessage - ? assistantTurnAlternatives(thread, latestTurn.assistantMessage.id).map( - (assistant) => ({ - assistantMessageId: assistant.id, - derivedThreadCount: thread.children.filter( - (childId) => - state.threads[childId]?.forkFromMsgId === assistant.id - ).length, - }) - ) - : [] - return [ - thread.id, - { - latestUserMessageId: latestTurn?.userMessage.id, - latestAssistantMessageId: latestTurn?.assistantMessage?.id, - alternatives, - sourceProvenance: childThreadSourceProvenance(state, thread.id), - }, - ] as const - }) - ) - - return { - recoverableByUserMessageId, - feedbackByMessageId, - activePathByThreadId, - presentationByThreadId, - } -} diff --git a/app/thread-chat/chat/actions/message-action-session-logic.ts b/app/thread-chat/chat/actions/message-action-session-logic.ts deleted file mode 100644 index c404f5c8..00000000 --- a/app/thread-chat/chat/actions/message-action-session-logic.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { MessageFeedback, MessageFeedbackSummary } from "../../core/types" -import type { RecoverableTurn } from "../../generation/types" - -export function indexRecoverableTurns(turns: readonly RecoverableTurn[]) { - return new Map(turns.map((turn) => [turn.userMessageId, turn])) -} - -export function indexMessageFeedbacks( - entries: readonly MessageFeedbackSummary[] -) { - return new Map(entries.map((entry) => [entry.messageId, entry.feedback])) -} - -export function withoutRecoverableTurn( - current: ReadonlyMap, - userMessageId: string -) { - const next = new Map(current) - next.delete(userMessageId) - return next -} - -export function withRecoverableTurn( - current: ReadonlyMap, - turn: RecoverableTurn -) { - const next = new Map(current) - next.set(turn.userMessageId, turn) - return next -} - -export function withMessageFeedback( - current: ReadonlyMap, - messageId: string, - feedback: MessageFeedback | null -) { - const next = new Map(current) - if (feedback) next.set(messageId, feedback) - else next.delete(messageId) - return next -} diff --git a/app/thread-chat/chat/actions/message-action-types.ts b/app/thread-chat/chat/actions/message-action-types.ts index 9a5c5198..2b6737e4 100644 --- a/app/thread-chat/chat/actions/message-action-types.ts +++ b/app/thread-chat/chat/actions/message-action-types.ts @@ -1,7 +1,20 @@ import type { Message, MessageFeedback } from "../../core/types" -import type { RecoverableTurn } from "../../generation/types" import type { ThreadMessageActionCommands } from "./message-action-commands" -import type { SourceProvenance } from "../../core/message-graph" + +export interface RecoverableTurn { + threadId: string + userMessageId: string + assistantMessageId?: string + reason: "missing_assistant" | "missing_generation" | "interrupted_generation" +} + +export interface SourceProvenance { + sourceThreadId: string + sourceMessageId: string + isOnActivePath: boolean + alternativeIndex: number | null + alternativeCount: number +} export const MESSAGE_ACTION_LABELS = { toolbar: "消息操作", diff --git a/app/thread-chat/chat/actions/use-message-actions.ts b/app/thread-chat/chat/actions/use-message-actions.ts deleted file mode 100644 index a08be4d7..00000000 --- a/app/thread-chat/chat/actions/use-message-actions.ts +++ /dev/null @@ -1,103 +0,0 @@ -"use client" - -import { useCallback, useMemo, useState } from "react" -import type { MessageFeedbackSummary, ThreadTreeState } from "../../core/types" -import type { RecoverableTurn } from "../../generation/types" -import type { ThreadMessageActionCommands } from "./message-action-commands" -import { buildMessageActionViewState } from "./message-action-presentation" -import { - indexMessageFeedbacks, - indexRecoverableTurns, - withMessageFeedback, - withRecoverableTurn, - withoutRecoverableTurn, -} from "./message-action-session-logic" - -export function useMessageActions({ - state, - version, - initialRecoverableTurns, - initialMessageFeedbacks, - commands, -}: { - state: ThreadTreeState - version: number - initialRecoverableTurns: RecoverableTurn[] - initialMessageFeedbacks: MessageFeedbackSummary[] - commands: ThreadMessageActionCommands -}) { - const [recoverableByUserMessageId, setRecoverableByUserMessageId] = useState( - () => indexRecoverableTurns(initialRecoverableTurns) - ) - const [feedbackByMessageId, setFeedbackByMessageId] = useState(() => - indexMessageFeedbacks(initialMessageFeedbacks) - ) - - const messageActionState = useMemo( - () => - buildMessageActionViewState({ - state, - recoverableByUserMessageId, - feedbackByMessageId, - }), - // state 对象原地变更,必须用 store version 作为派生键。 - // eslint-disable-next-line react-hooks/exhaustive-deps - [recoverableByUserMessageId, feedbackByMessageId, version] - ) - - const messageCommands = useMemo( - () => ({ - retryAssistant: commands.retryAssistant, - async retryUserTurn(threadId, userMessageId) { - const result = await commands.retryUserTurn(threadId, userMessageId) - if (result.ok) - setRecoverableByUserMessageId((current) => - withoutRecoverableTurn(current, userMessageId) - ) - return result - }, - async editAndRegenerate(threadId, userMessageId, text) { - const result = await commands.editAndRegenerate( - threadId, - userMessageId, - text - ) - if (result.ok) - setRecoverableByUserMessageId((current) => - withoutRecoverableTurn(current, userMessageId) - ) - return result - }, - switchTurnVariant: commands.switchTurnVariant, - async submitFeedback(threadId, messageId, feedback) { - const previous = feedbackByMessageId.get(messageId) ?? null - setFeedbackByMessageId((current) => - withMessageFeedback(current, messageId, feedback) - ) - try { - return await commands.submitFeedback(threadId, messageId, feedback) - } catch (error) { - setFeedbackByMessageId((current) => - withMessageFeedback(current, messageId, previous) - ) - throw error - } - }, - }), - [commands, feedbackByMessageId] - ) - - const registerRecoverableTurn = useCallback( - (turn: RecoverableTurn) => - setRecoverableByUserMessageId((current) => - withRecoverableTurn(current, turn) - ), - [] - ) - - return { - messageActionState, - messageCommands, - registerRecoverableTurn, - } -} diff --git a/app/thread-chat/core/regeneration.ts b/app/thread-chat/core/regeneration.ts deleted file mode 100644 index a94fa9d9..00000000 --- a/app/thread-chat/core/regeneration.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:regeneration patch 规则的唯一来源位于 lib/thread-chat/domain。 - */ -export * from "@/lib/thread-chat/domain/regeneration" diff --git a/app/thread-chat/core/store.ts b/app/thread-chat/core/store.ts index 1130fe73..38220f9f 100644 --- a/app/thread-chat/core/store.ts +++ b/app/thread-chat/core/store.ts @@ -1,421 +1,42 @@ /** - * core/store —— 外部可变 store(zustand vanilla 风格,零依赖,纯 TS)。 + * 只读 UI 投影 Store。 * - * 模型:会话树对象身份稳定、原地修改;每次 mutate 后 version++ 并通知订阅者, - * React 侧经 useSyncExternalStore 以 version 为快照触发重渲(见 use-thread-store.ts)。 - * 组件不允许直接改树,所有变更走这里的方法——这也是 demo 能通过 - * react-hooks/immutability 等规则的关键:mutation 全部收敛在非 React 代码里。 + * 规范化 `ThreadChatProjectStore` 是唯一领域权威;这个适配器只让既有 Canvas 组件读取 + * 投影树,并允许 `/new` 草稿在提交前切换展示模型。它不创建实体 ID、不写回整棵树、 + * 不发起网络请求,也不承载 Message/Thread 变更。 */ -import type { TextAnchor } from "@/lib/thread-chat/domain/text-anchor" -import type { - ArtifactSeed, - MarkdownGenerationProgress, - Message, - ThreadTreeState, -} from "./types" -import type { WebResearchActivity } from "@/lib/chat/web-research-activity" -import type { ResearchPlan, ResearchRoute } from "@/lib/chat/research-router" -import { - mergeGenerationResult, - type MergeGenerationResultInput, -} from "../generation/merge-result" -import type { PreparedTurnPatch } from "./regeneration" +import type { ThreadTreeState } from "./types" -export interface ForkInput { - /** 在哪个会话里划选的 */ - sourceThreadId: string - /** 划选的是哪条消息 */ - sourceMsgId: string - /** 被划选的原文(同时决定新会话标题与脚注锚点,= anchor.quote.exact) */ - anchorText: string - /** 文本锚点:渲染后 Markdown DOM 上的模糊恢复定位依据(采集失败时可缺省) */ - anchor?: TextAnchor +export interface ThreadStore { + getState(): ThreadTreeState + getVersion(): number + subscribe(listener: () => void): () => void + setThreadModel(threadId: string, modelId: string): void } -export interface ForkResult { - threadId: string - title: string -} - -/** - * 分支的默认标题:锚点原文截 13 字(fork 时的初始标题;异步语义标题 - * 生成前 / 失败时的兜底展示,也是壳层判断「还没生成过标题」的比对基准)。 - */ -export function defaultBranchTitle(anchorText: string): string { - return anchorText.length > 13 ? anchorText.slice(0, 13) + "…" : anchorText -} - -export type ThreadStore = ReturnType - export function createThreadStore( seed: ThreadTreeState, isValidModelId: (modelId: string) => boolean = () => true -) { - const state = seed +): ThreadStore { + const state = structuredClone(seed) let version = 0 const listeners = new Set<() => void>() - const notify = () => { - version++ - listeners.forEach((fn) => fn()) - } - - /** 活跃计数 + 最近访问(供 LRU 放置与 ⌘K「最近访问」chips 使用),不发通知 */ - const touchSilently = (id: string) => { - const t = state.threads[id] - if (!t) return - state.tick++ - t.lastActive = state.tick - if (id !== "main") - state.recents = [id, ...state.recents.filter((x) => x !== id)].slice(0, 6) - } - - /** 登记一个 artifact(含 id 分配与 tab 顺序),不发通知 */ - const registerSilently = ( - sourceThreadId: string, - sourceMessageId: string, - seed_: ArtifactSeed - ): string => { - const id = "a" + state.seq++ - state.artifacts[id] = { id, sourceThreadId, sourceMessageId, ...seed_ } - state.artifactOrder.push(id) - return id - } - - /** 从尾部反向查找消息(流式目标通常是最新消息,反向查找更快) */ - const findMessageFromTail = ( - messages: Message[], - msgId: string - ): Message | undefined => { - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].id === msgId) return messages[i] - } - return undefined - } - return { getState: () => state, getVersion: () => version, - subscribe: (fn: () => void) => { - listeners.add(fn) - return () => { - listeners.delete(fn) - } + subscribe(listener) { + listeners.add(listener) + return () => listeners.delete(listener) }, - - /** 标记某会话「刚被用过」:打开、发消息、被切换到时都要调 */ - touch(id: string) { - touchSilently(id) - notify() - }, - - /** 服务端已接受的生成 patch:一次通知内只追加节点并切换 head。 */ - applyPreparedTurn(patch: PreparedTurnPatch): boolean { - const thread = state.threads[patch.threadId] - if (!thread) return false - const existingIds = new Set(thread.messages.map((message) => message.id)) - if (patch.addedMessages.some((message) => existingIds.has(message.id))) - return false - thread.messages.push( - ...patch.addedMessages.map((message) => structuredClone(message)) - ) - thread.activeLeafMessageId = patch.nextActiveLeafMessageId - touchSilently(patch.threadId) - notify() - return true - }, - - setActiveLeaf(threadId: string, assistantMessageId: string): boolean { + setThreadModel(threadId, modelId) { const thread = state.threads[threadId] - const target = thread?.messages.find( - (message) => - message.id === assistantMessageId && message.role === "assistant" - ) - if (!thread || !target) return false - thread.activeLeafMessageId = target.id - touchSilently(threadId) - notify() - return true - }, - - /** 从一条消息的划选文字上开出新分支;新分支消息为空,首条回复由 chat-controller 触发流式生成 */ - fork(input: ForkInput): ForkResult | null { - const parent = state.threads[input.sourceThreadId] - if (!parent) return null - const srcMsg = parent.messages.find((m) => m.id === input.sourceMsgId) - if (!srcMsg) return null - - state.footnoteCounter++ - const id = "b" + state.seq++ - const depth = parent.depth + 1 - const title = defaultBranchTitle(input.anchorText) - - state.threads[id] = { - id, - modelId: parent.modelId, - parentId: input.sourceThreadId, - depth, - title, - anchorText: input.anchorText, - forkFromMsgId: input.sourceMsgId, - footnote: state.footnoteCounter, - children: [], - messages: [], - activeLeafMessageId: null, - lastActive: 0, - } - parent.children.push(id) - srcMsg.forks.push({ - text: input.anchorText, - num: state.footnoteCounter, - threadId: id, - depth, - anchor: input.anchor, - }) - - notify() - return { threadId: id, title } - }, - - /** 追加一条用户消息;返回消息 id,会话不存在时返回 null */ - appendUserMessage( - threadId: string, - text: string, - quote?: { text: string } - ): string | null { - const t = state.threads[threadId] - if (!t) return null - const id = "m" + state.seq++ - t.messages.push({ - id, - parentMessageId: t.activeLeafMessageId, - role: "user", - text, - forks: [], - ...(quote ? { quote } : {}), - }) - t.activeLeafMessageId = id - touchSilently(threadId) - notify() - return id - }, - - /** 新建一条 pending 的空 assistant 消息(流式回复的占位),返回消息 id */ - beginAssistantMessage( - threadId: string, - generationId?: string - ): string | null { - const t = state.threads[threadId] - if (!t) return null - const id = "m" + state.seq++ - t.messages.push({ - id, - parentMessageId: t.activeLeafMessageId, - role: "assistant", - text: "", - forks: [], - generationId, - backgroundGeneration: undefined, - status: "pending", - }) - t.activeLeafMessageId = id - notify() - return id - }, - - /** 给流式中的 assistant 消息追加一段文本增量 */ - appendAssistantDelta(threadId: string, msgId: string, delta: string): void { - const t = state.threads[threadId] - if (!t) return - const msg = findMessageFromTail(t.messages, msgId) - if (!msg) return - msg.text += delta - msg.status = "streaming" - notify() - }, - - /** 更新 Markdown 工具的临时生成进度;完整 Artifact 到达后会被原子清除。 */ - setMarkdownGenerationProgress( - threadId: string, - msgId: string, - progress: MarkdownGenerationProgress - ): void { - const t = state.threads[threadId] - if (!t) return - const msg = findMessageFromTail(t.messages, msgId) - if (!msg || msg.role !== "assistant") return - if (msg.status === "done" || msg.status === "error") return - msg.markdownGeneration = progress - msg.status = "streaming" - notify() - }, - - /** 聚合联网搜索/深读状态;同一 toolCallId 原位更新,保持真实调用顺序。 */ - setWebResearchActivity( - threadId: string, - msgId: string, - activity: WebResearchActivity - ): void { - const thread = state.threads[threadId] - if (!thread) return - const message = findMessageFromTail(thread.messages, msgId) - if (!message || message.role !== "assistant") return - if (message.status === "done" || message.status === "error") return - - const activities = message.webResearch ?? [] - if (message.webResearchTextOffset == null) - message.webResearchTextOffset = message.text.length - const index = activities.findIndex( - (item) => item.toolCallId === activity.toolCallId - ) - if (index === -1) activities.push(activity) - else activities[index] = { ...activities[index], ...activity } - message.webResearch = activities - message.status = "streaming" - notify() - }, - - /** 保存本轮联网路由决策;普通 answer 路由不改变可见状态。 */ - setResearchRoute( - threadId: string, - msgId: string, - route: ResearchRoute - ): void { - const thread = state.threads[threadId] - if (!thread) return - const message = findMessageFromTail(thread.messages, msgId) - if (!message || message.role !== "assistant") return - if (message.status === "done" || message.status === "error") return - message.researchRoute = route - notify() - }, - - /** 保存复杂研究的可审计计划摘要,不保存或展示模型原始思维链。 */ - setResearchPlan(threadId: string, msgId: string, plan: ResearchPlan): void { - const thread = state.threads[threadId] - if (!thread) return - const message = findMessageFromTail(thread.messages, msgId) - if (!message || message.role !== "assistant") return - if (message.status === "done" || message.status === "error") return - message.researchPlan = plan - message.status = "streaming" - notify() - }, - - /** 流式结束:标记消息完成 */ - finishAssistantMessage(threadId: string, msgId: string): void { - const t = state.threads[threadId] - if (!t) return - const msg = findMessageFromTail(t.messages, msgId) - if (!msg) return - msg.markdownGeneration = undefined - msg.webResearch = msg.webResearch?.map((activity) => ({ - ...activity, - status: "complete", - })) - msg.status = "done" - touchSilently(threadId) - notify() - }, - - /** 流式失败:标记错误(已收到的文本保留) */ - failAssistantMessage( - threadId: string, - msgId: string, - message: string - ): void { - const t = state.threads[threadId] - if (!t) return - const msg = findMessageFromTail(t.messages, msgId) - if (!msg) return - msg.markdownGeneration = undefined - msg.webResearch = msg.webResearch?.map((activity) => ({ - ...activity, - status: "complete", - })) - msg.status = "error" - msg.error = message - notify() - }, - - /** 轮询/加载终态的 generationId CAS 合并;旧 attempt 返回 false 且零写入。 */ - applyGenerationResult(input: MergeGenerationResultInput): boolean { - const merged = mergeGenerationResult(state, input) - if (merged === state) return false - Object.assign(state, merged) - notify() - return true - }, - - /** - * 写入模型成功生成的语义标题。成功状态与“已尝试”分离:主线失败时继续使用 - * 首条消息派生的回退标题。更新随整树防抖存盘,列头和会话列表同步重渲。 - */ - setGeneratedThreadTitle(threadId: string, title: string): void { - const t = state.threads[threadId] - if (!t) return - const v = title.trim() - if (!v || (t.title === v && t.titleGenerated)) return - t.title = v - t.titleGenerated = true - notify() - }, - - /** - * 原子记录一次主线或分支自动标题生成尝试。该标记随整棵树持久化,失败也不在 - * 刷新后重试,以避免可选功能反复消耗模型配额。 - */ - markTitleGenerationAttempted(threadId: string): boolean { - const t = state.threads[threadId] - if (!t || t.titleGenerationAttempted) return false - t.titleGenerationAttempted = true - notify() - return true - }, - - /** MVP 模型策略:仅根 Thread 可切换;分支由 fork 继承且保持锁定。 */ - setThreadModel(threadId: string, modelId: string): void { - const thread = state.threads[threadId] - if ( - !thread || - thread.parentId !== null || - !isValidModelId(modelId) || - thread.modelId === modelId - ) + if (!thread || !isValidModelId(modelId) || thread.modelId === modelId) return thread.modelId = modelId - notify() - }, - - /** 单独登记一个 artifact(fork 之外的入口,预留) */ - registerArtifact( - sourceThreadId: string, - sourceMessageId: string, - seed_: ArtifactSeed - ): string { - const id = registerSilently(sourceThreadId, sourceMessageId, seed_) - notify() - return id - }, - - /** 原子登记 artifact 并绑定到产生它的 assistant 消息;目标无效时零写入。 */ - attachArtifactToMessage( - threadId: string, - messageId: string, - seed_: ArtifactSeed - ): string | null { - const thread = state.threads[threadId] - if (!thread) return null - const message = findMessageFromTail(thread.messages, messageId) - if (!message || message.role !== "assistant") return null - const id = registerSilently(threadId, messageId, seed_) - message.artifactIds = [...(message.artifactIds ?? []), id] - message.markdownGeneration = undefined - // 完整工具输入已经到达:即使尚无正文,也不再显示 pending 三点占位。 - if (message.status === "pending") message.status = "streaming" - notify() - return id + version++ + listeners.forEach((listener) => listener()) }, } } diff --git a/app/thread-chat/generation/generation-reconciliation-logic.ts b/app/thread-chat/generation/generation-reconciliation-logic.ts deleted file mode 100644 index ca1da423..00000000 --- a/app/thread-chat/generation/generation-reconciliation-logic.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { MergeGenerationResultInput } from "./merge-result" -import type { ThreadTreeState } from "../core/types" -import { - isActiveGenerationStatus, - type GenerationSummary, - type RecoverableTurn, -} from "./types" - -export { isActiveGenerationStatus as isGenerationInFlight } from "./types" - -export function initialGenerationIds( - generations: readonly GenerationSummary[] -): Set { - return new Set( - generations - .filter((generation) => isActiveGenerationStatus(generation.status)) - .map((generation) => generation.id) - ) -} - -export function messageGenerationIds(state: ThreadTreeState): string[] { - return Object.values(state.threads).flatMap((thread) => - thread.messages.flatMap((message) => - message.role === "assistant" && - (message.status === "pending" || message.status === "streaming") && - message.generationId - ? [message.generationId] - : [] - ) - ) -} - -/** 将 owner-scoped generation 终态投影成 store 的窄 CAS 命令。 */ -export function terminalGenerationResultInput( - generation: GenerationSummary -): MergeGenerationResultInput | null { - if (!generation.result) return null - return { - threadId: generation.threadId, - assistantMessageId: generation.assistantMessageId, - generationId: generation.id, - result: generation.result, - } -} - -/** generation 记录消失时,只定位仍由它拥有的本地 pending turn。 */ -export function missingGenerationTurn( - state: ThreadTreeState, - generationId: string -): (RecoverableTurn & { assistantMessageId: string }) | null { - for (const thread of Object.values(state.threads)) { - const assistant = thread.messages.find( - (message) => - message.role === "assistant" && - message.generationId === generationId && - (message.status === "pending" || message.status === "streaming") - ) - if (!assistant) continue - const user = thread.messages.find( - (message) => - message.role === "user" && message.id === assistant.parentMessageId - ) - if (!user) return null - return { - threadId: thread.id, - userMessageId: user.id, - assistantMessageId: assistant.id, - reason: "missing_generation", - } - } - return null -} diff --git a/app/thread-chat/generation/merge-result.ts b/app/thread-chat/generation/merge-result.ts deleted file mode 100644 index 01829c8c..00000000 --- a/app/thread-chat/generation/merge-result.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:generation result 合并能力位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/merge-generation-result" diff --git a/app/thread-chat/generation/project-result.ts b/app/thread-chat/generation/project-result.ts deleted file mode 100644 index f93dd913..00000000 --- a/app/thread-chat/generation/project-result.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:generation stream result 投影位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/project-generation-result" diff --git a/app/thread-chat/generation/reconcile-turns.ts b/app/thread-chat/generation/reconcile-turns.ts deleted file mode 100644 index 75aea987..00000000 --- a/app/thread-chat/generation/reconcile-turns.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:generation 加载协调服务位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/reconcile-turns" diff --git a/app/thread-chat/generation/types.ts b/app/thread-chat/generation/types.ts deleted file mode 100644 index a252aab7..00000000 --- a/app/thread-chat/generation/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:generation 共享类型的唯一来源位于 lib/thread-chat/domain。 - */ -export * from "@/lib/thread-chat/domain/generation" diff --git a/app/thread-chat/generation/use-generation-reconciliation.ts b/app/thread-chat/generation/use-generation-reconciliation.ts deleted file mode 100644 index 8af88b8f..00000000 --- a/app/thread-chat/generation/use-generation-reconciliation.ts +++ /dev/null @@ -1,97 +0,0 @@ -"use client" - -import { useEffect, useRef } from "react" -import { - GENERATION_CLIENT_POLL_MS, - GENERATION_ERRORS, - GENERATION_HIDDEN_POLL_MS, -} from "@/constants/generation" -import { fetchWithAuth } from "@/lib/auth/session-recovery" -import type { ThreadStore } from "../core/store" -import type { GenerationSummary, RecoverableTurn } from "./types" -import { - initialGenerationIds, - isGenerationInFlight, - messageGenerationIds, - missingGenerationTurn, - terminalGenerationResultInput, -} from "./generation-reconciliation-logic" - -export function useGenerationReconciliation({ - store, - version, - initialGenerations, - registerRecoverableTurn, - isGenerationStreamingLocally, -}: { - store: ThreadStore - version: number - initialGenerations: GenerationSummary[] - registerRecoverableTurn(turn: RecoverableTurn): void - isGenerationStreamingLocally(generationId: string): boolean -}) { - const generationIdsRef = useRef(initialGenerationIds(initialGenerations)) - - useEffect(() => { - for (const generationId of messageGenerationIds(store.getState())) - generationIdsRef.current.add(generationId) - }, [version, store]) - - useEffect(() => { - let cancelled = false - let timer: ReturnType | null = null - - const schedule = () => { - if (cancelled) return - timer = setTimeout( - poll, - document.hidden ? GENERATION_HIDDEN_POLL_MS : GENERATION_CLIENT_POLL_MS - ) - } - const poll = async () => { - for (const generationId of [...generationIdsRef.current]) { - if (cancelled) return - // 本页已有 SSE 消费者时不发重复 GET;流结束/断开后下一轮恢复权威协调。 - if (isGenerationStreamingLocally(generationId)) continue - try { - const response = await fetchWithAuth( - `/api/branch-generations/${generationId}` - ) - if (response.status === 404) { - generationIdsRef.current.delete(generationId) - const recoverable = missingGenerationTurn( - store.getState(), - generationId - ) - if (recoverable) { - store.failAssistantMessage( - recoverable.threadId, - recoverable.assistantMessageId, - GENERATION_ERRORS.backgroundInterrupted - ) - registerRecoverableTurn(recoverable) - } - continue - } - if (!response.ok) continue - const data = (await response.json()) as { - generation: GenerationSummary - } - if (isGenerationInFlight(data.generation.status)) continue - - generationIdsRef.current.delete(generationId) - const resultInput = terminalGenerationResultInput(data.generation) - if (resultInput) store.applyGenerationResult(resultInput) - } catch (error) { - console.warn("[thread-chat] generation 轮询失败,将继续重试", error) - } - } - schedule() - } - schedule() - return () => { - cancelled = true - if (timer) clearTimeout(timer) - } - }, [store, registerRecoverableTurn, isGenerationStreamingLocally]) -} diff --git a/app/thread-chat/layout.tsx b/app/thread-chat/layout.tsx index 117a2c98..c7260d07 100644 --- a/app/thread-chat/layout.tsx +++ b/app/thread-chat/layout.tsx @@ -4,7 +4,7 @@ import { ROUTES, signInWithRedirect } from "@/constants/routes" import { ThreadChatAppProvider } from "@/lib/thread-chat/client/providers" import "./thread-chat.css" -// 旗舰访问门禁:一处服务端 layout 同时包住 /thread-chat 跳板与 /thread-chat/[treeId], +// 旗舰访问门禁:一处服务端 layout 同时包住 /thread-chat 跳板与 /thread-chat/[projectId], // 用「真会话」判定(getSession),未登录即 302 到带回跳的登录页。 // 用 server layout 而非 middleware:项目已主动撤除 middleware,且 better-auth 在 edge // 只建议查 cookie 存在性(非真校验);server layout 与 /account 页同构、做真会话校验, diff --git a/app/thread-chat/net/boot/thread-chat-boot.ts b/app/thread-chat/net/boot/thread-chat-boot.ts deleted file mode 100644 index 4c220a1e..00000000 --- a/app/thread-chat/net/boot/thread-chat-boot.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { resolveThreadChatModelId } from "@/constants/model" -import { emptySeedState } from "../../core/seed" -import type { ThreadTreeState } from "../../core/types" -import { sanitizeLoadedState, type LoadedTree } from "../persistence/persist" - -export function threadChatBootSeed( - loaded: Pick -): ThreadTreeState { - return loaded.state - ? sanitizeLoadedState( - loaded.state, - resolveThreadChatModelId, - loaded.generations - ) - : emptySeedState() -} - -/** 网络已成功但服务端快照仍不可解析时,也必须完成 boot 并降级为空树。 */ -export function threadChatBootSeedOrFallback( - loaded: Pick, - onInvalidState: (error: unknown) => void = (error) => - console.warn( - "[thread-chat] 分支树快照无效,以空树降级启动(本次不恢复历史):", - error - ) -): ThreadTreeState { - try { - return threadChatBootSeed(loaded) - } catch (error) { - onInvalidState(error) - return emptySeedState() - } -} diff --git a/app/thread-chat/net/boot/use-thread-chat-boot.ts b/app/thread-chat/net/boot/use-thread-chat-boot.ts deleted file mode 100644 index 8d917f1e..00000000 --- a/app/thread-chat/net/boot/use-thread-chat-boot.ts +++ /dev/null @@ -1,49 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import type { MessageFeedbackSummary, ThreadTreeState } from "../../core/types" -import type { GenerationSummary, RecoverableTurn } from "../../generation/types" -import { - loadTree, - loadUiState, - rememberTreeId, - type TreeUiState, -} from "../persistence/persist" -import { threadChatBootSeedOrFallback } from "./thread-chat-boot" - -export interface ThreadChatBoot { - seed: ThreadTreeState - ui: TreeUiState | null - customTitle: string | null - generations: GenerationSummary[] - messageFeedbacks: MessageFeedbackSummary[] - recoverableTurns: RecoverableTurn[] -} - -export function useThreadChatBoot(treeId: string): ThreadChatBoot | null { - const [boot, setBoot] = useState(null) - - useEffect(() => { - let cancelled = false - ;(async () => { - const loaded = await loadTree(treeId) - const seed = threadChatBootSeedOrFallback(loaded) - const ui = loadUiState(treeId, seed) - if (cancelled) return - rememberTreeId(treeId) - setBoot({ - seed, - ui, - customTitle: loaded.customTitle, - generations: loaded.generations, - messageFeedbacks: loaded.messageFeedbacks, - recoverableTurns: loaded.recoverableTurns, - }) - })() - return () => { - cancelled = true - } - }, [treeId]) - - return boot -} diff --git a/app/thread-chat/net/chat-controller.ts b/app/thread-chat/net/chat-controller.ts deleted file mode 100644 index 660582de..00000000 --- a/app/thread-chat/net/chat-controller.ts +++ /dev/null @@ -1,402 +0,0 @@ -/** - * net/chat-controller —— 会话的「发送 / 重试 / 中止」统一入口。 - * (分支首答不再由这里触发:开分支只预填 composer,用户回车确认后走普通 send。) - * - * 消费真实 /api/chat SSE(见 ui-stream.ts),把正文增量喂回 store 的细粒度 - * mutator(pending → streaming → done/error)。 - * - * 关键机制: - * - inflight:per-thread 的 AbortController,同一会话同时只允许一路在飞。 - * - 合帧缓冲:text-delta 不直接进 store,先攒进 buffer,用 rAF 合帧后每帧至多 - * 一次 appendAssistantDelta(即每帧至多一次 version++),避免高频 delta 全树重渲卡顿; - * 页面不可见 / 无 rAF 环境降级为 setTimeout(50ms)。finish/error/abort 前强制 flush 残余。 - * - 归属校验(isOwner):所有对目标消息的写入都要求「inflight 仍指向本次 controller」, - * 使 retry(先 abort 旧流、复位、再起新流)时,旧流的残余 delta / 收尾不会误写新流的消息。 - * - error chunk 的容错语义:实测 /api/chat 的流中会夹杂零星「瞬时」error chunk - * (疑似 MiniMax 个别 chunk 经 @ai-sdk/openai-compatible 解析失败,被 - * toUIMessageStreamResponse 掩码为 "An error occurred." 后发出),之后正文 - * text-delta / Markdown Artifact 继续到达并正常 finish。因此 onError 不立即判死:只记录 lastError - * (后到覆盖先到)并继续收流;终态统一裁决——收到过任何正文即按成功 finish - * (瞬时 error 忽略并 console.warn 留痕),正文/Artifact 都没有且有 error 用 lastError - * fail,两者都没有且无 error 也 fail。中止时即使已有部分输出也落为 error, - * 避免未完成消息暴露复制和评价操作;用户可通过错误态的重试入口重新生成。 - */ - -import type { ThreadStore } from "../core/store" -import { buildRequestBody } from "./prompt/prompt" -import { consumeUIMessageStream } from "./stream/ui-stream" -import type { MessageFeedback, MessageFeedbackSummary } from "../core/types" -import { GENERATION_ERRORS } from "@/constants/generation" -import { - getKnownTreeRevision, - setKnownTreeRevision, -} from "./persistence/persist" -import { activeLeafTurn } from "../core/message-graph" -import { submitMessageFeedback } from "./commands/message-feedback-command" -import { switchActiveLeaf } from "./commands/switch-active-leaf-command" -import { requestGenerationStop } from "./commands/stop-generation-command" -import { requestChatGeneration } from "./commands/chat-generation-command" -import { - ABORTED_ERROR, - createAssistantStreamRuntime, -} from "./stream/assistant-stream-runtime" -import { - prepareAssistantRetry, - prepareUserEdit, - prepareUserTurnRetry, - type PreparedRegenerationAction, - type PreparedRegenerationStart, -} from "./commands/regeneration-command" -import { createLocalGenerationExecutions } from "./stream/local-generation-executions" -import type { - GenerationActionResult, - ThreadMessageActionCommands, - VariantSwitchResult, -} from "../chat/actions/message-action-commands" - -export type { - GenerationActionResult, - MessageActionFailureCode, - VariantSwitchResult, -} from "../chat/actions/message-action-commands" - -/** 网络异常(非中止)的兜底错误文案 */ -const NETWORK_ERROR = "网络请求失败,请重试" - -export type ChatController = ReturnType & - ThreadMessageActionCommands - -/** 判断是否为「中止」类异常 */ -function isAbortError(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - (err as { name?: string }).name === "AbortError" - ) -} - -export interface ChatControllerOptions { - treeId: string - /** 严格整树存盘:失败必须 reject,确保不会调用付费模型。 */ - persistNow(): Promise - onError?(message: string): void -} - -export function createChatController( - store: ThreadStore, - options: ChatControllerOptions -) { - /** 每个会话同一时间只允许一路在飞,并供后台协调排除本页已有的 SSE。 */ - const localExecutions = createLocalGenerationExecutions() - - /** - * 对某会话的某条 assistant 消息发起真实流式请求。 - * 普通发送已由 beginAssistantMessage 备好目标;变体操作在服务端接受后原子应用 patch。 - */ - function startAssistant( - threadId: string, - msgId: string, - userMessageId: string, - generationId: string, - action?: PreparedRegenerationAction - ): Promise { - const execution = localExecutions.begin(threadId, generationId) - const { controller, isOwner } = execution - const { signal } = controller - - const streamRuntime = createAssistantStreamRuntime({ - store, - threadId, - messageId: msgId, - isOwner, - }) - - let streamHandedOff = false - return (async () => { - try { - if (!action) { - try { - await options.persistNow() - } catch (error) { - console.error("[thread-chat] 发送前持久化屏障失败", error) - streamRuntime.fail(GENERATION_ERRORS.persistenceBarrier) - return { - ok: false, - code: "persistence_failed", - message: GENERATION_ERRORS.persistenceBarrier, - } - } - } - if (signal.aborted) - return { ok: false, code: "network_error", message: ABORTED_ERROR } - - const state = store.getState() - const thread = state.threads[threadId] - if (!thread) { - streamRuntime.fail("会话不存在") - return { ok: false, code: "not_found", message: "会话不存在" } - } - - const body = buildRequestBody(state, thread, msgId, { - treeId: options.treeId, - userMessageId, - generationId, - intent: action?.intent ?? { kind: "persisted-turn" }, - }) - const command = await requestChatGeneration({ body, signal }) - if (command.kind === "replayed") { - // 同 generation 请求重放:服务端已在执行或已终态,不启动第二次模型; - // 保留 pending,由 generation 轮询取得权威状态。 - if (action) store.applyPreparedTurn(action.patch) - return { - ok: true, - generationId, - userMessageId, - assistantMessageId: msgId, - ...(action?.sourceUserMessageId - ? { sourceUserMessageId: action.sourceUserMessageId } - : {}), - ...(action?.sourceAssistantMessageId - ? { sourceAssistantMessageId: action.sourceAssistantMessageId } - : {}), - } - } - if (command.kind === "rejected") { - if (!action) streamRuntime.fail(command.failure.message) - return command.failure - } - const res = command.response - if (command.revision !== null) - setKnownTreeRevision(options.treeId, command.revision) - if (action && !store.applyPreparedTurn(action.patch)) { - return { - ok: false, - code: "generation_conflict", - message: "服务端已接受生成,但本地消息图需要刷新", - } - } - - const accepted: GenerationActionResult = { - ok: true, - generationId, - userMessageId, - assistantMessageId: msgId, - ...(action?.sourceUserMessageId - ? { sourceUserMessageId: action.sourceUserMessageId } - : {}), - ...(action?.sourceAssistantMessageId - ? { sourceAssistantMessageId: action.sourceAssistantMessageId } - : {}), - } - - if (action) { - streamHandedOff = true - void (async () => { - try { - await consumeUIMessageStream(res, streamRuntime.handlers, signal) - if (signal.aborted) streamRuntime.settleByAbort() - else streamRuntime.settleByOutcome() - } catch (error) { - if (signal.aborted || isAbortError(error)) - streamRuntime.settleByAbort() - else streamRuntime.fail(NETWORK_ERROR) - } finally { - streamRuntime.cancel() - localExecutions.clearIfOwner(threadId, controller) - } - })() - return accepted - } - - await consumeUIMessageStream(res, streamRuntime.handlers, signal) - if (signal.aborted) { - // 被 abort:consume 静默返回、onFinish 不触发——有正文保留 finish,零正文标可重试错误 - streamRuntime.settleByAbort() - } else { - // 正常结束时 handlers.onFinish 已 settle(幂等);这里兜底走同一套终态裁决 - streamRuntime.settleByOutcome() - } - return accepted - } catch (err) { - if (signal.aborted || isAbortError(err)) { - streamRuntime.settleByAbort() // 中止:有正文保留 finish,零正文标可重试错误 - } else { - if (!action) streamRuntime.fail(NETWORK_ERROR) // fetch reject 等 - } - return { - ok: false, - code: "network_error", - message: isAbortError(err) ? ABORTED_ERROR : NETWORK_ERROR, - } - } finally { - if (!streamHandedOff) { - streamRuntime.cancel() - // 仅当 inflight 仍指向本次 controller 时才清除,避免 retry 竞态误删新流的条目 - localExecutions.clearIfOwner(threadId, controller) - } - } - })() - } - - /** 只断开本地 fetch 消费者;不会向服务端表达 Stop。 */ - function detachThread(threadId: string): void { - localExecutions.detach(threadId) - } - - function startPreparedRegeneration(start: PreparedRegenerationStart) { - detachThread(start.threadId) - return startAssistant( - start.threadId, - start.messageId, - start.userMessageId, - start.generationId, - start.action - ) - } - - function activeAssistant(threadId: string) { - const thread = store.getState().threads[threadId] - if (!thread) return null - const turn = activeLeafTurn(thread) - const message = turn?.assistantMessage - if ( - !message || - (message.status !== "pending" && message.status !== "streaming") - ) - return null - return { message, index: thread.messages.indexOf(message) } - } - - async function requestStop(threadId: string): Promise { - const active = activeAssistant(threadId) - const generationId = active?.message.generationId - if (!generationId) return false - const result = await requestGenerationStop(generationId) - if (!result.ok) { - options.onError?.(result.message) - return false - } - detachThread(threadId) - return true - } - - return { - /** 在会话里发一条用户消息并触发流式回复;同会话已有在飞请求时直接忽略 */ - send(threadId: string, text: string, quote?: { text: string }): void { - if (localExecutions.hasThread(threadId) || activeAssistant(threadId)) - return - const userMessageId = store.appendUserMessage(threadId, text, quote) - if (!userMessageId) return - const generationId = crypto.randomUUID() - const msgId = store.beginAssistantMessage(threadId, generationId) - if (!msgId) return - void startAssistant(threadId, msgId, userMessageId, generationId) - }, - - /** 兼容旧宿主的 retry 入口;新语义为追加 sibling assistant。 */ - retry(threadId: string, msgId: string): void { - const prepared = prepareAssistantRetry(store.getState(), { - threadId, - sourceAssistantMessageId: msgId, - assistantMessageId: crypto.randomUUID(), - generationId: crypto.randomUUID(), - }) - if (!prepared.ok) return - void startPreparedRegeneration(prepared.start) - }, - - async retryAssistant( - threadId: string, - assistantMessageId: string - ): Promise { - const prepared = prepareAssistantRetry(store.getState(), { - threadId, - sourceAssistantMessageId: assistantMessageId, - assistantMessageId: crypto.randomUUID(), - generationId: crypto.randomUUID(), - }) - if (!prepared.ok) return prepared - return startPreparedRegeneration(prepared.start) - }, - - async retryUserTurn( - threadId: string, - userMessageId: string - ): Promise { - const prepared = prepareUserTurnRetry(store.getState(), { - threadId, - userMessageId, - assistantMessageId: crypto.randomUUID(), - generationId: crypto.randomUUID(), - }) - if (!prepared.ok) return prepared - return startPreparedRegeneration(prepared.start) - }, - - async editAndRegenerate( - threadId: string, - userMessageId: string, - text: string - ): Promise { - const prepared = prepareUserEdit(store.getState(), { - threadId, - sourceUserMessageId: userMessageId, - text, - userMessageId: crypto.randomUUID(), - assistantMessageId: crypto.randomUUID(), - generationId: crypto.randomUUID(), - }) - if (!prepared.ok) return prepared - return startPreparedRegeneration(prepared.start) - }, - - async switchTurnVariant( - threadId: string, - assistantMessageId: string - ): Promise { - const result = await switchActiveLeaf({ - treeId: options.treeId, - threadId, - assistantMessageId, - baseRevision: getKnownTreeRevision(options.treeId), - }) - if (!result.ok) return result - setKnownTreeRevision(options.treeId, result.revision) - if (!store.setActiveLeaf(threadId, assistantMessageId)) - return { - ok: false, - code: "generation_conflict", - message: "本地消息图需要刷新", - } - return result - }, - - async submitFeedback( - threadId: string, - messageId: string, - feedback: MessageFeedback | null - ): Promise { - return submitMessageFeedback({ - treeId: options.treeId, - threadId, - messageId, - feedback, - }) - }, - - /** 只有该显式操作才请求服务端停止模型;服务端确认后再断开本地流。 */ - stop(threadId: string): void { - void requestStop(threadId) - }, - - /** 页面卸载只 detach 本地消费者,服务端 generation 继续执行与计费。 */ - detachAll(): void { - localExecutions.detachAll() - }, - - /** reconciliation 用:本页已有 SSE 消费者时不再为同一 generation 发轮询 GET。 */ - isGenerationStreamingLocally(generationId: string): boolean { - return localExecutions.isGenerationActive(generationId) - }, - } -} diff --git a/app/thread-chat/net/commands/chat-generation-command.ts b/app/thread-chat/net/commands/chat-generation-command.ts deleted file mode 100644 index dc7aa51c..00000000 --- a/app/thread-chat/net/commands/chat-generation-command.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { handleUnauthorized } from "@/lib/auth/session-recovery" -import { messageActionFailureResponseSchema } from "@/lib/thread-chat/contracts/message-action-failure" -import type { GenerationActionResult } from "../../chat/actions/message-action-commands" - -type ChatGenerationCommandInput = { - body: unknown - signal: AbortSignal -} - -type ChatGenerationCommandDependencies = { - fetch: typeof globalThis.fetch - unauthorized(): void | Promise -} - -const defaultDependencies: ChatGenerationCommandDependencies = { - fetch: (input, init) => globalThis.fetch(input, init), - unauthorized: handleUnauthorized, -} - -export type ChatGenerationCommandResult = - | { kind: "replayed" } - | { kind: "stream"; response: Response; revision: number | null } - | { - kind: "rejected" - failure: Extract - } - -/** POST /api/chat,并把 HTTP 层结果归一为 replay、stream 或 rejected。 */ -export async function requestChatGeneration( - { body, signal }: ChatGenerationCommandInput, - dependencies: ChatGenerationCommandDependencies = defaultDependencies -): Promise { - const res = await dependencies.fetch("/api/chat", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - signal, - }) - - if (res.status === 202) return { kind: "replayed" } - if (!res.ok || !res.body) { - if (res.status === 401) void dependencies.unauthorized() - const payload = await res.json().catch(() => null) - const structured = messageActionFailureResponseSchema.safeParse(payload) - const stringMessage = - typeof payload === "object" && - payload !== null && - typeof (payload as Record).error === "string" - ? ((payload as Record).error as string) - : null - const message = - res.status === 401 - ? "登录已失效,正在跳转登录…" - : structured.success - ? structured.data.error.message - : (stringMessage ?? `请求失败(HTTP ${res.status})`) - return { - kind: "rejected", - failure: { - ok: false, - code: - res.status === 401 - ? "unauthorized" - : structured.success - ? structured.data.error.code - : "network_error", - message, - }, - } - } - - const revision = Number(res.headers.get("x-thread-tree-revision")) - return { - kind: "stream", - response: res, - revision: Number.isInteger(revision) ? revision : null, - } -} diff --git a/app/thread-chat/net/commands/message-feedback-command.ts b/app/thread-chat/net/commands/message-feedback-command.ts deleted file mode 100644 index 4da60f10..00000000 --- a/app/thread-chat/net/commands/message-feedback-command.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { fetchWithAuth } from "@/lib/auth/session-recovery" -import { - setMessageFeedbackErrorResponseSchema, - setMessageFeedbackSuccessResponseSchema, -} from "@/lib/thread-chat/contracts/message-feedback" -import type { MessageFeedback, MessageFeedbackSummary } from "../../core/types" - -type SubmitMessageFeedbackInput = { - treeId: string - threadId: string - messageId: string - feedback: MessageFeedback | null -} - -type SubmitMessageFeedbackDependencies = { - fetch: typeof fetchWithAuth -} - -const defaultDependencies: SubmitMessageFeedbackDependencies = { - fetch: fetchWithAuth, -} - -/** 持久化一条 assistant message 的反馈,并按共享契约校验响应。 */ -export async function submitMessageFeedback( - { treeId, threadId, messageId, feedback }: SubmitMessageFeedbackInput, - dependencies: SubmitMessageFeedbackDependencies = defaultDependencies -): Promise { - const res = await dependencies.fetch( - `/api/branch-trees/${treeId}/messages/${encodeURIComponent(messageId)}/feedback`, - { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ threadId, feedback }), - } - ) - const responseBody = await res.json().catch(() => null) - if (!res.ok) { - const failure = - setMessageFeedbackErrorResponseSchema.safeParse(responseBody) - throw new Error( - failure.success - ? failure.data.error.message - : `feedback failed: ${res.status}` - ) - } - const success = - setMessageFeedbackSuccessResponseSchema.safeParse(responseBody) - if (!success.success) throw new Error("feedback response invalid") - return success.data.feedback -} diff --git a/app/thread-chat/net/commands/regeneration-command.ts b/app/thread-chat/net/commands/regeneration-command.ts deleted file mode 100644 index c676a718..00000000 --- a/app/thread-chat/net/commands/regeneration-command.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { ThreadTreeState } from "../../core/types" -import { - prepareRegenerationPatch, - type PreparedTurnPatch, -} from "../../core/regeneration" -import type { ThreadChatGenerationIntent } from "../../generation/types" -import type { GenerationActionResult } from "../../chat/actions/message-action-commands" - -export type PreparedRegenerationAction = { - intent: Exclude - patch: PreparedTurnPatch - sourceUserMessageId?: string - sourceAssistantMessageId?: string -} - -export type PreparedRegenerationStart = { - threadId: string - messageId: string - userMessageId: string - generationId: string - action: PreparedRegenerationAction -} - -export type RegenerationPreparationResult = - | { ok: true; start: PreparedRegenerationStart } - | Extract - -/** 为“重新生成 assistant”准备纯追加 patch 与启动参数,不修改 store。 */ -export function prepareAssistantRetry( - state: ThreadTreeState, - input: { - threadId: string - sourceAssistantMessageId: string - assistantMessageId: string - generationId: string - } -): RegenerationPreparationResult { - const thread = state.threads[input.threadId] - const source = thread?.messages.find( - (message) => message.id === input.sourceAssistantMessageId - ) - if (!thread || source?.role !== "assistant" || !source.parentMessageId) { - return { ok: false, code: "not_found", message: "回复不存在" } - } - - const intent = { - kind: "regenerate-assistant" as const, - sourceAssistantMessageId: input.sourceAssistantMessageId, - } - const patch = prepareRegenerationPatch(state, { - threadId: input.threadId, - userMessageId: source.parentMessageId, - assistantMessageId: input.assistantMessageId, - generationId: input.generationId, - intent, - }) - if (!patch) { - return { - ok: false, - code: "not_latest_turn", - message: "只能重新生成当前最后一轮回复", - } - } - - return { - ok: true, - start: { - threadId: input.threadId, - messageId: input.assistantMessageId, - userMessageId: source.parentMessageId, - generationId: input.generationId, - action: { - intent, - patch, - sourceAssistantMessageId: input.sourceAssistantMessageId, - }, - }, - } -} - -/** 为“重试孤立 user turn”准备 pending assistant patch,不修改 store。 */ -export function prepareUserTurnRetry( - state: ThreadTreeState, - input: { - threadId: string - userMessageId: string - assistantMessageId: string - generationId: string - } -): RegenerationPreparationResult { - const intent = { kind: "retry-orphan-user" as const } - const patch = prepareRegenerationPatch(state, { - threadId: input.threadId, - userMessageId: input.userMessageId, - assistantMessageId: input.assistantMessageId, - generationId: input.generationId, - intent, - }) - if (!patch) { - return { - ok: false, - code: "not_latest_turn", - message: "该消息已不是可恢复的最后一轮", - } - } - - return { - ok: true, - start: { - threadId: input.threadId, - messageId: input.assistantMessageId, - userMessageId: input.userMessageId, - generationId: input.generationId, - action: { - intent, - patch, - sourceUserMessageId: input.userMessageId, - }, - }, - } -} - -/** 为“编辑最后一条 user 后重新生成”准备 user+assistant 追加 patch。 */ -export function prepareUserEdit( - state: ThreadTreeState, - input: { - threadId: string - sourceUserMessageId: string - text: string - userMessageId: string - assistantMessageId: string - generationId: string - } -): RegenerationPreparationResult { - const intent = { - kind: "edit-last-user" as const, - sourceUserMessageId: input.sourceUserMessageId, - text: input.text, - } - const patch = prepareRegenerationPatch(state, { - threadId: input.threadId, - userMessageId: input.userMessageId, - assistantMessageId: input.assistantMessageId, - generationId: input.generationId, - intent, - }) - if (!patch) { - return { - ok: false, - code: "not_latest_turn", - message: "只能编辑当前最后一轮用户消息", - } - } - - return { - ok: true, - start: { - threadId: input.threadId, - messageId: input.assistantMessageId, - userMessageId: input.userMessageId, - generationId: input.generationId, - action: { - intent, - patch, - sourceUserMessageId: input.sourceUserMessageId, - }, - }, - } -} diff --git a/app/thread-chat/net/commands/stop-generation-command.ts b/app/thread-chat/net/commands/stop-generation-command.ts deleted file mode 100644 index e874e60c..00000000 --- a/app/thread-chat/net/commands/stop-generation-command.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { fetchWithAuth } from "@/lib/auth/session-recovery" - -export type StopGenerationResult = { ok: true } | { ok: false; message: string } - -type StopGenerationDependencies = { - fetch: typeof fetchWithAuth - logError(message: string, error: unknown): void -} - -const defaultDependencies: StopGenerationDependencies = { - fetch: fetchWithAuth, - logError: (message, error) => console.error(message, error), -} - -/** 请求服务端停止指定 generation;不负责断开本地 stream consumer。 */ -export async function requestGenerationStop( - generationId: string, - dependencies: StopGenerationDependencies = defaultDependencies -): Promise { - try { - const res = await dependencies.fetch( - `/api/branch-generations/${generationId}/stop`, - { method: "POST" } - ) - return res.ok - ? { ok: true } - : { - ok: false, - message: `停止失败(HTTP ${res.status}),生成仍在继续`, - } - } catch (error) { - dependencies.logError("[thread-chat] Stop 请求失败", error) - return { ok: false, message: "停止失败,生成仍在继续" } - } -} diff --git a/app/thread-chat/net/commands/switch-active-leaf-command.ts b/app/thread-chat/net/commands/switch-active-leaf-command.ts deleted file mode 100644 index 39f9ef42..00000000 --- a/app/thread-chat/net/commands/switch-active-leaf-command.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { fetchWithAuth } from "@/lib/auth/session-recovery" -import { - switchActiveLeafErrorResponseSchema, - switchActiveLeafSuccessResponseSchema, -} from "@/lib/thread-chat/contracts/switch-active-leaf" -import type { VariantSwitchResult } from "../../chat/actions/message-action-commands" - -const NETWORK_ERROR = "网络请求失败,请重试" - -type SwitchActiveLeafInput = { - treeId: string - threadId: string - assistantMessageId: string - baseRevision: number | null -} - -type SwitchActiveLeafDependencies = { - fetch: typeof fetchWithAuth -} - -const defaultDependencies: SwitchActiveLeafDependencies = { - fetch: fetchWithAuth, -} - -/** 请求服务端原子切换 active leaf;本地 revision/store 更新由调用者负责。 */ -export async function switchActiveLeaf( - { treeId, threadId, assistantMessageId, baseRevision }: SwitchActiveLeafInput, - dependencies: SwitchActiveLeafDependencies = defaultDependencies -): Promise { - try { - const res = await dependencies.fetch( - `/api/branch-trees/${treeId}/active-leaf`, - { - method: "PATCH", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ threadId, assistantMessageId, baseRevision }), - } - ) - const responseBody = await res.json().catch(() => null) - if (!res.ok) { - const failure = - switchActiveLeafErrorResponseSchema.safeParse(responseBody) - return { - ok: false, - code: failure.success ? failure.data.error.code : "network_error", - message: failure.success - ? failure.data.error.message - : "切换回复版本失败", - } - } - const success = - switchActiveLeafSuccessResponseSchema.safeParse(responseBody) - if (!success.success) { - return { - ok: false, - code: "network_error", - message: "服务端未返回新的树修订号", - } - } - return { - ok: true, - threadId, - assistantMessageId, - revision: success.data.revision, - } - } catch { - return { ok: false, code: "network_error", message: NETWORK_ERROR } - } -} diff --git a/app/thread-chat/net/persistence/persist.ts b/app/thread-chat/net/persistence/persist.ts deleted file mode 100644 index 08b02860..00000000 --- a/app/thread-chat/net/persistence/persist.ts +++ /dev/null @@ -1,335 +0,0 @@ -/** - * net/persist —— 分支树持久化的客户端一侧:加载 / 防抖存盘的网络调用、 - * treeId 记忆、加载期 sanitize、每棵树的工作台状态(localStorage)。 - * - * 职责边界: - * · 对话数据(ThreadTreeState)走 DB(/api/branch-trees/{treeId}),整树 JSON 存取; - * · 设备本地 UI 态(列槽/列宽/列数/放置策略/视图)按 treeId 分键走 localStorage—— - * 列宽与视口强相关、丢了无伤,不值得进 DB; - * · localStorage / fetch 只允许在客户端(effect 或事件回调)里调用本模块的函数。 - * - * 为什么需要 sanitizeLoadedState:防抖存盘可能恰好在流式中途落盘,而 AbortController - * 不跨页面存活。服务端 tree GET 会先按 generation sidecar 协调状态;客户端仍做滚动 - * 部署期的防御性收敛。没有匹配 generation 的 pending/streaming 回复一律保留已有 - * 正文与 Artifact,并转为可重试 error;不能删除 Retry 所需的 messageId,也不能把 - * 部分输出猜成完整回复。strict-v2 parser 会拒绝坏消息图与无归属 Artifact。 - */ - -import { - LAST_TREE_ID_KEY, - TREE_TITLE_FALLBACK, - TREE_TITLE_MAX_LEN, - TREE_UI_KEY_PREFIX, -} from "@/constants/thread-chat" -import { isValidTreeId } from "@/lib/chat/tree-id" -import { fetchWithAuth } from "@/lib/auth/session-recovery" -import type { MessageFeedbackSummary, ThreadTreeState } from "../../core/types" -import type { GenerationSummary, RecoverableTurn } from "../../generation/types" -import type { PlacementMode, Slot } from "../../orchestration/columns/placement" -import { withoutTransientGenerationState } from "./transient-state" -import { readSaveTreeRevision, TreeRevisionError } from "./save-tree-response" -export { sanitizeLoadedState } from "./sanitize-loaded-state" -export { TreeRevisionError } from "./save-tree-response" - -export { isValidTreeId } - -/* ---------------- 「最近一棵」treeId 记忆(裸路径 /thread-chat 的跳转目标) ---------------- */ - -export function rememberTreeId(id: string): void { - try { - localStorage.setItem(LAST_TREE_ID_KEY, id) - } catch { - /* localStorage 不可用(隐私模式等):记忆失败无伤,裸路径会开新树 */ - } -} - -export function getLastTreeId(): string | null { - try { - const id = localStorage.getItem(LAST_TREE_ID_KEY) - return id && isValidTreeId(id) ? id : null - } catch { - return null - } -} - -/* ---------------- 整树加载 / 存盘(DB) ---------------- */ - -/** loadTree 的返回:state = 整树(未保存过为 null);customTitle = 用户重命名过的标题(未改过为 null) */ -export interface LoadedTree { - state: ThreadTreeState | null - revision: number - customTitle: string | null - generations: GenerationSummary[] - messageFeedbacks: MessageFeedbackSummary[] - recoverableTurns: RecoverableTurn[] -} - -const revisionByTreeId = new Map() - -export function getKnownTreeRevision(treeId: string): number { - return revisionByTreeId.get(treeId) ?? 0 -} - -export function setKnownTreeRevision(treeId: string, revision: number): void { - const current = revisionByTreeId.get(treeId) - revisionByTreeId.set( - treeId, - current === undefined ? revision : Math.max(current, revision) - ) -} - -/** GET 整树:未保存过 state 为 null(正常首访路径);请求失败也降级为空并 console.warn(空树启动) */ -export async function loadTree(id: string): Promise { - try { - const res = await fetchWithAuth(`/api/branch-trees/${id}`) - if (res.status === 404) { - setKnownTreeRevision(id, 0) - return { - state: null, - revision: 0, - customTitle: null, - generations: [], - messageFeedbacks: [], - recoverableTurns: [], - } - } - if (!res.ok) throw new Error(`GET /api/branch-trees ${res.status}`) - const data = (await res.json()) as LoadedTree - setKnownTreeRevision(id, data.revision ?? 0) - return { - state: data.state, - revision: data.revision ?? 0, - customTitle: data.customTitle ?? null, - generations: data.generations ?? [], - messageFeedbacks: data.messageFeedbacks ?? [], - recoverableTurns: data.recoverableTurns ?? [], - } - } catch (err) { - console.warn( - "[thread-chat] 加载分支树失败,以空树降级启动(本次不恢复历史):", - err - ) - setKnownTreeRevision(id, 0) - return { - state: null, - revision: 0, - customTitle: null, - generations: [], - messageFeedbacks: [], - recoverableTurns: [], - } - } -} - -/** PUT 整树 upsert:失败只 console.warn 不抛——持久化失败不能打断对话 */ -/* 每棵树一条客户端写链:saveTree / deleteTree 串行执行(codex review 两条 P2 的修复)—— - ① 慢的旧快照 PUT 不会后到覆盖新快照(同树写操作严格按入队序落库); - ② 删除总排在已入队/在飞的存盘之后,配合壳层「删除前置抑制位」堵死 DB 行复活竞态。 - 链上任务失败不断链(下一个任务照常执行);链清空后从 Map 摘除防泄漏。 */ -const writeChains = new Map>() -function enqueueTreeWrite(id: string, task: () => Promise): Promise { - const prev = writeChains.get(id) ?? Promise.resolve() - const run = prev.then(task, task) - const settled = run.then( - () => undefined, - () => undefined - ) - writeChains.set(id, settled) - void settled.then(() => { - if (writeChains.get(id) === settled) writeChains.delete(id) - }) - return run -} - -async function writeTree( - id: string, - state: ThreadTreeState, - title?: string -): Promise { - const res = await fetchWithAuth(`/api/branch-trees/${id}`, { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - state, - title, - baseRevision: getKnownTreeRevision(id), - }), - }) - const revision = await readSaveTreeRevision(res) - if (revision !== null) setKnownTreeRevision(id, revision) -} - -export async function saveTree( - id: string, - state: ThreadTreeState, - title?: string, - onRevisionConflict?: (error: TreeRevisionError) => void -): Promise { - const persistedState = withoutTransientGenerationState(state) - return enqueueTreeWrite(id, async () => { - try { - await writeTree(id, persistedState, title) - } catch (err) { - if (err instanceof TreeRevisionError) onRevisionConflict?.(err) - console.warn("[thread-chat] 分支树存盘失败(下次变更会再试):", err) - } - }) -} - -/** 发送模型前的严格持久化屏障:与普通防抖写共用 per-tree 写链,但失败必须抛出。 */ -export async function saveTreeStrict( - id: string, - state: ThreadTreeState, - title?: string -): Promise { - const persistedState = withoutTransientGenerationState(state) - return enqueueTreeWrite(id, () => writeTree(id, persistedState, title)) -} - -/* ---------------- 树列表 / 重命名 / 删除(会话列表 UI,openspec: add-tree-list-ui) ---------------- */ - -/** GET /api/branch-trees 的条目形状(轻量列表,无 state) */ -export interface TreeListItem { - id: string - /** 展示标题:服务端已做 coalesce(custom_title, title, 兜底) */ - title: string - /** ISO 时间字符串(JSON 序列化后的 timestamp) */ - updatedAt: string - threadCount: number -} - -/** GET 树列表:失败返回空数组并 console.warn(弹层显示空态,不打断页面) */ -export async function listTrees(): Promise { - try { - const res = await fetchWithAuth("/api/branch-trees") - if (!res.ok) throw new Error(`GET /api/branch-trees ${res.status}`) - const data = (await res.json()) as { trees: TreeListItem[] } - return data.trees - } catch (err) { - console.warn("[thread-chat] 拉取树列表失败:", err) - return [] - } -} - -/** PATCH 重命名(只写 custom_title):失败抛错——调用方做乐观更新回滚 + toast */ -export async function renameTree(id: string, title: string): Promise { - const res = await fetchWithAuth(`/api/branch-trees/${id}`, { - method: "PATCH", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ title }), - }) - if (!res.ok) throw new Error(`PATCH /api/branch-trees ${res.status}`) -} - -/** DELETE 树(幂等):失败抛错——调用方保留条目 + toast */ -export async function deleteTree(id: string): Promise { - return enqueueTreeWrite(id, async () => { - const res = await fetchWithAuth(`/api/branch-trees/${id}`, { - method: "DELETE", - }) - if (!res.ok) throw new Error(`DELETE /api/branch-trees ${res.status}`) - }) -} - -/** 删除某棵树后的本地善后(design D4):清工作台记忆;「最近一棵」若指向它则清除 */ -export function cleanupAfterTreeDelete(id: string): void { - revisionByTreeId.delete(id) - try { - localStorage.removeItem(uiKeyOf(id)) - if (localStorage.getItem(LAST_TREE_ID_KEY) === id) - localStorage.removeItem(LAST_TREE_ID_KEY) - } catch { - /* localStorage 不可用:孤儿键无伤,忽略 */ - } -} - -/** - * 派生树标题:优先使用主线成功生成的完整语义标题;未生成或生成失败时回退到首条 - * user 消息的前 TREE_TITLE_MAX_LEN 字,无消息则使用兜底文案。 - */ -export function deriveTreeTitle(state: ThreadTreeState): string { - const main = state.threads.main - if (main?.titleGenerated && main.title.trim()) return main.title.trim() - const firstUser = main?.messages.find((m) => m.role === "user") - const text = firstUser?.text.trim() - return text ? text.slice(0, TREE_TITLE_MAX_LEN) : TREE_TITLE_FALLBACK -} - -/* ---------------- 每棵树的工作台状态(localStorage,按 treeId 分键) ---------------- */ - -/** 视图形态:列(并排深读)| 画布(纵览全树)。与 thread-chat-demo 共用 */ -export type ViewMode = "columns" | "canvas" - -/** 一棵树的工作台状态:随树恢复的「桌面摆法」,不进 DB 的对话数据 */ -export interface TreeUiState { - /** 打开的分支列及折叠态(不含主线) */ - slots: Slot[] - /** 显式列宽(threadId → px),无条目的列自动均分 */ - widths: Record - /** 列数覆盖(null = 自适应) */ - forceCols: number | null - /** 列满放置策略 */ - mode: PlacementMode - /** 列 / 画布视图 */ - viewMode: ViewMode -} - -const uiKeyOf = (treeId: string) => `${TREE_UI_KEY_PREFIX}${treeId}` - -/** 存工作台状态(调用方负责防抖);写失败无伤,静默 */ -export function saveUiState(treeId: string, ui: TreeUiState): void { - try { - localStorage.setItem(uiKeyOf(treeId), JSON.stringify(ui)) - } catch { - /* 忽略:布局记忆丢失无伤,重摆即可 */ - } -} - -/** - * 读工作台状态并对照加载回来的树数据校验: - * slots / widths 里引用的 threadId 必须仍存在(失配过滤);字段形状不对整体作废。 - * 返回 null = 无可用记忆,回默认布局(只开主线)。 - */ -export function loadUiState( - treeId: string, - state: ThreadTreeState -): TreeUiState | null { - let raw: string | null - try { - raw = localStorage.getItem(uiKeyOf(treeId)) - } catch { - return null - } - if (!raw) return null - try { - const parsed = JSON.parse(raw) as Partial - const slots: Slot[] = Array.isArray(parsed.slots) - ? parsed.slots - .filter( - (s): s is Slot => - !!s && - typeof s.id === "string" && - typeof s.folded === "boolean" && - s.id !== "main" && - state.threads[s.id] !== undefined - ) - .map((s) => ({ id: s.id, folded: s.folded })) - : [] - const widths: Record = {} - if (parsed.widths && typeof parsed.widths === "object") { - for (const [id, w] of Object.entries(parsed.widths)) { - if (typeof w === "number" && Number.isFinite(w) && state.threads[id]) - widths[id] = w - } - } - return { - slots, - widths, - forceCols: typeof parsed.forceCols === "number" ? parsed.forceCols : null, - mode: parsed.mode === "fold" ? "fold" : "replace", - viewMode: parsed.viewMode === "canvas" ? "canvas" : "columns", - } - } catch { - return null // 记忆损坏:整体作废,回默认布局 - } -} diff --git a/app/thread-chat/net/persistence/sanitize-loaded-state.ts b/app/thread-chat/net/persistence/sanitize-loaded-state.ts deleted file mode 100644 index 5fd7970a..00000000 --- a/app/thread-chat/net/persistence/sanitize-loaded-state.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { Message, Thread, ThreadTreeState } from "../../core/types" -import { parseThreadTreeState } from "../../core/message-graph" -import { GENERATION_ERRORS } from "@/constants/generation" -import { - isActiveGenerationStatus, - type GenerationSummary, -} from "../../generation/types" - -/** - * 纯函数:防御性收敛流式残留并校验 Artifact 三方关系。 - * 没有匹配 active generation 的中断 assistant 一律保留内容并转为可重试 error; - * 绝不把部分正文或 Artifact 猜成完整回复。 - */ -export function sanitizeLoadedState( - inputState: ThreadTreeState, - resolveModelId: (modelId: string | undefined) => string, - activeGenerations: readonly Pick< - GenerationSummary, - "id" | "threadId" | "assistantMessageId" | "status" - >[] = [] -): ThreadTreeState { - const state = parseThreadTreeState(inputState) - let changed = false - const activeByMessage = new Map( - activeGenerations - .filter((generation) => isActiveGenerationStatus(generation.status)) - .map((generation) => [ - `${generation.threadId}:${generation.assistantMessageId}`, - generation.id, - ]) - ) - const threads: Record = {} - const referencedArtifactIds = new Set() - - for (const [id, thread] of Object.entries(state.threads)) { - let threadChanged = false - const messages: Message[] = [] - const modelId = resolveModelId(thread.modelId) - threadChanged ||= modelId !== thread.modelId - - for (const message of thread.messages) { - const validArtifactIds = - message.role === "assistant" - ? [...new Set(message.artifactIds ?? [])].filter((artifactId) => { - const artifact = state.artifacts[artifactId] - return ( - artifact?.sourceThreadId === id && - artifact.sourceMessageId === message.id - ) - }) - : [] - const hadArtifactIds = (message.artifactIds?.length ?? 0) > 0 - const artifactRefsChanged = - hadArtifactIds !== validArtifactIds.length > 0 || - (message.artifactIds?.length ?? 0) !== validArtifactIds.length || - validArtifactIds.some( - (artifactId, index) => message.artifactIds?.[index] !== artifactId - ) - const hasTransientGeneration = message.markdownGeneration !== undefined - let nextMessage = - artifactRefsChanged || hasTransientGeneration - ? { - ...message, - artifactIds: validArtifactIds.length - ? validArtifactIds - : undefined, - markdownGeneration: undefined, - } - : message - - if ( - nextMessage.role === "assistant" && - (nextMessage.status === "pending" || nextMessage.status === "streaming") - ) { - const activeGenerationId = activeByMessage.get( - `${id}:${nextMessage.id}` - ) - if (activeGenerationId) { - if ( - nextMessage.generationId !== activeGenerationId || - nextMessage.backgroundGeneration !== true - ) { - nextMessage = { - ...nextMessage, - generationId: activeGenerationId, - backgroundGeneration: true, - } - threadChanged = true - } - messages.push(nextMessage) - validArtifactIds.forEach((artifactId) => - referencedArtifactIds.add(artifactId) - ) - } else { - threadChanged = true - nextMessage = { - ...nextMessage, - status: "error", - error: GENERATION_ERRORS.backgroundInterrupted, - backgroundGeneration: undefined, - } - messages.push(nextMessage) - validArtifactIds.forEach((artifactId) => - referencedArtifactIds.add(artifactId) - ) - } - } else { - messages.push(nextMessage) - validArtifactIds.forEach((artifactId) => - referencedArtifactIds.add(artifactId) - ) - } - threadChanged ||= artifactRefsChanged || hasTransientGeneration - } - - threads[id] = threadChanged ? { ...thread, modelId, messages } : thread - changed ||= threadChanged - } - - const artifacts = Object.fromEntries( - Object.entries(state.artifacts).filter(([artifactId]) => - referencedArtifactIds.has(artifactId) - ) - ) - if (Object.keys(artifacts).length !== Object.keys(state.artifacts).length) - changed = true - - const orderedIds = new Set() - const artifactOrder = state.artifactOrder.filter((artifactId) => { - if (!artifacts[artifactId] || orderedIds.has(artifactId)) return false - orderedIds.add(artifactId) - return true - }) - referencedArtifactIds.forEach((artifactId) => { - if (artifacts[artifactId] && !orderedIds.has(artifactId)) { - orderedIds.add(artifactId) - artifactOrder.push(artifactId) - } - }) - if ( - artifactOrder.length !== state.artifactOrder.length || - artifactOrder.some( - (artifactId, index) => artifactId !== state.artifactOrder[index] - ) - ) - changed = true - - return changed ? { ...state, threads, artifacts, artifactOrder } : state -} diff --git a/app/thread-chat/net/persistence/save-tree-response.ts b/app/thread-chat/net/persistence/save-tree-response.ts deleted file mode 100644 index 8d3a71a9..00000000 --- a/app/thread-chat/net/persistence/save-tree-response.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - saveTreeErrorResponseSchema, - saveTreeSuccessResponseSchema, - type TreeWriteRevisionErrorCode, -} from "@/lib/thread-chat/contracts/save-tree" - -export class TreeRevisionError extends Error { - constructor( - readonly code: TreeWriteRevisionErrorCode, - readonly currentRevision?: number - ) { - super( - code === "tree_revision_conflict" - ? "该对话已在其他页面更新" - : "当前页面缺少树修订号" - ) - this.name = "TreeRevisionError" - } -} - -/** 解释整树 PUT 响应;无效的 2xx body 保持历史语义,不推进本地 revision。 */ -export async function readSaveTreeRevision( - response: Response -): Promise { - const body = await response.json().catch(() => null) - const failure = saveTreeErrorResponseSchema.safeParse(body) - - if (response.status === 409) - throw new TreeRevisionError( - "tree_revision_conflict", - failure.success && failure.data.error.code === "tree_revision_conflict" - ? failure.data.error.currentRevision - : undefined - ) - if (response.status === 428) throw new TreeRevisionError("revision_required") - if (!response.ok) throw new Error(`PUT /api/branch-trees ${response.status}`) - - const success = saveTreeSuccessResponseSchema.safeParse(body) - return success.success ? success.data.revision : null -} diff --git a/app/thread-chat/net/persistence/transient-state.ts b/app/thread-chat/net/persistence/transient-state.ts deleted file mode 100644 index 9adacaf1..00000000 --- a/app/thread-chat/net/persistence/transient-state.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Message, Thread, ThreadTreeState } from "../../core/types" - -/** - * 防止 Markdown 半成品进度进入 branch_trees.state。仅在确有临时字段时浅克隆, - * 正常完成态保持原对象,避免每次存盘无意义复制整棵树。 - */ -export function withoutTransientGenerationState( - state: ThreadTreeState -): ThreadTreeState { - let stateChanged = false - const threads: Record = {} - - for (const [threadId, thread] of Object.entries(state.threads)) { - let threadChanged = false - const messages: Message[] = thread.messages.map((message) => { - if (message.markdownGeneration === undefined) return message - threadChanged = true - const persisted = { ...message } - delete persisted.markdownGeneration - return persisted - }) - threads[threadId] = threadChanged ? { ...thread, messages } : thread - stateChanged ||= threadChanged - } - - return stateChanged ? { ...state, threads } : state -} diff --git a/app/thread-chat/net/persistence/tree-save-gate.ts b/app/thread-chat/net/persistence/tree-save-gate.ts deleted file mode 100644 index 534954a4..00000000 --- a/app/thread-chat/net/persistence/tree-save-gate.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface TreeSaveGate { - markPending(): void - finishDebounce(): boolean - takePendingFlush(): boolean - setSuppressed(value: boolean): void - isSuppressed(): boolean -} - -export function createTreeSaveGate(): TreeSaveGate { - let pending = false - let suppressed = false - - return { - markPending() { - pending = true - }, - finishDebounce() { - pending = false - return !suppressed - }, - takePendingFlush() { - if (!pending || suppressed) return false - pending = false - return true - }, - setSuppressed(value) { - suppressed = value - if (value) pending = false - }, - isSuppressed() { - return suppressed - }, - } -} diff --git a/app/thread-chat/net/persistence/use-tree-persistence.ts b/app/thread-chat/net/persistence/use-tree-persistence.ts deleted file mode 100644 index 7db2f92e..00000000 --- a/app/thread-chat/net/persistence/use-tree-persistence.ts +++ /dev/null @@ -1,61 +0,0 @@ -"use client" - -import { useCallback, useEffect, useRef } from "react" -import { TREE_SAVE_DEBOUNCE_MS } from "@/constants/thread-chat" -import type { ThreadStore } from "../../core/store" -import { deriveTreeTitle, saveTree } from "./persist" -import { createTreeSaveGate } from "./tree-save-gate" - -export function useTreePersistence({ - treeId, - store, - version, - onRevisionConflict, -}: { - treeId: string - store: ThreadStore - version: number - onRevisionConflict(): void -}) { - const initialVersionRef = useRef(version) - const gateRef = useRef(createTreeSaveGate()) - const onRevisionConflictRef = useRef(onRevisionConflict) - - useEffect(() => { - onRevisionConflictRef.current = onRevisionConflict - }, [onRevisionConflict]) - - useEffect(() => { - if (version === initialVersionRef.current) return - gateRef.current.markPending() - const timer = setTimeout(() => { - if (!gateRef.current.finishDebounce()) return - const state = store.getState() - void saveTree(treeId, state, deriveTreeTitle(state), () => { - onRevisionConflictRef.current() - }) - }, TREE_SAVE_DEBOUNCE_MS) - return () => clearTimeout(timer) - }, [version, treeId, store]) - - useEffect( - () => () => { - if (!gateRef.current.takePendingFlush()) return - const state = store.getState() - void saveTree(treeId, state, deriveTreeTitle(state), () => { - window.location.reload() - }) - }, - [treeId, store] - ) - - const setTreeSaveSuppressed = useCallback((value: boolean) => { - gateRef.current.setSuppressed(value) - }, []) - const isTreeSaveSuppressed = useCallback( - () => gateRef.current.isSuppressed(), - [] - ) - - return { setTreeSaveSuppressed, isTreeSaveSuppressed } -} diff --git a/app/thread-chat/net/prompt/message-context.ts b/app/thread-chat/net/prompt/message-context.ts deleted file mode 100644 index b80faa89..00000000 --- a/app/thread-chat/net/prompt/message-context.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:Thread Chat 模型上下文编译位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/compile-thread-chat-messages" diff --git a/app/thread-chat/net/prompt/message-serialization.ts b/app/thread-chat/net/prompt/message-serialization.ts deleted file mode 100644 index aa463908..00000000 --- a/app/thread-chat/net/prompt/message-serialization.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:模型上下文消息序列化位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/serialize-message-for-model" diff --git a/app/thread-chat/net/prompt/prompt-pure.ts b/app/thread-chat/net/prompt/prompt-pure.ts deleted file mode 100644 index 8d0177c1..00000000 --- a/app/thread-chat/net/prompt/prompt-pure.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 兼容入口:Thread Chat prompt policy 位于 lib/thread-chat/application。 - */ -export * from "@/lib/thread-chat/application/prompt-policy" diff --git a/app/thread-chat/net/prompt/prompt.ts b/app/thread-chat/net/prompt/prompt.ts deleted file mode 100644 index 5b80995a..00000000 --- a/app/thread-chat/net/prompt/prompt.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * net/prompt —— 把会话树状态编译成发给 /api/chat 的请求体(纯函数,无副作用)。 - * - * 消息序: - * 1. collectInherited:沿 lineage 向上继承的上文(映射为 user/assistant), - * 受 INHERITED_CHAR_BUDGET 字符预算约束(D8,超预算从最旧丢弃并插省略说明)。 - * 2. 当前会话已有消息(排除本次流式占位、error 消息、空正文的 assistant 消息)。 - * 分支的首问不在这里合成:留空开分支时壳层把 kickoffQuestion 预填进 composer、 - * 用户回车确认后作为真实 user 消息进入 store;气泡带问开分支时壳层 fork 后直接 - * chat.send——两条路径的首问都随消息列表自然进入 payload。 - * - * system 归服务端所有:AI SDK v7 的 streamText 不允许 messages 里出现 system 角色 - * (安全默认值,防客户端注入任意 system 指令)。因此客户端只在请求体上带 - * `threadChat: { anchorText }` 模式标记,结构化风格段与分支焦点段由 - * /api/chat 服务端的 buildThreadChatSystem(lib/chat/thread-chat-prompt.ts)统一构造。 - * - * 关于类型:这里自定义了轻量的 UIMessageLike,而不 `import type { UIMessage } from "ai"`。 - * 理由——请求体只是一段 JSON,字段校验发生在服务端的 convertToModelMessages; - * 客户端只需构造出结构匹配的对象即可,自定义最小类型既能保持 demo「零外部 import」 - * 的风格,也避免把 ai 的 UIMessage 泛型(带 metadata/dataParts 等)拖进客户端类型面。 - */ - -import type { Thread, ThreadTreeState } from "../../core/types" -import { kickoffQuestion } from "./prompt-pure" -import { serializeMessageForModel } from "./message-serialization" -import type { ThreadChatGenerationIntent } from "../../generation/types" -import type { ThreadChatGenerationIdentity } from "@/lib/thread-chat/contracts/generation-identity" -import { - compileThreadChatMessages, - type UIMessageLike, -} from "./message-context" - -// kickoff 文案模板定义在叶子模块 prompt-pure.ts(e2e 需 node 直载),这里保持原导入面 -export { kickoffQuestion } -export { serializeMessageForModel } - -/** 发给 /api/chat 的最小消息形状(结构匹配 ai 的 UIMessage,仅用纯文本 part) */ -export type { UIMessageLike } from "./message-context" - -/** /api/chat 的 thread-chat 模式请求体 */ -export interface ThreadChatRequestBody { - messages: UIMessageLike[] - /** 当前 Thread 拥有的模型;服务端仍会按统一注册表严格校验。 */ - modelId: string - /** 模式标记:服务端据此构造纯文本 system(anchorText 非空时追加分支焦点段) */ - threadChat: ThreadChatGenerationIdentity -} - -/** - * 组装本次请求的完整 body(messages + threadChat 模式标记)。 - * 主线 anchorText 为 null 时也照发 threadChat 字段——有意为之: - * 让主线同样吃到服务端的结构化风格 system,只是不带分支焦点段。 - * @param excludeMsgId 本次流式回复的占位消息 id(当前 pending/streaming 的空 assistant),需排除 - */ -export function buildRequestBody( - state: ThreadTreeState, - thread: Thread, - excludeMsgId: string, - identity: { - treeId: string - userMessageId: string - generationId: string - intent: ThreadChatGenerationIntent - } -): ThreadChatRequestBody { - const anchor = thread.anchorText?.trim() ? thread.anchorText : null - const messages = compileThreadChatMessages({ - state, - threadId: thread.id, - excludeAssistantMessageId: excludeMsgId, - }) - - return { - messages, - modelId: thread.modelId, - threadChat: { - anchorText: anchor, - treeId: identity.treeId, - threadId: thread.id, - userMessageId: identity.userMessageId, - assistantMessageId: excludeMsgId, - generationId: identity.generationId, - intent: identity.intent, - }, - } -} diff --git a/app/thread-chat/net/stream/assistant-delta-buffer.ts b/app/thread-chat/net/stream/assistant-delta-buffer.ts deleted file mode 100644 index 4b356864..00000000 --- a/app/thread-chat/net/stream/assistant-delta-buffer.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { ThreadStore } from "../../core/store" - -/** 页面不可见 / 无 requestAnimationFrame 时的降级刷新间隔(毫秒)。 */ -const FALLBACK_FLUSH_MS = 50 - -type AssistantDeltaBufferInput = { - store: ThreadStore - threadId: string - messageId: string - isOwner(): boolean -} - -/** 将高频正文 delta 与 Markdown 进度合并为每帧至多一次 store 更新。 */ -export function createAssistantDeltaBuffer({ - store, - threadId, - messageId, - isOwner, -}: AssistantDeltaBufferInput) { - let pendingText = "" - let pendingMarkdownProgress: - Parameters[2] | null = null - let frame: number | null = null - let usingAnimationFrame = false - - const flush = () => { - if (!pendingText && !pendingMarkdownProgress) return - if (!isOwner()) { - pendingText = "" - pendingMarkdownProgress = null - return - } - if (pendingText) { - const delta = pendingText - pendingText = "" - store.appendAssistantDelta(threadId, messageId, delta) - } - if (pendingMarkdownProgress) { - const progress = pendingMarkdownProgress - pendingMarkdownProgress = null - store.setMarkdownGenerationProgress(threadId, messageId, progress) - } - } - - const onFrame = () => { - frame = null - flush() - } - const canUseAnimationFrame = () => - typeof requestAnimationFrame !== "undefined" && - !(typeof document !== "undefined" && document.hidden) - const schedule = () => { - if (frame !== null) return - if (canUseAnimationFrame()) { - usingAnimationFrame = true - frame = requestAnimationFrame(onFrame) - } else { - usingAnimationFrame = false - frame = setTimeout(onFrame, FALLBACK_FLUSH_MS) as unknown as number - } - } - - return { - appendText(delta: string) { - pendingText += delta - schedule() - }, - - setMarkdownProgress( - progress: Parameters[2] - ) { - if (progress.phase === "starting") { - pendingMarkdownProgress = null - store.setMarkdownGenerationProgress(threadId, messageId, progress) - return - } - pendingMarkdownProgress = progress - schedule() - }, - - clearMarkdownProgress() { - pendingMarkdownProgress = null - }, - - flush, - - cancel() { - if (frame === null) return - if (usingAnimationFrame) cancelAnimationFrame(frame) - else clearTimeout(frame) - frame = null - }, - } -} diff --git a/app/thread-chat/net/stream/assistant-output.ts b/app/thread-chat/net/stream/assistant-output.ts deleted file mode 100644 index c71d8dfb..00000000 --- a/app/thread-chat/net/stream/assistant-output.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface AssistantOutputProgress { - receivedTextChars: number - attachedArtifactCount: number -} - -/** 正文与已原子绑定的 Artifact 都属于可保留的 assistant 输出。 */ -export function hasAssistantOutput(progress: AssistantOutputProgress): boolean { - return progress.receivedTextChars > 0 || progress.attachedArtifactCount > 0 -} diff --git a/app/thread-chat/net/stream/assistant-stream-runtime.ts b/app/thread-chat/net/stream/assistant-stream-runtime.ts deleted file mode 100644 index cb8d8da0..00000000 --- a/app/thread-chat/net/stream/assistant-stream-runtime.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { ThreadStore } from "../../core/store" -import type { ArtifactSeed } from "../../core/types" -import { hasAssistantOutput } from "./assistant-output" -import { createAssistantDeltaBuffer } from "./assistant-delta-buffer" -import type { UIStreamHandlers } from "./ui-stream" - -/** 流正常结束但正文和 Artifact 都为空时的可重试错误。 */ -const EMPTY_REPLY_ERROR = "未收到任何回复,请重试" -/** 用户停止或本地 detach 后统一使用的错误终态。 */ -export const ABORTED_ERROR = "已停止生成" - -type AssistantStreamRuntimeInput = { - store: ThreadStore - threadId: string - messageId: string - isOwner(): boolean -} - -/** 组合 delta buffer、SSE handlers 与一次性终态裁决。 */ -export function createAssistantStreamRuntime({ - store, - threadId, - messageId, - isOwner, -}: AssistantStreamRuntimeInput) { - const deltaBuffer = createAssistantDeltaBuffer({ - store, - threadId, - messageId, - isOwner, - }) - let settled = false - let lastError: string | null = null - let receivedChars = 0 - let attachedArtifactCount = 0 - - const settle = (apply: () => void) => { - if (settled) return - settled = true - deltaBuffer.cancel() - if (!isOwner()) return - deltaBuffer.flush() - apply() - } - - const settleByOutcome = () => { - settle(() => { - if ( - hasAssistantOutput({ - receivedTextChars: receivedChars, - attachedArtifactCount, - }) - ) { - if (lastError !== null) { - console.warn( - "[thread-chat] 流中出现瞬时 error chunk(已忽略):", - lastError - ) - } - store.finishAssistantMessage(threadId, messageId) - } else if (lastError !== null) { - store.failAssistantMessage(threadId, messageId, lastError) - } else { - store.failAssistantMessage(threadId, messageId, EMPTY_REPLY_ERROR) - } - }) - } - - const settleByAbort = () => { - settle(() => store.failAssistantMessage(threadId, messageId, ABORTED_ERROR)) - } - - const handlers: UIStreamHandlers = { - onTextDelta(delta) { - if (settled) return - receivedChars += delta.replace(/\s/g, "").length - deltaBuffer.appendText(delta) - }, - onMarkdownArtifactProgress(event) { - if (settled || !isOwner()) return - deltaBuffer.setMarkdownProgress(event) - }, - onMarkdownArtifact(event) { - if (settled || !isOwner()) return - deltaBuffer.clearMarkdownProgress() - const seed: ArtifactSeed = { - kind: "markdown", - title: event.input.title, - content: event.input.content, - } - if (store.attachArtifactToMessage(threadId, messageId, seed) !== null) { - attachedArtifactCount++ - } - }, - onWebResearchActivity(activity) { - if (settled || !isOwner()) return - deltaBuffer.flush() - store.setWebResearchActivity(threadId, messageId, activity) - }, - onResearchRoute(route) { - if (settled || !isOwner()) return - store.setResearchRoute(threadId, messageId, route) - }, - onResearchPlan(plan) { - if (settled || !isOwner()) return - store.setResearchPlan(threadId, messageId, plan) - }, - onError(message) { - if (settled) return - lastError = message - }, - onFinish() { - settleByOutcome() - }, - } - - return { - handlers, - settleByOutcome, - settleByAbort, - fail(message: string) { - settle(() => store.failAssistantMessage(threadId, messageId, message)) - }, - cancel() { - deltaBuffer.cancel() - }, - } -} diff --git a/app/thread-chat/net/stream/local-generation-executions.ts b/app/thread-chat/net/stream/local-generation-executions.ts deleted file mode 100644 index ea915ddb..00000000 --- a/app/thread-chat/net/stream/local-generation-executions.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** 本页正在消费的 generation 流注册表:按 thread 互斥,并提供 generation 只读查询。 */ -export function createLocalGenerationExecutions() { - const byThread = new Map< - string, - { generationId: string; controller: AbortController } - >() - const activeGenerationIds = new Set() - - function begin(threadId: string, generationId: string) { - const controller = new AbortController() - const previous = byThread.get(threadId) - if (previous) activeGenerationIds.delete(previous.generationId) - byThread.set(threadId, { generationId, controller }) - activeGenerationIds.add(generationId) - return { - controller, - isOwner: () => byThread.get(threadId)?.controller === controller, - } - } - - function clearIfOwner(threadId: string, controller: AbortController): void { - const current = byThread.get(threadId) - if (current?.controller === controller) { - byThread.delete(threadId) - activeGenerationIds.delete(current.generationId) - } - } - - function detach(threadId: string): void { - byThread.get(threadId)?.controller.abort() - } - - return { - begin, - clearIfOwner, - detach, - hasThread: (threadId: string) => byThread.has(threadId), - isGenerationActive: (generationId: string) => - activeGenerationIds.has(generationId), - detachAll: () => - byThread.forEach((execution) => execution.controller.abort()), - } -} diff --git a/app/thread-chat/net/stream/ui-stream.ts b/app/thread-chat/net/stream/ui-stream.ts deleted file mode 100644 index df566887..00000000 --- a/app/thread-chat/net/stream/ui-stream.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * net/ui-stream —— AI SDK v7「UI Message Stream」的纯 TS 消费器(无 React、无 DOM 依赖)。 - * - * 服务端 `toUIMessageStreamResponse()` 以 SSE 输出:每个事件是一行 - * `data: ${JSON.stringify(chunk)}`,事件之间用空行(\n\n)分隔,整条流以 - * `data: [DONE]` 收尾。本模块把这条字节流切成一个个 chunk,按类型分派: - * - text-delta → onTextDelta(chunk.delta)(正文增量) - * - error → onError(chunk.errorText) - * - finish / 流自然结束 → onFinish(只回调一次) - * - tool-input-start(createMarkdownArtifact) → 立即发出不可点击的生成占位态 - * - tool-input-delta → 解析局部 JSON,发出标题/字符/行数/章节进度 - * - tool-input-available(createMarkdownArtifact) → onMarkdownArtifact - * - webSearch / readUrl 的 tool-* → onWebResearchActivity(聚合活动与来源) - * - reasoning-* / 其它 tool-* / start / 其它未知类型 → 静默跳过 - * (MiniMax 的 已被服务端 extractReasoningMiddleware 抽成 reasoning-* chunk, - * 本 demo 只渲染「思考中…」指示器,不展示 reasoning 内容,故这里丢弃。) - * - * signal 被 abort 时静默返回(不外抛 AbortError),已收到的文本由上层保留。 - */ - -import { - createMarkdownArtifactEventDispatcher, - createMarkdownArtifactProgressDispatcher, - type MarkdownArtifactProgressEvent, - type MarkdownArtifactStreamEvent, -} from "../../../../lib/chat/markdown-artifact" -import { - createWebResearchActivityDispatcher, - type WebResearchActivity, -} from "../../../../lib/chat/web-research-activity" -import { - isResearchPlanStreamEvent, - isResearchRouteStreamEvent, -} from "../../../../lib/chat/research-events" -import type { - ResearchPlan, - ResearchRoute, -} from "../../../../lib/chat/research-router" - -export type { - MarkdownArtifactStreamEvent, - MarkdownArtifactProgressEvent, - ToolInputDeltaChunk, - ToolInputStartChunk, - ToolInputAvailableChunk, -} from "../../../../lib/chat/markdown-artifact" - -export interface UIStreamHandlers { - /** 收到一段正文增量(text-delta.delta) */ - onTextDelta(delta: string): void - /** 收到完整且已校验、响应内去重后的 Markdown Artifact 工具输入 */ - onMarkdownArtifact(event: MarkdownArtifactStreamEvent): void - /** Markdown 工具开始或参数增量解析后的临时进度(不持久化) */ - onMarkdownArtifactProgress(event: MarkdownArtifactProgressEvent): void - /** 联网搜索/深读调用的聚合状态与来源结果。 */ - onWebResearchActivity(activity: WebResearchActivity): void - /** 后端编排器给出的结构化联网路由。 */ - onResearchRoute(route: ResearchRoute): void - /** 复杂研究的结构化计划摘要。 */ - onResearchPlan(plan: ResearchPlan): void - /** 收到 error chunk(errorText 缺失时给出兜底文案) */ - onError(message: string): void - /** finish chunk 或流自然结束时回调;实现内部保证只触发一次 */ - onFinish(): void -} - -/** 判断是否为「中止」类异常(fetch/reader 在 abort 时抛出) */ -function isAbortError(err: unknown): boolean { - return err instanceof DOMException - ? err.name === "AbortError" - : typeof err === "object" && - err !== null && - (err as { name?: string }).name === "AbortError" -} - -export async function consumeUIMessageStream( - res: Response, - handlers: UIStreamHandlers, - signal: AbortSignal -): Promise { - const body = res.body - if (!body) return - - const reader = body.getReader() - const decoder = new TextDecoder() - let buffer = "" - let finished = false - const dispatchMarkdownArtifact = createMarkdownArtifactEventDispatcher( - handlers.onMarkdownArtifact - ) - const dispatchMarkdownArtifactProgress = - createMarkdownArtifactProgressDispatcher( - handlers.onMarkdownArtifactProgress - ) - const dispatchWebResearchActivity = createWebResearchActivityDispatcher( - handlers.onWebResearchActivity - ) - - // onFinish 只回调一次(finish chunk 与「流自然结束」可能都想触发) - const emitFinish = () => { - if (finished) return - finished = true - handlers.onFinish() - } - - /** 处理一个 SSE 事件文本;返回 true 表示遇到 [DONE],应终止整条流 */ - const handleEvent = async (rawEvent: string): Promise => { - // 一个事件可能包含多行;SSE 规范里同一事件的多个 data: 行以 \n 拼接 - const dataLines: string[] = [] - for (const line of rawEvent.split("\n")) { - if (!line.startsWith("data:")) continue // 注释行(:...)、event:/id: 等一律忽略 - let d = line.slice(5) - if (d.startsWith(" ")) d = d.slice(1) // 去掉 "data:" 后的单个前导空格 - dataLines.push(d) - } - if (dataLines.length === 0) return false - - const payload = dataLines.join("\n") - if (payload === "[DONE]") return true - - let chunk: unknown - try { - chunk = JSON.parse(payload) - } catch { - return false // 半个/损坏的 JSON:跳过(跨 chunk 的半行由 buffer 兜住,正常不会到这) - } - - if (await dispatchMarkdownArtifactProgress(chunk)) return false - if (dispatchMarkdownArtifact(chunk)) return false - if (dispatchWebResearchActivity(chunk)) return false - if (isResearchRouteStreamEvent(chunk)) { - handlers.onResearchRoute(chunk.data) - return false - } - if (isResearchPlanStreamEvent(chunk)) { - handlers.onResearchPlan(chunk.data) - return false - } - - if (typeof chunk !== "object" || chunk === null) return false - const value = chunk as { - type?: string - delta?: unknown - errorText?: unknown - } - switch (value.type) { - case "text-delta": - if (typeof value.delta === "string") handlers.onTextDelta(value.delta) - break - case "error": - handlers.onError( - typeof value.errorText === "string" && value.errorText - ? value.errorText - : "流式响应发生错误" - ) - break - case "finish": - emitFinish() - break - default: - break // reasoning-* / 非联网 tool-* / start / text-start / text-end / 未知类型:静默跳过 - } - return false - } - - try { - let done = false - while (!done) { - if (signal.aborted) return // 中止:静默返回,保留已收文本 - const { done: streamDone, value } = await reader.read() - if (streamDone) break - buffer += decoder.decode(value, { stream: true }) - // 按空行切出完整事件,最后半个事件留在 buffer 里等下一片 - let sep: number - while ((sep = buffer.indexOf("\n\n")) !== -1) { - const rawEvent = buffer.slice(0, sep) - buffer = buffer.slice(sep + 2) - if (await handleEvent(rawEvent)) { - done = true // 遇到 [DONE] - break - } - } - } - // 流自然结束:处理可能残留的、无末尾空行的最后一个事件 - if (!signal.aborted && buffer.trim().length > 0) await handleEvent(buffer) - } catch (err) { - if (signal.aborted || isAbortError(err)) return // 中止:静默 - throw err - } finally { - try { - reader.releaseLock() - } catch { - // 忽略:reader 可能已随流关闭 - } - } - - if (!signal.aborted) emitFinish() // 流自然结束但没有 finish chunk 时兜底 -} diff --git a/app/thread-chat/net/titles/thread-title-candidate.ts b/app/thread-chat/net/titles/thread-title-candidate.ts deleted file mode 100644 index 9552a473..00000000 --- a/app/thread-chat/net/titles/thread-title-candidate.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { hasRenderableAssistantOutput } from "../../core/selectors" -import { defaultBranchTitle } from "../../core/store" -import type { Thread, ThreadTreeState } from "../../core/types" -import { serializeMessageForModel } from "../prompt/message-serialization" -import type { ThreadTitleInput } from "./thread-title" - -export interface ThreadTitleCandidate { - threadId: string - input: ThreadTitleInput -} - -function mainTitleCandidate(thread: Thread): ThreadTitleCandidate | null { - if (thread.id !== "main" || thread.titleGenerationAttempted) return null - const firstUserMessage = thread.messages.find( - (message) => message.role === "user" && message.text.trim() - ) - if (!firstUserMessage) return null - return { - threadId: thread.id, - input: { kind: "main", question: firstUserMessage.text }, - } -} - -function branchTitleCandidate( - state: ThreadTreeState, - thread: Thread -): ThreadTitleCandidate | null { - if (!thread.parentId || !thread.anchorText) return null - if (thread.titleGenerationAttempted) return null - if (thread.title !== defaultBranchTitle(thread.anchorText)) return null - - const question = thread.messages.find((message) => message.role === "user") - const answer = thread.messages.find( - (message) => - message.role === "assistant" && - message.status === "done" && - hasRenderableAssistantOutput(state, message) - ) - if (!question || !answer) return null - - return { - threadId: thread.id, - input: { - kind: "branch", - anchorText: thread.anchorText, - question: question.text, - answer: serializeMessageForModel(state, answer) ?? answer.text, - }, - } -} - -export function threadTitleCandidate( - state: ThreadTreeState, - thread: Thread -): ThreadTitleCandidate | null { - return thread.id === "main" - ? mainTitleCandidate(thread) - : branchTitleCandidate(state, thread) -} diff --git a/app/thread-chat/net/titles/thread-title.ts b/app/thread-chat/net/titles/thread-title.ts deleted file mode 100644 index 9ff8987b..00000000 --- a/app/thread-chat/net/titles/thread-title.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 会话标题生成的客户端请求。服务端保留完整标题;展示层按自己的布局决定是否省略。 - */ -import type { ThreadTitleInput } from "@/lib/thread-chat/contracts/title-request" - -export type { ThreadTitleInput } from "@/lib/thread-chat/contracts/title-request" - -/** - * 请求一次主线或分支标题生成。模型不可用或未生成有效标题时返回 null; - * 调用方保留相应的回退标题即可。 - */ -export async function requestThreadTitle( - input: ThreadTitleInput -): Promise { - const res = await fetch("/api/title", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(input), - }) - if (!res.ok) throw new Error(`POST /api/title ${res.status}`) - const data = (await res.json()) as { title?: string | null } - const title = typeof data.title === "string" ? data.title.trim() : "" - return title || null -} diff --git a/app/thread-chat/net/titles/use-thread-titles.ts b/app/thread-chat/net/titles/use-thread-titles.ts deleted file mode 100644 index 6a97b672..00000000 --- a/app/thread-chat/net/titles/use-thread-titles.ts +++ /dev/null @@ -1,64 +0,0 @@ -"use client" - -import { useEffect, useRef } from "react" -import { THREAD_TITLE_ATTEMPT_STORAGE_KEY_PREFIX } from "@/constants/thread-chat" -import type { ThreadStore } from "../../core/store" -import { requestThreadTitle } from "./thread-title" -import { threadTitleCandidate } from "./thread-title-candidate" - -function attemptStorageKey(treeId: string, threadId: string): string { - return `${THREAD_TITLE_ATTEMPT_STORAGE_KEY_PREFIX}${treeId}:${threadId}` -} - -function hasAttemptInCurrentTab(treeId: string, threadId: string): boolean { - try { - return sessionStorage.getItem(attemptStorageKey(treeId, threadId)) === "1" - } catch { - return false - } -} - -function rememberAttemptInCurrentTab(treeId: string, threadId: string): void { - try { - sessionStorage.setItem(attemptStorageKey(treeId, threadId), "1") - } catch { - // sessionStorage 被禁用时,仍由持久化到树状态的标记防重。 - } -} - -/** 主线与分支都只自动生成一次标题;主线首条用户消息可立即触发。 */ -export function useThreadTitles({ - treeId, - store, - version, -}: { - treeId: string - store: ThreadStore - version: number -}) { - const requestedThreadIdsRef = useRef(new Set()) - - useEffect(() => { - const state = store.getState() - for (const thread of Object.values(state.threads)) { - if (requestedThreadIdsRef.current.has(thread.id)) continue - if (hasAttemptInCurrentTab(treeId, thread.id)) continue - const candidate = threadTitleCandidate(state, thread) - if (!candidate) continue - - if (!store.markTitleGenerationAttempted(candidate.threadId)) continue - requestedThreadIdsRef.current.add(candidate.threadId) - rememberAttemptInCurrentTab(treeId, candidate.threadId) - void requestThreadTitle(candidate.input) - .then((title) => { - if (title) store.setGeneratedThreadTitle(candidate.threadId, title) - }) - .catch((error) => { - console.warn( - "[thread-chat] 会话标题生成失败(保留回退标题):", - error - ) - }) - } - }, [treeId, version, store]) -} diff --git a/app/thread-chat/normalized/thread-chat-project.tsx b/app/thread-chat/normalized/thread-chat-project.tsx index 7e72cab5..2181c11d 100644 --- a/app/thread-chat/normalized/thread-chat-project.tsx +++ b/app/thread-chat/normalized/thread-chat-project.tsx @@ -41,7 +41,7 @@ import { ArtifactDrawer } from "../orchestration/artifacts/artifact-drawer" import { ProjectList } from "./project-list" import { projectLegacyTreeView } from "./project-view-model" import { useProjectWorkbench } from "./use-project-workbench" -import { kickoffQuestion } from "../net/prompt/prompt-pure" +import { kickoffQuestion } from "@/lib/thread-chat/application/prompt-policy" const ThreadCanvas = dynamic( () => @@ -335,8 +335,6 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { const value = runtime.store.getState().entities.feedbackByMessageId[messageId] return value?.value ? { - treeId: projectId, - threadId, messageId, feedback: value.value, updatedAt: value.updatedAt, @@ -344,7 +342,7 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { : null }, }), - [projectId, rootModelId, runtime] + [rootModelId, runtime] ) const send = useCallback( diff --git a/app/thread-chat/orchestration/canvas/canvas-actions.ts b/app/thread-chat/orchestration/canvas/canvas-actions.ts index bb07443c..ec55979e 100644 --- a/app/thread-chat/orchestration/canvas/canvas-actions.ts +++ b/app/thread-chat/orchestration/canvas/canvas-actions.ts @@ -5,7 +5,7 @@ import type { ThreadTreeState } from "../../core/types" import type { MessageActionViewState } from "../../chat/actions/message-action-types" import type { ThreadMessageActionCommands } from "../../chat/actions/message-action-commands" -/** 壳层用 chat-controller 组装后注入画布的会话动作。 */ +/** 壳层用 Application Commands 组装后注入画布的会话动作。 */ export interface CanvasChatActions extends ThreadMessageActionCommands { send: (threadId: string, text: string) => void stop: (threadId: string) => void diff --git a/app/thread-chat/orchestration/canvas/canvas-node.tsx b/app/thread-chat/orchestration/canvas/canvas-node.tsx index 4a8d406a..8735d873 100644 --- a/app/thread-chat/orchestration/canvas/canvas-node.tsx +++ b/app/thread-chat/orchestration/canvas/canvas-node.tsx @@ -12,7 +12,7 @@ * 消息渲染复用列模式全套(AnchoredAssistantBody:Markdown + SmoothText + 锚点 * 手绘 effect,D2)并挂列模式的划选 DOM 契约(.msg-list[data-list] / * .message[data-msg-id] / .bubble[data-role]),document 级划选气泡零改动生效; - * 发送 / 停止 / 重试经 CanvasActionsContext 直达壳层 chat-controller(D3); + * 发送 / 停止 / 重试经 CanvasActionsContext 直达 Application Commands; * 手势共处(D5):面板挂 nodrag/nowheel(选字不拖节点、内滚不缩放画布), * 双击 stopPropagation 不误触「回列模式」。 * diff --git a/app/thread-chat/orchestration/canvas/thread-canvas.tsx b/app/thread-chat/orchestration/canvas/thread-canvas.tsx index ce8d83d5..ccb96aba 100644 --- a/app/thread-chat/orchestration/canvas/thread-canvas.tsx +++ b/app/thread-chat/orchestration/canvas/thread-canvas.tsx @@ -8,7 +8,7 @@ * * Phase 2 节点内对话(openspec: add-canvas-conversations): * · 单击选中节点 = 展开外挂面板(canvas-node 的 CanvasExpand); - * · CanvasActionsContext 注入 send/abort/retry(壳层 chat-controller)+ 画布内 + * · CanvasActionsContext 注入 send/abort/retry(Application Commands)+ 画布内 * 聚焦(focusThread)+ 树快照读取,穿过 React Flow 直达自定义节点(D3); * · focusNode:{id,n}(壳层在画布内 fork 时置值,n 递增去重)→ selectNode + * setCenter 平滑跟随(D4);偏移按 LR 布局适配(见 focusThread 注释)。 @@ -72,7 +72,7 @@ export interface ThreadCanvasProps { onOpenThread: (threadId: string) => void /** 打开全局 Markdown 面板并选中对应交付物。 */ onOpenArtifact: (artifactId: string) => void - /** 会话动作(send/abort/retry):壳层用 chat-controller 组装(D3,同一发送链路) */ + /** 会话动作(send/abort/retry):壳层用 Application Commands 组装。 */ chat: CanvasChatActions messageActionState: MessageActionViewState /** 画布内 fork 的视口跟随指令:壳层每次 fork 置 {id, n}(n 递增去重), @@ -128,7 +128,7 @@ function CanvasFlow({ [selectNode, setCenter, getZoom] ) - /* 节点面板的动作面(D3):chat 三件套直达壳层 chat-controller; + /* 节点面板的动作面:chat 三件套直达 Application Commands; getState 供面板渲染读树快照(锚点 title 文案等) */ const actions = useMemo( () => ({ diff --git a/app/thread-chat/orchestration/canvas/use-canvas-layout.ts b/app/thread-chat/orchestration/canvas/use-canvas-layout.ts index 76ed5de8..25769194 100644 --- a/app/thread-chat/orchestration/canvas/use-canvas-layout.ts +++ b/app/thread-chat/orchestration/canvas/use-canvas-layout.ts @@ -31,7 +31,7 @@ import { } from "../../core/selectors" import type { MessageActionViewState } from "../../chat/actions/message-action-types" import { accentOf, dotColorOf, dvar } from "../../theme" -import { kickoffQuestion } from "../../net/prompt/prompt-pure" +import { kickoffQuestion } from "@/lib/thread-chat/application/prompt-policy" import type { CanvasCardData, CanvasCardNode } from "./canvas-node" import { CANVAS_CARD_ANCHOR_CHROME_HEIGHT, diff --git a/app/thread-chat/orchestration/columns/use-column-slots.ts b/app/thread-chat/orchestration/columns/use-column-slots.ts deleted file mode 100644 index f5eb0911..00000000 --- a/app/thread-chat/orchestration/columns/use-column-slots.ts +++ /dev/null @@ -1,219 +0,0 @@ -"use client" - -import { useEffect, useRef, useState } from "react" -import type { ThreadStore } from "../../core/store" -import { - normalizeForReplace, - place, - trimSlots, - type PlaceEffect, - type PlacementHint, - type PlacementMode, - type Slot, -} from "./placement" - -export interface UseColumnSlotsArgs { - store: ThreadStore - /** 展开列上限(= 总列数 - 主线一列) */ - maxExpanded: number - /** 列满策略:替换⑥ / 细条⑤ */ - mode: PlacementMode - /** 可选初始槽位(工作台记忆恢复用,调用方已校验 threadId 存在性) */ - initialSlots?: Slot[] - /** 可选初始列宽映射;不传 = 全部自动均分 */ - initialWidths?: Record -} - -/** 从宽度映射里删掉若干条目;全都不存在时保留原引用。 */ -function omitWidths( - widths: Record, - ids: readonly string[] -): Record { - if (!ids.some((id) => widths[id] !== undefined)) return widths - return Object.fromEntries( - Object.entries(widths).filter(([id]) => !ids.includes(id)) - ) -} - -/** 列槽、显式列宽与放置策略组成的视口状态能力。 */ -export function useColumnSlots({ - store, - maxExpanded, - mode, - initialSlots, - initialWidths, -}: UseColumnSlotsArgs) { - const [slots, setSlots] = useState(initialSlots ?? []) - /** 显式列宽(px,threadId → width):有值的列以 flex-basis 承载宽度,无值 = 自动均分。 - 拖拽/键盘 commit 以整行为单位落条目(fill 模型下 basis 总和==容器才无跳动), - 双击复位删除整行条目。条目跟随「槽位空间」走:替换/原地切换会话时转移给新 id; - 收起/裁掉清条目;fold/unfold 保留条目(细条固定 30px 不参与)。 */ - const [widths, setWidths] = useState>( - initialWidths ?? {} - ) - const [flash, setFlash] = useState<{ id: string; n: number } | null>(null) - const flashSequence = useRef(0) - const colsRef = useRef(null) - - // 窗口变窄 / 强制列数调小时:从左裁掉最早的槽(细条一并参与,见 trimSlots)。 - // 这是 React 官方的「渲染期间调整派生状态」写法:条件自熄,比 effect 少一轮往返。 - const effectiveSlots = trimSlots(slots, maxExpanded) - if (effectiveSlots.length !== slots.length) { - setSlots(effectiveSlots) - const kept = new Set(effectiveSlots.map((slot) => slot.id)) - const dropped = slots - .filter((slot) => !kept.has(slot.id)) - .map((slot) => slot.id) - if (dropped.length) setWidths((current) => omitWidths(current, dropped)) - } - - /** 闪烁提示某列(并滚动到可视区)。 */ - const flashThread = (id: string) => - setFlash({ id, n: ++flashSequence.current }) - - useEffect(() => { - if (!flash) return - const element = colsRef.current?.querySelector( - `.column[data-thread-id="${flash.id}"]` - ) - element?.scrollIntoView({ - inline: "nearest", - block: "nearest", - behavior: "smooth", - }) - const timer = setTimeout(() => setFlash(null), 950) - return () => clearTimeout(timer) - }, [flash]) - - /** 统一放置入口;返回发生的替换/折叠副作用供上层提示。 */ - function openThread( - id: string, - sourceId: string | null, - hint?: PlacementHint - ): PlaceEffect { - store.touch(id) - const state = store.getState() - const { slots: next, effect } = place(mode, effectiveSlots, id, { - sourceId, - maxExpanded, - lastActiveOf: (threadId) => state.threads[threadId]?.lastActive ?? 0, - hint, - }) - setSlots(next) - if (effect.kind === "replaced") { - setWidths((current) => { - const inherited = current[effect.replacedId] - const rest = omitWidths(current, [effect.replacedId, id]) - return inherited !== undefined ? { ...rest, [id]: inherited } : rest - }) - } - flashThread(id) - return effect - } - - /** 列内导航:面包屑 = collapse(收起重复列);切换器 = swap(交换两列)。 */ - function navColumn( - viewportIndex: number, - targetId: string, - duplicate: "collapse" | "swap" = "collapse" - ) { - const next = effectiveSlots.map((slot) => ({ ...slot })) - const fromId = next[viewportIndex].id - if (targetId === "main") { - next.splice(viewportIndex, 1) - setSlots(next) - setWidths((current) => omitWidths(current, [fromId])) - flashThread("main") - return - } - store.touch(targetId) - const other = next.findIndex((slot) => slot.id === targetId) - if (other >= 0 && other !== viewportIndex) { - if (duplicate === "swap") { - const otherId = next[other].id - next[other].id = next[viewportIndex].id - next[viewportIndex].id = otherId - setWidths((current) => { - const fromWidth = current[fromId] - const targetWidth = current[targetId] - if (fromWidth === undefined && targetWidth === undefined) - return current - return { - ...omitWidths(current, [fromId, targetId]), - ...(fromWidth !== undefined ? { [targetId]: fromWidth } : null), - ...(targetWidth !== undefined ? { [fromId]: targetWidth } : null), - } - }) - } else { - next[other].folded = false - next.splice(viewportIndex, 1) - setWidths((current) => omitWidths(current, [fromId])) - } - } else { - next[viewportIndex].id = targetId - if (fromId !== targetId) { - setWidths((current) => { - const fromWidth = current[fromId] - const rest = omitWidths(current, [fromId, targetId]) - return fromWidth !== undefined - ? { ...rest, [targetId]: fromWidth } - : rest - }) - } - } - setSlots(next) - flashThread(targetId) - } - - function closeColumn(viewportIndex: number) { - const next = effectiveSlots.map((slot) => ({ ...slot })) - const removed = next.splice(viewportIndex, 1) - setSlots(next) - if (removed.length) - setWidths((current) => - omitWidths( - current, - removed.map((slot) => slot.id) - ) - ) - } - - /** 撤销 replace 策略:整体恢复替换前的槽位。 */ - function restoreSlots(previous: Slot[]) { - setSlots(previous) - } - - /** fold → replace:细条全部展开,从左裁掉超限列。 */ - function normalizeToReplace(): string[] { - const { slots: next, dropped } = normalizeForReplace( - effectiveSlots, - maxExpanded - ) - setSlots(next) - if (dropped.length) setWidths((current) => omitWidths(current, dropped)) - return dropped - } - - function commitWidths(patch: Record) { - setWidths((current) => ({ ...current, ...patch })) - } - - function resetWidths(ids: string[]) { - setWidths((current) => omitWidths(current, ids)) - } - - return { - slots: effectiveSlots, - widths, - flashId: flash?.id ?? null, - colsRef, - openThread, - navColumn, - closeColumn, - restoreSlots, - flashThread, - normalizeToReplace, - commitWidths, - resetWidths, - } -} diff --git a/app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx b/app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx index 5d35edc1..2e4f499b 100644 --- a/app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx +++ b/app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx @@ -9,13 +9,14 @@ import { Waypoints, } from "lucide-react" import { THREAD_CHAT_SHORTCUTS } from "@/constants/thread-chat" -import type { ViewMode } from "../../net/persistence/persist" import type { PlacementMode } from "../columns/placement" import { AccountButton } from "./account-button" import { ShortcutHint } from "../overlays/shortcut-hint" import { COL_MIN_W } from "../columns/use-column-viewport" import { columnCountChoices } from "./thread-chat-topbar-logic" +type ViewMode = "columns" | "canvas" + export function ThreadChatTopbar({ viewMode, showHelp, diff --git a/app/thread-chat/orchestration/navigation/tree-list-row.tsx b/app/thread-chat/orchestration/navigation/tree-list-row.tsx index 7af0ab9a..ce97b41e 100644 --- a/app/thread-chat/orchestration/navigation/tree-list-row.tsx +++ b/app/thread-chat/orchestration/navigation/tree-list-row.tsx @@ -2,7 +2,13 @@ import { Check, Pencil, Trash2, X } from "lucide-react" import { CUSTOM_TITLE_MAX_LEN } from "@/constants/thread-chat" -import type { TreeListItem } from "../../net/persistence/persist" + +export interface TreeListItem { + id: string + title: string + updatedAt: string + threadCount: number +} /** 相对时间:「刚刚 / N 分钟前 / N 小时前 / N 天前 / M月D日」 */ function relativeTime(iso: string): string { diff --git a/app/thread-chat/orchestration/navigation/tree-list.tsx b/app/thread-chat/orchestration/navigation/tree-list.tsx deleted file mode 100644 index 84abe394..00000000 --- a/app/thread-chat/orchestration/navigation/tree-list.tsx +++ /dev/null @@ -1,274 +0,0 @@ -"use client" -/** - * orchestration/tree-list —— 会话列表弹层(⌘⇧K / 顶栏「对话列表」按钮)。 - * - * 视觉沿用 ⌘K 切换器的 swx 弹层语言(tlx-* 类在 CSS 里复用同一套 token); - * 数据每次打开现拉(design D3:无缓存/无轮询,壳层以重挂方式打开保证归零)。 - * · 条目 = 展示标题(coalesce 双轨,服务端已做)+ 相对更新时间 + 分支数徽标; - * · 当前树高亮置顶——尚未入库(空树未保存)时以本地信息合成「未保存」条目; - * · 内联重命名(悬停铅笔 → 输入框,Enter 提交 / Esc 取消 / 失焦放弃): - * 乐观更新,PATCH 失败回滚 + 壳层 toast(design D5); - * · 删除二段确认(垃圾桶 → 变「确认删除」,点它处 / Esc 复位),成功后就地 - * 清理 localStorage 善后;删的是当前树时把「下一站」交回壳层跳转(design D4)。 - * - * Esc 语义:编辑态 / 确认态先被本组件的捕获期监听消费(stopPropagation), - * 其余 Esc 冒泡到壳层关闭链(弹层在链的最外层,先关它)。 - * - * 外壳是 shadcn/ui Dialog(Base UI):借它的 data-starting/ending-style 过渡状态机 - * 做进出双向动效(样式仍是 .tc 纸面 token,见 thread-chat.css)。Esc 的内建关闭被 - * dialogCloseToShell 取消并放行冒泡——上面这条捕获期 stopPropagation 的 Esc 链依然 - * 先于 Dialog 与壳层生效,行为不变。 - */ - -import React, { useEffect, useState } from "react" -import { ListTodo } from "lucide-react" -import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" -import { Dialog, DialogPortal } from "@/components/ui/dialog" -import { - CUSTOM_TITLE_MAX_LEN, - THREAD_CHAT_SHORTCUTS, -} from "@/constants/thread-chat" -import { dialogCloseToShell } from "../overlays/dialog-close-to-shell" -import { ShortcutHint } from "../overlays/shortcut-hint" -import { TreeListRow } from "./tree-list-row" -import { - cleanupAfterTreeDelete, - deleteTree, - listTrees, - renameTree, - type TreeListItem, -} from "../../net/persistence/persist" - -export interface TreeListProps { - /** 当前打开的树(用于高亮置顶与「未保存」合成) */ - currentTreeId: string - /** 当前树的本地合成信息:未入库时用它拼「未保存」条目 */ - currentTitle: string - currentThreadCount: number - /** 点击非当前树条目:壳层负责跳转(组件已先自关) */ - onSwitch: (treeId: string) => void - /** 删除的是当前树:nextTreeId = 剩余最近一棵(null = 一棵不剩,开新树) */ - onDeleteCurrent: (nextTreeId: string | null) => void - /** 删除当前树前抑制其存盘(true),失败时恢复(false)——防 DELETE 期间防抖 PUT 复活 */ - onSuppressCurrentSave?: (suppressed: boolean) => void - /** 重命名成功且改的是当前树时回调新标题——壳层用它同步主线列头副标题 */ - onRenamedCurrent?: (title: string) => void - onClose: () => void - /** 轻提示(沿用壳层 toast) */ - onToast: (msg: string) => void - /** 壳层的退场标记:true = Dialog 置 open=false 播放关闭动画(随后壳层卸载本组件) */ - closing?: boolean - /** Dialog Portal 的挂载点(.tc 根):保证 .swx / .tlx 选择器与纸面 CSS 变量继续生效 */ - container?: React.RefObject -} - -export function TreeList({ - currentTreeId, - currentTitle, - currentThreadCount, - onSwitch, - onDeleteCurrent, - onSuppressCurrentSave, - onRenamedCurrent, - onClose, - onToast, - closing = false, - container, -}: TreeListProps) { - /** null = 拉取中 */ - const [items, setItems] = useState(null) - /** 内联重命名中的树 id + 草稿 */ - const [editingId, setEditingId] = useState(null) - const [draft, setDraft] = useState("") - /** 二段删除确认中的树 id */ - const [confirmId, setConfirmId] = useState(null) - /** 删除请求进行中的树 id(防连点) */ - const [deletingId, setDeletingId] = useState(null) - - // 打开现拉(组件每次打开重挂,天然只拉一次) - useEffect(() => { - let cancelled = false - void listTrees().then((trees) => { - if (!cancelled) setItems(trees) - }) - return () => { - cancelled = true - } - }, []) - - // Esc:编辑态 / 确认态在捕获期先于壳层关闭链被消费 - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key !== "Escape") return - if (editingId !== null) { - e.stopPropagation() - setEditingId(null) - } else if (confirmId !== null) { - e.stopPropagation() - setConfirmId(null) - } - } - document.addEventListener("keydown", onKey, true) - return () => document.removeEventListener("keydown", onKey, true) - }, [editingId, confirmId]) - - /* ---------- 列表拼装:当前树置顶(未入库合成「未保存」条目) ---------- */ - const saved = items ?? [] - const currentSaved = saved.find((t) => t.id === currentTreeId) ?? null - const rest = saved.filter((t) => t.id !== currentTreeId) - const currentRow: TreeListItem = currentSaved ?? { - id: currentTreeId, - title: currentTitle, - updatedAt: "", - threadCount: currentThreadCount, - } - const rows: { item: TreeListItem; isCurrent: boolean; unsaved: boolean }[] = [ - { item: currentRow, isCurrent: true, unsaved: currentSaved === null }, - ...rest.map((item) => ({ item, isCurrent: false, unsaved: false })), - ] - - /* ---------- 内联重命名:乐观更新 + 失败回滚(design D5) ---------- */ - function startEdit(item: TreeListItem) { - setConfirmId(null) - setEditingId(item.id) - setDraft(item.title) - } - function commitEdit(id: string) { - const prev = - id === currentTreeId - ? currentRow.title - : (saved.find((t) => t.id === id)?.title ?? "") - const next = draft.trim() - setEditingId(null) - if (next === "" || next === prev) return - if (next.length > CUSTOM_TITLE_MAX_LEN) { - onToast(`标题最长 ${CUSTOM_TITLE_MAX_LEN} 字,未保存`) - return - } - // 乐观改本地列表;未入库的当前树没有可 PATCH 的行,直接提示 - if (id === currentTreeId && currentSaved === null) { - onToast("当前对话尚未保存,发出第一条消息后才能重命名") - return - } - setItems((list) => - (list ?? []).map((t) => (t.id === id ? { ...t, title: next } : t)) - ) - renameTree(id, next) - .then(() => { - // 改的是当前树:通知壳层同步本地 customTitle(主线列头副标题即时更新) - if (id === currentTreeId) onRenamedCurrent?.(next) - }) - .catch(() => { - setItems((list) => - (list ?? []).map((t) => (t.id === id ? { ...t, title: prev } : t)) - ) - onToast("重命名失败,已恢复原名") - }) - } - - /* ---------- 二段删除 + 善后(design D4) ---------- */ - async function doDelete(id: string) { - setConfirmId(null) - setDeletingId(id) - // 删的是当前树:先行抑制存盘(codex review:DELETE 往返期间防抖定时器可能触发 PUT, - // 晚到的 upsert 会复活刚删的行——抑制位挡新写,persist 写链保证在飞旧写先于 DELETE 落库) - if (id === currentTreeId) onSuppressCurrentSave?.(true) - try { - await deleteTree(id) - } catch { - if (id === currentTreeId) onSuppressCurrentSave?.(false) // 删除失败:树还在,恢复存盘 - setDeletingId(null) - onToast("删除失败,请重试") - return - } - cleanupAfterTreeDelete(id) // 清工作台记忆 + 悬空「最近一棵」指针 - const remaining = (items ?? []).filter((t) => t.id !== id) - setItems(remaining) - setDeletingId(null) - if (id === currentTreeId) { - // 跳剩余最近一棵(列表本就按 updated_at 降序);一棵不剩开新树 - const next = remaining.find((t) => t.id !== currentTreeId) - onDeleteCurrent(next?.id ?? null) - } else { - onToast("对话已删除") - } - } - - return ( - // Dialog 受控 open:closing 期间置 false 触发 data-ending-style 退场(Base UI 保持 - // Popup 挂载到 transition 结束)。modal=false + disablePointerDismissal 复刻旧行为: - // 不锁滚动 / 不困焦点 / 点外关闭由 Backdrop 的 onMouseDown 自己接;initialFocus=false - // 保持「打开不夺焦点」的旧语义(重命名输入框的 autoFocus 不受影响)。 -

- - - {/* 面板内任意处 mousedown 复位删除确认态(确认按钮自身已 stopPropagation) */} - setConfirmId(null)} - > -
- - 对话列表 - -
-
- {items === null &&
加载中…
} - {items !== null && - rows.map(({ item, isCurrent, unsaved }) => { - const editing = editingId === item.id - const confirming = confirmId === item.id - return ( - { - onClose() - if (!isCurrent) onSwitch(item.id) - }} - onDraftChange={setDraft} - onCancelEdit={() => setEditingId(null)} - onCommitEdit={() => commitEdit(item.id)} - onStartEdit={() => startEdit(item)} - onRequestDelete={() => { - setEditingId(null) - setConfirmId(item.id) - }} - onConfirmDelete={() => void doDelete(item.id)} - onCancelDelete={() => setConfirmId(null)} - /> - ) - })} - {items !== null && rows.length === 1 && rows[0].unsaved && ( -
- 还没有保存过的对话——发出第一条消息即自动保存 -
- )} -
-
- 点击切换 - 悬停条目可重命名 / 删除 - - 关闭 - -
-
-
-
- ) -} diff --git a/app/thread-chat/orchestration/workspace/branch-workspace-actions.ts b/app/thread-chat/orchestration/workspace/branch-workspace-actions.ts deleted file mode 100644 index b15a2106..00000000 --- a/app/thread-chat/orchestration/workspace/branch-workspace-actions.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { activeLeafTurn, threadTitle, type TreeRow } from "../../core/selectors" -import type { ThreadStore } from "../../core/store" -import type { ThreadTreeState } from "../../core/types" -import type { SelectionInfo } from "../../branching/selection/use-assistant-text-selection" -import { kickoffQuestion } from "../../net/prompt/prompt" -import type { ChatController } from "../../net/chat-controller" -import type { PlacementHint, PlacementMode } from "../columns/placement" -import type { SwitcherMode } from "../navigation/thread-switcher" -import type { useColumnSlots } from "../columns/use-column-slots" -import type { ViewMode } from "../../net/persistence/persist" - -type ColumnWorkspace = ReturnType - -/** 页面视图可直接消费的分支导航命令组合。 */ -export function createBranchWorkspaceActions({ - state, - store, - chat, - columns, - viewMode, - mode, - setMode, - showColumnsView, - focusCanvasNode, - closeSwitcher, - showToast, -}: { - state: ThreadTreeState - store: ThreadStore - chat: ChatController - columns: ColumnWorkspace - viewMode: ViewMode - mode: PlacementMode - setMode(mode: PlacementMode): void - showColumnsView(): void - focusCanvasNode(threadId: string): void - closeSwitcher(): void - showToast(message: string, undo?: () => void): void -}) { - function openBranchUI( - id: string, - sourceId?: string | null, - hint?: PlacementHint - ) { - showColumnsView() - if (id === "main") { - columns.flashThread("main") - return - } - const effect = columns.openThread(id, sourceId ?? null, hint) - if (effect.kind === "replaced") { - showToast( - `第 ${effect.idx + 2} 列已替换:「${threadTitle(state, effect.replacedId)}」→「${threadTitle(state, id)}」`, - () => { - columns.restoreSlots(effect.prevSlots) - columns.flashThread(effect.replacedId) - } - ) - } else if (effect.kind === "folded") { - showToast( - `已打开「${threadTitle(state, id)}」,「${threadTitle(state, effect.foldedId)}」已折叠为细条` - ) - } - } - - function handleFork( - selection: SelectionInfo, - hint?: PlacementHint, - question?: string - ) { - const fork = store.fork({ - sourceThreadId: selection.threadId, - sourceMsgId: selection.msgId, - anchorText: selection.text, - anchor: selection.anchor, - }) - if (!fork) return - const trimmedQuestion = question?.trim() - if (trimmedQuestion) - chat.send(fork.threadId, trimmedQuestion, { text: selection.text }) - if (viewMode === "canvas") { - focusCanvasNode(fork.threadId) - showToast(`已开启分支 · ${fork.title}`) - return - } - const effect = columns.openThread(fork.threadId, selection.threadId, hint) - if (effect.kind === "replaced") { - showToast( - `已开启分支「${fork.title}」,替换了第 ${effect.idx + 2} 列的「${threadTitle(state, effect.replacedId)}」`, - () => { - columns.restoreSlots(effect.prevSlots) - columns.flashThread(effect.replacedId) - } - ) - } else if (effect.kind === "folded") { - showToast( - `已开启分支「${fork.title}」,「${threadTitle(state, effect.foldedId)}」已折叠为细条` - ) - } else { - showToast(`已开启分支 · ${fork.title}`) - } - } - - function changeMode(nextMode: PlacementMode) { - if (nextMode === mode) return - setMode(nextMode) - if (nextMode !== "replace") return - const dropped = columns.normalizeToReplace() - if (dropped.length) - showToast( - `已切回替换⑥:细条全部展开后,超出列数的「${dropped.map((id) => threadTitle(state, id)).join("」「")}」已收起` - ) - } - - function pickRow(row: TreeRow, switcherMode: SwitcherMode) { - closeSwitcher() - if (switcherMode.kind === "column") { - if (columns.slots[switcherMode.vpIndex]?.id === row.id) { - columns.flashThread(row.id) - return - } - columns.navColumn(switcherMode.vpIndex, row.id, "swap") - } else if (switcherMode.kind === "subtree") { - openBranchUI(row.id, switcherMode.rootId) - } else { - openBranchUI(row.id, null) - } - } - - function isThreadBusy(threadId: string): boolean { - const thread = state.threads[threadId] - const last = thread ? activeLeafTurn(thread)?.assistantMessage : null - return Boolean( - last && (last.status === "pending" || last.status === "streaming") - ) - } - - function composerPrefillFor(threadId: string): string | undefined { - const thread = state.threads[threadId] - return thread?.anchorText && thread.messages.length === 0 - ? kickoffQuestion(thread.anchorText) - : undefined - } - - return { - openBranchUI, - handleFork, - changeMode, - pickRow, - isThreadBusy, - composerPrefillFor, - } -} diff --git a/app/thread-chat/orchestration/workspace/ui-state-snapshot.ts b/app/thread-chat/orchestration/workspace/ui-state-snapshot.ts deleted file mode 100644 index 4e2ab5e9..00000000 --- a/app/thread-chat/orchestration/workspace/ui-state-snapshot.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { TreeUiState } from "../../net/persistence/persist" - -export function createTreeUiStateSnapshot(state: TreeUiState): TreeUiState { - return { - slots: state.slots, - widths: state.widths, - forceCols: state.forceCols, - mode: state.mode, - viewMode: state.viewMode, - } -} diff --git a/app/thread-chat/orchestration/workspace/use-thread-chat-runtime.ts b/app/thread-chat/orchestration/workspace/use-thread-chat-runtime.ts deleted file mode 100644 index 1cc07b6d..00000000 --- a/app/thread-chat/orchestration/workspace/use-thread-chat-runtime.ts +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { isThreadChatModelId } from "@/constants/model" -import { createThreadStore } from "../../core/store" -import { useThreadStore } from "../../core/use-thread-store" -import type { MessageFeedbackSummary, ThreadTreeState } from "../../core/types" -import type { GenerationSummary, RecoverableTurn } from "../../generation/types" -import { useGenerationReconciliation } from "../../generation/use-generation-reconciliation" -import { useMessageActions } from "../../chat/actions/use-message-actions" -import { createChatController } from "../../net/chat-controller" -import { - deriveTreeTitle, - saveTreeStrict, - TreeRevisionError, -} from "../../net/persistence/persist" -import { useThreadTitles } from "../../net/titles/use-thread-titles" -import { useTreePersistence } from "../../net/persistence/use-tree-persistence" - -/** - * 页面壳使用的 thread-chat 运行时组合根:store、命令、generation 协调与持久化 - * 在这里接线,视图只消费组合后的能力。 - */ -export function useThreadChatRuntime({ - treeId, - initialState, - initialGenerations, - initialMessageFeedbacks, - initialRecoverableTurns, - onToast, -}: { - treeId: string - initialState: ThreadTreeState - initialGenerations: GenerationSummary[] - initialMessageFeedbacks: MessageFeedbackSummary[] - initialRecoverableTurns: RecoverableTurn[] - onToast: (message: string) => void -}) { - const [store] = useState(() => - createThreadStore(initialState, isThreadChatModelId) - ) - const version = useThreadStore(store) - const state = store.getState() - const reloadAfterRevisionConflict = () => { - onToast("其他标签页已更新,正在重新加载…") - window.location.reload() - } - - const [chat] = useState(() => - createChatController(store, { - treeId, - persistNow: () => { - const current = store.getState() - return saveTreeStrict(treeId, current, deriveTreeTitle(current)).catch( - (error) => { - if (error instanceof TreeRevisionError) - reloadAfterRevisionConflict() - throw error - } - ) - }, - onError: onToast, - }) - ) - const { messageActionState, messageCommands, registerRecoverableTurn } = - useMessageActions({ - state, - version, - initialRecoverableTurns, - initialMessageFeedbacks, - commands: chat, - }) - useGenerationReconciliation({ - store, - version, - initialGenerations, - registerRecoverableTurn, - isGenerationStreamingLocally: chat.isGenerationStreamingLocally, - }) - useEffect(() => () => chat.detachAll(), [chat]) - - const { setTreeSaveSuppressed, isTreeSaveSuppressed } = useTreePersistence({ - treeId, - store, - version, - onRevisionConflict: reloadAfterRevisionConflict, - }) - useThreadTitles({ treeId, store, version }) - - return { - store, - state, - chat, - messageActionState, - messageCommands, - setTreeSaveSuppressed, - isTreeSaveSuppressed, - } -} diff --git a/app/thread-chat/orchestration/workspace/use-thread-chat-workspace.ts b/app/thread-chat/orchestration/workspace/use-thread-chat-workspace.ts deleted file mode 100644 index ecaa5921..00000000 --- a/app/thread-chat/orchestration/workspace/use-thread-chat-workspace.ts +++ /dev/null @@ -1,104 +0,0 @@ -"use client" - -import { useCallback, useMemo, useRef, useState } from "react" -import type { ThreadStore } from "../../core/store" -import type { ThreadMessageActionCommands } from "../../chat/actions/message-action-commands" -import type { ChatController } from "../../net/chat-controller" -import type { TreeUiState, ViewMode } from "../../net/persistence/persist" -import type { CanvasChatActions } from "../canvas/canvas-actions" -import type { CanvasViewState } from "../canvas/use-canvas-layout" -import { useColumnSlots } from "../columns/use-column-slots" -import { useColumnViewport } from "../columns/use-column-viewport" -import type { PlacementMode } from "../columns/placement" -import { useUiStatePersistence } from "./use-ui-state-persistence" - -/** 列/画布工作台的响应式状态、持久化与画布命令适配组合。 */ -export function useThreadChatWorkspace({ - treeId, - store, - chat, - messageCommands, - initialUi, - isSaveSuppressed, -}: { - treeId: string - store: ThreadStore - chat: ChatController - messageCommands: ThreadMessageActionCommands - initialUi: TreeUiState | null - isSaveSuppressed(): boolean -}) { - const { windowWidth, autoColumnCount } = useColumnViewport() - const [forceCols, setForceCols] = useState( - initialUi?.forceCols ?? null - ) - const totalCols = forceCols ?? autoColumnCount - const maxExpanded = totalCols - 1 - const [mode, setMode] = useState(initialUi?.mode ?? "replace") - const columns = useColumnSlots({ - store, - maxExpanded, - mode, - initialSlots: initialUi?.slots, - initialWidths: initialUi?.widths, - }) - - const [viewMode, setViewMode] = useState( - initialUi?.viewMode ?? "columns" - ) - const [focusNode, setFocusNode] = useState<{ id: string; n: number } | null>( - null - ) - const focusSequence = useRef(0) - const showColumnsView = useCallback(() => { - setViewMode("columns") - setFocusNode(null) - }, []) - const focusCanvasNode = useCallback((id: string) => { - setFocusNode({ id, n: ++focusSequence.current }) - }, []) - - const canvasChat = useMemo( - () => ({ - send: chat.send, - stop: chat.stop, - retry: chat.retry, - retryAssistant: messageCommands.retryAssistant, - retryUserTurn: messageCommands.retryUserTurn, - editAndRegenerate: messageCommands.editAndRegenerate, - switchTurnVariant: messageCommands.switchTurnVariant, - submitFeedback: messageCommands.submitFeedback, - }), - [chat, messageCommands] - ) - const [canvasViewState] = useState(() => ({ - pins: new Map(), - })) - - useUiStatePersistence({ - treeId, - slots: columns.slots, - widths: columns.widths, - forceCols, - mode, - viewMode, - isSaveSuppressed, - }) - - return { - windowWidth, - forceCols, - setForceCols, - maxExpanded, - mode, - setMode, - columns, - viewMode, - setViewMode, - focusNode, - focusCanvasNode, - showColumnsView, - canvasChat, - canvasViewState, - } -} diff --git a/app/thread-chat/orchestration/workspace/use-ui-state-persistence.ts b/app/thread-chat/orchestration/workspace/use-ui-state-persistence.ts deleted file mode 100644 index 6c2a447e..00000000 --- a/app/thread-chat/orchestration/workspace/use-ui-state-persistence.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { UI_SAVE_DEBOUNCE_MS } from "@/constants/thread-chat" -import type { TreeUiState } from "../../net/persistence/persist" -import { saveUiState } from "../../net/persistence/persist" -import { createTreeUiStateSnapshot } from "./ui-state-snapshot" - -export function useUiStatePersistence({ - treeId, - slots, - widths, - forceCols, - mode, - viewMode, - isSaveSuppressed, -}: TreeUiState & { - treeId: string - isSaveSuppressed(): boolean -}) { - useEffect(() => { - const timer = setTimeout(() => { - if (isSaveSuppressed()) return - saveUiState( - treeId, - createTreeUiStateSnapshot({ slots, widths, forceCols, mode, viewMode }) - ) - }, UI_SAVE_DEBOUNCE_MS) - return () => clearTimeout(timer) - }, [treeId, slots, widths, forceCols, mode, viewMode, isSaveSuppressed]) -} diff --git a/app/thread-chat/page-metadata.ts b/app/thread-chat/page-metadata.ts index 34f07128..9f9f3b2b 100644 --- a/app/thread-chat/page-metadata.ts +++ b/app/thread-chat/page-metadata.ts @@ -1,6 +1,6 @@ import type { Metadata } from "next" -/** /thread-chat 与 /thread-chat/[treeId] 共用的页面 metadata(两个路由是同一页面的跳板与本体) */ +/** /thread-chat 与 /thread-chat/[projectId] 共用的页面 metadata。 */ export const threadChatMetadata: Metadata = { title: "Thread Chat · 分支对话", description: diff --git a/app/thread-chat/page.tsx b/app/thread-chat/page.tsx index ce2d5503..145716a9 100644 --- a/app/thread-chat/page.tsx +++ b/app/thread-chat/page.tsx @@ -1,9 +1,9 @@ -import { TreeRedirect } from "./tree-redirect" +import { ProjectRedirect } from "./project-redirect" import { threadChatMetadata } from "./page-metadata" export const metadata = threadChatMetadata -/** 裸路径入口跳板:replace 到「最近一棵」或新生成的 /thread-chat/{treeId} */ +/** 裸路径入口跳板:replace 到最近 Project;没有 Project 时进入 `/new`。 */ export default function ThreadChatPage() { - return + return } diff --git a/app/thread-chat/tree-redirect.tsx b/app/thread-chat/project-redirect.tsx similarity index 68% rename from app/thread-chat/tree-redirect.tsx rename to app/thread-chat/project-redirect.tsx index db4d2933..83b4746d 100644 --- a/app/thread-chat/tree-redirect.tsx +++ b/app/thread-chat/project-redirect.tsx @@ -1,10 +1,4 @@ "use client" -/** - * 裸路径 /thread-chat 的入口跳板:客户端 effect 里读 localStorage 的「最近一棵」 - * treeId(无则生成新 UUID),router.replace 到 /thread-chat/{treeId}。 - * replace 不留历史——回退键不会弹回跳板页。localStorage 只在 effect 里碰(避免 - * SSR/hydration 问题),跳转前渲染 .tc 风格的一行轻量占位。 - */ import { useEffect } from "react" import { useRouter } from "next/navigation" @@ -12,7 +6,8 @@ import "./thread-chat.css" import { useThreadChatAppRuntime } from "@/lib/thread-chat/client/providers" import { threadChatRoutes } from "@/lib/thread-chat/api/routes" -export function TreeRedirect() { +/** 裸路径只读取服务端 Project Catalog;客户端不生成或记忆领域实体 ID。 */ +export function ProjectRedirect() { const router = useRouter() const runtime = useThreadChatAppRuntime() useEffect(() => { diff --git a/app/thread-chat/thread-chat-demo.tsx b/app/thread-chat/thread-chat-demo.tsx deleted file mode 100644 index 16d8ca83..00000000 --- a/app/thread-chat/thread-chat-demo.tsx +++ /dev/null @@ -1,406 +0,0 @@ -"use client" -/** - * -------------------------------------------------------------------------- - * Thread Chat · 分支对话(方案⑥ 自适应列 + 列满策略:替换⑥ / 细条⑤) - * -------------------------------------------------------------------------- - * 顶层壳:只负责状态编排与各层拼装,具体能力分四层实现—— - * · core/ headless 会话树 store + 选择器(useSyncExternalStore 绑定); - * · chat/ 单会话视图(消息列表 + composer),不知道树/列/分支; - * · branching/ 把「分支能力」注入 chat:锚点/脚注/面包屑/继承上文/划选气泡; - * · orchestration/ 视图编排:列视图(放置策略:替换⑥/细条⑤、切换器、Artifact 抽屉) - * 与画布视图(thread-canvas,React Flow 全树纵览,懒加载)两个平级视图层。 - * - * 「打开某会话」的统一意图入口是 openBranchUI:脚注 / ⌘K / 每列 ⇄ / 子树弹层 / - * Artifact 定位来源 / 画布双击节点全部走它——画布模式下先切回列视图(打开 = 去列里读), - * 列满时按当前策略替换(可撤销)或折叠细条。 - * - * 持久化(loader + inner 拆分):默认导出 ThreadChatDemo 通过 useThreadChatBoot - * 完成远端加载、strict-v2 清理与工作台恢复,随后才渲染 ThreadChatDemoInner - * (store 以已存状态为种子一次性创建)。inner 订阅 store - * version,useTreePersistence 防抖整树 PUT(流式高频跳变合并)+ 卸载 flush; - * 工作台状态(列槽/列宽/列数/策略/视图)按 treeId 分键防抖写 localStorage。 - * -------------------------------------------------------------------------- - */ - -import dynamic from "next/dynamic" -import { useRouter } from "next/navigation" -import React, { useState } from "react" -import "./thread-chat.css" -import { activePathArtifacts } from "./core/selectors" -import type { - Message, - MessageFeedbackSummary, - ThreadTreeState, -} from "./core/types" -import { deriveTreeTitle, type TreeUiState } from "./net/persistence/persist" -import type { GenerationSummary } from "./generation/types" -import type { RecoverableTurn } from "./generation/types" -import { useThreadChatBoot } from "./net/boot/use-thread-chat-boot" -import { BranchableChat } from "./branching/branchable-chat" -import { SelectionBubble } from "./branching/selection/selection-bubble" -import { type Slot } from "./orchestration/columns/placement" -import { ThreadColumns } from "./orchestration/columns/thread-columns" -import { ThreadSwitcher } from "./orchestration/navigation/thread-switcher" -import { TreeList } from "./orchestration/navigation/tree-list" -import { ArtifactDrawer } from "./orchestration/artifacts/artifact-drawer" -import { HelpPanel, UsageHint } from "./orchestration/overlays/help-panel" -import { ThreadChatTopbar } from "./orchestration/navigation/thread-chat-topbar" -import { useWorkspaceOverlays } from "./orchestration/overlays/use-workspace-overlays" -import { - useWorkspaceToast, - WorkspaceToast, -} from "./orchestration/overlays/workspace-toast" -import { useThreadChatRuntime } from "./orchestration/workspace/use-thread-chat-runtime" -import { useThreadChatWorkspace } from "./orchestration/workspace/use-thread-chat-workspace" -import { createBranchWorkspaceActions } from "./orchestration/workspace/branch-workspace-actions" - -/** 画布视图层懒加载:React Flow 只在首次进入画布模式时才落地(且跳过 SSR) */ -const ThreadCanvas = dynamic( - () => - import("./orchestration/canvas/thread-canvas").then((m) => m.ThreadCanvas), - { - ssr: false, - loading: () =>
画布加载中…
, - } -) - -/** 主线列头副标题的兜底:整棵树还没有任何用户消息(也没被重命名)时展示 */ -const SUBTITLE_FALLBACK = "新对话" - -/** 画布模式喂给划选气泡的空列槽(稳定引用):画布 fork 不占列槽(D4), - 气泡据此不渲染迷你列条(hasMap=false),提交路径由 handleFork 按视图分流 */ -const EMPTY_SLOTS: Slot[] = [] - -/** - * 默认导出的 loader:先完成远端加载(GET → sanitize → 读工作台记忆)再渲染 inner。 - * 加载失败 / 未命中都以空树降级(loadTree 内部已 console.warn),不阻塞页面。 - * treeId 变化由上层路由的 key={treeId} 整体重挂,不在此处处理切树。 - */ -export function ThreadChatDemo({ treeId }: { treeId: string }) { - const boot = useThreadChatBoot(treeId) - - if (!boot) { - return ( -
-
对话加载中…
-
- ) - } - return ( - - ) -} - -interface ThreadChatDemoInnerProps { - treeId: string - /** store 种子:已 sanitize 的持久化状态,或空树 */ - initialState: ThreadTreeState - /** 该树的工作台记忆(loader 已校验),null = 默认布局(只开主线) */ - initialUi: TreeUiState | null - /** 用户重命名过的标题(未改过为 null)——主线列头副标题优先展示 */ - initialCustomTitle?: string | null - initialGenerations: GenerationSummary[] - initialMessageFeedbacks: MessageFeedbackSummary[] - initialRecoverableTurns: RecoverableTurn[] -} - -export function ThreadChatDemoInner({ - treeId, - initialState, - initialUi, - initialCustomTitle = null, - initialGenerations, - initialMessageFeedbacks, - initialRecoverableTurns, -}: ThreadChatDemoInnerProps) { - const router = useRouter() - - const { toast, showToast, dismissToast } = useWorkspaceToast() - const { - store, - state, - chat, - messageActionState, - messageCommands, - setTreeSaveSuppressed, - isTreeSaveSuppressed, - } = useThreadChatRuntime({ - treeId, - initialState, - initialGenerations, - initialMessageFeedbacks, - initialRecoverableTurns, - onToast: showToast, - }) - - const { - windowWidth: winW, - forceCols, - setForceCols, - maxExpanded, - mode, - setMode, - columns: cols, - viewMode, - setViewMode, - focusNode, - focusCanvasNode, - showColumnsView, - canvasChat, - canvasViewState, - } = useThreadChatWorkspace({ - treeId, - store, - chat, - messageCommands, - initialUi, - isSaveSuppressed: isTreeSaveSuppressed, - }) - - /* ---------- 主线列头副标题:customTitle(用户重命名)→ 自动标题 / 派生回退 → 兜底 ---------- - customTitle 本地态由对话列表的 onRenamedCurrent 同步(重命名当前树立即生效,无需重载) */ - const [customTitle, setCustomTitle] = useState( - initialCustomTitle - ) - const mainHasMessage = (state.threads.main?.messages.length ?? 0) > 0 - const mainSubtitle = - customTitle ?? (mainHasMessage ? deriveTreeTitle(state) : SUBTITLE_FALLBACK) - - /* ---------- 其余 UI 状态 ---------- */ - /* 首次内联提示:仅「未关过 && 还没开始聊」时可见;顶栏帮助另走 Dialog。 */ - const [hintDismissed, setHintDismissed] = useState(false) - const { - rootRef: tcRootRef, - selection: sel, - setSelection: setSel, - switcher, - closeSwitcher, - toggleGlobalSwitcher, - openColumnSwitcher, - openSubtree, - treeList, - closeTreeList, - toggleTreeList, - helpPanel, - closeHelpPanel, - openHelpPanel, - drawerOpen, - activeArtifactId: activeArt, - setActiveArtifactId: setActiveArt, - openArtifact, - toggleDrawer, - closeDrawer, - } = useWorkspaceOverlays() - - const { - openBranchUI, - handleFork, - changeMode, - pickRow, - isThreadBusy, - composerPrefillFor, - } = createBranchWorkspaceActions({ - state, - store, - chat, - columns: cols, - viewMode, - mode, - setMode, - showColumnsView, - focusCanvasNode, - closeSwitcher, - showToast, - }) - - /* ---------- 主线 hint 卡片:仅整棵树还没有任何消息时展示(判 main 即可—— - 分支必经主线产生),首条消息一出现即随派生状态消失;× 可提前手动关。 ---------- */ - const hintVisible = !hintDismissed && !mainHasMessage - const hintNode = hintVisible ? ( - setHintDismissed(true)} /> - ) : null - - /* ---------- 顶栏数据 ---------- */ - const branchCount = Object.keys(state.threads).length - 1 - const markdownCount = activePathArtifacts(state).reduce( - (count, artifact) => count + (artifact.kind === "markdown" ? 1 : 0), - 0 - ) - - return ( -
- { - // 空树已经是新对话;反复点击不应让 URL 持续变化。 - if (!mainHasMessage) { - showToast("当前就是全新对话,直接开聊吧") - return - } - router.push(`/thread-chat/${crypto.randomUUID()}`) - }} - onToggleTreeList={toggleTreeList} - onOpenHelp={openHelpPanel} - onShowColumns={showColumnsView} - onShowCanvas={() => setViewMode("canvas")} - onForceCols={setForceCols} - onPlacementModeChange={changeMode} - onToggleThreadTree={toggleGlobalSwitcher} - onToggleMarkdown={toggleDrawer} - /> - - {viewMode === "columns" ? ( - openBranchUI(id, null)} - onCommitWidths={cols.commitWidths} - onResetWidths={cols.resetWidths} - renderThread={(threadId, vpIndex) => ( - - openBranchUI(target, threadId, opts) - } - onOpenArtifact={openArtifact} - onCrumbNav={(target) => - cols.navColumn(vpIndex, target, "collapse") - } - onOpenSwitcher={(btn) => openColumnSwitcher(vpIndex, btn)} - onOpenSubtree={(btn) => openSubtree(threadId, btn)} - onCollapse={() => cols.closeColumn(vpIndex)} - busy={isThreadBusy(threadId)} - composerPrefill={composerPrefillFor(threadId)} - onModelChange={(modelId) => - store.setThreadModel(threadId, modelId) - } - onRetry={(msg: Message) => chat.retry(threadId, msg.id)} - onStop={() => chat.stop(threadId)} - onSend={(text) => chat.send(threadId, text)} - messageActionState={messageActionState} - messageCommands={messageCommands} - /> - )} - /> - ) : ( - openBranchUI(id, null)} - onOpenArtifact={openArtifact} - /> - )} - - {/* 划选气泡两种视图都在(画布面板消息与列模式同一套 .md-body 划选 DOM 契约, - openspec: add-canvas-conversations)。列模式:列槽上下文喂迷你列条,预览与 - 提交共用 placement 规则;画布模式:喂空槽(不渲染列条,fork 不占列槽 D4)。 */} - state.threads[id]?.lastActive ?? 0} - /> - - {treeList !== null && ( - router.push(`/thread-chat/${id}`)} - onSuppressCurrentSave={(v) => { - // 删除前置位(失败恢复):挡住防抖回调与卸载 flush 的新写; - // 已在飞的 PUT 由 persist 写链保证先于 DELETE 落库,两头闭环 - setTreeSaveSuppressed(v) - }} - onDeleteCurrent={(nextId) => { - // 当前树已被删除:抑制卸载 flush / 防抖尾巴的回写(否则 DB 行复活), - // 再跳剩余最近一棵;一棵不剩则开新 UUID。replace 不给被删 URL 留历史。 - setTreeSaveSuppressed(true) - closeTreeList() - router.replace(`/thread-chat/${nextId ?? crypto.randomUUID()}`) - }} - onRenamedCurrent={setCustomTitle} - onToast={showToast} - /> - )} - - {switcher && ( - - )} - - {helpPanel && ( - - )} - - { - const sourceThread = state.threads[threadId] - if ( - sourceThread && - sourceThread.activeLeafMessageId !== sourceMessageId - ) - void chat - .switchTurnVariant(threadId, sourceMessageId) - .then((result) => { - if (result.ok) openBranchUI(threadId, null) - }) - else openBranchUI(threadId, null) - }} - /> - - -
- ) -} diff --git a/e2e/thread-chat/README.md b/e2e/thread-chat/README.md deleted file mode 100644 index d1217487..00000000 --- a/e2e/thread-chat/README.md +++ /dev/null @@ -1,265 +0,0 @@ -# thread-chat e2e 验收脚本 - -`/thread-chat`(分支对话页)的验收脚本,均无需测试框架。 - -注:verify-live / verify-bubble-composer 直接 import 产品代码(`.ts`)生成断言 -期望值(kickoff 预填文案、默认分支标题等——文案改一处,测试期望自动跟随), -因此需要带 `--experimental-strip-types` 运行(与 text-anchor.test.mjs 同一机制)。 - -## text-anchor.test.mjs — 锚点定位纯函数用例(无需 dev server) - -```bash -node --experimental-strip-types e2e/thread-chat/text-anchor.test.mjs -``` - -覆盖 `app/thread-chat/branching/selection/text-anchor.ts` 纯字符串层的三层降级定位 -(position → exact → fuzzy):position 直接命中、exact 多处命中经 prefix/suffix -上下文消歧、**fuzzy 原文被改几个字后仍以 score≥阈值 命中正确区间**、 -阈值抬高 / 彻底无关锚点判定丢失(返回 null)、fuzzySubstring 单字错漏容忍。 - -## prompt-budget.test.mjs — 继承段字符预算纯函数用例(无需 dev server) - -```bash -node --experimental-strip-types e2e/thread-chat/prompt-budget.test.mjs -``` - -覆盖 `app/thread-chat/net/prompt/prompt-pure.ts` 的继承段预算截断(openspec: -add-bubble-composer D8):预算内不截断、超预算从最旧整条丢弃(顺序保持)、 -恰好等于预算的边界、**最新 1 条独超预算仍保留(保底 1 条)**、省略说明与 -kickoff 文案形状。 - -## Markdown Artifact 纯链路测试(无需 dev server) - -```bash -node --experimental-strip-types e2e/thread-chat/markdown-artifact.test.mjs -node --experimental-strip-types e2e/thread-chat/markdown-artifact-state.test.mjs -``` - -覆盖 Zod schema、整份外层 fence 归一化、双语高置信意图正反例、工具事件守卫与 -`toolCallId` 去重、`tool-input-start/delta` 局部 JSON 进度、真实字符/行数/最近章节、 -store 临时进度与完整 Artifact 原子替换、存盘剥离、Artifact-only 终态、retry 清理、 -加载 sanitize 及后续模型上下文序列化。 - -## Generation 刷新持久化测试(P0) - -结构化投影与整树合并是无数据库纯测试: - -```bash -node --import tsx e2e/thread-chat/generation-persistence.test.mjs -``` - -覆盖正文、Markdown Artifact、联网来源/研究上下文、partial error、空回复、确定性 -Artifact id、重复投影/合并幂等、fork 保留、旧 attempt CAS、缺消息读修复,以及只有 -服务端 active generation 能保留 pending 的加载 sanitize。 - -repository/finalize 是开发数据库测试;读取 `.env.local` 后运行,脚本创建随机测试用户 -与树,并在 `finally` 中级联清理: - -```bash -node --env-file=.env.local --import tsx e2e/thread-chat/generation-db.test.mjs -``` - -覆盖并发重复 start、单 current attempt、supersede、Stop-vs-complete、stale heartbeat、 -跨用户 404 语义,以及 superseded attempt 的结果审计与 finalize 重入只扣费一次。 - -浏览器手工验收必须使用当前 checkout 的 dev server:发送可控慢回复后刷新,页面应显示 -“正在后台生成,完成后显示”,服务端继续生成且只计费一次,轮询后原子显示完整结构化 -结果。明确 Stop 才会中止;普通刷新不得产生 stopped。P0 不恢复错过的逐 token 动画,只 -恢复后台状态和最终答案。 - -## 消息操作与轻量消息 DAG - -无需浏览器的纯行为测试: - -```bash -node --import tsx e2e/thread-chat/message-graph.test.mjs -node --import tsx e2e/thread-chat/reconcile-turns.test.mjs -node --import tsx e2e/thread-chat/regeneration-patch.test.mjs -node --import tsx e2e/thread-chat/message-actions-controller.test.mjs -node --import tsx e2e/thread-chat/message-action-availability.test.mjs -``` - -覆盖 strict schema-v2 拒绝旧线性树、active/exact-source path、回复版本、Artifact 来源、恢复状态、 -不可变 edit/regenerate patch,以及 controller 接受/拒绝/冲突/反馈命令。 - -以下脚本连接 `.env.local` 的开发数据库并在 `finally` 中清理随机测试用户: - -```bash -node --env-file=.env.local --import tsx e2e/thread-chat/generation-actions-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/tree-revision-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/tree-deletion-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/tree-ownership-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/tree-save-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/tree-rename-db.test.mjs -node --env-file=.env.local --import tsx e2e/thread-chat/message-feedback-db.test.mjs -``` - -覆盖 generation intent 的原子落库、幂等 replay、running attempt supersede、terminal -source/Artifact 保留、active-leaf CAS、跨用户拒绝、generation-vs-switch revision -竞态、删除与 generation start 的行锁串行化及幂等删除、历史无主树的单次原子认领、 -整树保存的 owner/revision CAS,以及 message feedback 的 set/repeat/switch/clear、 -用户命名与派生标题隔离、完成态约束和 owner isolation。 -真实 UI 验收按仓库规则使用 `ego-browser nodejs` 访问 `localhost:4040`,不得用 -本目录旧的 Playwright 脚本替代本 change 的浏览器验收。 - -## Markdown Artifact 浏览器验收(mock API) - -前提:dev server 与本机 Chrome。脚本在浏览器层 mock 对话与整树持久化 API,不需要 -数据库、登录账号或模型额度。 - -```bash -CHROMIUM_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ -BASE_URL=http://localhost:4040 \ -node e2e/thread-chat/verify-markdown-artifact.mjs -``` - -覆盖列视图/Canvas 共用卡片、Artifact-only 无空气泡、右侧 GFM 预览、停止/重试、 -整树保存与刷新恢复、坏引用/孤儿清理,以及“修改刚才的 Markdown”的上下文回放。 - -## Shiki 代码高亮浏览器验收(mock API) - -前提:dev server、本机 Chrome,以及已登录会话的 Playwright storage state。脚本 mock -客户端 `/api/**`,因此真正运行的是当前 `/thread-chat` 的 `MarkdownBody`、Shiki -动态加载、Artifact drawer 和锚点交互;但服务端 layout 在请求进入浏览器拦截前已经 -校验真实会话,所以仍需登录态,不需要模型额度。 - -```bash -CHROMIUM_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ -STORAGE_STATE=/path/to/authenticated-storage-state.json \ -BASE_URL=http://localhost:4040 \ -node e2e/thread-chat/verify-syntax-highlighting.mjs -``` - -覆盖稳定消息与静态 Artifact 的 Shiki DOM、未知语言 plaintext fallback、代码内 -`", - language: "future-lang", - themeMode: "dark", -}) -ok("未知语言返回 plaintext", unknownResult.status === "plaintext") -ok("plaintext 不生成 HAST", unknownResult.hast === null) -ok( - "plaintext 保留完整原始代码", - unknownResult.code === "" -) - -const shellSessionResult = await highlightMarkdownCode({ - code: "$ echo must-stay-plaintext", - language: "shell-session", - themeMode: "light", -}) -ok("shell-session 高亮请求返回 plaintext", shellSessionResult.status === "plaintext") - -const staleBeforeStart = await highlightMarkdownCode({ - code: "const stale = true", - language: "ts", - themeMode: "dark", - isCurrent: () => false, -}) -ok("已过期 revision 不启动/提交高亮", staleBeforeStart.status === "stale") - -let current = true -const racingResultPromise = highlightMarkdownCode({ - code: "const revision: number = 1", - language: "ts", - themeMode: "dark", - isCurrent: () => current, -}) -current = false -const racingResult = await racingResultPromise -ok("初始化期间过期的 revision 被丢弃", racingResult.status === "stale") - -const highlighted = await highlightMarkdownCode({ - code: "const value = 1", - language: "js", - meta: "{1}", - themeMode: "light", -}) -ok("受支持语言生成 HAST", highlighted.status === "highlighted") -ok("高亮结果不使用 raw HTML 节点", !JSON.stringify(highlighted.hast).includes('"type":"raw"')) -ok( - "light 模式使用 Vitesse Light", - JSON.stringify(highlighted.hast).includes("vitesse-light") -) -ok( - "fence meta transformer 标记目标行", - JSON.stringify(highlighted.hast).includes("highlighted") -) -ok("高亮结果仍保留复制用原始代码", highlighted.code === "const value = 1") - -const darkHighlighted = await highlightMarkdownCode({ - code: "const dark = true", - language: "ts", - themeMode: "dark", -}) -ok( - "dark 模式使用 Vitesse Dark", - JSON.stringify(darkHighlighted.hast).includes("vitesse-dark") -) - -let attempts = 0 -const getRetryable = createRetryableSingleton(async () => { - attempts += 1 - if (attempts === 1) throw new Error("first initialization failed") - return { ready: true } -}) -const firstAttempt = await Promise.allSettled([getRetryable(), getRetryable()]) -ok( - "并发初始化失败只执行一次 factory", - attempts === 1 && firstAttempt.every((result) => result.status === "rejected") -) -const retried = await getRetryable() -ok("初始化失败后可重试", attempts === 2 && retried.ready) -ok("成功结果被 singleton 复用", (await getRetryable()) === retried && attempts === 2) - -process.exit(failed) diff --git a/e2e/thread-chat/text-anchor.test.mjs b/e2e/thread-chat/text-anchor.test.mjs deleted file mode 100644 index 0d912f0b..00000000 --- a/e2e/thread-chat/text-anchor.test.mjs +++ /dev/null @@ -1,105 +0,0 @@ -/** - * text-anchor 纯字符串层用例:验证锚点三层降级定位(position → exact → fuzzy)。 - * 只 import 纯字符串 API(不碰 DOM),可直接跑: - * node --experimental-strip-types e2e/thread-chat/text-anchor.test.mjs - */ -import { - buildQuoteSelector, - fuzzySubstring, - locateOffsets, - normalizeWhitespace, -} from "../../app/thread-chat/branching/selection/text-anchor.ts" - -let failed = 0 -const ok = (label, cond) => { - console.log(`${cond ? "PASS" : "FAIL"} ${label}`) - if (!cond) failed = 1 -} - -/* 同一短语「量子纠缠」出现 3 次的源文本(位置动态计算) */ -const text = - "量子纠缠是量子力学的核心现象,量子纠缠让两个粒子彼此关联,量子纠缠违反贝尔不等式。" -const idx = [...text.matchAll(/量子纠缠/g)].map((m) => m.index) -ok(`前置:短语出现 3 次(位置 ${idx.join("/")})`, idx.length === 3) - -/* 1) position 命中:position 指向第 2 次出现,切片与 exact 等价 → strategy=position */ -{ - const at = idx[1] - const anchor = { - quote: buildQuoteSelector(text, at, at + 4), - position: { start: at, end: at + 4 }, - } - const r = locateOffsets(text, anchor) - ok( - "position:直接命中第 2 次出现(strategy=position, score=1)", - r !== null && r.strategy === "position" && r.start === at && r.score === 1 - ) -} - -/* 2) exact 多处命中 → prefix/suffix 上下文消歧:无 position,靠上下文选中第 3 次 */ -{ - const at = idx[2] - const anchor = { - quote: buildQuoteSelector(text, at, at + 4), // prefix/suffix 采自第 3 次出现 - // 故意不给 position,强制走 exact 层 - } - const r = locateOffsets(text, anchor) - ok( - "exact:多处命中经 prefix/suffix 消歧 → 选中第 3 次(strategy=exact)", - r !== null && r.strategy === "exact" && r.start === idx[2] - ) -} - -/* 3) fuzzy:原文被改几个字后 exact 搜不到,仍以 score≥阈值 命中正确区间 */ -const longText = - "纠缠态在被测量之前并不具有确定的取值,这一点是理解量子力学非定域性的关键所在。" -{ - // 把「确定」改成「明确」、「关键」改成「要点」:exact 搜不到,fuzzy 应命中原句区间 - const mutated = - "纠缠态在被测量之前并不具有明确的取值,这一点是理解量子力学非定域性的要点所在" - const anchor = { quote: { exact: mutated, prefix: "", suffix: "" } } - const r = locateOffsets(longText, anchor) - const slice = r ? longText.slice(r.start, r.end) : "" - ok( - `fuzzy:改字后仍命中(strategy=fuzzy, score=${r ? r.score.toFixed(3) : "null"} ≥ 0.7)`, - r !== null && r.strategy === "fuzzy" && r.score >= 0.7 - ) - ok( - "fuzzy:命中区间落在原句上(含未改动的稳定尾串)", - slice.includes("这一点是理解量子力学") - ) -} - -/* 4) 低于阈值判丢失:同一改字锚点,把阈值抬到 0.98 → score 达不到 → 返回 null */ -{ - const mutated = - "纠缠态在被测量之前并不具有明确的取值,这一点是理解量子力学非定域性的要点所在" - const anchor = { quote: { exact: mutated, prefix: "", suffix: "" } } - const r = locateOffsets(longText, anchor, { fuzzyThreshold: 0.98 }) - ok("阈值 0.98:改动超过容忍 → 判定丢失(返回 null)", r === null) -} - -/* 5) 彻底无关的锚点:fuzzy 相似度很低 → 默认阈值下也判丢失 */ -{ - const anchor = { - quote: { - exact: "这是一段与原文毫不相干的陌生句子内容", - prefix: "", - suffix: "", - }, - } - const r = locateOffsets(longText, anchor) - ok("无关锚点:默认阈值下判定丢失(返回 null)", r === null) -} - -/* 6) fuzzySubstring 直接单测:空白归一化后子串近似匹配 */ -{ - const hay = normalizeWhitespace("alpha beta gamma delta") - const m = fuzzySubstring(hay, "beta gama") // gamma 少一个 m - ok( - "fuzzySubstring:容忍单字错漏,定位到 beta gamma 区间", - m !== null && hay.slice(m.start, m.end).startsWith("beta ") - ) -} - -process.exit(failed) diff --git a/e2e/thread-chat/thread-chat-boot.test.mjs b/e2e/thread-chat/thread-chat-boot.test.mjs deleted file mode 100644 index 2fdeaaba..00000000 --- a/e2e/thread-chat/thread-chat-boot.test.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import assert from "node:assert/strict" -import { - threadChatBootSeed, - threadChatBootSeedOrFallback, -} from "../../app/thread-chat/net/boot/thread-chat-boot.ts" - -const empty = threadChatBootSeed({ state: null, generations: [] }) -assert.equal(empty.schemaVersion, 2) -assert.deepEqual(Object.keys(empty.threads), ["main"]) -assert.equal(empty.threads.main.activeLeafMessageId, null) - -const stored = structuredClone(empty) -stored.threads.main.title = "restored" -const restored = threadChatBootSeed({ state: stored, generations: [] }) -assert.equal(restored.threads.main.title, "restored") -assert.equal(restored.schemaVersion, 2) - -const invalidWarnings = [] -const fallback = threadChatBootSeedOrFallback( - { - state: { ...stored, schemaVersion: 1 }, - generations: [], - }, - (error) => invalidWarnings.push(error) -) -assert.equal(fallback.schemaVersion, 2) -assert.deepEqual(Object.keys(fallback.threads), ["main"]) -assert.equal(invalidWarnings.length, 1) - -console.log( - "PASS thread chat boot selects empty, sanitized, or invalid-state fallback seed" -) diff --git a/e2e/thread-chat/thread-chat-topbar.test.mjs b/e2e/thread-chat/thread-chat-topbar.test.mjs deleted file mode 100644 index cf47dc6a..00000000 --- a/e2e/thread-chat/thread-chat-topbar.test.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" -import { columnCountChoices } from "../../app/thread-chat/orchestration/navigation/thread-chat-topbar-logic.ts" - -assert.deepEqual(columnCountChoices(null), [ - { value: "auto", label: "自适应", active: true }, - { value: 2, label: "2", active: false }, - { value: 3, label: "3", active: false }, - { value: 4, label: "4", active: false }, -]) -assert.deepEqual( - columnCountChoices(3).filter((choice) => choice.active), - [{ value: 3, label: "3", active: true }] -) - -const [topbar, variantPicker] = await Promise.all([ - readFile( - new URL( - "../../app/thread-chat/orchestration/navigation/thread-chat-topbar.tsx", - import.meta.url - ), - "utf8" - ), - readFile( - new URL( - "../../app/thread-chat/chat/actions/turn-variant-picker.tsx", - import.meta.url - ), - "utf8" - ), -]) -assert.match(topbar, /aria-label="视图模式"/) -assert.match(topbar, /aria-label="列数"/) -assert.match(topbar, /aria-pressed=\{viewMode === "columns"\}/) -assert.match(topbar, /aria-pressed=\{choice\.active\}/) -assert.match(topbar, /aria-pressed=\{placementMode === "replace"\}/) -assert.match(variantPicker, /role="group"[\s\S]*aria-label="回复版本切换"/) - -console.log( - "PASS Thread Chat topbar exposes one active auto/forced column choice" -) diff --git a/e2e/thread-chat/thread-generation-context.test.mjs b/e2e/thread-chat/thread-generation-context.test.mjs deleted file mode 100644 index 7e69a250..00000000 --- a/e2e/thread-chat/thread-generation-context.test.mjs +++ /dev/null @@ -1,170 +0,0 @@ -import assert from "node:assert/strict" -import { prepareThreadGenerationContext } from "../../app/api/chat/thread-generation-context.ts" - -const messages = [ - { id: "u0", role: "user", parts: [{ type: "text", text: "client" }] }, -] -const identity = { - treeId: "11111111-1111-4111-8111-111111111111", - threadId: "main", - userMessageId: "u1", - assistantMessageId: "a1", - generationId: "22222222-2222-4222-8222-222222222222", - intent: { kind: "persisted-turn" }, -} - -function dependencies(overrides = {}) { - return { - async prepare() { - assert.fail("prepare should be overridden for a valid Thread request") - }, - summarize: (generation) => generation, - compile: () => [], - createController: () => new AbortController(), - register() {}, - unregister() {}, - observe: () => ({ stop() {}, done: Promise.resolve() }), - async settleInitializationFailure() {}, - startErrorResponse: () => Response.json({ mapped: true }, { status: 409 }), - ...overrides, - } -} - -const linear = await prepareThreadGenerationContext( - { - userId: "user-1", - modelId: "minimax-m2", - messages, - threadChat: undefined, - }, - dependencies() -) -assert.equal(linear.kind, "ready") -assert.equal(linear.persistence, null) -assert.equal(linear.authoritativeMessages, messages) -assert.equal(linear.generationController, null) - -const existingGeneration = { id: identity.generationId, status: "streaming" } -const duplicate = await prepareThreadGenerationContext( - { - userId: "user-1", - modelId: "glm-5.3", - messages, - threadChat: identity, - }, - dependencies({ - async prepare(input) { - assert.equal(input.userId, "user-1") - assert.equal(input.modelId, "glm-5.3") - assert.equal(input.generationId, identity.generationId) - return { created: false, generation: existingGeneration } - }, - summarize(generation) { - return { id: generation.id, status: generation.status } - }, - }) -) -assert.equal(duplicate.kind, "response") -assert.equal(duplicate.response.status, 202) -assert.deepEqual(await duplicate.response.json(), { - generation: existingGeneration, -}) - -const authoritativeMessages = [ - { id: "persisted", role: "user", parts: [{ type: "text", text: "db" }] }, -] -const controller = new AbortController() -const observer = { stop() {}, done: Promise.resolve() } -const registrations = [] -const success = await prepareThreadGenerationContext( - { - userId: "user-1", - modelId: "glm-5.3", - messages, - threadChat: identity, - }, - dependencies({ - async prepare() { - return { - created: true, - revision: 7, - state: { - threads: { main: { anchorText: " authoritative anchor " } }, - }, - } - }, - compile(input) { - assert.equal(input.threadId, "main") - assert.equal(input.excludeAssistantMessageId, "a1") - return authoritativeMessages - }, - createController: () => controller, - register(generationId, receivedController) { - registrations.push([generationId, receivedController]) - }, - observe: () => observer, - }) -) -assert.equal(success.kind, "ready") -assert.equal(success.persistence.generationId, identity.generationId) -assert.equal(success.authoritativeMessages, authoritativeMessages) -assert.equal(success.authoritativeAnchorText, " authoritative anchor ") -assert.equal(success.preparedRevision, 7) -assert.equal(success.generationController, controller) -assert.equal(success.generationObserver, observer) -assert.deepEqual(registrations, [[identity.generationId, controller]]) - -const mappedFailure = await prepareThreadGenerationContext( - { - userId: "user-1", - modelId: "glm-5.3", - messages, - threadChat: identity, - }, - dependencies({ - async prepare() { - throw new Error("conflict") - }, - }) -) -assert.equal(mappedFailure.kind, "response") -assert.equal(mappedFailure.response.status, 409) -assert.deepEqual(await mappedFailure.response.json(), { mapped: true }) - -const initializationSettlements = [] -const initializationFailure = await prepareThreadGenerationContext( - { - userId: "user-1", - modelId: "glm-5.3", - messages, - threadChat: identity, - unbilledPreview: false, - }, - dependencies({ - async prepare() { - return { - created: true, - revision: 8, - state: { threads: { main: { anchorText: null } } }, - } - }, - compile() { - throw new Error("compile failed after generation creation") - }, - async settleInitializationFailure(input) { - initializationSettlements.push(input) - }, - }) -) -assert.equal(initializationFailure.kind, "response") -assert.equal(initializationFailure.response.status, 500) -assert.equal(initializationSettlements.length, 1) -assert.equal( - initializationSettlements[0].persistence.generationId, - identity.generationId -) -assert.equal(initializationSettlements[0].usageUnavailable, true) - -console.log( - "PASS thread generation context owns start idempotency, authoritative state, cancellation, and post-start settlement" -) diff --git a/e2e/thread-chat/thread-switcher-panel.test.mjs b/e2e/thread-chat/thread-switcher-panel.test.mjs deleted file mode 100644 index edb2171f..00000000 --- a/e2e/thread-chat/thread-switcher-panel.test.mjs +++ /dev/null @@ -1,93 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" -import React from "react" -import { renderToStaticMarkup } from "react-dom/server" -import test from "node:test" - -import { ThreadSwitcherPanel } from "../../app/thread-chat/orchestration/navigation/thread-switcher-panel.tsx" - -const thread = (overrides) => ({ - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title: "Main research", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: ["branch"], - messages: [], - activeLeafMessageId: null, - lastActive: 1, - ...overrides, -}) -const state = { - schemaVersion: 2, - threads: { - main: thread({}), - branch: thread({ - id: "branch", - parentId: "main", - depth: 1, - title: "Focused branch", - anchorText: "selected source", - footnote: 1, - children: [], - }), - }, - artifacts: {}, - artifactOrder: [], - recents: ["branch"], - footnoteCounter: 1, - seq: 1, - tick: 1, -} -const noop = () => {} - -await test("global panel renders search, recents, tree rows, and placement status", () => { - const html = renderToStaticMarkup( - React.createElement(ThreadSwitcherPanel, { - state, - mode: { kind: "global" }, - slots: [{ id: "branch", folded: false }], - recents: ["branch"], - onPick: noop, - }) - ) - - assert.match(html, /搜索会话(标题 \/ 划选原文)/) - assert.match(html, /最近访问/) - assert.match(html, /Main research/) - assert.match(html, /Focused branch/) - assert.match(html, /第 2 列/) -}) - -await test("subtree panel owns its title, empty state, and compact footer", () => { - const html = renderToStaticMarkup( - React.createElement(ThreadSwitcherPanel, { - state, - mode: { kind: "subtree", rootId: "branch", x: 10, y: 20 }, - slots: [], - recents: [], - onPick: noop, - }) - ) - - assert.match(html, /『Focused branch』的子分支/) - assert.match(html, /还没有子分支/) - assert.match(html, /点击行打开(列满走当前策略)/) - assert.doesNotMatch(html, / { - const shell = await readFile( - new URL( - "../../app/thread-chat/orchestration/navigation/thread-switcher.tsx", - import.meta.url - ), - "utf8" - ) - - assert.match(shell, / - message.role === "assistant" - ? { ...message, status: "streaming" } - : message - ), - }), - null -) - -const originalFetch = globalThis.fetch -let capturedRequest = null -try { - globalThis.fetch = async (input, init) => { - capturedRequest = { input, init } - return Response.json({ title: " Unified title " }) - } - assert.equal( - await requestThreadTitle({ kind: "main", question: "hello" }), - "Unified title" - ) -} finally { - globalThis.fetch = originalFetch -} - -assert.equal(capturedRequest.input, "/api/title") -assert.equal(capturedRequest.init.method, "POST") -assert.deepEqual(JSON.parse(capturedRequest.init.body), { - kind: "main", - question: "hello", -}) -assert.equal( - parseThreadTitleInput({ - anchorText, - question: "legacy request", - answer: "missing explicit kind", - }), - null -) -assert.deepEqual( - parseThreadTitleInput({ - kind: "branch", - anchorText, - question: "explicit request", - answer: "accepted", - }), - { - kind: "branch", - anchorText, - question: "explicit request", - answer: "accepted", - } -) - -console.log( - "PASS thread title candidates and client request share the unified explicit title contract" -) diff --git a/e2e/thread-chat/thread-tree-schema-source.test.mjs b/e2e/thread-chat/thread-tree-schema-source.test.mjs deleted file mode 100644 index e7f17e5f..00000000 --- a/e2e/thread-chat/thread-tree-schema-source.test.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" -import test from "node:test" - -import { THREAD_TREE_SCHEMA_VERSION } from "../../constants/thread-chat.ts" - -const typesUrl = new URL( - "../../lib/thread-chat/domain/types.ts", - import.meta.url -) -const seedUrl = new URL("../../app/thread-chat/core/seed.ts", import.meta.url) -const treeRouteUrl = new URL( - "../../app/api/branch-trees/[treeId]/route.ts", - import.meta.url -) - -test("thread tree type, parser, and seed share one schema version", async () => { - const [types, seed, treeRoute] = await Promise.all([ - readFile(typesUrl, "utf8"), - readFile(seedUrl, "utf8"), - readFile(treeRouteUrl, "utf8"), - ]) - - assert.equal(THREAD_TREE_SCHEMA_VERSION, 2) - assert.match(types, /schemaVersion:\s*typeof THREAD_TREE_SCHEMA_VERSION/) - assert.match(seed, /schemaVersion:\s*THREAD_TREE_SCHEMA_VERSION/) - assert.doesNotMatch(types, /schemaVersion:\s*2/) - assert.doesNotMatch(seed, /schemaVersion:\s*2/) - assert.doesNotMatch(treeRoute, /incomingSchemaVersion\s*!==\s*2/) - assert.match(treeRoute, /incomingSchemaVersion\s*!==\s*THREAD_TREE_SCHEMA_VERSION/) -}) diff --git a/e2e/thread-chat/tool-step-policy.test.mjs b/e2e/thread-chat/tool-step-policy.test.mjs deleted file mode 100644 index 0a17bac5..00000000 --- a/e2e/thread-chat/tool-step-policy.test.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import assert from "node:assert/strict" -import { createToolStepPolicy } from "../../app/api/chat/tool-step-policy.ts" -import { researchToolNames } from "../../app/api/chat/research-tool-capabilities.ts" - -assert.deepEqual(researchToolNames("answer"), []) -assert.deepEqual(researchToolNames("fetch"), ["readUrl"]) -assert.deepEqual(researchToolNames("search"), ["webSearch", "readUrl"]) -assert.deepEqual(researchToolNames("research"), ["webSearch", "readUrl"]) - -assert.equal( - createToolStepPolicy({ - isThreadChat: false, - markdownArtifactRequested: false, - researchMode: "answer", - }), - undefined -) - -const fetchPolicy = createToolStepPolicy({ - isThreadChat: true, - markdownArtifactRequested: true, - researchMode: "fetch", -}) -assert.deepEqual(fetchPolicy?.({ stepNumber: 0 }), { - activeTools: ["createMarkdownArtifact", "readUrl"], - toolChoice: { type: "tool", toolName: "readUrl" }, -}) -assert.deepEqual(fetchPolicy?.({ stepNumber: 1 }), { - activeTools: ["createMarkdownArtifact", "readUrl"], -}) - -for (const researchMode of ["search", "research"]) { - const policy = createToolStepPolicy({ - isThreadChat: false, - markdownArtifactRequested: false, - researchMode, - }) - assert.deepEqual(policy?.({ stepNumber: 0 }), { - activeTools: ["webSearch", "readUrl"], - toolChoice: { type: "tool", toolName: "webSearch" }, - }) - assert.deepEqual(policy?.({ stepNumber: 2 }), { - activeTools: ["webSearch", "readUrl"], - }) -} - -const markdownPolicy = createToolStepPolicy({ - isThreadChat: true, - markdownArtifactRequested: true, - researchMode: "answer", -}) -assert.deepEqual(markdownPolicy?.({ stepNumber: 0 }), { - activeTools: ["createMarkdownArtifact"], - toolChoice: { type: "tool", toolName: "createMarkdownArtifact" }, -}) -assert.deepEqual(markdownPolicy?.({ stepNumber: 1 }), { - activeTools: ["createMarkdownArtifact"], -}) - -console.log( - "PASS chat tool step policy preserves route priority and later-step availability" -) diff --git a/e2e/thread-chat/topbar-css-ownership.test.mjs b/e2e/thread-chat/topbar-css-ownership.test.mjs deleted file mode 100644 index d8114807..00000000 --- a/e2e/thread-chat/topbar-css-ownership.test.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import assert from "node:assert/strict" -import { existsSync, readFileSync } from "node:fs" - -const topbar = readFileSync( - new URL("../../app/thread-chat/styles/topbar.css", import.meta.url), - "utf8" -) -const canvas = readFileSync( - new URL("../../app/thread-chat/styles/canvas.css", import.meta.url), - "utf8" -) -const entry = readFileSync( - new URL("../../app/thread-chat/thread-chat.css", import.meta.url), - "utf8" -) -const legacyHelpUrl = new URL( - "../../app/thread-chat/styles/topbar-help.css", - import.meta.url -) - -assert.equal(topbar.match(/\.tc \.seg button\.mode\s*\{/g)?.length, 1) -assert.match( - topbar, - /\.tc \.seg button\.mode\s*\{[\s\S]*?display:\s*inline-flex;[\s\S]*?align-items:\s*center;[\s\S]*?gap:\s*5px;/ -) -assert.doesNotMatch(canvas, /\.seg button\.mode/) -assert.equal(topbar.match(/\.tc \.tbtn\.help\s*\{/g)?.length, 1) -assert.equal(topbar.match(/\.tc \.tbtn\.help:hover\s*\{/g)?.length, 1) -assert.doesNotMatch(entry, /topbar-help\.css/) -assert.equal(existsSync(legacyHelpUrl), false) - -console.log("PASS topbar controls are owned only by topbar.css") diff --git a/e2e/thread-chat/tree-deletion-db.test.mjs b/e2e/thread-chat/tree-deletion-db.test.mjs deleted file mode 100644 index 4e653a63..00000000 --- a/e2e/thread-chat/tree-deletion-db.test.mjs +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Atomic tree deletion and generation-start race: - * node --env-file=.env.local --import tsx e2e/thread-chat/tree-deletion-db.test.mjs - */ -import assert from "node:assert/strict" -import { randomUUID } from "node:crypto" -import { eq } from "drizzle-orm" -import { db } from "../../lib/db/index.ts" -import { branchGenerations, branchTrees, user } from "../../lib/db/schema.ts" -import { - GenerationRepositoryError, - prepareGeneration, -} from "../../lib/thread-chat-generation/start-generation-repository.ts" -import { deleteOwnedTreeIfIdle } from "../../lib/thread-chat-generation/tree-repository.ts" - -const suffix = randomUUID() -const userId = `tree-deletion-${suffix}` -const treeId = randomUUID() -const generationId = randomUUID() - -const state = { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title: "主线", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages: [ - { - id: "u1", - parentMessageId: null, - role: "user", - text: "并发删除测试", - forks: [], - }, - { - id: "a1", - parentMessageId: "u1", - role: "assistant", - text: "", - forks: [], - generationId, - status: "pending", - }, - ], - activeLeafMessageId: "a1", - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 2, - tick: 1, -} - -async function run() { - await db.insert(user).values({ - id: userId, - name: "tree deletion test", - email: `${userId}@example.test`, - emailVerified: true, - }) - await db.insert(branchTrees).values({ id: treeId, userId, state }) - - const [deleteAttempt, startAttempt] = await Promise.allSettled([ - deleteOwnedTreeIfIdle({ userId, treeId }), - prepareGeneration({ - userId, - treeId, - threadId: "main", - modelId: "glm-5.3", - userMessageId: "u1", - assistantMessageId: "a1", - generationId, - intent: { kind: "persisted-turn" }, - }), - ]) - - assert.equal(deleteAttempt.status, "fulfilled") - if (deleteAttempt.value === "deleted") { - assert.equal(startAttempt.status, "rejected") - assert.ok(startAttempt.reason instanceof GenerationRepositoryError) - assert.equal(startAttempt.reason.code, "not_found") - } else { - assert.equal(deleteAttempt.value, "generation_running") - assert.equal(startAttempt.status, "fulfilled") - assert.equal(startAttempt.value.created, true) - - await db - .update(branchGenerations) - .set({ status: "completed", finishedAt: new Date() }) - .where(eq(branchGenerations.id, generationId)) - assert.equal( - await deleteOwnedTreeIfIdle({ userId, treeId }), - "deleted" - ) - } - - assert.equal( - await deleteOwnedTreeIfIdle({ userId, treeId }), - "not_found", - "repeated deletion must remain idempotent" - ) - assert.equal( - (await db.select().from(branchTrees).where(eq(branchTrees.id, treeId))) - .length, - 0 - ) - - console.log( - "PASS tree deletion serializes with generation start and remains idempotent" - ) -} - -try { - await run() -} finally { - await db.delete(user).where(eq(user.id, userId)) -} diff --git a/e2e/thread-chat/tree-list-db.test.mjs b/e2e/thread-chat/tree-list-db.test.mjs deleted file mode 100644 index 4c27d52d..00000000 --- a/e2e/thread-chat/tree-list-db.test.mjs +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Owner-scoped tree list projection: - * node --env-file=.env.local --import tsx e2e/thread-chat/tree-list-db.test.mjs - */ -import assert from "node:assert/strict" -import { randomUUID } from "node:crypto" -import { inArray } from "drizzle-orm" -import { db } from "../../lib/db/index.ts" -import { branchTrees, user } from "../../lib/db/schema.ts" -import { listOwnedTreeSummaries } from "../../lib/thread-chat-generation/tree-repository.ts" - -const suffix = randomUUID() -const ownerId = `tree-list-owner-${suffix}` -const strangerId = `tree-list-stranger-${suffix}` -const treeIds = [randomUUID(), randomUUID(), randomUUID()] - -const stateWithThreads = (threads) => ({ schemaVersion: 2, threads }) - -async function run() { - await db.insert(user).values([ - { - id: ownerId, - name: "tree list owner", - email: `${ownerId}@example.test`, - emailVerified: true, - }, - { - id: strangerId, - name: "tree list stranger", - email: `${strangerId}@example.test`, - emailVerified: true, - }, - ]) - - const older = new Date("2026-01-01T00:00:00.000Z") - const newer = new Date("2026-01-02T00:00:00.000Z") - await db.insert(branchTrees).values([ - { - id: treeIds[0], - userId: ownerId, - title: "Derived title", - customTitle: "Custom title", - state: stateWithThreads({ main: {}, child: {} }), - updatedAt: older, - }, - { - id: treeIds[1], - userId: ownerId, - title: null, - customTitle: null, - state: stateWithThreads([]), - updatedAt: newer, - }, - { - id: treeIds[2], - userId: strangerId, - title: "Private stranger tree", - state: stateWithThreads({ main: {} }), - updatedAt: new Date("2026-01-03T00:00:00.000Z"), - }, - ]) - - const rows = await listOwnedTreeSummaries(ownerId) - - assert.deepEqual( - rows.map(({ id, title, threadCount }) => ({ id, title, threadCount })), - [ - { id: treeIds[1], title: "未命名对话", threadCount: 0 }, - { id: treeIds[0], title: "Custom title", threadCount: 2 }, - ] - ) - assert.deepEqual( - rows.map(({ updatedAt }) => updatedAt.toISOString()), - [newer.toISOString(), older.toISOString()] - ) - - console.log( - "PASS tree list preserves owner isolation, title fallback, poison-row defense, count, and ordering" - ) -} - -try { - await run() -} finally { - await db.delete(branchTrees).where(inArray(branchTrees.id, treeIds)) - await db.delete(user).where(inArray(user.id, [ownerId, strangerId])) -} diff --git a/e2e/thread-chat/tree-list-repository-ownership.test.mjs b/e2e/thread-chat/tree-list-repository-ownership.test.mjs deleted file mode 100644 index ad94b7af..00000000 --- a/e2e/thread-chat/tree-list-repository-ownership.test.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" - -const route = await readFile( - new URL("../../app/api/branch-trees/route.ts", import.meta.url), - "utf8" -) -const repository = await readFile( - new URL( - "../../lib/thread-chat-generation/tree-repository.ts", - import.meta.url - ), - "utf8" -) - -assert.match(route, /listOwnedTreeSummaries\(userId\)/) -assert.doesNotMatch(route, /drizzle-orm|@\/lib\/db|branchTrees/) -assert.match(repository, /function listOwnedTreeSummaries/) -assert.match(repository, /jsonb_object_keys/) -assert.match(repository, /\.limit\(100\)/) - -console.log("PASS tree list route delegates persistence to the repository") diff --git a/e2e/thread-chat/tree-list-row.test.mjs b/e2e/thread-chat/tree-list-row.test.mjs deleted file mode 100644 index c3d7ea0e..00000000 --- a/e2e/thread-chat/tree-list-row.test.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" -import React from "react" -import { renderToStaticMarkup } from "react-dom/server" -import test from "node:test" - -import { CUSTOM_TITLE_MAX_LEN } from "../../constants/thread-chat.ts" -import { TreeListRow } from "../../app/thread-chat/orchestration/navigation/tree-list-row.tsx" - -const noop = () => {} -const baseProps = { - item: { - id: "tree-1", - title: "Research notes", - updatedAt: "", - threadCount: 3, - }, - isCurrent: true, - unsaved: false, - editing: false, - confirming: false, - deleting: false, - draft: "Research notes", - onSelect: noop, - onDraftChange: noop, - onCancelEdit: noop, - onCommitEdit: noop, - onStartEdit: noop, - onRequestDelete: noop, - onConfirmDelete: noop, - onCancelDelete: noop, -} - -await test("saved tree row owns title, current badge, branch count, and actions", () => { - const html = renderToStaticMarkup(React.createElement(TreeListRow, baseProps)) - - assert.match(html, /Research notes/) - assert.match(html, />当前 { - const unsaved = renderToStaticMarkup( - React.createElement(TreeListRow, { ...baseProps, unsaved: true }) - ) - assert.match(unsaved, />未保存 { - const html = renderToStaticMarkup( - React.createElement(TreeListRow, { ...baseProps, confirming: true }) - ) - - assert.match(html, /确认删除/) - assert.match(html, /title="取消"/) - assert.doesNotMatch(html, /title="重命名"|title="删除此对话"/) -}) - -await test("tree list composes the row instead of owning its icon actions", async () => { - const source = await readFile( - new URL( - "../../app/thread-chat/orchestration/navigation/tree-list.tsx", - import.meta.url - ), - "utf8" - ) - - assert.match(source, / ({ - id, - name: `tree claimant ${index}`, - email: `${id}@example.test`, - emailVerified: true, - })) - ) - await db.insert(branchTrees).values({ id: treeId, userId: null, state }) - - const claims = await Promise.all( - claimantIds.map((userId) => - loadOwnedOrClaimLegacyTree({ userId, treeId }) - ) - ) - assert.equal( - claims.filter(Boolean).length, - 1, - "only one concurrent exact-URL visitor may claim the legacy tree" - ) - - const winnerIndex = claims.findIndex(Boolean) - const winnerId = claimantIds[winnerIndex] - const loserId = claimantIds[1 - winnerIndex] - const [persisted] = await db - .select({ userId: branchTrees.userId }) - .from(branchTrees) - .where(eq(branchTrees.id, treeId)) - assert.equal(persisted.userId, winnerId) - assert.ok(await loadOwnedOrClaimLegacyTree({ userId: winnerId, treeId })) - assert.equal( - await loadOwnedOrClaimLegacyTree({ userId: loserId, treeId }), - null, - "a claimed tree must never transfer to a second user" - ) - - console.log( - "PASS legacy tree exact-URL claim is atomic, sticky, and owner-isolated" - ) -} - -try { - await run() -} finally { - await db.delete(branchTrees).where(eq(branchTrees.id, treeId)) - await db.delete(user).where(inArray(user.id, claimantIds)) -} diff --git a/e2e/thread-chat/tree-persistence-write.test.mjs b/e2e/thread-chat/tree-persistence-write.test.mjs deleted file mode 100644 index 4fd66f54..00000000 --- a/e2e/thread-chat/tree-persistence-write.test.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import assert from "node:assert/strict" -import { readFile } from "node:fs/promises" - -import { emptySeedState } from "../../app/thread-chat/core/seed.ts" -import { - getKnownTreeRevision, - saveTree, - saveTreeStrict, - setKnownTreeRevision, -} from "../../app/thread-chat/net/persistence/persist.ts" - -const source = await readFile( - new URL("../../app/thread-chat/net/persistence/persist.ts", import.meta.url), - "utf8" -) -assert.equal( - source.match(/method: "PUT"/g)?.length, - 1, - "one shared PUT primitive should own the tree write protocol" -) - -const treeId = "11111111-1111-4111-8111-111111111111" -const calls = [] -const originalFetch = globalThis.fetch -globalThis.fetch = async (url, init) => { - calls.push({ url, init, body: JSON.parse(init.body) }) - return Response.json({ ok: true, revision: calls.length }) -} - -try { - setKnownTreeRevision(treeId, 0) - await saveTreeStrict(treeId, emptySeedState(), "strict") - await saveTree(treeId, emptySeedState(), "best effort") - // /api/chat 可能在上面的第二次存盘之后才返回启动时捕获的旧 revision; - // 旧响应不得让下一次存盘倒退到已经失效的 baseRevision。 - setKnownTreeRevision(treeId, 1) - await saveTreeStrict(treeId, emptySeedState(), "after stale response") -} finally { - globalThis.fetch = originalFetch -} - -assert.equal(calls.length, 3) -assert.deepEqual( - calls.map((call) => [call.init.method, call.body.title, call.body.baseRevision]), - [ - ["PUT", "strict", 0], - ["PUT", "best effort", 1], - ["PUT", "after stale response", 2], - ] -) -assert.equal(getKnownTreeRevision(treeId), 3) - -console.log( - "PASS tree saves share one PUT primitive and stale responses cannot regress revision" -) diff --git a/e2e/thread-chat/tree-rename-db.test.mjs b/e2e/thread-chat/tree-rename-db.test.mjs deleted file mode 100644 index aff02bbe..00000000 --- a/e2e/thread-chat/tree-rename-db.test.mjs +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Owner-scoped tree rename: - * node --env-file=.env.local --import tsx e2e/thread-chat/tree-rename-db.test.mjs - */ -import assert from "node:assert/strict" -import { randomUUID } from "node:crypto" -import { eq, inArray } from "drizzle-orm" -import { db } from "../../lib/db/index.ts" -import { branchTrees, user } from "../../lib/db/schema.ts" -import { renameOwnedTree } from "../../lib/thread-chat-generation/tree-repository.ts" - -const suffix = randomUUID() -const ownerId = `tree-rename-owner-${suffix}` -const strangerId = `tree-rename-stranger-${suffix}` -const treeId = randomUUID() - -const state = { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title: "主线", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages: [], - activeLeafMessageId: null, - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 0, - tick: 1, -} - -async function run() { - await db.insert(user).values([ - { - id: ownerId, - name: "tree rename owner", - email: `${ownerId}@example.test`, - emailVerified: true, - }, - { - id: strangerId, - name: "tree rename stranger", - email: `${strangerId}@example.test`, - emailVerified: true, - }, - ]) - await db.insert(branchTrees).values({ - id: treeId, - userId: ownerId, - title: "Derived title", - state, - }) - - assert.equal( - await renameOwnedTree({ - userId: strangerId, - treeId, - customTitle: "Hijacked", - }), - false - ) - assert.equal( - await renameOwnedTree({ - userId: ownerId, - treeId, - customTitle: "My research", - }), - true - ) - assert.equal( - await renameOwnedTree({ - userId: ownerId, - treeId: randomUUID(), - customTitle: "Missing", - }), - false - ) - - const [persisted] = await db - .select({ - title: branchTrees.title, - customTitle: branchTrees.customTitle, - }) - .from(branchTrees) - .where(eq(branchTrees.id, treeId)) - assert.equal(persisted.title, "Derived title") - assert.equal(persisted.customTitle, "My research") - - console.log("PASS tree rename is owner-scoped and preserves derived title") -} - -try { - await run() -} finally { - await db.delete(branchTrees).where(eq(branchTrees.id, treeId)) - await db.delete(user).where(inArray(user.id, [ownerId, strangerId])) -} diff --git a/e2e/thread-chat/tree-revision-db.test.mjs b/e2e/thread-chat/tree-revision-db.test.mjs deleted file mode 100644 index 63ee28e2..00000000 --- a/e2e/thread-chat/tree-revision-db.test.mjs +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Revision-controlled message graph commands: - * node --import tsx e2e/thread-chat/tree-revision-db.test.mjs - */ -import assert from "node:assert/strict" -import { randomUUID } from "node:crypto" -import { eq } from "drizzle-orm" -import { db } from "../../lib/db/index.ts" -import { branchTrees, user } from "../../lib/db/schema.ts" -import { prepareGeneration } from "../../lib/thread-chat-generation/start-generation-repository.ts" -import { - switchActiveLeafForOwner, - TreeCommandError, -} from "../../lib/thread-chat-generation/tree-repository.ts" - -const suffix = randomUUID() -const ownerId = `tree-revision-owner-${suffix}` -const strangerId = `tree-revision-stranger-${suffix}` -const treeId = randomUUID() - -const state = { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.2", - parentId: null, - depth: 0, - title: "主线", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages: [ - { - id: "u1", - parentMessageId: null, - role: "user", - text: "问题", - forks: [], - }, - { - id: "a1", - parentMessageId: "u1", - role: "assistant", - text: "版本 A", - forks: [], - status: "done", - }, - { - id: "a2", - parentMessageId: "u1", - role: "assistant", - text: "版本 B", - forks: [], - status: "done", - }, - ], - activeLeafMessageId: "a1", - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 3, - tick: 1, -} - -async function expectTreeError(promise, code, currentRevision) { - await assert.rejects( - promise, - (error) => - error instanceof TreeCommandError && - error.code === code && - (currentRevision === undefined || - error.currentRevision === currentRevision) - ) -} - -async function run() { - await db.insert(user).values([ - { - id: ownerId, - name: "tree revision owner", - email: `${ownerId}@example.test`, - emailVerified: true, - }, - { - id: strangerId, - name: "tree revision stranger", - email: `${strangerId}@example.test`, - emailVerified: true, - }, - ]) - await db.insert(branchTrees).values({ - id: treeId, - userId: ownerId, - state, - revision: 0, - }) - - const switched = await switchActiveLeafForOwner({ - userId: ownerId, - treeId, - threadId: "main", - assistantMessageId: "a2", - baseRevision: 0, - }) - assert.equal(switched.revision, 1) - assert.equal(switched.thread.activeLeafMessageId, "a2") - - await expectTreeError( - switchActiveLeafForOwner({ - userId: ownerId, - treeId, - threadId: "main", - assistantMessageId: "a1", - baseRevision: 0, - }), - "tree_revision_conflict", - 1 - ) - await expectTreeError( - switchActiveLeafForOwner({ - userId: strangerId, - treeId, - threadId: "main", - assistantMessageId: "a1", - baseRevision: 1, - }), - "not_found" - ) - await expectTreeError( - switchActiveLeafForOwner({ - userId: ownerId, - treeId, - threadId: "main", - assistantMessageId: "missing", - baseRevision: 1, - }), - "invalid_turn" - ) - - const generationId = randomUUID() - const prepared = await prepareGeneration({ - userId: ownerId, - treeId, - threadId: "main", - modelId: "glm-5.2", - userMessageId: "u1", - assistantMessageId: "a3", - generationId, - intent: { - kind: "regenerate-assistant", - sourceAssistantMessageId: "a2", - }, - }) - assert.equal(prepared.revision, 2) - - await expectTreeError( - switchActiveLeafForOwner({ - userId: ownerId, - treeId, - threadId: "main", - assistantMessageId: "a1", - baseRevision: 1, - }), - "tree_revision_conflict", - 2 - ) - - const [persisted] = await db - .select({ state: branchTrees.state, revision: branchTrees.revision }) - .from(branchTrees) - .where(eq(branchTrees.id, treeId)) - assert.equal(persisted.revision, 2) - assert.equal(persisted.state.threads.main.activeLeafMessageId, "a3") - assert.equal( - persisted.state.threads.main.messages.filter( - (message) => message.id === "a3" - ).length, - 1 - ) - - console.log( - "PASS active-leaf CAS, stale/unauthorized rejection and generation race" - ) -} - -try { - await run() -} finally { - await db.delete(user).where(eq(user.id, ownerId)) - await db.delete(user).where(eq(user.id, strangerId)) -} diff --git a/e2e/thread-chat/tree-save-db.test.mjs b/e2e/thread-chat/tree-save-db.test.mjs deleted file mode 100644 index 1ab46617..00000000 --- a/e2e/thread-chat/tree-save-db.test.mjs +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Owner-scoped tree CAS upsert: - * node --env-file=.env.local --import tsx e2e/thread-chat/tree-save-db.test.mjs - */ -import assert from "node:assert/strict" -import { randomUUID } from "node:crypto" -import { eq, inArray } from "drizzle-orm" -import { db } from "../../lib/db/index.ts" -import { branchTrees, user } from "../../lib/db/schema.ts" -import { saveOwnedTree } from "../../lib/thread-chat-generation/tree-repository.ts" - -const suffix = randomUUID() -const ownerId = `tree-save-owner-${suffix}` -const strangerId = `tree-save-stranger-${suffix}` -const treeId = randomUUID() - -function state(title) { - return { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title, - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages: [], - activeLeafMessageId: null, - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 0, - tick: 1, - } -} - -async function run() { - await db.insert(user).values([ - { - id: ownerId, - name: "tree save owner", - email: `${ownerId}@example.test`, - emailVerified: true, - }, - { - id: strangerId, - name: "tree save stranger", - email: `${strangerId}@example.test`, - emailVerified: true, - }, - ]) - - assert.deepEqual( - await saveOwnedTree({ - userId: ownerId, - treeId, - state: state("first"), - title: "First", - baseRevision: 0, - }), - { kind: "saved", revision: 1 } - ) - assert.deepEqual( - await saveOwnedTree({ - userId: ownerId, - treeId, - state: state("stale"), - title: "Stale", - baseRevision: 0, - }), - { kind: "conflict", revision: 1 } - ) - assert.deepEqual( - await saveOwnedTree({ - userId: strangerId, - treeId, - state: state("stranger"), - title: "Stranger", - baseRevision: 1, - }), - { kind: "not_found" } - ) - assert.deepEqual( - await saveOwnedTree({ - userId: ownerId, - treeId, - state: state("second"), - title: "Second", - baseRevision: 1, - }), - { kind: "saved", revision: 2 } - ) - - const [persisted] = await db - .select({ - userId: branchTrees.userId, - revision: branchTrees.revision, - title: branchTrees.title, - state: branchTrees.state, - }) - .from(branchTrees) - .where(eq(branchTrees.id, treeId)) - assert.equal(persisted.userId, ownerId) - assert.equal(persisted.revision, 2) - assert.equal(persisted.title, "Second") - assert.equal(persisted.state.threads.main.title, "second") - - console.log("PASS tree save owns create, revision CAS, and owner isolation") -} - -try { - await run() -} finally { - await db.delete(branchTrees).where(eq(branchTrees.id, treeId)) - await db.delete(user).where(inArray(user.id, [ownerId, strangerId])) -} diff --git a/e2e/thread-chat/tree-save-gate.test.mjs b/e2e/thread-chat/tree-save-gate.test.mjs deleted file mode 100644 index 6e6b32ea..00000000 --- a/e2e/thread-chat/tree-save-gate.test.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import assert from "node:assert/strict" -import { createTreeSaveGate } from "../../app/thread-chat/net/persistence/tree-save-gate.ts" - -const normal = createTreeSaveGate() -normal.markPending() -assert.equal(normal.takePendingFlush(), true) -assert.equal(normal.takePendingFlush(), false) - -const debounced = createTreeSaveGate() -debounced.markPending() -assert.equal(debounced.finishDebounce(), true) -assert.equal(debounced.takePendingFlush(), false) - -const deleted = createTreeSaveGate() -deleted.markPending() -deleted.setSuppressed(true) -assert.equal(deleted.isSuppressed(), true) -assert.equal(deleted.finishDebounce(), false) -assert.equal(deleted.takePendingFlush(), false) -deleted.setSuppressed(false) -assert.equal(deleted.isSuppressed(), false) - -console.log( - "PASS tree save gate flushes once and suppression clears pending resurrection writes" -) diff --git a/e2e/thread-chat/ui-state-snapshot.test.mjs b/e2e/thread-chat/ui-state-snapshot.test.mjs deleted file mode 100644 index eff038e6..00000000 --- a/e2e/thread-chat/ui-state-snapshot.test.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import assert from "node:assert/strict" -import { createTreeUiStateSnapshot } from "../../app/thread-chat/orchestration/workspace/ui-state-snapshot.ts" - -const slots = [{ id: "main", folded: false }] -const widths = { main: 420 } -const source = { - slots, - widths, - forceCols: 3, - mode: "replace", - viewMode: "canvas", - ignored: "not persisted", -} -const snapshot = createTreeUiStateSnapshot(source) - -assert.deepEqual(snapshot, { - slots, - widths, - forceCols: 3, - mode: "replace", - viewMode: "canvas", -}) -assert.equal(snapshot.slots, slots) -assert.equal(snapshot.widths, widths) -assert.equal("ignored" in snapshot, false) - -console.log( - "PASS UI persistence snapshots exactly the five tree-scoped workspace fields" -) diff --git a/e2e/thread-chat/umapis-models.test.mjs b/e2e/thread-chat/umapis-models.test.mjs deleted file mode 100644 index a6278e29..00000000 --- a/e2e/thread-chat/umapis-models.test.mjs +++ /dev/null @@ -1,87 +0,0 @@ -/** - * UMAPIS 模型注册与预览边界的纯验证: - * node --experimental-strip-types e2e/thread-chat/umapis-models.test.mjs - */ -import assert from "node:assert/strict" -import { - CHAT_MODELS, - THREAD_CHAT_MODELS, - UMAPIS_MODEL_IDS, - getChatModel, - isUnbilledPreviewModel, -} from "../../constants/model.ts" -import { MODEL_COST } from "../../constants/pricing.ts" -import { - DEFAULT_UMAPIS_BASE_URL, - getUMAPISApiKey, - isUMAPISConfigured, - normalizeUMAPISBaseURL, -} from "../../lib/ai/umapis.ts" - -const expectedModels = [ - ["claude-opus-4-6", "claude"], - ["claude-opus-4-6-thinking", "claude"], - ["claude-sonnet-4-6", "claude"], - ["claude-sonnet-4-6-thinking", "claude"], - ["claude-opus-4-7", "claude"], - ["claude-opus-4-7-thinking", "claude"], - ["claude-fable-5", "claude"], - ["claude-opus-5", "claude"], - ["claude-sonnet-5", "claude"], - ["claude-opus-4-8", "claude"], - ["claude-opus-4-8-thinking", "claude"], - ["claude-haiku-4-5", "claude"], - ["gemini-3.7-flash", "claude"], - ["grok-4.6", "claude"], - ["gpt-5.6-sol", "gpt"], - ["gpt-5.6-terra", "gpt"], -] -const models = CHAT_MODELS.filter((model) => model.provider === "umapis") - -assert.equal(models.length, expectedModels.length) -assert.deepEqual( - [...UMAPIS_MODEL_IDS], - expectedModels.map(([id]) => id) -) -assert.deepEqual( - models.map((model) => [model.upstreamModel, model.umapisCredentialGroup]), - expectedModels -) -assert.ok(models.every((model) => model.id.startsWith("umapis-"))) -assert.ok(models.every((model) => model.name.startsWith("UMAPIS · "))) -assert.ok(models.every((model) => model.unbilledPreview === true)) -assert.ok(models.every((model) => THREAD_CHAT_MODELS.includes(model))) -assert.ok(models.every((model) => !MODEL_COST[model.id])) -assert.ok(models.every(isUnbilledPreviewModel)) -assert.equal(isUnbilledPreviewModel(getChatModel("glm-5.3")), false) -assert.equal( - normalizeUMAPISBaseURL("https://www.umapis.com"), - DEFAULT_UMAPIS_BASE_URL -) -assert.equal( - normalizeUMAPISBaseURL("https://example.test/proxy/v1/"), - "https://example.test/proxy/v1" -) -assert.equal(normalizeUMAPISBaseURL(""), DEFAULT_UMAPIS_BASE_URL) - -const originalClaudeKey = process.env.UMAPIS_API_KEY_CLAUDE -const originalGptKey = process.env.UMAPIS_API_KEY_GPT -try { - process.env.UMAPIS_API_KEY_CLAUDE = " claude-test-key " - process.env.UMAPIS_API_KEY_GPT = " gpt-test-key " - assert.equal(getUMAPISApiKey("claude"), "claude-test-key") - assert.equal(getUMAPISApiKey("gpt"), "gpt-test-key") - assert.equal(isUMAPISConfigured("claude"), true) - assert.equal(isUMAPISConfigured("gpt"), true) - - delete process.env.UMAPIS_API_KEY_GPT - assert.equal(isUMAPISConfigured("gpt"), false) - assert.equal(isUMAPISConfigured("claude"), true) -} finally { - if (originalClaudeKey === undefined) delete process.env.UMAPIS_API_KEY_CLAUDE - else process.env.UMAPIS_API_KEY_CLAUDE = originalClaudeKey - if (originalGptKey === undefined) delete process.env.UMAPIS_API_KEY_GPT - else process.env.UMAPIS_API_KEY_GPT = originalGptKey -} - -console.log("PASS UMAPIS 注册、凭据分组、Prompt 输入可见性与未计费预览边界") diff --git a/e2e/thread-chat/usage-cost-evidence.test.mjs b/e2e/thread-chat/usage-cost-evidence.test.mjs deleted file mode 100644 index b32b6a81..00000000 --- a/e2e/thread-chat/usage-cost-evidence.test.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import assert from "node:assert/strict" -import { usageCostEvidence } from "../../lib/billing/usage-cost-evidence.ts" - -const gatewayMetadata = { gateway: { generationId: "gateway-generation" } } - -assert.deepEqual( - usageCostEvidence({ - provider: "openrouter", - steps: [ - { providerMetadata: { openrouter: { usage: { cost: 0.012 } } } }, - { providerMetadata: { openrouter: { usage: { cost: 0.008 } } } }, - ], - providerMetadata: gatewayMetadata, - }), - { source: "openrouter", costUsd: 0.02 } -) - -assert.deepEqual( - usageCostEvidence({ - provider: "openrouter", - steps: [{ providerMetadata: {} }], - providerMetadata: gatewayMetadata, - }), - { source: "vercel-gateway", generationId: "gateway-generation" } -) - -assert.deepEqual( - usageCostEvidence({ - provider: "ark", - steps: [{ providerMetadata: { openrouter: { usage: { cost: 999 } } } }], - providerMetadata: gatewayMetadata, - }), - { source: "vercel-gateway", generationId: "gateway-generation" } -) - -for (const providerMetadata of [ - undefined, - null, - {}, - { gateway: null }, - { gateway: { generationId: 42 } }, -]) { - assert.deepEqual( - usageCostEvidence({ - provider: "umapis", - steps: [], - providerMetadata, - }), - { source: "estimate" } - ) -} - -console.log( - "PASS usage cost evidence preserves OpenRouter, Gateway, and estimate priority" -) diff --git a/e2e/thread-chat/user-edit-command.test.mjs b/e2e/thread-chat/user-edit-command.test.mjs deleted file mode 100644 index e945bd4e..00000000 --- a/e2e/thread-chat/user-edit-command.test.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import assert from "node:assert/strict" -import { prepareUserEdit } from "../../app/thread-chat/net/commands/regeneration-command.ts" - -function state(activeLeafMessageId = "a1") { - return { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title: "主线", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages: [ - { - id: "u1", - parentMessageId: null, - role: "user", - text: "original", - quote: { text: "quoted" }, - forks: [], - }, - { - id: "a1", - parentMessageId: "u1", - role: "assistant", - text: "answer", - forks: [], - status: "done", - }, - ], - activeLeafMessageId, - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 1, - tick: 1, - } -} - -const originalState = state() -const prepared = prepareUserEdit(originalState, { - threadId: "main", - sourceUserMessageId: "u1", - text: " edited question ", - userMessageId: "u2", - assistantMessageId: "a2", - generationId: "11111111-1111-4111-8111-111111111111", -}) -assert.equal(prepared.ok, true) -assert.equal(prepared.start.userMessageId, "u2") -assert.equal(prepared.start.action.intent.kind, "edit-last-user") -assert.equal(prepared.start.action.sourceUserMessageId, "u1") -assert.deepEqual(prepared.start.action.patch.addedMessages, [ - { - id: "u2", - parentMessageId: null, - role: "user", - text: "edited question", - forks: [], - quote: { text: "quoted" }, - }, - { - id: "a2", - parentMessageId: "u2", - role: "assistant", - text: "", - forks: [], - generationId: "11111111-1111-4111-8111-111111111111", - status: "pending", - }, -]) -assert.equal(originalState.threads.main.messages[0].text, "original") -assert.equal(originalState.threads.main.messages.length, 2) - -for (const input of [ - { sourceUserMessageId: "missing", text: "edited" }, - { sourceUserMessageId: "u1", text: " " }, -]) { - assert.deepEqual( - prepareUserEdit(state(), { - threadId: "main", - ...input, - userMessageId: "u2", - assistantMessageId: "a2", - generationId: "g2", - }), - { - ok: false, - code: "not_latest_turn", - message: "只能编辑当前最后一轮用户消息", - } - ) -} - -console.log( - "PASS user edit command preserves source/quote and prepares trimmed user plus pending assistant siblings" -) diff --git a/e2e/thread-chat/user-turn-retry-command.test.mjs b/e2e/thread-chat/user-turn-retry-command.test.mjs deleted file mode 100644 index 6d9dbf90..00000000 --- a/e2e/thread-chat/user-turn-retry-command.test.mjs +++ /dev/null @@ -1,98 +0,0 @@ -import assert from "node:assert/strict" -import { prepareUserTurnRetry } from "../../app/thread-chat/net/commands/regeneration-command.ts" - -function state(messages, activeLeafMessageId) { - return { - schemaVersion: 2, - threads: { - main: { - id: "main", - modelId: "glm-5.3", - parentId: null, - depth: 0, - title: "主线", - anchorText: null, - forkFromMsgId: null, - footnote: null, - children: [], - messages, - activeLeafMessageId, - lastActive: 1, - }, - }, - artifacts: {}, - artifactOrder: [], - recents: [], - footnoteCounter: 0, - seq: 1, - tick: 1, - } -} - -const orphanUser = { - id: "u1", - parentMessageId: null, - role: "user", - text: "question", - forks: [], -} -const prepared = prepareUserTurnRetry(state([orphanUser], "u1"), { - threadId: "main", - userMessageId: "u1", - assistantMessageId: "a2", - generationId: "11111111-1111-4111-8111-111111111111", -}) -assert.equal(prepared.ok, true) -assert.equal(prepared.start.action.intent.kind, "retry-orphan-user") -assert.equal(prepared.start.action.sourceUserMessageId, "u1") -assert.deepEqual(prepared.start.action.patch.addedMessages, [ - { - id: "a2", - parentMessageId: "u1", - role: "assistant", - text: "", - forks: [], - generationId: "11111111-1111-4111-8111-111111111111", - status: "pending", - }, -]) - -const completedAssistant = { - id: "a1", - parentMessageId: "u1", - role: "assistant", - text: "answer", - forks: [], - status: "done", -} -assert.deepEqual( - prepareUserTurnRetry(state([orphanUser, completedAssistant], "a1"), { - threadId: "main", - userMessageId: "u1", - assistantMessageId: "a2", - generationId: "g2", - }), - { - ok: false, - code: "not_latest_turn", - message: "该消息已不是可恢复的最后一轮", - } -) - -assert.deepEqual( - prepareUserTurnRetry(state([orphanUser], "u1"), { - threadId: "main", - userMessageId: "missing", - assistantMessageId: "a2", - generationId: "g2", - }), - { - ok: false, - code: "not_latest_turn", - message: "该消息已不是可恢复的最后一轮", - } -) - -console.log( - "PASS user-turn retry command prepares one pending assistant only for the recoverable active orphan" -) diff --git a/e2e/thread-chat/verify-bubble-composer.mjs b/e2e/thread-chat/verify-bubble-composer.mjs deleted file mode 100644 index 89993055..00000000 --- a/e2e/thread-chat/verify-bubble-composer.mjs +++ /dev/null @@ -1,631 +0,0 @@ -/** - * 气泡输入框(openspec: add-bubble-composer Phase A)真实后端端到端验收。 - * - * 前提同 verify-live:dev server 已起、MiniMax key 已配、本机有 Chromium。运行: - * CHROMIUM_PATH=... BASE_URL=http://localhost:4040 \ - * node --experimental-strip-types e2e/thread-chat/verify-bubble-composer.mjs - * (--experimental-strip-types:直接 import 产品代码的 kickoffQuestion / - * defaultBranchTitle 生成断言期望值。) - * - * 断言面(参考 playground verify6 + 本仓 IME/标题/预算关注点): - * · 气泡结构:输入框存在 / placeholder 提示可留空 / 弹出即聚焦; - * · 按钮文案四态:默认 / 有输入 / ⌘ 按住 / 列条 override(含优先级与复位); - * · Shift+Enter 换行不提交;长问题自增高 + 内滚不自毁气泡(scroll 放行修复); - * · 非来源列滚动不关气泡、来源列滚动仍关闭;输入中 Esc 关气泡、无消息入树; - * · IME:CDP imeSetComposition + keyCode 229 的 Enter 不提交,insertText 上屏后 - * 真实 Enter 才提交; - * · 带问 Enter:新列第 1 条 = 该 user 消息、第 2 条 assistant 流式、composer 无预填、 - * payload 契约(threadChat.anchorText / user 原文入 messages); - * · 留空 Enter:空分支 + composer 预填 kickoffQuestion() + 2 秒内无 /api/chat POST; - * · ⌘Enter keepSource:来源列保留、新列开在紧邻右侧、首条为 user 问题; - * · 异步分支标题:首答完成后标题变为完整语义标题(非锚点截断)、刷新后仍在、 - * 全程 /api/title 恰好请求一次; - * · 深树继承段预算的纯函数用例在 prompt-budget.test.mjs(无需 dev server,见 README)。 - * 走真实模型,回复内容非确定,断言只卡结构与契约;测试树跑完自动清理。 - */ -import { mkdirSync } from "node:fs" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" -import { chromium } from "playwright-core" -import { defaultBranchTitle } from "../../app/thread-chat/core/store.ts" -import { kickoffQuestion } from "../../lib/thread-chat/application/prompt-policy.ts" - -const here = dirname(fileURLToPath(import.meta.url)) -const shotsDir = join(here, "shots") -mkdirSync(shotsDir, { recursive: true }) -const SHOT = (n) => join(shotsDir, `${n}.png`) - -let failed = 0 -const ok = (label, cond, detail = "") => { - console.log( - `${cond ? "PASS" : "FAIL"} ${label}${detail ? `(${detail})` : ""}` - ) - if (!cond) failed = 1 -} -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) -/** node 侧轮询等待(等 PUT 计数等非页面条件) */ -async function waitUntil(fn, timeout = 8000, step = 200) { - const t0 = Date.now() - while (Date.now() - t0 < timeout) { - if (fn()) return true - await sleep(step) - } - return fn() -} - -const BASE_URL = process.env.BASE_URL || "http://localhost:4040" -const browser = await chromium.launch({ - executablePath: process.env.CHROMIUM_PATH || undefined, - headless: true, -}) -// 1920 宽 → 自适应 4 列(COL_MIN_W=430),⌘Enter keepSource 场景不触发列满替换 -const page = await browser.newPage({ viewport: { width: 1920, height: 950 } }) - -const chatPosts = [] // /api/chat POST payload(契约断言) -const treePuts = [] // /api/branch-trees PUT(防抖存盘观测) -function isBranchTitleRequest(req) { - if (!req.url().includes("/api/title") || req.method() !== "POST") return false - try { - return req.postDataJSON()?.kind === "branch" - } catch { - return false - } -} - -let titlePosts = 0 // /api/title 的 kind=branch POST(「至多一次」断言) -page.on("request", (req) => { - const url = req.url() - if (url.includes("/api/chat") && req.method() === "POST") { - try { - chatPosts.push(JSON.parse(req.postData() ?? "{}")) - } catch { - /* 非 JSON 忽略 */ - } - } - if (url.includes("/api/branch-trees/") && req.method() === "PUT") - treePuts.push(url) - if (isBranchTitleRequest(req)) titlePosts++ -}) -let titleResponses = 0 // 收尾时等标题请求收口,避免删树后被迟到的 PUT 复活 -page.on("response", (res) => { - if (isBranchTitleRequest(res.request())) titleResponses++ -}) -const pageErrors = [] -page.on("pageerror", (e) => pageErrors.push(String(e))) - -/* ---------- 工具:列内划选(needle 命中单个文本节点;needle=null 时自动挑 - 该列最后一条 assistant .md-body 里首个 ≥minLen 字的文本节点开头) ---------- */ -async function selectInColumn(colIdx, needle, minLen = 8) { - const picked = await page.evaluate( - async ([colIdx, needle, minLen]) => { - const cols = document.querySelectorAll(".tc .cols > .column") - const col = cols[colIdx] - const bodies = col?.querySelectorAll(".message.assistant .md-body") - const md = bodies?.[bodies.length - 1] - if (!md) return null - const walker = document.createTreeWalker(md, NodeFilter.SHOW_TEXT) - let node - let hit = null - let fallback = null - while ((node = walker.nextNode())) { - const t = node.textContent ?? "" - const i = needle ? t.indexOf(needle) : -1 - if (needle && i >= 0) { - hit = { node, start: i, end: i + needle.length } - break - } - if (!fallback && t.trim().length >= minLen) { - const s = t.indexOf(t.trim()) - fallback = { node, start: s, end: s + minLen } - } - } - const target = hit ?? fallback - if (!target) return null - target.node.parentElement?.scrollIntoView({ block: "center" }) - await new Promise((r) => setTimeout(r, 120)) - const range = document.createRange() - range.setStart(target.node, target.start) - range.setEnd(target.node, target.end) - const sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) - document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })) - return (target.node.textContent ?? "").slice(target.start, target.end) - }, - [colIdx, needle, minLen] - ) - if (!picked) return null - await page - .locator(".tc .sel-bubble") - .waitFor({ state: "visible", timeout: 5000 }) - await page.waitForTimeout(150) - return picked -} - -/** 某列(0 基,仅数展开列)流式完成:发送键回到「发送」且末条 assistant 有正文 */ -async function waitColDone(colIdx, timeout = 120000) { - await page.waitForFunction( - (idx) => { - const cols = document.querySelectorAll(".tc .cols > .column") - const col = cols[idx] - const bubbles = col?.querySelectorAll(".message.assistant .bubble") - const last = bubbles?.[bubbles.length - 1] - const btn = col?.querySelector(".composer .send") - return ( - last && - last.textContent.trim().length > 20 && - btn && - !btn.classList.contains("stop") - ) - }, - colIdx, - { timeout } - ) - await page.waitForTimeout(150) // 平滑打字 snap 落地(同 verify-live 注释) -} - -/** 列消息快照:[{role, text}] */ -const colMsgs = (colIdx) => - page.evaluate((idx) => { - const cols = document.querySelectorAll(".tc .cols > .column") - return Array.from(cols[idx]?.querySelectorAll(".message") ?? []).map( - (el) => ({ - role: el.classList.contains("user") ? "user" : "assistant", - // 正文断言只看正文:排除 .msg-quote 引用条(方向 C 的划选引用展示件) - text: (() => { - const b = el.querySelector(".bubble") - if (!b) return "" - const c = b.cloneNode(true) - c.querySelector(".msg-quote")?.remove() - return (c.textContent ?? "").trim() - })(), - quote: - el.querySelector(".bubble .msg-quote")?.textContent?.trim() ?? null, - }) - ) - }, colIdx) - -/** 展开列标题序列(主线列头是 .ctitle.main,文本「主线」) */ -const colTitles = () => - page.evaluate(() => - Array.from(document.querySelectorAll(".tc .cols > .column")).map( - (el) => el.querySelector(".ctitle")?.textContent?.trim() ?? "" - ) - ) -const colCount = () => - page.evaluate(() => document.querySelectorAll(".tc .cols > .column").length) - -const bubbleTextarea = () => page.locator(".sel-bubble .ask textarea") -const bubbleLabel = () => page.locator(".sel-bubble > button").innerText() - -/* ================= 1. 主线真实首答(正文里埋两个可划选短语) ================= */ -await page.goto(`${BASE_URL}/thread-chat`, { waitUntil: "networkidle" }) -ok("页面加载:.tc 壳存在", (await page.locator(".tc").count()) === 1) - -const PHRASE_A = "量子纠缠现象无法用来传递任何信息" // 16 字 > 13:默认标题必带截断省略号 -const PHRASE_B = "贝尔不等式" -await page - .locator(".column") - .first() - .locator("textarea") - .fill( - "请较详细地讲解量子纠缠(小标题 + 分点),并务必在普通正文中(不加粗、不放进标题)" + - `原样包含这两个短语:「${PHRASE_A}」和「${PHRASE_B}」。` - ) -await page.locator(".column").first().locator("textarea").press("Enter") -await waitColDone(0) -ok("主线收到真实流式回复", true) - -/* ================= 2. 气泡结构 + 文案态(默认/有输入)+ 键位守卫 ================= */ -let anchorA = await selectInColumn(0, PHRASE_A, 16) -ok("划选主线正文(气泡浮出)", anchorA !== null, anchorA ?? "") -ok("气泡含输入框", (await bubbleTextarea().count()) === 1) -ok( - "输入框 placeholder 提示可留空", - ((await bubbleTextarea().getAttribute("placeholder")) ?? "").includes("留空") -) -ok( - "弹出即聚焦输入框", - await page.evaluate( - () => document.activeElement?.closest?.(".sel-bubble .ask") != null - ) -) -ok("空输入按钮文案 = 开启分支讨论", (await bubbleLabel()) === "开启分支讨论") -await bubbleTextarea().pressSequentially("测") -ok("有输入时文案 = 带着问题开分支", (await bubbleLabel()) === "带着问题开分支") -await bubbleTextarea().fill("") -ok("清空后文案恢复 = 开启分支讨论", (await bubbleLabel()) === "开启分支讨论") -await page.screenshot({ path: SHOT("bc-1-bubble-input") }) - -/* —— Shift+Enter 换行不提交 —— */ -await bubbleTextarea().pressSequentially("第一行") -await bubbleTextarea().press("Shift+Enter") -await bubbleTextarea().pressSequentially("第二行") -await page.waitForTimeout(150) -{ - const stillOpen = (await page.locator(".sel-bubble").count()) === 1 - const val = await bubbleTextarea().inputValue() - ok( - "Shift+Enter 换行不提交(气泡仍开、值含换行)", - stillOpen && val.includes("\n") && (await colCount()) === 1 - ) -} - -/* —— 长问题自增高 + textarea 内滚:气泡不自毁、输入不丢(scroll 放行修复) —— */ -const LONG_Q = "这个问题很长,".repeat(20) -await bubbleTextarea().fill(LONG_Q) -{ - const h = await bubbleTextarea().evaluate((ta) => ta.clientHeight) - ok(`长问题触发自增高(clamp 68px,当前 ${h}px)`, h >= 60 && h <= 72) - await bubbleTextarea().evaluate((ta) => { - ta.scrollTop = ta.scrollHeight // 触发气泡内部 scroll(capture 监听必须放行) - }) - await page.waitForTimeout(200) - const stillOpen = (await page.locator(".sel-bubble").count()) === 1 - const val = await bubbleTextarea().inputValue() - ok("textarea 内滚后气泡不自毁、输入完整保留", stillOpen && val === LONG_Q) - await page.screenshot({ path: SHOT("bc-2-long-question") }) -} - -/* —— 输入中 Esc:走壳层关闭链关气泡,无消息入树 —— */ -const postsBeforeEsc = chatPosts.length -await page.keyboard.press("Escape") -await page.waitForTimeout(200) -ok( - "输入中 Esc 关气泡、无新列、无 /api/chat 请求", - (await page.locator(".sel-bubble").count()) === 0 && - (await colCount()) === 1 && - chatPosts.length === postsBeforeEsc -) - -/* —— 来源消息列表真实滚动仍关气泡 —— */ -anchorA = await selectInColumn(0, PHRASE_A, 16) -ok("重新划选(回归用)", anchorA !== null) -await page.evaluate(() => { - const list = document.querySelector(".tc .msg-list") - list.scrollTop = Math.max(0, list.scrollTop - 120) -}) -await page.waitForTimeout(250) -ok( - "来源消息列表滚动仍关闭气泡", - (await page.locator(".sel-bubble").count()) === 0 -) - -/* ================= 3. IME 组合态守卫 + 带问 Enter 开分支 ================= */ -anchorA = await selectInColumn(0, PHRASE_A, 16) -ok("划选(带问路径)", anchorA !== null, anchorA ?? "") -const cdp = await page.context().newCDPSession(page) -await cdp.send("Input.imeSetComposition", { - text: "wenti", - selectionStart: 5, - selectionEnd: 5, -}) -// 组合态回车:真实 IME 里这次 keydown 的 keyCode 是 229(isComposing 同时为 true) -await cdp.send("Input.dispatchKeyEvent", { - type: "rawKeyDown", - key: "Enter", - code: "Enter", - windowsVirtualKeyCode: 229, - nativeVirtualKeyCode: 229, -}) -await cdp.send("Input.dispatchKeyEvent", { - type: "keyUp", - key: "Enter", - code: "Enter", -}) -await page.waitForTimeout(250) -ok( - "IME 组合态 Enter 不提交(气泡仍开、无新列)", - (await page.locator(".sel-bubble").count()) === 1 && (await colCount()) === 1 -) -await cdp.send("Input.insertText", { text: "问题" }) // 上屏(提交组合文本) -await page.waitForTimeout(150) -{ - const val = await bubbleTextarea().inputValue() - ok( - "insertText 上屏后输入框为已上屏文本(无组合残留)", - val.includes("问题") && !val.includes("wenti"), - JSON.stringify(val) - ) -} - -const Q1 = - "它为什么不能用来传递信息?请通俗解释,并在回答的普通正文中原样包含短语「超光速通信不可行」。" -await bubbleTextarea().fill(Q1) -const postsBeforeQ1 = chatPosts.length -await bubbleTextarea().press("Enter") -await page.waitForFunction( - () => document.querySelectorAll(".tc .cols > .column").length >= 2, - undefined, - { timeout: 10000 } -) -ok("带问 Enter:分支列打开", true) -{ - const msgs = await colMsgs(1) - ok( - "新列第 1 条 = 所输入问题(user 原文)", - msgs[0]?.role === "user" && msgs[0].text === Q1, - JSON.stringify(msgs[0] ?? null) - ) - ok( - "新列第 1 条带划选引用条(方向 C)", - msgs[0]?.quote === anchorA, - JSON.stringify(msgs[0]?.quote ?? null) - ) - ok("新列第 2 条 = assistant(流式首答已就位)", msgs[1]?.role === "assistant") - const composerVal = await page - .locator(".tc .cols > .column") - .nth(1) - .locator(".composer textarea") - .inputValue() - ok("带问路径 composer 无预填", composerVal === "") - await waitUntil(() => chatPosts.length > postsBeforeQ1, 5000) - const payload = chatPosts[chatPosts.length - 1] - const msgsInPayload = payload?.messages ?? [] - ok( - "payload 契约:threadChat.anchorText = 划选原文", - payload?.threadChat?.anchorText === anchorA - ) - // 锚点 grounding 契约:分支首条 user 在发送线上加「就我划选的这段话」前缀 - // (裸问题的指代会被模型就近解析到上文结尾——用户实测踩过),UI 仍显示原文 - const grounded = `就我划选的这段话:「${anchorA}」——${Q1}` - ok( - "payload 契约:分支首问带锚点 grounding 前缀(仅发送线)", - msgsInPayload.some( - (m) => m.role === "user" && m.parts?.[0]?.text === grounded - ) - ) - const ui = await page.evaluate(() => { - const cols = document.querySelectorAll(".tc .cols > .column") - const bubble = cols[1]?.querySelector(".message.user .bubble") - const quote = bubble?.querySelector(".msg-quote")?.textContent?.trim() ?? "" - const clone = bubble?.cloneNode(true) - clone?.querySelector(".msg-quote")?.remove() - return { quote, text: clone?.textContent?.trim() ?? "" } - }) - ok("UI 契约:user 气泡正文 = 原始问题(无前缀)", ui.text === Q1) - ok("UI 契约:气泡内引用条 = 划选原文(方向 C)", ui.quote === anchorA) - - /* 右侧列继续流式/自动贴底时,左侧仍可成为当前划选来源;其他列滚动不改变 - 来源选区坐标,不应关闭气泡,来源列滚动则必须关闭以免 rect 过期。 */ - const pickedWhileBranchActive = await selectInColumn(0, PHRASE_A, 16) - ok( - "右侧分支回复期间仍可划选左侧正文", - pickedWhileBranchActive !== null, - pickedWhileBranchActive ?? "" - ) - await page.evaluate(() => { - const cols = document.querySelectorAll(".tc .cols > .column") - cols[1]?.querySelector(".msg-list")?.dispatchEvent(new Event("scroll")) - }) - await page.waitForTimeout(200) - ok( - "非来源列滚动不关闭划选气泡", - (await page.locator(".sel-bubble").count()) === 1 - ) - await page.evaluate(() => { - const cols = document.querySelectorAll(".tc .cols > .column") - cols[0]?.querySelector(".msg-list")?.dispatchEvent(new Event("scroll")) - }) - await page.waitForTimeout(200) - ok( - "来源列滚动仍关闭划选气泡", - (await page.locator(".sel-bubble").count()) === 0 - ) - - // 方向 C 的核心承诺:绑定关系是数据——刷新后引用条仍在(quote 随整树 JSON 落库) - await page.waitForTimeout(2000) // 过防抖存库 - await page.reload({ waitUntil: "networkidle" }) - await page.waitForSelector(".tc .message.user .bubble", { timeout: 15000 }) - const afterReload = await page.evaluate(() => { - const cols = document.querySelectorAll(".tc .cols > .column") - for (const col of cols) { - const q = col.querySelector(".message.user .bubble .msg-quote") - if (q) return q.textContent?.trim() ?? "" - } - // 分支列可能未在恢复布局里展开:全 DOM 兜底找 - return ( - document - .querySelector(".message.user .bubble .msg-quote") - ?.textContent?.trim() ?? "" - ) - }) - ok( - "持久化契约:刷新后引用条仍在(quote 是数据不是代码行为)", - afterReload === anchorA - ) -} -await page.screenshot({ path: SHOT("bc-3-question-branch") }) -await waitColDone(1) -const branchReply = await page.evaluate(() => { - const cols = document.querySelectorAll(".tc .cols > .column") - const bubbles = cols[1]?.querySelectorAll(".message.assistant .bubble") - return bubbles?.[bubbles.length - 1]?.textContent ?? "" -}) -ok("带问分支首答流式完成", branchReply.trim().length > 20) - -/* ================= 4. 异步分支标题:首答完成 → 语义标题 → 刷新仍在 ================= */ -const DEFAULT_TITLE_A = defaultBranchTitle(anchorA) -const titleChanged = await page - .waitForFunction( - (dft) => { - const cols = document.querySelectorAll(".tc .cols > .column") - const t = cols[1]?.querySelector(".ctitle")?.textContent?.trim() - return !!t && t !== dft - }, - DEFAULT_TITLE_A, - { timeout: 60000 } - ) - .then(() => true) - .catch(() => false) -const genTitle = (await colTitles())[1] -ok( - `分支标题异步变为语义标题(非锚点截断):「${genTitle}」`, - titleChanged && genTitle !== DEFAULT_TITLE_A && !genTitle.endsWith("…") -) -ok("语义标题至少包含 2 个字符", titleChanged && genTitle.length >= 2) -// 标题变更随整树防抖存盘(1.5s):必须等「标题变更之后」的那次 PUT—— -// 首答完成本身也会触发一次 PUT(标题请求彼时还在飞),拿它当依据会在 -// 带标题的 PUT 落库前刷新,DB 里还是默认标题(本脚本首版踩过的竞态) -const putsAtTitleChange = treePuts.length -ok( - "标题变更触发防抖整树 PUT", - await waitUntil(() => treePuts.length > putsAtTitleChange, 8000) -) -await sleep(800) // 等 PUT 响应落库 -const treeUrl = page.url() -await page.goto(treeUrl, { waitUntil: "networkidle" }) -await page.locator(".tc .cols > .column").nth(1).waitFor({ timeout: 10000 }) -{ - const titles = await colTitles() - ok( - "刷新后语义标题仍在(随树持久化)", - titleChanged && titles[1] === genTitle, - JSON.stringify(titles) - ) -} -await sleep(2500) // 「至多一次」:重载后不应因既有语义标题再次请求 -ok( - "全程 /api/title 的 kind=branch 恰好请求一次", - titlePosts === 1, - `实际 ${titlePosts}` -) -await page.screenshot({ path: SHOT("bc-4-async-title") }) - -/* ================= 5. 留空 Enter = 现有预填流原样保留 ================= */ -const anchorB = await selectInColumn(0, PHRASE_B, 6) -ok("划选(留空路径)", anchorB !== null, anchorB ?? "") -const postsBeforeEmpty = chatPosts.length -await bubbleTextarea().press("Enter") // 留空直接回车 -await page.waitForFunction( - () => document.querySelectorAll(".tc .cols > .column").length >= 3, - undefined, - { timeout: 10000 } -) -{ - const emptyColIdx = 2 - const prefill = await page - .locator(".tc .cols > .column") - .nth(emptyColIdx) - .locator(".composer textarea") - .inputValue() - ok( - "留空分支 composer 预填 kickoffQuestion()(期望值由产品代码生成)", - prefill === kickoffQuestion(anchorB), - JSON.stringify(prefill) - ) - const msgs = await colMsgs(emptyColIdx) - ok("留空分支消息区为空(未自动发请求)", msgs.length === 0) - await sleep(2000) - ok( - "留空开分支 2 秒内无新 /api/chat POST", - chatPosts.length === postsBeforeEmpty - ) -} -await page.screenshot({ path: SHOT("bc-5-empty-prefill") }) - -/* ================= 6. 文案四态(override / ⌘)+ ⌘Enter keepSource ================= */ -// 在带问分支(第 2 列)的首答里划选:优先埋好的短语,缺失则退回自动挑选 -const anchorC = - (await selectInColumn(1, "超光速通信不可行", 8)) ?? - (await selectInColumn(1, null, 8)) -ok("在带问分支列内划选(keepSource 用)", anchorC !== null, anchorC ?? "") -await bubbleTextarea().pressSequentially("对比一下?") -ok("有输入 → 带着问题开分支", (await bubbleLabel()) === "带着问题开分支") -// 新契约(用户定稿):按钮只表达动作、两态恒定;放置后果在 .place-hint 提示行 -const placeHint = async () => - await page - .locator(".sel-bubble .place-hint") - .innerText() - .catch(() => "") -await page.keyboard.down("Meta") -ok( - "⌘ 按住:按钮文案不变(动作恒定)", - (await bubbleLabel()) === "带着问题开分支" -) -ok( - "⌘ 按住:提示行 = 保留本列·右侧新开", - (await placeHint()).includes("保留本列") -) -await page.keyboard.up("Meta") -{ - // 列条 override(点非来源、未折叠小格):后果进提示行,按钮不变 - const cell = page.locator( - ".sel-bubble .smcell:not(.main):not(.src):not(.ghost):not(.folded)" - ) - ok("气泡含迷你列条(≥1 个可点小格)", (await cell.count()) >= 1) - const hintBefore = await placeHint() - ok( - "默认态提示行说明放置后果(替换/折叠/新开)", - /将?(默认)?(替换|折叠|新开)/.test(hintBefore), - hintBefore - ) - await cell.first().click() - const ovHint = await placeHint() - ok( - "override 态提示行 = 将替换/折叠『列名』", - /^将(替换|折叠)『.+』$/.test(ovHint), - ovHint - ) - ok( - "override 态按钮文案不变(动作恒定)", - (await bubbleLabel()) === "带着问题开分支" - ) - await cell.first().click() // 再点同格取消 - ok("取消 override → 提示行回落默认态", (await placeHint()) === hintBefore) -} -const Q2 = "把它和经典信道对比一下?" -await bubbleTextarea().fill(Q2) -const titlesBeforeMeta = await colTitles() -await page.keyboard.press("Meta+Enter") -await page.waitForFunction( - (n) => document.querySelectorAll(".tc .cols > .column").length === n + 1, - titlesBeforeMeta.length, - { timeout: 10000 } -) -{ - const titles = await colTitles() - ok( - "⌘Enter:来源列保留(第 2 列标题不变)", - titles[1] === titlesBeforeMeta[1], - JSON.stringify(titles) - ) - ok( - "⌘Enter:新列开在来源紧邻右侧且标题取锚点", - titles[2] === defaultBranchTitle(anchorC), - `期望「${defaultBranchTitle(anchorC)}」,实际「${titles[2]}」` - ) - const msgs = await colMsgs(2) - ok( - "⌘Enter 新列第 1 条 = user 问题(带问 + keepSource 组合成立)", - msgs[0]?.role === "user" && msgs[0].text === Q2 - ) -} -await page.screenshot({ path: SHOT("bc-6-meta-enter") }) - -/* ================= 7. 收尾:停掉在飞流、清理测试树 ================= */ -{ - const stopBtn = page - .locator(".tc .cols > .column") - .nth(2) - .locator(".composer .send.stop") - if ((await stopBtn.count()) > 0) await stopBtn.click() -} -ok( - "全程无页面错误(pageerror)", - pageErrors.length === 0, - pageErrors.join(" | ") -) -// 若停止时已有正文(finish→done),标题请求会随即发出:等它收口 + 防抖 PUT 落库 -// 再删行,否则迟到的标题 PUT 会把刚删的 DB 行复活 -await sleep(500) -await waitUntil(() => titleResponses >= titlePosts, 30000) -await sleep(2500) // 让最后一轮防抖 PUT 落库,再删行(写链保证 DELETE 排最后) -const ownTreeId = page.url().split("/").pop() -if (/^[0-9a-f-]{36}$/.test(ownTreeId)) { - await page.request.delete(`${BASE_URL}/api/branch-trees/${ownTreeId}`) - console.log(`已清理本次测试树 ${ownTreeId.slice(0, 8)}…`) -} -await browser.close() -console.log(failed ? "\n==== 存在 FAIL ====" : "\n==== 全部 PASS ====") -process.exit(failed) diff --git a/e2e/thread-chat/verify-canvas-chat.mjs b/e2e/thread-chat/verify-canvas-chat.mjs deleted file mode 100644 index 6116cc01..00000000 --- a/e2e/thread-chat/verify-canvas-chat.mjs +++ /dev/null @@ -1,610 +0,0 @@ -/** - * 画布 Phase 2(openspec: add-canvas-conversations)真实后端端到端验收。 - * - * 前提同 verify-live:dev server 已起、MiniMax key 已配、本机有 Chromium。运行: - * CHROMIUM_PATH=... BASE_URL=http://localhost:4040 \ - * node --experimental-strip-types e2e/thread-chat/verify-canvas-chat.mjs - * (--experimental-strip-types:直接 import 产品代码的 kickoffQuestion 生成 - * 空分支 composer 预填的断言期望值。) - * - * 断言面(参考 playground verify10 + 本仓富文本 / LR / zoom 关注点): - * · 单击节点展开外挂面板(消息列表 + composer),展开零重排(其余节点坐标不变, - * D1 外挂面板不参与 dagre);摘要收起; - * · 面板内追问真实流式:user + pending 立即入树、busy 时发送键变「停止」、 - * 完成后 Markdown 结构断言(.md-body 内有结构化元素——富文本契约 D2); - * · 手势共处(D5):面板内滚轮 = 列表内滚、画布 zoom 不变;空白处滚轮正常缩放; - * · zoom 0.75 / 1.5 两档:面板内划选 → 气泡按选区视口坐标正确定位(fixed 免疫 - * zoom),画布气泡无迷你列条(fork 不占列槽 D4); - * · 带问提交:新节点 + 边长出、focusNode 跟随(setCenter 后新节点整体可见、 - * zoom 不缩小)、新节点选中展开、面板首条 = 所提问题、首答流式完成; - * · 留空提交:新节点面板 composer 预填 kickoffQuestion()(语义同列模式); - * · 收起/再展开零重排(多节点下);选中节点 zIndex 抬升盖过兄弟卡(LR 遮挡); - * · 列槽隔离:回列视图仅主线一列(画布 fork 不占槽),主线正文可见 fork 脚注; - * · 双击面板不误触回列;双击节点卡回列模式(Phase 1 行为保留)。 - * 走真实模型,回复内容非确定,断言只卡结构与契约;测试树跑完自动清理。 - */ -import { mkdirSync } from "node:fs" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" -import { chromium } from "playwright-core" -import { kickoffQuestion } from "../../lib/thread-chat/application/prompt-policy.ts" - -const here = dirname(fileURLToPath(import.meta.url)) -const shotsDir = join(here, "shots") -mkdirSync(shotsDir, { recursive: true }) -const SHOT = (n) => join(shotsDir, `${n}.png`) - -let failed = 0 -const ok = (label, cond, detail = "") => { - console.log( - `${cond ? "PASS" : "FAIL"} ${label}${detail ? `(${detail})` : ""}` - ) - if (!cond) failed = 1 -} -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) -async function waitUntil(fn, timeout = 8000, step = 200) { - const t0 = Date.now() - while (Date.now() - t0 < timeout) { - if (await fn()) return true - await sleep(step) - } - return fn() -} - -const BASE_URL = process.env.BASE_URL || "http://localhost:4040" -const browser = await chromium.launch({ - executablePath: process.env.CHROMIUM_PATH || undefined, - headless: true, -}) -const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }) - -function isBranchTitleRequest(req) { - if (!req.url().includes("/api/title") || req.method() !== "POST") return false - try { - return req.postDataJSON()?.kind === "branch" - } catch { - return false - } -} - -let titlePosts = 0 -let titleResponses = 0 -page.on("request", (req) => { - if (isBranchTitleRequest(req)) titlePosts++ -}) -page.on("response", (res) => { - if (isBranchTitleRequest(res.request())) titleResponses++ -}) -const pageErrors = [] -page.on("pageerror", (e) => pageErrors.push(String(e))) - -/* ---------------- 画布通用工具 ---------------- */ - -/** 画布 zoom(解析 .react-flow__viewport 的 transform scale) */ -const getScale = () => - page.evaluate(() => { - const vp = document.querySelector(".react-flow__viewport") - const m = /scale\(([\d.]+)\)/.exec(vp?.style.transform ?? "") - return m ? Number(m[1]) : NaN - }) - -/** 全部节点的世界坐标快照:data-id → style.transform(zoom/pan 不影响它,重排才变) */ -const nodeTransforms = () => - page.evaluate(() => - Object.fromEntries( - Array.from(document.querySelectorAll(".react-flow__node")).map((el) => [ - el.getAttribute("data-id"), - el.style.transform, - ]) - ) - ) -const sameTransforms = (a, b) => - Object.keys(a).length === Object.keys(b).length && - Object.entries(a).every(([k, v]) => b[k] === v) - -/** 画布空白点(pane 上、不压节点 / 面板 / 控件)——滚轮缩放与点空白收起用 */ -const blankPoint = () => - page.evaluate(() => { - const wrap = document.querySelector(".canvas-wrap")?.getBoundingClientRect() - if (!wrap) return null - for (let y = wrap.top + 70; y < wrap.bottom - 70; y += 36) { - for (let x = wrap.left + 60; x < wrap.right - 60; x += 48) { - const el = document.elementFromPoint(x, y) - if ( - el && - el.closest(".react-flow__pane") && - !el.closest(".react-flow__node") && - !el.closest(".react-flow__panel") && - !el.closest(".react-flow__edge") - ) - return { x, y } - } - } - return null - }) - -/** 在空白处滚轮把 zoom 调到 target(d3-zoom:scale' = scale·2^(−deltaY·0.002)) */ -async function setZoom(target, tol = 0.04) { - for (let i = 0; i < 14; i++) { - const cur = await getScale() - if (Math.abs(cur - target) <= tol) return cur - const pt = await blankPoint() - if (!pt) return cur - await page.mouse.move(pt.x, pt.y) - const dy = Math.log2(cur / target) / 0.002 - await page.mouse.wheel(0, Math.max(-480, Math.min(480, dy))) - await sleep(140) - } - return getScale() -} - -/** 在外挂面板的 assistant .md-body 里划选文字(needle 命中;否则挑首个 ≥minLen 的 - 文本节点开头),dispatch mouseup 触发划选气泡;返回 { text, rect, iw, ih }。 - rect 必须在 mouseup 前采集:气泡弹出即聚焦输入框,Chromium 下焦点进 textarea - 会把 document selection 挪走(rect 归零),事后再读选区就测不到了 */ -async function selectInPanel(needle, minLen = 8) { - const picked = await page.evaluate( - async ([needle, minLen]) => { - const panel = document.querySelector(".canvas-expand") - const bodies = panel?.querySelectorAll(".message.assistant .md-body") - if (!bodies?.length) return null - let target = null - for (const md of bodies) { - const walker = document.createTreeWalker(md, NodeFilter.SHOW_TEXT) - let node - while ((node = walker.nextNode())) { - const t = node.textContent ?? "" - const i = needle ? t.indexOf(needle) : -1 - if (needle && i >= 0) { - target = { node, start: i, end: i + needle.length } - break - } - if (!needle && t.trim().length >= minLen) { - const s = t.indexOf(t.trim()) - target = { node, start: s, end: s + minLen } - break - } - } - if (target) break - } - if (!target) return null - target.node.parentElement?.scrollIntoView({ block: "center" }) - await new Promise((r) => setTimeout(r, 150)) - const range = document.createRange() - range.setStart(target.node, target.start) - range.setEnd(target.node, target.end) - const sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) - const r = range.getBoundingClientRect() // mouseup 前采集(见函数头注) - document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })) - return { - text: (target.node.textContent ?? "").slice(target.start, target.end), - rect: { left: r.left, top: r.top, bottom: r.bottom }, - iw: window.innerWidth, - ih: window.innerHeight, - } - }, - [needle, minLen] - ) - if (!picked) return null - await page - .locator(".tc .sel-bubble") - .waitFor({ state: "visible", timeout: 5000 }) - await page.waitForTimeout(250) // 等 tc-pop 入场动画结束(期间 transform 偏移) - return picked -} - -/** 断言气泡定位 = 选区视口坐标推导(selection-bubble 的公式,画布 extraH=0) */ -async function assertBubblePos(tag, sel) { - const bub = await page.evaluate(() => { - const b = document.querySelector(".sel-bubble")?.getBoundingClientRect() - return b ? { left: b.left, top: b.top } : null - }) - if (!bub) { - ok(`${tag}:气泡可测量`, false) - return - } - const expLeft = Math.max(10, Math.min(sel.rect.left, sel.iw - 244)) - let expTop = sel.rect.bottom + 9 - if (expTop > sel.ih - 190) expTop = Math.max(10, sel.rect.top - 172) - ok( - `${tag}:气泡贴选区定位正确(fixed 视口坐标)`, - Math.abs(bub.left - expLeft) <= 2 && Math.abs(bub.top - expTop) <= 2, - `期望 (${expLeft.toFixed(1)}, ${expTop.toFixed(1)}),实际 (${bub.left.toFixed(1)}, ${bub.top.toFixed(1)})` - ) -} - -/** 等外挂面板流式完成:末条 assistant 有正文且发送键回到「发送」 */ -async function waitPanelDone(timeout = 120000) { - await page.waitForFunction( - () => { - const panel = document.querySelector(".canvas-expand") - const bubbles = panel?.querySelectorAll(".message.assistant .bubble") - const last = bubbles?.[bubbles.length - 1] - const btn = panel?.querySelector(".cv-send") - return ( - last && - (last.textContent ?? "").trim().length > 20 && - btn && - !btn.classList.contains("stop") - ) - }, - undefined, - { timeout } - ) - await page.waitForTimeout(200) // 平滑打字 snap 落地 -} - -/* ================= 0. 列模式种子:主线真实首答(埋可划选短语) ================= */ -await page.goto(`${BASE_URL}/thread-chat`, { waitUntil: "networkidle" }) -ok("页面加载:.tc 壳存在", (await page.locator(".tc").count()) === 1) - -const PHRASE_A = "量子纠缠现象无法用来传递任何信息" -const PHRASE_B = "贝尔不等式" -await page - .locator(".column") - .first() - .locator("textarea") - .fill( - "请较详细地讲解量子纠缠(用小标题和无序列表组织,回复要足够长),并务必在" + - `普通正文中(不加粗、不放进标题)原样包含这两个短语:「${PHRASE_A}」和「${PHRASE_B}」。` - ) -await page.locator(".column").first().locator("textarea").press("Enter") -await page.waitForFunction( - () => { - const col = document.querySelector(".tc .cols > .column") - const bubbles = col?.querySelectorAll(".message.assistant .bubble") - const last = bubbles?.[bubbles.length - 1] - const btn = col?.querySelector(".composer .send") - return ( - last && - (last.textContent ?? "").trim().length > 60 && - btn && - !btn.classList.contains("stop") - ) - }, - undefined, - { timeout: 120000 } -) -ok("主线收到真实流式回复", true) - -/* ================= 1. 进画布 → 单击展开外挂面板(零重排) ================= */ -await page.locator(".topbar button.mode", { hasText: "画布" }).click() -await page.waitForSelector(".react-flow__node", { timeout: 10000 }) -await page.waitForTimeout(700) // fitView 结算 -// 先缩到 ~0.7:单节点 fitView 时 zoom=1,卡下方展开的面板会探出视口底部 -await page.locator(".react-flow__controls-zoomout").click() -await page.locator(".react-flow__controls-zoomout").click() -await page.waitForTimeout(350) - -const t0 = await nodeTransforms() -await page.locator('.react-flow__node[data-id="main"]').click() -await page.waitForTimeout(350) -ok( - "单击节点:外挂面板出现(消息列表 + composer)", - (await page.locator(".canvas-expand").count()) === 1 && - (await page - .locator('.canvas-expand .msg-list[data-list="main"] .message') - .count()) === 2 && - (await page.locator(".canvas-expand .cv-composer textarea").count()) === 1 -) -ok( - "面板消息走列模式同款富文本(assistant 气泡内有 .md-body)", - (await page - .locator('.canvas-expand .bubble[data-role="assistant"] .md-body') - .count()) === 1 -) -ok( - "展开时卡片摘要收起(面板已含完整末条)", - (await page - .locator(".react-flow__node.selected .canvas-card > .sum") - .count()) === 0 -) -{ - const t1 = await nodeTransforms() - ok("展开零重排:节点坐标不变(面板不参与 dagre)", sameTransforms(t0, t1)) -} -await page.screenshot({ path: SHOT("cc-1-expand") }) - -/* ================= 2. 面板内追问:真实流式 + busy 停止语义 + Markdown 结构 ================= */ -const PHRASE_C = "退相干过程" -await page - .locator(".canvas-expand .cv-composer textarea") - .fill( - "在画布面板里追问:请用一个小标题加一个无序列表,简述测量为何导致纠缠态坍缩," + - `并在普通正文中原样包含短语「${PHRASE_C}」。` - ) -await page.locator(".canvas-expand .cv-composer textarea").press("Enter") -await page.waitForTimeout(400) -ok( - "面板发送:user + assistant 占位立即入树(共 4 条)", - (await page.locator(".canvas-expand .msg-list .message").count()) === 4 -) -ok( - "流式期间发送键变「停止」(busy 语义同列模式)", - (await page.locator(".canvas-expand .cv-send.stop").count()) === 1 -) -await waitPanelDone() -{ - const structured = await page.evaluate(() => { - const bodies = document.querySelectorAll( - ".canvas-expand .message.assistant .md-body" - ) - const md = bodies[bodies.length - 1] - return md - ? md.querySelectorAll("h1,h2,h3,h4,ul,ol,li,strong,code,table").length - : 0 - }) - ok( - "面板内首答流式完成且为 Markdown 结构(结构化元素 > 0)", - structured > 0, - `结构化元素 ${structured} 个` - ) - const meta = await page - .locator(".react-flow__node.selected .canvas-card .meta") - .innerText() - ok("卡片消息计数同步 = 4", meta.includes("4 条消息"), meta) -} -await page.screenshot({ path: SHOT("cc-2-panel-stream") }) - -/* ================= 3. 手势共处:面板内滚 ≠ 缩放,空白滚轮 = 缩放 ================= */ -{ - const list = page.locator(".canvas-expand .msg-list.mini") - const scrollable = await list.evaluate( - (el) => el.scrollHeight > el.clientHeight + 20 - ) - ok("面板列表可滚(clamp 内滚前提成立)", scrollable) - const st0 = await list.evaluate((el) => el.scrollTop) - const s0 = await getScale() - await list.hover() - await page.mouse.wheel(0, -160) - await sleep(250) - const st1 = await list.evaluate((el) => el.scrollTop) - const s1 = await getScale() - ok( - "面板内滚轮:列表内滚、画布 zoom 不变(nowheel)", - st1 < st0 && Math.abs(s1 - s0) < 1e-6, - `scrollTop ${st0}→${st1},zoom ${s0}→${s1}` - ) - const pt = await blankPoint() - await page.mouse.move(pt.x, pt.y) - await page.mouse.wheel(0, -160) - await sleep(250) - const s2 = await getScale() - ok("空白处滚轮:画布正常缩放", Math.abs(s2 - s1) > 0.01, `zoom ${s1}→${s2}`) -} - -/* ================= 4. zoom 0.75:面板内划选 → 气泡定位 + 无列条 ================= */ -{ - const z = await setZoom(0.75) - ok("zoom 调至 ≈0.75", Math.abs(z - 0.75) <= 0.05, `实际 ${z}`) - const picked = await selectInPanel(PHRASE_B, 5) - ok("zoom 0.75:面板内划选出气泡", picked !== null, picked?.text ?? "") - if (picked) await assertBubblePos("zoom 0.75", picked) - ok( - "画布气泡无迷你列条(fork 不占列槽)", - (await page.locator(".sel-bubble .slotmap").count()) === 0 - ) - ok( - "气泡含 Phase A 输入框", - (await page.locator(".sel-bubble .ask textarea").count()) === 1 - ) - await page.screenshot({ path: SHOT("cc-3-zoom075-bubble") }) - await page.keyboard.press("Escape") - await sleep(200) - ok( - "Esc 关气泡(画布内关闭链正常)", - (await page.locator(".sel-bubble").count()) === 0 - ) -} - -/* ================= 5. zoom 1.5:划选 → 带问提交 → 新节点 + 视口跟随 ================= */ -const Q2 = "画布里带问开分支:它和经典关联的本质区别是什么?请用列表简答。" -{ - const z = await setZoom(1.5) - ok("zoom 调至 ≈1.5", Math.abs(z - 1.5) <= 0.1, `实际 ${z}`) - const picked = await selectInPanel(PHRASE_A, 8) - ok("zoom 1.5:面板内划选出气泡", picked !== null, picked?.text ?? "") - if (picked) await assertBubblePos("zoom 1.5", picked) - await page.screenshot({ path: SHOT("cc-4-zoom150-bubble") }) - - const nodesBefore = await page.locator(".react-flow__node").count() - await page.locator(".sel-bubble .ask textarea").fill(Q2) - await page.locator(".sel-bubble .ask textarea").press("Enter") - await page.waitForFunction( - (n) => document.querySelectorAll(".react-flow__node").length === n + 1, - nodesBefore, - { timeout: 8000 } - ) - ok("带问提交:新节点长出(+1)", true) - ok( - "边 +1(父子边就位)", - (await page.locator(".react-flow__edge").count()) === 1 - ) - await sleep(700) // setCenter 动画(320ms)+ 结算余量 - const selId = await page.evaluate(() => - document - .querySelector(".react-flow__node.selected") - ?.getAttribute("data-id") - ) - ok("新节点自动选中(focusNode)", !!selId && selId !== "main", selId ?? "") - ok( - "仅一个外挂面板(单选语义,主线面板已收起)", - (await page.locator(".canvas-expand").count()) === 1 - ) - const firstMsg = page - .locator(".react-flow__node.selected .canvas-expand .msg-list .message") - .first() - ok( - "新节点面板首条 = 所提问题(user 原文)", - (await firstMsg.getAttribute("class"))?.includes("user") && - (await firstMsg.innerText()).includes(Q2.slice(0, 12)) - ) - const box = await page.locator(".react-flow__node.selected").boundingBox() - const vp = page.viewportSize() - ok( - "setCenter 跟随:新节点卡整体在视口内", - box && - box.x >= -2 && - box.y >= -2 && - box.x + box.width <= vp.width + 2 && - box.y + box.height <= vp.height + 2, - box - ? `x=${box.x.toFixed(0)} y=${box.y.toFixed(0)} w=${box.width.toFixed(0)} h=${box.height.toFixed(0)}` - : "无 box" - ) - const zAfter = await getScale() - ok("跟随不缩小 zoom(max(当前, 0.85))", zAfter >= 1.4, `zoom ${z}→${zAfter}`) - await waitPanelDone() - ok( - "新分支首答在面板内流式完成(Markdown 渲染)", - (await page.evaluate(() => { - const bodies = document.querySelectorAll( - ".canvas-expand .message.assistant .md-body" - ) - const last = bodies[bodies.length - 1] - return (last?.textContent ?? "").trim().length > 20 - })) === true - ) - await page.screenshot({ path: SHOT("cc-5-new-node") }) -} - -/* 分支首答完成会触发一次异步标题生成:等它收口(避免与后续划选/存盘竞态) */ -await waitUntil(() => titleResponses >= titlePosts, 30000) -await sleep(400) - -/* ================= 6. 留空提交:新节点面板 composer 预填(同列模式语义) ================= */ -let emptyAnchor = null -{ - // 回主线面板再划选(第二个分支也挂主线,形成同 rank 兄弟节点,供遮挡与重排断言) - await setZoom(0.8) - await page.locator('.react-flow__node[data-id="main"]').click() - await page.waitForTimeout(300) - emptyAnchor = await selectInPanel(PHRASE_C, 5) - ok( - "主线面板再划选(留空路径)", - emptyAnchor !== null, - emptyAnchor?.text ?? "" - ) - const nodesBefore = await page.locator(".react-flow__node").count() - await page.locator(".sel-bubble .ask textarea").press("Enter") // 留空直接回车 - await page.waitForFunction( - (n) => document.querySelectorAll(".react-flow__node").length === n + 1, - nodesBefore, - { timeout: 8000 } - ) - await sleep(700) - const prefill = await page - .locator(".react-flow__node.selected .canvas-expand .cv-composer textarea") - .inputValue() - ok( - "留空分支:新节点面板 composer 预填 kickoffQuestion()(期望值由产品代码生成)", - prefill === kickoffQuestion(emptyAnchor.text), - JSON.stringify(prefill) - ) - ok( - "留空分支消息区为空(未自动发请求)", - (await page - .locator(".react-flow__node.selected .canvas-expand .msg-list .message") - .count()) === 0 - ) -} - -/* ================= 7. 多节点下收起/再展开零重排 + zIndex 遮挡 ================= */ -{ - const before = await nodeTransforms() - const pt = await blankPoint() - await page.mouse.click(pt.x, pt.y) // 点空白:取消选中 = 收起面板 - await sleep(300) - ok("点空白收起面板", (await page.locator(".canvas-expand").count()) === 0) - const afterCollapse = await nodeTransforms() - ok("收起零重排:全部节点坐标不变", sameTransforms(before, afterCollapse)) - - // 展开「上方的兄弟分支」:其面板向下悬垂,盖住下方兄弟卡(LR 特有遮挡面) - const upperSibling = await page.evaluate(() => { - const nodes = Array.from( - document.querySelectorAll('.react-flow__node:not([data-id="main"])') - ).map((el) => ({ - id: el.getAttribute("data-id"), - y: el.getBoundingClientRect().top, - })) - nodes.sort((a, b) => a.y - b.y) - return nodes[0]?.id ?? null - }) - ok("存在两个分支节点(兄弟同 rank)", upperSibling !== null) - await page.locator(`.react-flow__node[data-id="${upperSibling}"]`).click() - await sleep(300) - const afterExpand = await nodeTransforms() - ok("再展开零重排:全部节点坐标不变", sameTransforms(before, afterExpand)) - const zRaised = await page.evaluate(() => { - const sel = document.querySelector(".react-flow__node.selected") - const other = Array.from( - document.querySelectorAll(".react-flow__node:not(.selected)") - )[0] - return Number(sel?.style.zIndex || 0) > Number(other?.style.zIndex || 0) - }) - ok("选中节点 zIndex 抬升(面板盖过兄弟卡)", zRaised) - await page.screenshot({ path: SHOT("cc-6-occlusion") }) -} - -/* ================= 8. 列槽隔离:回列视图布局未变 ================= */ -{ - await page.locator(".topbar button.mode", { hasText: "列" }).click() - await page.waitForSelector(".tc .cols", { timeout: 5000 }) - await sleep(300) - const cols = await page.locator(".tc .cols > .column").count() - ok("画布 fork 不占列槽:回列后仅主线一列", cols === 1, `实际 ${cols} 列`) - const marks = await page.evaluate( - () => document.querySelectorAll(".tc .cols .md-body sup.fn-mark").length - ) - ok("画布 fork 的锚点脚注在列模式原文可见(≥2)", marks >= 2, `实际 ${marks}`) - await page.screenshot({ path: SHOT("cc-7-columns-isolated") }) -} - -/* ================= 9. 双击语义:面板内不误触,节点卡回列 ================= */ -{ - await page.locator(".topbar button.mode", { hasText: "画布" }).click() - await page.waitForSelector(".react-flow__node", { timeout: 10000 }) - await page.waitForTimeout(700) - await page.locator(".react-flow__controls-zoomout").click() - await page.waitForTimeout(300) - await page.locator('.react-flow__node[data-id="main"]').click() - await page.waitForTimeout(300) - ok( - "重进画布:单击主线再展开", - (await page.locator(".canvas-expand").count()) === 1 - ) - await page.locator(".canvas-expand .msg-list.mini").dblclick() - await sleep(400) - ok( - "面板内双击不误触回列(仍在画布)", - (await page.locator(".react-flow").count()) === 1 - ) - await page - .locator('.react-flow__node[data-id="main"] .canvas-card .chead') - .dblclick() - await sleep(500) - ok( - "双击节点卡回列模式(Phase 1 行为保留)", - (await page.locator(".react-flow").count()) === 0 && - (await page.locator(".tc .cols > .column").count()) >= 1 - ) -} - -/* ================= 10. 收尾 ================= */ -ok( - "全程无页面错误(pageerror)", - pageErrors.length === 0, - pageErrors.join(" | ") -) -await waitUntil(() => titleResponses >= titlePosts, 30000) -await sleep(2500) // 最后一轮防抖 PUT 落库后再删行 -const ownTreeId = page.url().split("/").pop() -if (/^[0-9a-f-]{36}$/.test(ownTreeId)) { - await page.request.delete(`${BASE_URL}/api/branch-trees/${ownTreeId}`) - console.log(`已清理本次测试树 ${ownTreeId.slice(0, 8)}…`) -} -await browser.close() -console.log(failed ? "\n==== 存在 FAIL ====" : "\n==== 全部 PASS ====") -process.exit(failed) diff --git a/e2e/thread-chat/verify-live.mjs b/e2e/thread-chat/verify-live.mjs deleted file mode 100644 index 8ae9a179..00000000 --- a/e2e/thread-chat/verify-live.mjs +++ /dev/null @@ -1,328 +0,0 @@ -/** - * ThreadChat(/thread-chat 分支对话页)真实后端端到端验收。 - * - * 运行前提: - * 1. dev server 已在 localhost:4040 跑着(pnpm dev); - * 2. .env.local 配好 MiniMax(MINIMAX_API_KEY / MINIMAX_BASE_URL / LLM_MODEL_ID); - * 3. 本机有 Chromium:优先取环境变量 CHROMIUM_PATH,否则用 playwright-core 默认发现逻辑。 - * 运行(默认 http://localhost:4040,可用 BASE_URL 覆盖;--experimental-strip-types - * 是因为脚本直接 import prompt-pure.ts 生成 kickoff 预填的期望值——文案改一处即全跟随): - * CHROMIUM_PATH=/opt/pw-browsers/chromium node --experimental-strip-types e2e/thread-chat/verify-live.mjs - * - * 断言覆盖:页面加载 → 主线真实流式回复(富文本 Markdown:.md-body 渲染出结构化元素、 - * 无裸 Markdown 记号)→ 划选渲染后的正文开分支(气泡)→ 分支列打开但不自动发请求 - * (composer 预填代拟问题 / 消息区为空 / 2 秒内无新 /api/chat POST)→ 回车确认后 kickoff - * 成为真实 user 气泡 + assistant 流式首答 → payload 契约(继承上文 / kickoff 以真实 user - * 消息在 messages 里 / threadChat.anchorText / 无 system 角色 / 无指令前缀折叠 / - * 无空 assistant)→ 主线源消息出现锚点高亮 / 脚注 → 分支内追问二轮流式。 - * 走真实模型,回复内容非确定,断言只卡结构与契约。截图输出到同目录 shots/(已 gitignore)。 - */ -import { mkdirSync } from "node:fs" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" -import { chromium } from "playwright-core" -import { kickoffQuestion } from "../../lib/thread-chat/application/prompt-policy.ts" - -const here = dirname(fileURLToPath(import.meta.url)) -const shotsDir = join(here, "shots") -mkdirSync(shotsDir, { recursive: true }) -const SHOT = (n) => join(shotsDir, `${n}.png`) -const ok = (label, cond) => { - console.log(`${cond ? "PASS" : "FAIL"} ${label}`) - if (!cond) process.exitCode = 1 -} - -const browser = await chromium.launch({ - executablePath: process.env.CHROMIUM_PATH || undefined, - headless: true, -}) -const page = await browser.newPage({ viewport: { width: 1600, height: 950 } }) - -// 记录发往 /api/chat 的 payload(校验请求契约用) -const payloads = [] -page.on("request", (req) => { - if (req.url().includes("/api/chat") && req.method() === "POST") { - try { - payloads.push(JSON.parse(req.postData() ?? "{}")) - } catch { - /* 非 JSON 忽略 */ - } - } -}) -page.on("pageerror", (e) => console.log("PAGEERROR:", e.message)) - -const BASE_URL = process.env.BASE_URL || "http://localhost:4040" -await page.goto(`${BASE_URL}/thread-chat`, { - waitUntil: "networkidle", -}) -ok("页面加载:.tc 壳存在", (await page.locator(".tc").count()) === 1) - -// ---- 1. 主线发消息,等真实流式回复完成 ---- -const composer = page.locator(".column").first().locator("textarea") -await composer.fill( - "请用小标题和分点列表,较详细地讲解量子纠缠(至少列 4 个要点)," + - "并务必在正文中出现「贝尔不等式」这个词。" -) -await composer.press("Enter") - -await page.waitForSelector(".tc .message.assistant", { timeout: 20000 }) -// 流式完成的判据:正文非空且发送键回到「发送」(busy 解除) -await page.waitForFunction( - () => { - const col = document.querySelector(".column") - const bubbles = col?.querySelectorAll(".message.assistant .bubble") - const last = bubbles?.[bubbles.length - 1] - const btn = col?.querySelector(".composer .send") - return ( - last && - last.textContent.trim().length > 20 && - btn && - !btn.classList.contains("stop") - ) - }, - undefined, - { timeout: 120000 } -) -// 平滑打字(useSmoothText)与「busy 解除」判据存在一帧级竞态:assistant.status 转为完成态、 -// display 尚未从追赶态 snap 到完整 target 的那一帧里,「发送键回到『发送』」就已经为真—— -// snap 发生在下一个 passive effect + 重渲染里,通常 <1 帧但不为 0。直接断言裸 Markdown 记号 -// 会偶发命中这个半途文本(截断处若正落在 "**" 中间会误判成"未渲染")。这里加一小段静置, -// 等 snap 的重渲染落地,不是掩盖真实回归——最终必然收敛到与 msg.text 完全一致的完整正文。 -await page.waitForTimeout(150) -const mainReply = await page - .locator(".column") - .first() - .locator(".message.assistant .bubble") - .last() - .innerText() -ok("主线收到真实流式回复(>20 字)", mainReply.trim().length > 20) -ok( - "回复包含「贝尔不等式」(可校验上下文真实来自模型)", - mainReply.includes("贝尔不等式") -) - -// 富文本断言:.md-body 存在、渲染出结构化元素、且正文无裸 Markdown 记号 -const mdInfo = await page.evaluate(() => { - const col = document.querySelector(".column") - const bubbles = col?.querySelectorAll(".message.assistant .bubble") - const last = bubbles?.[bubbles.length - 1] - const md = last?.querySelector(".md-body") - if (!md) return { hasMd: false } - const structural = md.querySelector( - "strong, em, ul, ol, h1, h2, h3, h4, code, table, blockquote" - ) - // 裸记号只在【代码块之外】检测——/
 的 textContent 合法保留 ** 与 #
-  // (如 Python 注释 # 或 shell 的 **),把它们算作未渲染记号会造成非确定性误报。
-  const clone = md.cloneNode(true)
-  clone.querySelectorAll("code, pre").forEach((n) => n.remove())
-  const prose = clone.textContent || ""
-  // 只有「系统性」记号才判为未渲染:渲染器坏掉时是成片 **/#(每处加粗、每个标题都漏),
-  // 而模型偶发输出的不成对 `**`(CommonMark 语义下字面保留)会留下 1–2 处孤记号,
-  // 那是内容非确定性、不是渲染回归——单跑两次就能出现,此前按「出现即 FAIL」误报。
-  const starHits = [...prose.matchAll(/\*\*/g)]
-  const hashHits = [...prose.matchAll(/(^|\n)#{1,6}\s/g)]
-  return {
-    hasMd: true,
-    structured: !!structural,
-    raw: starHits.length >= 3 || hashHits.length >= 2,
-    strayDetail: [...starHits, ...hashHits]
-      .slice(0, 3)
-      .map((m) => prose.slice(Math.max(0, m.index - 20), m.index + 22))
-      .join(" ⧸ "),
-  }
-})
-ok("assistant 正文进入 .md-body(Markdown 渲染容器)", mdInfo.hasMd)
-ok(
-  "assistant 正文渲染出结构化元素(strong / 列表 / 标题 / 代码等)",
-  mdInfo.structured === true
-)
-ok(
-  "assistant 正文无系统性裸 Markdown 记号(≥3 处 ** / ≥2 行首 #)",
-  mdInfo.raw === false
-)
-if (mdInfo.strayDetail)
-  console.log(
-    `      (零星记号上下文,通常为模型输出的不成对 **:${mdInfo.strayDetail})`
-  )
-await page.screenshot({ path: SHOT("1-main-reply") })
-
-// ---- 2. 划选回复文字 → 气泡 → 开分支 ----
-const anchor = "贝尔不等式"
-const selected = await page.evaluate(async (needle) => {
-  const bubbles = document.querySelectorAll(
-    ".column .message.assistant .bubble"
-  )
-  const bubble = bubbles[bubbles.length - 1]
-  const walker = document.createTreeWalker(bubble, NodeFilter.SHOW_TEXT)
-  let node
-  while ((node = walker.nextNode())) {
-    const i = node.textContent.indexOf(needle)
-    if (i >= 0) {
-      // 先把命中处滚进视口(模拟真实用户看着划选),气泡定位才落在可视区
-      node.parentElement?.scrollIntoView({ block: "center" })
-      await new Promise((r) => setTimeout(r, 120))
-      const range = document.createRange()
-      range.setStart(node, i)
-      range.setEnd(node, i + needle.length)
-      const sel = window.getSelection()
-      sel.removeAllRanges()
-      sel.addRange(range)
-      document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }))
-      return true
-    }
-  }
-  return false
-}, anchor)
-ok("成功划选「贝尔不等式」", selected)
-
-await page.waitForSelector(".tc .sel-bubble", { timeout: 5000 })
-ok("划选气泡浮出", true)
-await page.screenshot({ path: SHOT("2-selection-bubble") })
-const postsBeforeFork = payloads.length
-await page.getByText("开启分支讨论").click()
-
-// ---- 分支列出现:不自动发请求,composer 预填代拟问题等用户确认 ----
-await page.waitForFunction(
-  () => document.querySelectorAll(".column").length >= 2,
-  undefined,
-  {
-    timeout: 10000,
-  }
-)
-ok("分支列已打开", true)
-
-// 预填期望值直接由产品代码的 kickoffQuestion() 生成(文案变更不再破测试)
-const kickoffExpected = kickoffQuestion(anchor)
-const branchCol = page.locator(".column").last()
-const prefillValue = await branchCol.locator("textarea").inputValue()
-ok("分支 composer 预填代拟问题(含锚点原文)", prefillValue === kickoffExpected)
-ok(
-  "分支消息区为空(未自动生成首答)",
-  (await branchCol.locator(".message").count()) === 0
-)
-await page.waitForTimeout(2000)
-ok(
-  "开分支后 2 秒内无新的 /api/chat POST(不再自动发请求)",
-  payloads.length === postsBeforeFork
-)
-await page.screenshot({ path: SHOT("3-branch-prefilled") })
-
-// ---- 回车确认:kickoff 成为真实 user 消息,assistant 流式首答 ----
-await branchCol.locator("textarea").press("Enter")
-await page.waitForFunction(
-  (expected) => {
-    const cols = document.querySelectorAll(".column")
-    const col = cols[cols.length - 1]
-    const userBubble = col?.querySelector(".message.user .bubble")
-    return userBubble && userBubble.textContent.trim() === expected
-  },
-  kickoffExpected,
-  { timeout: 10000 }
-)
-ok("回车后代拟问题成为真实 user 气泡", true)
-await page.waitForFunction(
-  () => {
-    const cols = document.querySelectorAll(".column")
-    const col = cols[cols.length - 1]
-    const bubbles = col?.querySelectorAll(".message.assistant .bubble")
-    const last = bubbles?.[bubbles.length - 1]
-    const btn = col?.querySelector(".composer .send")
-    return (
-      last &&
-      last.textContent.trim().length > 20 &&
-      btn &&
-      !btn.classList.contains("stop")
-    )
-  },
-  undefined,
-  { timeout: 120000 }
-)
-const branchReply = await page
-  .locator(".column")
-  .last()
-  .locator(".message.assistant .bubble")
-  .last()
-  .innerText()
-ok("分支首答已流式完成(>20 字)", branchReply.trim().length > 20)
-
-// 锚点定位生效:主线源消息在渲染后的 .md-body 上出现高亮或脚注(手绘 DOM)
-await page
-  .locator(
-    ".column .message.assistant .md-body [data-text-anchor-mark], .column .message.assistant .md-body sup.fn-mark"
-  )
-  .first()
-  .waitFor({ timeout: 5000 })
-  .catch(() => {})
-const markCount = await page
-  .locator(
-    ".column .message.assistant .md-body [data-text-anchor-mark], .column .message.assistant .md-body sup.fn-mark"
-  )
-  .count()
-ok(`主线源消息出现锚点高亮 / 脚注(当前 ${markCount} 个标记)`, markCount >= 1)
-await page.screenshot({ path: SHOT("4-branch-streamed") })
-
-// ---- 3. 校验分支请求 payload 契约 ----
-const branchPayload = payloads[payloads.length - 1]
-const msgs = branchPayload?.messages ?? []
-const texts = msgs.map((m) => m.parts?.[0]?.text ?? "")
-ok(
-  "分支请求含继承上文(主线的用户提问在 payload 里)",
-  texts.some((t) => t.includes("量子纠缠"))
-)
-ok(
-  "kickoff 以真实 user 消息形式在 messages 里(用户确认后入 store)",
-  msgs.some((m) => m.role === "user" && m.parts?.[0]?.text === kickoffExpected)
-)
-ok(
-  "分支请求带 threadChat.anchorText(system 归服务端构造)",
-  branchPayload?.threadChat?.anchorText === anchor
-)
-ok(
-  "user 消息干净(不再折叠指令前缀)",
-  !(msgs.find((m) => m.role === "user")?.parts?.[0]?.text ?? "").includes(
-    "Markdown"
-  )
-)
-ok(
-  "payload 无 system 角色",
-  msgs.every((m) => m.role !== "system")
-)
-ok(
-  "payload 无空 assistant 消息",
-  msgs.every(
-    (m) => m.role !== "assistant" || (m.parts?.[0]?.text ?? "").trim() !== ""
-  )
-)
-
-// ---- 4. 分支内追问(验证多轮 + kickoff 每轮重建) ----
-const branchComposer = page.locator(".column").last().locator("textarea")
-await branchComposer.fill("换一个通俗的比喻再解释一次。")
-await branchComposer.press("Enter")
-await page.waitForFunction(
-  () => {
-    const cols = document.querySelectorAll(".column")
-    const col = cols[cols.length - 1]
-    const bubbles = col?.querySelectorAll(".message.assistant .bubble")
-    return (
-      bubbles &&
-      bubbles.length >= 2 &&
-      bubbles[bubbles.length - 1].textContent.trim().length > 10
-    )
-  },
-  undefined,
-  { timeout: 120000 }
-)
-ok("分支内追问收到第二条流式回复", true)
-await page.screenshot({ path: SHOT("5-branch-followup") })
-
-console.log("\n--- 主线回复节选 ---\n" + mainReply.slice(0, 160))
-console.log("\n--- 分支首答节选 ---\n" + branchReply.slice(0, 160))
-
-// 自清理:持久化上线后本脚本每次跑都会经防抖存下一棵测试树,删掉避免污染开发库
-// (DELETE 幂等;treeId 从当前 URL 取——裸路径已被 replace 到 /thread-chat/{uuid})
-const ownTreeId = page.url().split("/").pop()
-if (/^[0-9a-f-]{36}$/.test(ownTreeId)) {
-  await page.request.delete(`${BASE_URL}/api/branch-trees/${ownTreeId}`)
-  console.log(`已清理本次测试树 ${ownTreeId.slice(0, 8)}…`)
-}
-await browser.close()
diff --git a/e2e/thread-chat/verify-markdown-artifact.mjs b/e2e/thread-chat/verify-markdown-artifact.mjs
deleted file mode 100644
index 8706d086..00000000
--- a/e2e/thread-chat/verify-markdown-artifact.mjs
+++ /dev/null
@@ -1,362 +0,0 @@
-/**
- * ThreadChat Markdown Artifact 的浏览器链路验收(后端与持久化 API 均在浏览器层 mock)。
- *
- * 运行:
- *   CHROMIUM_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
- *   BASE_URL=http://localhost:4040 \
- *   node e2e/thread-chat/verify-markdown-artifact.mjs
- */
-import { chromium } from "playwright-core"
-
-const BASE_URL = process.env.BASE_URL || "http://localhost:4040"
-const TREE_ID = "00000000-0000-4000-8000-000000000042"
-const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
-
-let failed = false
-function ok(label, condition, detail = "") {
-  console.log(
-    `${condition ? "PASS" : "FAIL"}  ${label}${detail ? `(${detail})` : ""}`
-  )
-  if (!condition) failed = true
-}
-
-function emptyState() {
-  return {
-    threads: {
-      main: {
-        id: "main",
-        parentId: null,
-        depth: 0,
-        title: "主线",
-        anchorText: null,
-        forkFromMsgId: null,
-        footnote: null,
-        children: [],
-        messages: [],
-        lastActive: 1,
-      },
-    },
-    artifacts: {},
-    artifactOrder: [],
-    recents: [],
-    footnoteCounter: 0,
-    seq: 1,
-    tick: 1,
-  }
-}
-
-function markdownSse(index) {
-  const title = index === 1 ? "发布计划" : `修订版 ${index}`
-  const content =
-    index === 1
-      ? "# 发布计划\n\n- 项目 A\n- 项目 B\n\n| 阶段 | 状态 |\n| --- | --- |\n| 开发 | 完成 |"
-      : `# ${title}\n\n- 已根据后续要求更新`
-  const inputText = JSON.stringify({ title, content })
-  const splitAt = Math.ceil(inputText.length / 2)
-  const chunks = [
-    { type: "text-delta", id: `text-${index}`, delta: "\n" },
-    {
-      type: "tool-input-start",
-      toolCallId: `call-${index}`,
-      toolName: "createMarkdownArtifact",
-    },
-    {
-      type: "tool-input-delta",
-      toolCallId: `call-${index}`,
-      inputTextDelta: inputText.slice(0, splitAt),
-    },
-    {
-      type: "tool-input-delta",
-      toolCallId: `call-${index}`,
-      inputTextDelta: inputText.slice(splitAt),
-    },
-    {
-      type: "tool-input-available",
-      toolCallId: `call-${index}`,
-      toolName: "createMarkdownArtifact",
-      input: { title, content },
-    },
-    { type: "finish" },
-  ]
-  return `${chunks
-    .map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`)
-    .join("")}data: [DONE]\n\n`
-}
-
-const browser = await chromium.launch({
-  executablePath: process.env.CHROMIUM_PATH || undefined,
-  headless: true,
-})
-const context = await browser.newContext({
-  viewport: { width: 1600, height: 950 },
-})
-await context.addCookies([
-  {
-    name: "better-auth.session_token",
-    value: "thread-chat-markdown-e2e",
-    url: BASE_URL,
-  },
-])
-
-let persistedState = emptyState()
-let chatRequestCount = 0
-const chatBodies = []
-const savedStates = []
-
-await context.route("**/api/**", async (route) => {
-  const request = route.request()
-  const url = new URL(request.url())
-
-  if (url.pathname === `/api/branch-trees/${TREE_ID}`) {
-    if (request.method() === "GET") {
-      await route.fulfill({
-        contentType: "application/json",
-        body: JSON.stringify({ state: persistedState, customTitle: null }),
-      })
-      return
-    }
-    if (request.method() === "PUT") {
-      const body = request.postDataJSON()
-      persistedState = structuredClone(body.state)
-      savedStates.push(structuredClone(body.state))
-      await route.fulfill({
-        status: 200,
-        contentType: "application/json",
-        body: "{}",
-      })
-      return
-    }
-  }
-
-  if (url.pathname === "/api/branch-trees" && request.method() === "GET") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: JSON.stringify({ trees: [] }),
-    })
-    return
-  }
-
-  if (url.pathname === "/api/chat" && request.method() === "POST") {
-    chatRequestCount++
-    chatBodies.push(request.postDataJSON())
-    const requestIndex = chatRequestCount
-
-    // 第三次请求保持 pending,供页面点击“停止”;retry 的第四次请求正常产出。
-    if (requestIndex === 3) {
-      await sleep(6_000)
-      try {
-        await route.fulfill({
-          status: 200,
-          contentType: "text/event-stream",
-          body: "data: [DONE]\n\n",
-        })
-      } catch {
-        // 浏览器 abort 后 route 已关闭,符合预期。
-      }
-      return
-    }
-
-    await route.fulfill({
-      status: 200,
-      headers: {
-        "content-type": "text/event-stream",
-        "cache-control": "no-cache",
-      },
-      body: markdownSse(requestIndex),
-    })
-    return
-  }
-
-  if (url.pathname === "/api/title") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: JSON.stringify({ title: "Markdown 分支" }),
-    })
-    return
-  }
-
-  if (url.pathname === "/api/auth/get-session") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: JSON.stringify({
-        user: { id: "e2e", name: "E2E", email: "e2e@example.com" },
-        session: { id: "e2e-session" },
-      }),
-    })
-    return
-  }
-
-  if (url.pathname === "/api/billing/summary") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: JSON.stringify({ balanceMicros: 5_000_000, totalUsageMicros: 0 }),
-    })
-    return
-  }
-
-  await route.fulfill({
-    status: 404,
-    contentType: "application/json",
-    body: "{}",
-  })
-})
-
-const page = await context.newPage()
-const pageErrors = []
-page.on("pageerror", (error) => pageErrors.push(String(error)))
-
-try {
-  await page.goto(`${BASE_URL}/thread-chat/${TREE_ID}`, {
-    waitUntil: "networkidle",
-  })
-  const composer = page.locator(".column").first().locator("textarea")
-
-  await composer.fill("请帮我生成一个 Markdown,总结发布计划")
-  await composer.press("Enter")
-  const firstMessage = page.locator(".message.assistant").last()
-  await firstMessage.locator(".acard").waitFor({ state: "visible" })
-  ok("列视图:Markdown 卡片插入 assistant 消息", true)
-  ok(
-    "Artifact-only:不渲染空气泡",
-    (await firstMessage.locator('.bubble[data-role="assistant"]').count()) === 0
-  )
-  ok(
-    "卡片文案:标题、MARKDOWN、打开预览",
-    (await firstMessage.textContent()).includes("发布计划") &&
-      (await firstMessage.textContent()).includes("MARKDOWN") &&
-      (await firstMessage.textContent()).includes("打开预览")
-  )
-
-  await firstMessage.locator(".acard").click()
-  const drawer = page.locator(".art-drawer.open")
-  await drawer.waitFor({ state: "visible" })
-  ok("点击卡片打开右侧 Markdown 面板", (await drawer.count()) === 1)
-  ok("GFM:标题渲染", (await drawer.locator(".art-body h1").count()) === 1)
-  ok("GFM:列表渲染", (await drawer.locator(".art-body li").count()) === 2)
-  ok("GFM:表格渲染", (await drawer.locator(".art-body table").count()) === 1)
-
-  await page.waitForTimeout(1_900)
-  ok("防抖整树保存包含 Markdown", savedStates.length > 0)
-  ok(
-    "持久化:kind、消息关联、来源 thread 与 tab 顺序齐全",
-    persistedState.artifactOrder.length === 1 &&
-      persistedState.artifacts[persistedState.artifactOrder[0]]?.kind ===
-        "markdown" &&
-      persistedState.artifacts[persistedState.artifactOrder[0]]
-        ?.sourceThreadId === "main" &&
-      persistedState.threads.main.messages.some((message) =>
-        message.artifactIds?.includes(persistedState.artifactOrder[0])
-      ) &&
-      persistedState.threads.main.messages.every(
-        (message) => message.markdownGeneration === undefined
-      )
-  )
-
-  await page.reload({ waitUntil: "networkidle" })
-  await page.locator(".message.assistant .acard").waitFor({ state: "visible" })
-  ok("刷新:Markdown 卡片恢复", true)
-
-  const composerAfterReload = page
-    .locator(".column")
-    .first()
-    .locator("textarea")
-  await composerAfterReload.fill("请修改刚才的 Markdown,补充验收部分")
-  await composerAfterReload.press("Enter")
-  await page
-    .locator(".message.assistant .acard")
-    .nth(1)
-    .waitFor({ state: "visible" })
-  const replayed = JSON.stringify(chatBodies[1]?.messages ?? [])
-  ok(
-    "后续请求上下文包含旧文档标题与正文",
-    replayed.includes("[Markdown Artifact: 发布计划]") &&
-      replayed.includes("| 开发 | 完成 |")
-  )
-
-  await page.getByRole("button", { name: /画布/ }).click()
-  await page.locator('.react-flow__node[data-id="main"]').waitFor()
-  await page.locator('.react-flow__node[data-id="main"]').click()
-  const canvasCards = page.locator(".canvas-expand .acard")
-  await canvasCards.first().waitFor({ state: "visible" })
-  ok("画布:复用同一 Markdown 卡片", (await canvasCards.count()) === 2)
-  ok(
-    "画布 Artifact-only:不渲染空气泡",
-    (await page
-      .locator(".canvas-expand .message.assistant .bubble")
-      .count()) === 0
-  )
-  await canvasCards.last().click()
-  ok(
-    "画布卡片可打开全局面板",
-    (await page.locator(".art-drawer.open").count()) === 1
-  )
-
-  await page.locator(".art-drawer.open .art-x").click()
-  await page.locator(".art-drawer.open").waitFor({ state: "hidden" })
-  await page.getByRole("button", { name: /^列$/ }).click()
-  const columnComposer = page.locator(".column").first().locator("textarea")
-  await columnComposer.fill("开始一个可停止的请求")
-  await columnComposer.press("Enter")
-  const stopButton = page
-    .locator(".column")
-    .first()
-    .locator(".composer .send.stop")
-  await stopButton.waitFor({ state: "visible" })
-  await stopButton.click()
-  await page.getByText("已停止生成").waitFor({ state: "visible" })
-  ok("停止:零输出请求进入可重试错误态", true)
-  await page.getByRole("button", { name: "重试" }).last().click()
-  await page
-    .locator(".message.assistant .acard")
-    .nth(2)
-    .waitFor({ state: "visible" })
-  ok("重试:重新生成 Markdown 卡片", true)
-
-  await page.waitForTimeout(1_900)
-  const dirty = structuredClone(persistedState)
-  const firstAssistant = dirty.threads.main.messages.find(
-    (message) => message.role === "assistant" && message.artifactIds?.length
-  )
-  firstAssistant.status = "streaming"
-  firstAssistant.artifactIds.push("missing-artifact")
-  dirty.threads.main.messages.push({
-    id: "pending-empty",
-    role: "assistant",
-    text: "",
-    forks: [],
-    status: "pending",
-  })
-  dirty.artifacts.orphan = {
-    id: "orphan",
-    kind: "markdown",
-    title: "孤儿 Markdown",
-    content: "# 不应显示",
-    sourceThreadId: "main",
-  }
-  dirty.artifactOrder.unshift("orphan")
-  persistedState = dirty
-
-  await page.reload({ waitUntil: "networkidle" })
-  await page.locator(".message.assistant .acard").first().waitFor()
-  ok(
-    "sanitize:Artifact-only streaming 恢复 done 且空 pending 被删",
-    (await page.locator(".typing, .caret").count()) === 0 &&
-      (await page.locator('[data-msg-id="pending-empty"]').count()) === 0
-  )
-  ok(
-    "sanitize:坏引用与孤儿 Artifact 不显示",
-    (await page.getByText("孤儿 Markdown").count()) === 0 &&
-      !(await page.locator(".topbar").textContent()).includes("Markdown4")
-  )
-
-  ok(
-    "浏览器运行期无 pageerror",
-    pageErrors.length === 0,
-    pageErrors.join(" | ")
-  )
-} finally {
-  await context.close()
-  await browser.close()
-}
-
-if (failed) process.exitCode = 1
diff --git a/e2e/thread-chat/verify-markdown-model.mjs b/e2e/thread-chat/verify-markdown-model.mjs
deleted file mode 100644
index e395ca88..00000000
--- a/e2e/thread-chat/verify-markdown-model.mjs
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * 默认 MiniMax 的真实 Markdown 工具选择语料验收。会产生少量真实模型用量。
- * 运行:node --experimental-strip-types e2e/thread-chat/verify-markdown-model.mjs
- */
-import dotenv from "dotenv"
-
-dotenv.config({ path: ".env.local", quiet: true })
-
-const [{ createOpenAICompatible }, { generateText, isStepCount, tool }] =
-  await Promise.all([import("@ai-sdk/openai-compatible"), import("ai")])
-const {
-  MARKDOWN_ARTIFACT_TOOL_DESCRIPTION,
-  MARKDOWN_ARTIFACT_TOOL_NAME,
-  markdownArtifactInputSchema,
-} = await import("../../lib/chat/markdown-artifact.ts")
-const { THREAD_CHAT_SYSTEM } = await import("../../constants/thread-chat.ts")
-
-if (!process.env.MINIMAX_API_KEY) {
-  console.log("SKIP  MINIMAX_API_KEY 未配置")
-  process.exit(0)
-}
-
-const provider = createOpenAICompatible({
-  name: "minimax-markdown-e2e",
-  baseURL: process.env.MINIMAX_BASE_URL ?? "https://api.minimaxi.com/v1",
-  apiKey: process.env.MINIMAX_API_KEY,
-  includeUsage: true,
-})
-const model = provider(process.env.LLM_MODEL_ID ?? "MiniMax-M2")
-const createMarkdownArtifact = tool({
-  description: MARKDOWN_ARTIFACT_TOOL_DESCRIPTION,
-  inputSchema: markdownArtifactInputSchema,
-  execute: async () => ({ created: true }),
-})
-
-const cases = [
-  {
-    label: "中文显式交付",
-    prompt: "请把登录、导出和监控这三点整理成一份简短的 Markdown 文档。",
-    expected: true,
-  },
-  {
-    label: "英文显式交付",
-    prompt:
-      "Deliver a short .md release note for version 1.2: fixed login and added export.",
-    expected: true,
-  },
-  {
-    label: "等价改写表达",
-    prompt:
-      "我要把这段会议纪要直接存成 README.md:周五上线,负责人是 Alex,发布前跑回归测试。",
-    expected: true,
-  },
-  {
-    label: "概念问答反例",
-    prompt: "Markdown 是什么?请用两句话解释。",
-    expected: false,
-  },
-]
-
-let failed = false
-for (const item of cases) {
-  const result = await generateText({
-    model,
-    system: THREAD_CHAT_SYSTEM,
-    prompt: item.prompt,
-    tools: { [MARKDOWN_ARTIFACT_TOOL_NAME]: createMarkdownArtifact },
-    stopWhen: isStepCount(2),
-    prepareStep: ({ stepNumber }) =>
-      stepNumber === 0
-        ? { activeTools: [MARKDOWN_ARTIFACT_TOOL_NAME] }
-        : { activeTools: [] },
-    maxOutputTokens: 512,
-  })
-  const calls = result.steps.flatMap((step) => step.toolCalls)
-  const selected = calls.some(
-    (call) => call.toolName === MARKDOWN_ARTIFACT_TOOL_NAME
-  )
-  const once = calls.length <= 1
-  const pass = selected === item.expected && once
-  console.log(
-    `${pass ? "PASS" : "FAIL"}  ${item.label}:${selected ? "调用 Markdown 工具" : "普通回答"}${once ? "" : `,调用 ${calls.length} 次`}`
-  )
-  failed ||= !pass
-}
-
-if (failed) process.exitCode = 1
diff --git a/e2e/thread-chat/verify-persist.mjs b/e2e/thread-chat/verify-persist.mjs
deleted file mode 100644
index a910d4c2..00000000
--- a/e2e/thread-chat/verify-persist.mjs
+++ /dev/null
@@ -1,263 +0,0 @@
-/**
- * ThreadChat 分支树 DB 持久化端到端验收(openspec: add-branch-tree-persistence)。
- *
- * 前提与 verify-live.mjs 相同(dev server + MiniMax key + Chromium),另需
- * DATABASE_URL 可连且已应用 branch_trees 迁移。运行:
- *   CHROMIUM_PATH=... BASE_URL=http://localhost:4040 node e2e/thread-chat/verify-persist.mjs
- *
- * 断言链:裸路径 replace 到 /thread-chat/{uuid} → 主线流式 + 划选开分支(回车首答)
- * → 过防抖观测整树 PUT → 同 context 重载全恢复(消息/分支列=工作台记忆/锚点/无转圈)
- * → 全新 context 直访同 URL 恢复(URL 即身份)→ 新对话空树 + 原树回访仍在
- * → DB 行断言 → sanitize(直写脏快照后加载收敛)。测试树行跑完清理。
- */
-import { readFileSync } from "node:fs"
-import { dirname, join } from "node:path"
-import { fileURLToPath } from "node:url"
-import { chromium } from "playwright-core"
-import postgres from "postgres"
-
-const here = dirname(fileURLToPath(import.meta.url))
-const SHOT = (n) => join(here, "shots", `${n}.png`)
-const ok = (label, cond) => {
-  console.log(`${cond ? "PASS" : "FAIL"}  ${label}`)
-  if (!cond) process.exitCode = 1
-}
-const BASE = process.env.BASE_URL || "http://localhost:4040"
-
-// DATABASE_URL:优先环境变量,回退 .env.local
-const env = Object.fromEntries(
-  readFileSync(join(here, "../../.env.local"), "utf8")
-    .split("\n")
-    .filter((l) => l.includes("=") && !l.startsWith("#"))
-    .map((l) => {
-      const i = l.indexOf("=")
-      return [
-        l.slice(0, i).trim(),
-        l
-          .slice(i + 1)
-          .trim()
-          .replace(/^["']|["']$/g, ""),
-      ]
-    })
-)
-const sql = postgres(process.env.DATABASE_URL || env.DATABASE_URL, { max: 1 })
-const testTreeIds = []
-
-const browser = await chromium.launch({
-  executablePath: process.env.CHROMIUM_PATH || undefined,
-  headless: true,
-})
-
-// ---- 1. 裸路径 → replace 到 UUID URL ----
-const ctx1 = await browser.newContext()
-const page = await ctx1.newPage()
-await page.goto(`${BASE}/thread-chat`, { waitUntil: "networkidle" })
-await page.waitForURL(/\/thread-chat\/[0-9a-f-]{36}$/, { timeout: 10000 })
-const treeUrl = page.url()
-const treeId = treeUrl.split("/").pop()
-testTreeIds.push(treeId)
-ok("裸路径 replace 到 /thread-chat/{uuid}", /^[0-9a-f-]{36}$/.test(treeId))
-
-// ---- 2. 主线发消息 → 划选开分支 → 回车首答 ----
-const puts = []
-page.on("request", (r) => {
-  if (r.url().includes("/api/branch-trees/") && r.method() === "PUT")
-    puts.push(r.url())
-})
-await page
-  .locator(".column")
-  .first()
-  .locator("textarea")
-  .fill("用两三句话介绍二分查找,请包含「时间复杂度」这个词。")
-await page.locator(".column").first().locator("textarea").press("Enter")
-await page.waitForFunction(
-  () => {
-    const col = document.querySelector(".column")
-    const b = col?.querySelectorAll(".message.assistant .bubble")
-    const btn = col?.querySelector(".composer .send")
-    return (
-      b?.length &&
-      b[b.length - 1].textContent.trim().length > 20 &&
-      btn &&
-      !btn.classList.contains("stop")
-    )
-  },
-  undefined,
-  { timeout: 120000 }
-)
-await page.waitForTimeout(200)
-const selected = await page.evaluate((needle) => {
-  const md = [
-    ...document.querySelectorAll(".column .message.assistant .md-body"),
-  ].pop()
-  const w = document.createTreeWalker(md, NodeFilter.SHOW_TEXT)
-  let n
-  while ((n = w.nextNode())) {
-    const i = n.textContent.indexOf(needle)
-    if (i >= 0) {
-      const r = document.createRange()
-      r.setStart(n, i)
-      r.setEnd(n, i + needle.length)
-      const s = getSelection()
-      s.removeAllRanges()
-      s.addRange(r)
-      document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }))
-      return true
-    }
-  }
-  return false
-}, "时间复杂度")
-ok("划选「时间复杂度」", selected)
-await page
-  .locator(".tc .sel-bubble")
-  .waitFor({ state: "visible", timeout: 5000 })
-await page.waitForTimeout(300)
-await page.getByText("开启分支讨论").click()
-await page.waitForFunction(
-  () => document.querySelectorAll(".column").length >= 2,
-  undefined,
-  { timeout: 8000 }
-)
-await page.locator(".column").last().locator("textarea").press("Enter")
-await page.waitForFunction(
-  () => {
-    const cols = document.querySelectorAll(".column")
-    const col = cols[cols.length - 1]
-    const b = col?.querySelectorAll(".message.assistant .bubble")
-    const btn = col?.querySelector(".composer .send")
-    return (
-      b?.length &&
-      b[b.length - 1].textContent.trim().length > 20 &&
-      btn &&
-      !btn.classList.contains("stop")
-    )
-  },
-  undefined,
-  { timeout: 120000 }
-)
-ok("分支首答流式完成", true)
-
-// ---- 3. 过防抖 → 整树 PUT 发生 ----
-await page.waitForTimeout(2200)
-ok("防抖后发生整树 PUT", puts.length >= 1)
-
-// ---- 4. 同 context 重载:全恢复(含工作台记忆)----
-await page.goto(treeUrl, { waitUntil: "networkidle" })
-await page.waitForSelector(".tc .message.assistant", { timeout: 15000 })
-const restored = await page.evaluate(() => ({
-  cols: document.querySelectorAll(".column").length,
-  mainMsgs:
-    document.querySelector(".column")?.querySelectorAll(".message").length ?? 0,
-  anchors: document.querySelectorAll(
-    ".md-body [data-text-anchor-mark], .md-body sup.fn-mark"
-  ).length,
-  spinning: document.querySelectorAll(".typing, .caret").length,
-}))
-ok("重载:主线消息恢复", restored.mainMsgs >= 2)
-ok("重载:分支列仍开着(工作台记忆)", restored.cols >= 2)
-ok("重载:锚点高亮/脚注恢复", restored.anchors >= 1)
-ok("重载:无 pending 转圈残留", restored.spinning === 0)
-await page.screenshot({ path: SHOT("persist-restored") })
-await ctx1.close()
-
-// ---- 5. 全新 context(无 localStorage)直访同 URL ----
-const ctx2 = await browser.newContext()
-const p2 = await ctx2.newPage()
-await p2.goto(treeUrl, { waitUntil: "networkidle" })
-await p2.waitForSelector(".tc .message.assistant", { timeout: 15000 })
-const fresh = await p2.evaluate(() => ({
-  mainMsgs:
-    document.querySelector(".column")?.querySelectorAll(".message").length ?? 0,
-  anchors: document.querySelectorAll(
-    ".md-body [data-text-anchor-mark], .md-body sup.fn-mark"
-  ).length,
-}))
-ok("新 context 直访 URL:消息恢复(URL 即身份)", fresh.mainMsgs >= 2)
-ok("新 context 直访 URL:锚点恢复", fresh.anchors >= 1)
-
-// ---- 6. 新对话:空树,原树可回访 ----
-await p2.getByRole("button", { name: "新对话" }).click()
-await p2.waitForURL(
-  (u) =>
-    /\/thread-chat\/[0-9a-f-]{36}$/.test(u.toString()) &&
-    !u.toString().includes(treeId),
-  { timeout: 8000 }
-)
-testTreeIds.push(p2.url().split("/").pop())
-await p2.waitForTimeout(400)
-ok("新对话:空树", (await p2.locator(".message").count()) === 0)
-await p2.goto(treeUrl, { waitUntil: "networkidle" })
-await p2.waitForSelector(".tc .message.assistant", { timeout: 15000 })
-ok("原 URL 回访:原树仍在", (await p2.locator(".message").count()) >= 2)
-await ctx2.close()
-
-// ---- 7. DB 行断言 ----
-const rows =
-  await sql`SELECT state, title FROM branch_trees WHERE id = ${treeId}`
-ok("DB:treeId 行存在", rows.length === 1)
-const threads = rows[0] ? Object.keys(rows[0].state?.threads ?? {}) : []
-ok(
-  "DB:state.threads 含 main + 分支",
-  threads.includes("main") && threads.length >= 2
-)
-ok("DB:派生标题非空", !!rows[0]?.title)
-
-// ---- 8. sanitize:直写脏快照后加载收敛 ----
-const dirtyId = crypto.randomUUID()
-testTreeIds.push(dirtyId)
-const dirtyState = {
-  threads: {
-    main: {
-      id: "main",
-      parentId: null,
-      depth: 0,
-      title: "主线",
-      anchorText: null,
-      forkFromMsgId: null,
-      footnote: null,
-      children: [],
-      lastActive: 2,
-      messages: [
-        { id: "m1", role: "user", text: "测试", forks: [] },
-        {
-          id: "m2",
-          role: "assistant",
-          text: "这是流式到一半的内容",
-          forks: [],
-          status: "streaming",
-        },
-        { id: "m3", role: "assistant", text: "", forks: [], status: "pending" },
-      ],
-    },
-  },
-  artifacts: {},
-  artifactOrder: [],
-  recents: [],
-  footnoteCounter: 0,
-  seq: 4,
-  tick: 2,
-}
-await sql`INSERT INTO branch_trees (id, title, state) VALUES (${dirtyId}, '脏快照测试', ${sql.json(dirtyState)})`
-const ctx3 = await browser.newContext()
-const p3 = await ctx3.newPage()
-await p3.goto(`${BASE}/thread-chat/${dirtyId}`, { waitUntil: "networkidle" })
-await p3.waitForSelector(".tc .message", { timeout: 15000 })
-const sane = await p3.evaluate(() => ({
-  msgs: document.querySelectorAll(".message").length,
-  spinning: document.querySelectorAll(".typing, .caret").length,
-  lastText:
-    [...document.querySelectorAll(".message.assistant .bubble")].pop()
-      ?.textContent ?? "",
-}))
-ok("sanitize:空 pending 占位被删(3→2 条)", sane.msgs === 2)
-ok(
-  "sanitize:半截 streaming 以正文显示为 done",
-  sane.lastText.includes("流式到一半") && sane.spinning === 0
-)
-await ctx3.close()
-
-// ---- 清理测试树行 ----
-await sql`DELETE FROM branch_trees WHERE id = ANY(${testTreeIds})`
-console.log(`已清理 ${testTreeIds.length} 行测试树`)
-await sql.end()
-await browser.close()
diff --git a/e2e/thread-chat/verify-syntax-highlighting.mjs b/e2e/thread-chat/verify-syntax-highlighting.mjs
deleted file mode 100644
index d1ac2942..00000000
--- a/e2e/thread-chat/verify-syntax-highlighting.mjs
+++ /dev/null
@@ -1,288 +0,0 @@
-/**
- * Thread Chat Shiki 浏览器验收(业务 API 在浏览器层 mock;服务端 layout 仍校验会话)。
- *
- * 运行:
- *   CHROMIUM_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
- *   STORAGE_STATE=/path/to/authenticated-storage-state.json \
- *   BASE_URL=http://localhost:4040 \
- *   node e2e/thread-chat/verify-syntax-highlighting.mjs
- *
- * 覆盖真实 renderer,而非检查源码:稳定消息与 Artifact 的 Shiki DOM、未知语言
- * plaintext、HTML/script 文本转义、复制原文,以及高亮异步结算后的持久锚点恢复、
- * 锚点点击与正常文本选择。
- */
-import { chromium } from "playwright-core"
-
-const BASE_URL = process.env.BASE_URL || "http://localhost:4040"
-const STORAGE_STATE = process.env.STORAGE_STATE
-const TREE_ID = "00000000-0000-4000-8000-000000000043"
-const ANCHOR_TEXT = "可点击的持久锚点"
-const MESSAGE_CODE =
-  "const escaped = ''"
-const ARTIFACT_CODE =
-  "console.log('')"
-const SHELL_SESSION_CODE = "$ echo should-remain-plaintext"
-
-let failed = false
-function ok(label, condition, detail = "") {
-  console.log(
-    `${condition ? "PASS" : "FAIL"}  ${label}${detail ? `(${detail})` : ""}`
-  )
-  if (!condition) failed = true
-}
-
-function seededState() {
-  return {
-    threads: {
-      main: {
-        id: "main",
-        modelId: "minimax-m2",
-        parentId: null,
-        depth: 0,
-        title: "主线",
-        anchorText: null,
-        forkFromMsgId: null,
-        footnote: null,
-        children: ["branch-1"],
-        lastActive: 2,
-        messages: [
-          {
-            id: "message-1",
-            role: "assistant",
-            status: "done",
-            text: `# 安全与持久锚点\n\n这里有${ANCHOR_TEXT},用于验证异步高亮完成后仍能恢复脚注。\n\n\`\`\`ts {1}\n${MESSAGE_CODE}\n\`\`\`\n\n\`\`\`future-lang\n\n\`\`\`\n\n\`\`\`shell-session\n${SHELL_SESSION_CODE}\n\`\`\``,
-            forks: [
-              {
-                text: ANCHOR_TEXT,
-                num: 1,
-                threadId: "branch-1",
-                depth: 1,
-                anchor: {
-                  quote: { exact: ANCHOR_TEXT, prefix: "", suffix: "" },
-                },
-              },
-            ],
-            artifactIds: ["artifact-1"],
-          },
-        ],
-      },
-      "branch-1": {
-        id: "branch-1",
-        modelId: "minimax-m2",
-        parentId: "main",
-        depth: 1,
-        title: "锚点分支",
-        anchorText: ANCHOR_TEXT,
-        forkFromMsgId: "message-1",
-        footnote: 1,
-        children: [],
-        lastActive: 1,
-        messages: [
-          {
-            id: "branch-message-1",
-            role: "assistant",
-            status: "done",
-            text: "这是一条已持久化的分支回复。",
-            forks: [],
-          },
-        ],
-      },
-    },
-    artifacts: {
-      "artifact-1": {
-        id: "artifact-1",
-        kind: "markdown",
-        title: "含代码的静态 Artifact",
-        content: `# Artifact 安全性\n\n\`\`\`js\n${ARTIFACT_CODE}\n\`\`\``,
-        sourceThreadId: "main",
-      },
-    },
-    artifactOrder: ["artifact-1"],
-    recents: [],
-    footnoteCounter: 1,
-    seq: 4,
-    tick: 2,
-  }
-}
-
-const browser = await chromium.launch({
-  executablePath: process.env.CHROMIUM_PATH || undefined,
-  headless: true,
-})
-const context = await browser.newContext({
-  viewport: { width: 1600, height: 950 },
-  ...(STORAGE_STATE ? { storageState: STORAGE_STATE } : {}),
-})
-let persistedState = seededState()
-
-await context.addInitScript(() => {
-  Object.defineProperty(navigator, "clipboard", {
-    configurable: true,
-    value: {
-      writeText(text) {
-        globalThis.__threadChatCopiedCode = text
-        return Promise.resolve()
-      },
-    },
-  })
-})
-
-await context.route("**/api/**", async (route) => {
-  const request = route.request()
-  const url = new URL(request.url())
-
-  if (url.pathname === `/api/branch-trees/${TREE_ID}`) {
-    if (request.method() === "GET") {
-      await route.fulfill({
-        contentType: "application/json",
-        body: JSON.stringify({ state: persistedState, customTitle: null }),
-      })
-      return
-    }
-    if (request.method() === "PUT") {
-      persistedState = structuredClone(request.postDataJSON().state)
-      await route.fulfill({ contentType: "application/json", body: "{}" })
-      return
-    }
-  }
-
-  if (url.pathname === "/api/branch-trees" && request.method() === "GET") {
-    await route.fulfill({ contentType: "application/json", body: '{"trees":[]}' })
-    return
-  }
-  if (url.pathname === "/api/auth/get-session") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: JSON.stringify({
-        user: { id: "e2e", name: "E2E", email: "e2e@example.com" },
-        session: { id: "e2e-session" },
-      }),
-    })
-    return
-  }
-  if (url.pathname === "/api/billing/summary") {
-    await route.fulfill({
-      contentType: "application/json",
-      body: '{"balanceMicros":5000000,"totalUsageMicros":0}',
-    })
-    return
-  }
-
-  await route.fulfill({ status: 404, contentType: "application/json", body: "{}" })
-})
-
-const page = await context.newPage()
-const pageErrors = []
-page.on("pageerror", (error) => pageErrors.push(String(error)))
-
-try {
-  await page.goto(`${BASE_URL}/thread-chat/${TREE_ID}`, { waitUntil: "networkidle" })
-  if (new URL(page.url()).pathname === "/sign-in") {
-    throw new Error(
-      "Thread Chat 的服务端 layout 需要真实会话;请通过 STORAGE_STATE 提供已登录的 Playwright storage state。"
-    )
-  }
-  const sourceMessage = page.locator('.column[data-thread-id="main"] .message.assistant')
-  await sourceMessage.locator(".md-body").waitFor({ state: "visible" })
-  await sourceMessage
-    .locator('.md-body[data-content-settled="true"]')
-    .waitFor({ state: "attached", timeout: 20_000 })
-
-  const messageCode = sourceMessage.locator(".md-code").filter({ hasText: MESSAGE_CODE })
-  const unknownCode = sourceMessage.locator(".md-code").filter({ hasText: "__thread_unknown_xss" })
-  const shellSessionCode = sourceMessage
-    .locator(".md-code")
-    .filter({ hasText: SHELL_SESSION_CODE })
-  ok("稳定消息:受支持语言在实际 renderer 中生成 Shiki DOM", (await messageCode.locator("pre.shiki").count()) === 1)
-  ok("未知语言:在实际 renderer 中保留 plaintext fallback", (await unknownCode.locator("pre.shiki").count()) === 0)
-  ok(
-    "未知语言:fence 标签完整保留为 future-lang",
-    (await unknownCode.locator(".lang").innerText()) === "future-lang"
-  )
-  ok(
-    "shell-session:不截成 shell 且不误用 Bash 高亮",
-    (await shellSessionCode.locator(".lang").innerText()) === "shell-session" &&
-      (await shellSessionCode.locator("pre.shiki").count()) === 0
-  )
-  ok(
-    "代码文本:Shiki renderer 保留原文",
-    (await messageCode.locator("code").innerText()) === MESSAGE_CODE
-  )
-  ok(
-    "安全:消息代码的 script 文本不会执行或插入 script 节点",
-    await page.evaluate(() =>
-      globalThis.__thread_message_xss === undefined &&
-      globalThis.__thread_unknown_xss === undefined &&
-      ![...document.scripts].some((script) => script.textContent?.includes("__thread_message_xss"))
-    )
-  )
-
-  await messageCode.locator("button.copy").click()
-  ok(
-    "复制:按钮写入未变形的原始代码",
-    (await page.evaluate(() => globalThis.__threadChatCopiedCode)) === MESSAGE_CODE
-  )
-
-  // 锚点的 DOM 手绘必须等 MarkdownBody 的异步 batch settled 后才发生。
-  const anchorMark = sourceMessage.locator('[data-text-anchor-mark="branch-1"]')
-  await anchorMark.waitFor({ state: "visible", timeout: 20_000 })
-  ok("结算:高亮完成后恢复持久锚点标记与脚注", (await sourceMessage.locator("sup.fn-mark").count()) === 1)
-
-  const selectionCreated = await page.evaluate((needle) => {
-    const root = document.querySelector('.column[data-thread-id="main"] .md-body')
-    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
-    let node
-    while ((node = walker.nextNode())) {
-      const index = node.textContent.indexOf(needle)
-      if (index < 0) continue
-      const range = document.createRange()
-      range.setStart(node, index)
-      range.setEnd(node, index + needle.length)
-      const selection = window.getSelection()
-      selection.removeAllRanges()
-      selection.addRange(range)
-      document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }))
-      return true
-    }
-    return false
-  }, ANCHOR_TEXT)
-  await page.locator(".sel-bubble").waitFor({ state: "visible" })
-  ok("交互:带 Shiki 代码的消息仍可正常划选正文", selectionCreated)
-  await page.keyboard.press("Escape")
-
-  await sourceMessage.locator("sup.fn-mark").click()
-  await page.locator('.column[data-thread-id="branch-1"]').waitFor({ state: "visible" })
-  ok("交互:持久锚点脚注点击仍会打开对应分支", true)
-
-  await sourceMessage.locator(".acard").click()
-  const drawer = page.locator(".art-drawer.open")
-  await drawer.waitFor({ state: "visible" })
-  await drawer
-    .locator('.md-body[data-content-settled="true"]')
-    .waitFor({ state: "attached", timeout: 20_000 })
-  const artifactCode = drawer.locator(".md-code").filter({ hasText: ARTIFACT_CODE })
-  ok("Artifact:静态 Markdown 同样生成 Shiki DOM", (await artifactCode.locator("pre.shiki").count()) === 1)
-  ok(
-    "安全:Artifact 代码的 script 文本不会执行",
-    await page.evaluate(() =>
-      globalThis.__thread_artifact_xss === undefined &&
-      ![...document.scripts].some((script) => script.textContent?.includes("__thread_artifact_xss"))
-    )
-  )
-
-  await page.reload({ waitUntil: "networkidle" })
-  const reloadedSource = page.locator('.column[data-thread-id="main"] .message.assistant')
-  await reloadedSource
-    .locator('.md-body[data-content-settled="true"]')
-    .waitFor({ state: "attached", timeout: 20_000 })
-  await reloadedSource
-    .locator('[data-text-anchor-mark="branch-1"]')
-    .waitFor({ state: "visible", timeout: 20_000 })
-  ok("刷新恢复:高亮消息的持久锚点在重新结算后再次绘制", true)
-  ok("浏览器无页面运行时错误", pageErrors.length === 0, pageErrors.join(" | "))
-} finally {
-  await context.close()
-  await browser.close()
-}
-
-process.exit(failed ? 1 : 0)
diff --git a/e2e/thread-chat/verify-tree-list.mjs b/e2e/thread-chat/verify-tree-list.mjs
deleted file mode 100644
index 82eef4ae..00000000
--- a/e2e/thread-chat/verify-tree-list.mjs
+++ /dev/null
@@ -1,351 +0,0 @@
-/**
- * ThreadChat 会话列表 UI 端到端验收(openspec: add-tree-list-ui)。
- *
- * 前提与 verify-persist.mjs 相同:dev server + MiniMax key + Chromium + DATABASE_URL
- * (已应用 0005 custom_title 迁移)。运行:
- *   CHROMIUM_PATH=... BASE_URL=http://localhost:4040 node e2e/thread-chat/verify-tree-list.mjs
- *
- * 断言链:SQL 直插三棵种子树 → 空树上 ⌘⇧K 打开列表(当前「未保存」条目置顶 +
- * 种子按 updated_at 降序 + 分支数徽标 + 点当前树仅关闭)→ 点击切换恢复 →
- * 内联重命名(Esc 取消 / Enter 提交乐观更新 + DB custom_title)→ **继续聊天触发
- * 防抖 PUT 后名字不被派生标题覆盖(design D1 的存在意义)** → 二段删除非当前树
- * (Esc / 点它处复位确认态 + localStorage 工作台记忆清理)→ 删除当前树跳转剩余
- * 最近一棵 + 「最近一棵」指针善后。测试树行 finally 清理。
- */
-import { readFileSync } from "node:fs"
-import { dirname, join } from "node:path"
-import { fileURLToPath } from "node:url"
-import { chromium } from "playwright-core"
-import postgres from "postgres"
-
-const here = dirname(fileURLToPath(import.meta.url))
-const SHOT = (n) => join(here, "shots", `${n}.png`)
-const ok = (label, cond) => {
-  console.log(`${cond ? "PASS" : "FAIL"}  ${label}`)
-  if (!cond) process.exitCode = 1
-}
-const BASE = process.env.BASE_URL || "http://localhost:4040"
-
-// DATABASE_URL:优先环境变量,回退 .env.local
-const env = Object.fromEntries(
-  readFileSync(join(here, "../../.env.local"), "utf8")
-    .split("\n")
-    .filter((l) => l.includes("=") && !l.startsWith("#"))
-    .map((l) => {
-      const i = l.indexOf("=")
-      return [
-        l.slice(0, i).trim(),
-        l
-          .slice(i + 1)
-          .trim()
-          .replace(/^["']|["']$/g, ""),
-      ]
-    })
-)
-const sql = postgres(process.env.DATABASE_URL || env.DATABASE_URL, { max: 1 })
-const testTreeIds = []
-
-/** 造一棵可被页面正常加载的最小种子树;withBranch = 主线外再挂一个分支(threadCount=2) */
-function seedState(label, withBranch) {
-  return {
-    threads: {
-      main: {
-        id: "main",
-        parentId: null,
-        depth: 0,
-        title: "主线",
-        anchorText: null,
-        forkFromMsgId: null,
-        footnote: null,
-        children: withBranch ? ["b1"] : [],
-        lastActive: 1,
-        messages: [
-          {
-            id: "m1",
-            role: "user",
-            text: `${label}主线首问的完整文本`,
-            forks: [],
-          },
-          {
-            id: "m2",
-            role: "assistant",
-            text: `${label}的回答正文,用于恢复断言。`,
-            forks: withBranch
-              ? [{ text: "回答正文", num: 1, threadId: "b1", depth: 1 }]
-              : [],
-            status: "done",
-          },
-        ],
-      },
-      ...(withBranch
-        ? {
-            b1: {
-              id: "b1",
-              parentId: "main",
-              depth: 1,
-              title: "回答正文",
-              anchorText: "回答正文",
-              forkFromMsgId: "m2",
-              footnote: 1,
-              children: [],
-              lastActive: 2,
-              messages: [],
-            },
-          }
-        : {}),
-    },
-    artifacts: {},
-    artifactOrder: [],
-    recents: withBranch ? ["b1"] : [],
-    footnoteCounter: withBranch ? 1 : 0,
-    seq: 10,
-    tick: 3,
-  }
-}
-
-const browser = await chromium.launch({
-  executablePath: process.env.CHROMIUM_PATH || undefined,
-  headless: true,
-})
-
-try {
-  // ---- 0. SQL 直插三棵种子树(A 最旧带分支 / C 居中 / B 最新) ----
-  const idA = crypto.randomUUID()
-  const idB = crypto.randomUUID()
-  const idC = crypto.randomUUID()
-  testTreeIds.push(idA, idB, idC)
-  await sql`INSERT INTO branch_trees (id, title, state, updated_at) VALUES
-    (${idA}, '种子甲', ${sql.json(seedState("种子甲", true))}, now() - interval '3 hours'),
-    (${idC}, '种子丙', ${sql.json(seedState("种子丙", false))}, now() - interval '2 hours'),
-    (${idB}, '种子乙', ${sql.json(seedState("种子乙", false))}, now() - interval '1 hour')`
-
-  const ctx = await browser.newContext()
-  const page = await ctx.newPage()
-  const puts = []
-  page.on("request", (r) => {
-    if (r.url().includes("/api/branch-trees/") && r.method() === "PUT")
-      puts.push(r.url())
-  })
-
-  // ---- 1. 空树(未保存)上 ⌘⇧K 打开列表 ----
-  const t0 = crypto.randomUUID()
-  testTreeIds.push(t0) // 正常不会入库(空树不写库),保险起见列入清理
-  await page.goto(`${BASE}/thread-chat/${t0}`, { waitUntil: "networkidle" })
-  await page.keyboard.press("Meta+Shift+K")
-  await page.locator(".swx.tlx").waitFor({ state: "visible", timeout: 5000 })
-  ok("⌘⇧K 打开会话列表弹层", true)
-  await page
-    .locator(".tlx-row")
-    .nth(3)
-    .waitFor({ state: "visible", timeout: 5000 })
-  const rows0 = await page.evaluate(() =>
-    [...document.querySelectorAll(".tlx-row")].map((r) => ({
-      cur: r.classList.contains("cur"),
-      unsaved: !!r.querySelector(".tlx-unsaved"),
-      title: r.querySelector(".t")?.textContent ?? "",
-      badge: r.querySelector(".tlx-badge")?.textContent?.trim() ?? null,
-      time: r.querySelector(".tlx-time")?.textContent ?? null,
-    }))
-  )
-  // 开发库可能存有真实树,断言不假设「只有种子」:种子齐全 + 相对顺序正确即可
-  const iB = rows0.findIndex((r) => r.title === "种子乙")
-  const iC = rows0.findIndex((r) => r.title === "种子丙")
-  const iA = rows0.findIndex((r) => r.title === "种子甲")
-  ok(
-    "列表含当前未保存条目 + 三棵种子",
-    rows0.length >= 4 && iA > 0 && iB > 0 && iC > 0
-  )
-  ok(
-    "当前树置顶高亮并标注「未保存」",
-    rows0[0].cur && rows0[0].unsaved && rows0[0].title === "未命名对话"
-  )
-  ok("已保存树按 updated_at 降序(乙 → 丙 → 甲)", iB < iC && iC < iA)
-  ok("分支数徽标(种子甲 ⑂ 1)", rows0[iA]?.badge === "⑂ 1")
-  ok("相对时间显示", (rows0[iB]?.time ?? "").includes("小时前"))
-  await page.screenshot({ path: SHOT("tree-list-open") })
-
-  // 点当前树条目 = 仅关闭弹层,不跳转
-  await page.locator(".tlx-row").first().click()
-  await page.locator(".swx.tlx").waitFor({ state: "hidden", timeout: 3000 })
-  ok("点当前树条目仅关闭弹层(URL 不变)", page.url().endsWith(t0))
-
-  // ---- 2. 点击切换到种子甲:URL 跳转 + 数据恢复 ----
-  await page.keyboard.press("Meta+Shift+K")
-  await page.getByText("种子甲", { exact: true }).click()
-  await page.waitForURL(`**/thread-chat/${idA}`, { timeout: 8000 })
-  await page.waitForSelector(".tc .message.assistant", { timeout: 15000 })
-  const restored = await page.evaluate(() => ({
-    msgs: document.querySelector(".column")?.querySelectorAll(".message")
-      .length,
-    text: document.body.textContent.includes("种子甲的回答正文"),
-  }))
-  ok("点击切换:跳转到该树并恢复消息", restored.msgs === 2 && restored.text)
-
-  // ---- 3. 内联重命名:Esc 取消 → Enter 提交(乐观更新 + DB custom_title) ----
-  await page.keyboard.press("Meta+Shift+K")
-  await page.locator(".swx.tlx").waitFor({ state: "visible", timeout: 5000 })
-  const curRow = page.locator(".tlx-row.cur")
-  await curRow.hover()
-  await curRow.locator('.tlx-act[title="重命名"]').click()
-  await page.locator(".tlx-edit").fill("不该生效的名字")
-  await page.keyboard.press("Escape")
-  const afterEsc = await page.evaluate(() => ({
-    panelOpen: !!document.querySelector(".swx.tlx"),
-    title: document.querySelector(".tlx-row.cur .t")?.textContent ?? "",
-  }))
-  ok(
-    "重命名 Esc 取消:保留原名且弹层不被连带关闭",
-    afterEsc.panelOpen && afterEsc.title === "种子甲"
-  )
-  await curRow.hover()
-  await curRow.locator('.tlx-act[title="重命名"]').click()
-  await page.locator(".tlx-edit").fill("我的调研")
-  await page.keyboard.press("Enter")
-  ok(
-    "重命名 Enter 提交:条目就地更新(乐观)",
-    (await curRow.locator(".t").textContent()) === "我的调研"
-  )
-  await page.waitForTimeout(500)
-  const [renamed] =
-    await sql`SELECT title, custom_title FROM branch_trees WHERE id = ${idA}`
-  ok(
-    "DB:custom_title 写入且派生 title 未被触碰(双轨)",
-    renamed.custom_title === "我的调研" && renamed.title === "种子甲"
-  )
-  await page.keyboard.press("Escape") // 关闭弹层
-
-  // ---- 4. D1 关键路径:继续聊天触发防抖 PUT,名字不被派生标题覆盖 ----
-  await page
-    .locator(".column")
-    .first()
-    .locator("textarea")
-    .fill("请用一句话补充说明。")
-  await page.locator(".column").first().locator("textarea").press("Enter")
-  await page.waitForFunction(
-    () => {
-      const col = document.querySelector(".column")
-      const b = col?.querySelectorAll(".message.assistant .bubble")
-      const btn = col?.querySelector(".composer .send")
-      return (
-        b?.length >= 2 &&
-        b[b.length - 1].textContent.trim().length > 0 &&
-        btn &&
-        !btn.classList.contains("stop")
-      )
-    },
-    undefined,
-    { timeout: 120000 }
-  )
-  await page.waitForTimeout(2200) // 过 1.5s 防抖
-  ok(
-    "继续聊天后发生整树 PUT",
-    puts.some((u) => u.includes(idA))
-  )
-  const [afterPut] =
-    await sql`SELECT title, custom_title FROM branch_trees WHERE id = ${idA}`
-  ok(
-    "PUT 只更新派生 title(取主线首问前 20 字)",
-    afterPut.title === "种子甲主线首问的完整文本" && afterPut.title !== "种子甲"
-  )
-  ok(
-    "custom_title 不被防抖 PUT 覆盖(design D1)",
-    afterPut.custom_title === "我的调研"
-  )
-  await page.keyboard.press("Meta+Shift+K")
-  await page.locator(".swx.tlx").waitFor({ state: "visible", timeout: 5000 })
-  ok(
-    "列表展示仍为自定义名「我的调研」",
-    (await page.locator(".tlx-row.cur .t").textContent()) === "我的调研"
-  )
-
-  // ---- 5. 二段删除非当前树(种子乙):确认态复位 + 删除 + localStorage 善后 ----
-  await page.evaluate(
-    ([a, b]) => {
-      localStorage.setItem(`thread-chat:ui:${a}`, "{}")
-      localStorage.setItem(`thread-chat:ui:${b}`, "{}")
-    },
-    [idA, idB]
-  )
-  const rowB = page.locator(".tlx-row", { hasText: "种子乙" })
-  await rowB.hover()
-  await rowB.locator('.tlx-act[title="删除此对话"]').click()
-  await rowB
-    .locator(".tlx-act.confirm")
-    .waitFor({ state: "visible", timeout: 3000 })
-  ok("首次点删除进入「确认删除」态", true)
-  await page.keyboard.press("Escape")
-  const afterConfirmEsc = await page.evaluate(() => ({
-    panelOpen: !!document.querySelector(".swx.tlx"),
-    confirming: !!document.querySelector(".tlx-act.confirm"),
-  }))
-  ok(
-    "Esc 复位确认态(弹层不被连带关闭)",
-    afterConfirmEsc.panelOpen && !afterConfirmEsc.confirming
-  )
-  await rowB.hover()
-  await rowB.locator('.tlx-act[title="删除此对话"]').click()
-  await page.locator(".swx-title").click() // 点它处
-  ok("点它处复位确认态", !(await page.locator(".tlx-act.confirm").count()))
-  await rowB.hover()
-  await rowB.locator('.tlx-act[title="删除此对话"]').click()
-  await rowB.locator(".tlx-act.confirm").click()
-  await rowB.waitFor({ state: "detached", timeout: 5000 })
-  ok("确认删除后条目从列表消失", true)
-  const bRows = await sql`SELECT 1 FROM branch_trees WHERE id = ${idB}`
-  ok("DB:种子乙行已删除", bRows.length === 0)
-  const lsAfterB = await page.evaluate(
-    ([a, b]) => ({
-      uiB: localStorage.getItem(`thread-chat:ui:${b}`),
-      uiA: localStorage.getItem(`thread-chat:ui:${a}`),
-    }),
-    [idA, idB]
-  )
-  ok(
-    "localStorage:被删树的工作台记忆已清、当前树的保留",
-    lsAfterB.uiB === null && lsAfterB.uiA !== null
-  )
-  ok("当前树不受影响(弹层仍在当前树上)", page.url().endsWith(idA))
-
-  // ---- 6. 删除当前树(甲):跳转剩余最近一棵(丙)+ 指针善后 ----
-  const rowCur = page.locator(".tlx-row.cur")
-  await rowCur.hover()
-  await rowCur.locator('.tlx-act[title="删除此对话"]').click()
-  await rowCur.locator(".tlx-act.confirm").click()
-  // 共享开发库里可能存在比种子更新的真实树(用户在用/其它 e2e 留下),
-  // 「剩余最近一棵」不必然是种子丙——只断言跳转链本身:离开被删树、落到某棵合法存在的树上。
-  await page.waitForURL(
-    (u) =>
-      /\/thread-chat\/[0-9a-f-]{36}$/.test(u.toString()) &&
-      !u.toString().includes(idA),
-    { timeout: 8000 }
-  )
-  const jumpedTo = page.url().split("/").pop()
-  const jumpedRows =
-    await sql`SELECT 1 FROM branch_trees WHERE id = ${jumpedTo}`
-  ok("删除当前树:跳转到某棵仍存在的树(跳转链生效)", jumpedRows.length === 1)
-  const aRows = await sql`SELECT 1 FROM branch_trees WHERE id = ${idA}`
-  ok("DB:当前树行已删除", aRows.length === 0)
-  await page.waitForTimeout(2500) // 若有存盘尾巴此时也该到了
-  const aResurrect = await sql`SELECT 1 FROM branch_trees WHERE id = ${idA}`
-  ok("被删树未被卸载 flush 复活(抑制回写)", aResurrect.length === 0)
-  const lsAfterA = await page.evaluate(
-    ([a, c]) => ({
-      uiA: localStorage.getItem(`thread-chat:ui:${a}`),
-      last: localStorage.getItem("thread-chat:last-tree-id"),
-    }),
-    [idA, idC]
-  )
-  ok("localStorage:当前树的工作台记忆已清", lsAfterA.uiA === null)
-  ok(
-    "「最近一棵」指针不再指向被删树(已指向落地页)",
-    lsAfterA.last !== idA && lsAfterA.last === jumpedTo
-  )
-  await page.screenshot({ path: SHOT("tree-list-after-delete") })
-  await ctx.close()
-} finally {
-  // ---- 清理测试树行 ----
-  await sql`DELETE FROM branch_trees WHERE id = ANY(${testTreeIds})`
-  console.log(`已清理 ${testTreeIds.length} 行测试树(含已被用例删除的)`)
-  await sql.end()
-  await browser.close()
-}
diff --git a/e2e/thread-chat/web-research-placement.test.mjs b/e2e/thread-chat/web-research-placement.test.mjs
deleted file mode 100644
index acf20797..00000000
--- a/e2e/thread-chat/web-research-placement.test.mjs
+++ /dev/null
@@ -1,16 +0,0 @@
-import assert from "node:assert/strict"
-import { webResearchPlacement } from "../../app/thread-chat/branching/assistant/web-research-placement.ts"
-
-const empty = webResearchPlacement({ webResearchTextOffset: 12 })
-assert.deepEqual(empty, { activities: [], insertAt: undefined })
-
-const activity = { kind: "search", toolCallId: "tool-1" }
-assert.deepEqual(
-  webResearchPlacement({ webResearch: [activity], webResearchTextOffset: 12 }),
-  { activities: [activity], insertAt: 12 }
-)
-assert.equal(webResearchPlacement({ webResearch: [activity] }).insertAt, 0)
-
-console.log(
-  "PASS  web research placement waits for activity and preserves recorded/fallback offsets"
-)
diff --git a/e2e/thread-chat/web-research-sources.test.mjs b/e2e/thread-chat/web-research-sources.test.mjs
deleted file mode 100644
index 7ef38bf0..00000000
--- a/e2e/thread-chat/web-research-sources.test.mjs
+++ /dev/null
@@ -1,60 +0,0 @@
-import assert from "node:assert/strict"
-import {
-  createWebResearchActivityDispatcher,
-  webResearchSourcesFromOutput,
-} from "../../lib/chat/web-research-activity.ts"
-import { projectGenerationResult } from "../../lib/thread-chat/application/project-generation-result.ts"
-
-const output = {
-  results: [
-    { title: " Primary ", url: " https://example.com/a " },
-    { title: "Duplicate", url: "https://example.com/a" },
-    { title: "", url: "https://example.com/b" },
-    { title: "missing URL" },
-  ],
-}
-const canonicalSources = [
-  { title: "Primary", url: "https://example.com/a" },
-  { title: "https://example.com/b", url: "https://example.com/b" },
-]
-assert.deepEqual(webResearchSourcesFromOutput(output), canonicalSources)
-
-const activities = []
-const dispatch = createWebResearchActivityDispatcher((activity) =>
-  activities.push(activity)
-)
-dispatch({
-  type: "tool-input-available",
-  toolCallId: "search-1",
-  toolName: "webSearch",
-  input: { query: "source normalization" },
-})
-dispatch({
-  type: "tool-output-available",
-  toolCallId: "search-1",
-  output,
-})
-assert.deepEqual(activities.at(-1).sources, canonicalSources)
-
-const projected = projectGenerationResult({
-  generationId: "generation-1",
-  threadId: "main",
-  assistantMessageId: "assistant-1",
-  terminalStatus: "completed",
-  responseMessage: {
-    parts: [
-      {
-        type: "tool-webSearch",
-        toolCallId: "search-1",
-        state: "output-available",
-        input: { query: "source normalization" },
-        output,
-      },
-    ],
-  },
-})
-assert.deepEqual(projected.result.webResearch[0].sources, canonicalSources)
-
-console.log(
-  "PASS  live and persisted web research share one source normalization contract"
-)
diff --git a/e2e/thread-chat/workspace-composition-ownership.test.mjs b/e2e/thread-chat/workspace-composition-ownership.test.mjs
deleted file mode 100644
index 6f60de07..00000000
--- a/e2e/thread-chat/workspace-composition-ownership.test.mjs
+++ /dev/null
@@ -1,35 +0,0 @@
-import assert from "node:assert/strict"
-import { readFile } from "node:fs/promises"
-
-const shell = await readFile(
-  new URL("../../app/thread-chat/thread-chat-demo.tsx", import.meta.url),
-  "utf8"
-)
-const workspace = await readFile(
-  new URL(
-    "../../app/thread-chat/orchestration/workspace/use-thread-chat-workspace.ts",
-    import.meta.url
-  ),
-  "utf8"
-)
-
-assert.match(shell, /useThreadChatWorkspace\(/)
-for (const capability of [
-  "useColumnViewport",
-  "useColumnSlots",
-  "useUiStatePersistence",
-]) {
-  assert.doesNotMatch(shell, new RegExp(`${capability}\\(`))
-  assert.match(workspace, new RegExp(`${capability}\\(`))
-}
-assert.doesNotMatch(shell, /focusSeq/)
-assert.match(workspace, /focusCanvasNode/)
-assert.match(workspace, /CanvasChatActions/)
-assert.match(workspace, /CanvasViewState/)
-assert.match(workspace, /useMemo/)
-assert.match(workspace, /\[chat, messageCommands\]/)
-assert.doesNotMatch(workspace, /useState/)
-
-console.log(
-  "PASS  page shell consumes one composed column/canvas workspace capability"
-)
diff --git a/e2e/thread-chat/workspace-overlay.test.mjs b/e2e/thread-chat/workspace-overlay.test.mjs
deleted file mode 100644
index 3bd8b630..00000000
--- a/e2e/thread-chat/workspace-overlay.test.mjs
+++ /dev/null
@@ -1,64 +0,0 @@
-import assert from "node:assert/strict"
-import {
-  escapeOverlayTarget,
-  popupPosition,
-} from "../../app/thread-chat/orchestration/overlays/workspace-overlay-logic.ts"
-import { SWITCHER_DIMENSIONS } from "../../app/thread-chat/orchestration/navigation/switcher-dimensions.ts"
-
-assert.equal(
-  escapeOverlayTarget({
-    helpOpen: true,
-    treeListOpen: true,
-    selectionOpen: true,
-    switcherOpen: true,
-    drawerOpen: true,
-  }),
-  "help"
-)
-assert.equal(
-  escapeOverlayTarget({
-    helpOpen: false,
-    treeListOpen: false,
-    selectionOpen: true,
-    switcherOpen: true,
-    drawerOpen: true,
-  }),
-  "selection"
-)
-assert.equal(
-  escapeOverlayTarget({
-    helpOpen: false,
-    treeListOpen: false,
-    selectionOpen: false,
-    switcherOpen: false,
-    drawerOpen: false,
-  }),
-  null
-)
-
-assert.deepEqual(
-  popupPosition({
-    right: 900,
-    bottom: 700,
-    panelWidth: SWITCHER_DIMENSIONS.column.width,
-    panelHeight: SWITCHER_DIMENSIONS.column.height,
-    viewportWidth: 800,
-    viewportHeight: 600,
-  }),
-  { x: 462, y: 170 }
-)
-assert.deepEqual(
-  popupPosition({
-    right: 100,
-    bottom: 50,
-    panelWidth: SWITCHER_DIMENSIONS.column.width,
-    panelHeight: SWITCHER_DIMENSIONS.column.height,
-    viewportWidth: 800,
-    viewportHeight: 600,
-  }),
-  { x: 8, y: 56 }
-)
-
-console.log(
-  "PASS  workspace overlays preserve Escape priority and clamp popup positions"
-)
diff --git a/e2e/thread-chat/workspace-toast.test.mjs b/e2e/thread-chat/workspace-toast.test.mjs
deleted file mode 100644
index e694056b..00000000
--- a/e2e/thread-chat/workspace-toast.test.mjs
+++ /dev/null
@@ -1,7 +0,0 @@
-import assert from "node:assert/strict"
-import { workspaceToastDuration } from "../../app/thread-chat/orchestration/overlays/workspace-toast-logic.ts"
-
-assert.equal(workspaceToastDuration({ message: "saved" }), 2600)
-assert.equal(workspaceToastDuration({ message: "replaced", undo() {} }), 5200)
-
-console.log("PASS  workspace toast keeps normal and undo visibility windows")
diff --git a/lib/ai/model-call-logger.ts b/lib/ai/model-call-logger.ts
index c73cfbd0..a8c5d00c 100644
--- a/lib/ai/model-call-logger.ts
+++ b/lib/ai/model-call-logger.ts
@@ -10,7 +10,7 @@ import type { ModelCallPurpose } from "@/constants/model-call"
 
 export type ModelCallTrace = {
   requestId?: string
-  treeId?: string
+  projectId?: string
   threadId?: string
   generationId?: string
   assistantMessageId?: string
diff --git a/lib/chat/thread-chat-prompt.ts b/lib/chat/thread-chat-prompt.ts
deleted file mode 100644
index 4bbf2e29..00000000
--- a/lib/chat/thread-chat-prompt.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// thread-chat 模式的服务端 system 提示构造(app/api/chat/route.ts 使用)。
-// system 归服务端所有:AI SDK v7 的 streamText 不允许 messages 里出现 system 角色
-// (安全默认值,防客户端注入任意 system),所以客户端只发 threadChat 标记与锚点原文,
-// 指令模板在这里拼装。
-
-import {
-  THREAD_CHAT_BRANCH_PREFIX,
-  THREAD_CHAT_BRANCH_SUFFIX,
-  THREAD_CHAT_MARKDOWN_ARTIFACT_SYSTEM,
-  THREAD_CHAT_SYSTEM,
-} from "@/constants/thread-chat"
-
-/**
- * 构造 thread-chat 模式的 system 提示:
- * 通用结构化风格段 +(anchorText 非空时)分支焦点段(锚点原文作为数据嵌入「」内)。
- */
-export function buildThreadChatSystem(
-  anchorText?: string | null,
-  options?: { enableMarkdownArtifact?: boolean }
-): string {
-  const anchor = anchorText?.trim()
-  return [
-    THREAD_CHAT_SYSTEM,
-    options?.enableMarkdownArtifact
-      ? THREAD_CHAT_MARKDOWN_ARTIFACT_SYSTEM
-      : null,
-    anchor
-      ? `${THREAD_CHAT_BRANCH_PREFIX}「${anchor}」。${THREAD_CHAT_BRANCH_SUFFIX}`
-      : null,
-  ]
-    .filter((part): part is string => part !== null)
-    .join("\n\n")
-}
diff --git a/lib/chat/tree-id.ts b/lib/chat/tree-id.ts
deleted file mode 100644
index aae865f7..00000000
--- a/lib/chat/tree-id.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * lib/chat/tree-id —— 分支树 treeId 的形状校验(UUID)。
- *
- * treeId 由客户端 crypto.randomUUID() 生成、URL 路径段承载(/thread-chat/{treeId})。
- * 路由([treeId]/page.tsx)与 API(/api/branch-trees/[treeId])共用同一校验作为安全阀,
- * 避免任意字符串打到 DB 主键——放 lib/ 供服务端与客户端两侧复用。
- */
-
-const UUID_RE =
-  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
-
-/** treeId 是否为 UUID 形状(大小写不敏感) */
-export function isValidTreeId(id: string): boolean {
-  return UUID_RE.test(id)
-}
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index 615ec982..bc61a9af 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -5,9 +5,7 @@ import {
   index,
   uniqueIndex,
   integer,
-  boolean,
   vector,
-  primaryKey,
   bigint,
   check,
   uuid,
@@ -18,13 +16,6 @@ import type { UIMessage } from "ai"
 import { dbSchema } from "./pg-schema"
 import { EMBEDDING_DIMENSIONS } from "@/constants/rag"
 import { user } from "./auth-schema"
-import type {
-  GenerationBillingStatus,
-  GenerationResultV1,
-  GenerationStatus,
-  GenerationTurnSnapshot,
-} from "@/lib/thread-chat/domain/generation"
-import type { MessageFeedback } from "@/lib/thread-chat/domain/types"
 
 // drizzle-kit 必须能从 schema 入口发现自定义 namespace;只导出其中的表会把
 // thread_chat 误判为待删除 schema。
@@ -57,129 +48,6 @@ export const attachments = dbSchema.table("attachments", {
     .defaultNow(),
 })
 
-// 分支对话树(app/thread-chat)的整棵树持久化:一棵树一行,state 存完整
-// ThreadTreeState(JSON)。与上面 assistant-ui 线性模型的 threads/messages 表分开,
-// 互不复用——那两张表是线性会话,这张是树形分支态。treeId 由客户端生成
-// (crypto.randomUUID()),URL 路径段承载(/thread-chat/{treeId}),URL 即树身份。
-export const branchTrees = dbSchema.table(
-  "branch_trees",
-  {
-    id: text("id").primaryKey(), // 客户端生成的 treeId(UUID,URL 路径段承载)
-    // 迁移期允许历史树无主;新写入必须由 API 绑定当前 session user。
-    userId: text("user_id").references(() => user.id, {
-      onDelete: "cascade",
-    }),
-    title: text("title"), // 可空:取 main 首条 user 文本前若干字,纯展示(机器派生轨)
-    // 双轨标题(design D1):用户重命名只写这列(PATCH),防抖整树 PUT 只写上面的派生
-    // title——两条写路径互不踩踏;对外展示一律 coalesce(custom_title, title)。
-    customTitle: text("custom_title"),
-    state: jsonb("state").notNull(), // 完整 ThreadTreeState
-    revision: integer("revision").notNull().default(0),
-    createdAt: timestamp("created_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-    updatedAt: timestamp("updated_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-  },
-  (table) => [index("branch_trees_user_id_idx").on(table.userId)]
-)
-
-/**
- * 每次 thread-chat assistant attempt 的服务端权威 sidecar。终态不直接改整树 JSON,
- * 读取时再把 current generation result 合并进去,避免旧浏览器快照覆盖最终答案。
- */
-export const branchGenerations = dbSchema.table(
-  "branch_generations",
-  {
-    id: text("id").primaryKey(),
-    userId: text("user_id")
-      .notNull()
-      .references(() => user.id, { onDelete: "cascade" }),
-    treeId: text("tree_id")
-      .notNull()
-      .references(() => branchTrees.id, { onDelete: "cascade" }),
-    threadId: text("thread_id").notNull(),
-    userMessageId: text("user_message_id").notNull(),
-    assistantMessageId: text("assistant_message_id").notNull(),
-    attempt: integer("attempt").notNull(),
-    isCurrent: boolean("is_current").notNull().default(true),
-    status: text("status").$type().notNull(),
-    modelId: text("model_id").notNull(),
-    assistantMessageIndex: integer("assistant_message_index").notNull(),
-    turnSnapshot: jsonb("turn_snapshot")
-      .$type()
-      .notNull(),
-    result: jsonb("result").$type(),
-    error: text("error"),
-    billingStatus: text("billing_status")
-      .$type()
-      .notNull()
-      .default("pending"),
-    heartbeatAt: timestamp("heartbeat_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-    stopRequestedAt: timestamp("stop_requested_at", { withTimezone: true }),
-    finishedAt: timestamp("finished_at", { withTimezone: true }),
-    createdAt: timestamp("created_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-    updatedAt: timestamp("updated_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-  },
-  (table) => [
-    uniqueIndex("branch_generations_current_assistant_uq")
-      .on(table.treeId, table.threadId, table.assistantMessageId)
-      .where(sql`${table.isCurrent} = true`),
-    uniqueIndex("branch_generations_assistant_attempt_uq").on(
-      table.treeId,
-      table.threadId,
-      table.assistantMessageId,
-      table.attempt
-    ),
-    index("branch_generations_user_id_idx").on(table.userId),
-    index("branch_generations_tree_current_idx").on(
-      table.treeId,
-      table.isCurrent
-    ),
-    index("branch_generations_user_status_idx").on(table.userId, table.status),
-    index("branch_generations_heartbeat_idx").on(
-      table.status,
-      table.heartbeatAt
-    ),
-  ]
-)
-
-/** 产品层 assistant message 的当前互斥反馈;不与 generation 执行身份耦合。 */
-export const branchMessageFeedback = dbSchema.table(
-  "branch_message_feedback",
-  {
-    userId: text("user_id")
-      .notNull()
-      .references(() => user.id, { onDelete: "cascade" }),
-    treeId: text("tree_id")
-      .notNull()
-      .references(() => branchTrees.id, { onDelete: "cascade" }),
-    threadId: text("thread_id").notNull(),
-    messageId: text("message_id").notNull(),
-    feedback: text("feedback").$type().notNull(),
-    createdAt: timestamp("created_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-    updatedAt: timestamp("updated_at", { withTimezone: true })
-      .notNull()
-      .defaultNow(),
-  },
-  (table) => [
-    primaryKey({
-      name: "branch_message_feedback_pk",
-      columns: [table.userId, table.treeId, table.threadId, table.messageId],
-    }),
-    index("branch_message_feedback_tree_idx").on(table.userId, table.treeId),
-  ]
-)
-
 /** Project 是一整簇 Root/Branch Thread 的 owner scope 与永久删除边界。 */
 export const projects = dbSchema.table(
   "projects",
diff --git a/lib/thread-chat-generation/execution-state-repository.ts b/lib/thread-chat-generation/execution-state-repository.ts
deleted file mode 100644
index 2eb68da9..00000000
--- a/lib/thread-chat-generation/execution-state-repository.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { and, eq, inArray, sql } from "drizzle-orm"
-import { ACTIVE_GENERATION_STATUSES } from "@/constants/generation"
-import { db } from "@/lib/db"
-import { branchGenerations } from "@/lib/db/schema"
-import type { GenerationRow } from "@/lib/thread-chat-generation/query-repository"
-
-/** Owner-scoped running → stop_requested transition; terminal states remain idempotent. */
-export async function requestGenerationStop(
-  userId: string,
-  generationId: string
-): Promise {
-  return db.transaction(async (tx) => {
-    const locked = await tx.execute(sql`
-      select ${branchGenerations.id}
-      from ${branchGenerations}
-      where ${branchGenerations.id} = ${generationId}
-        and ${branchGenerations.userId} = ${userId}
-      for update
-    `)
-    if (locked.length === 0) return null
-
-    const now = new Date()
-    const [updated] = await tx
-      .update(branchGenerations)
-      .set({
-        status: "stop_requested",
-        stopRequestedAt: now,
-        updatedAt: now,
-      })
-      .where(
-        and(
-          eq(branchGenerations.id, generationId),
-          eq(branchGenerations.userId, userId),
-          eq(branchGenerations.status, "running")
-        )
-      )
-      .returning()
-    if (updated) return updated
-
-    const [current] = await tx
-      .select()
-      .from(branchGenerations)
-      .where(
-        and(
-          eq(branchGenerations.id, generationId),
-          eq(branchGenerations.userId, userId)
-        )
-      )
-    return current ?? null
-  })
-}
-
-/** 延长 active generation 的 lease;终态调用是无副作用的 no-op。 */
-export async function heartbeatGeneration(generationId: string) {
-  const now = new Date()
-  await db
-    .update(branchGenerations)
-    .set({ heartbeatAt: now, updatedAt: now })
-    .where(
-      and(
-        eq(branchGenerations.id, generationId),
-        inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES)
-      )
-    )
-}
diff --git a/lib/thread-chat-generation/execution.ts b/lib/thread-chat-generation/execution.ts
deleted file mode 100644
index 8763068c..00000000
--- a/lib/thread-chat-generation/execution.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
-  GENERATION_CANCEL_POLL_MS,
-  GENERATION_HEARTBEAT_MS,
-} from "@/constants/generation"
-import { heartbeatGeneration } from "@/lib/thread-chat-generation/execution-state-repository"
-import { getGenerationExecutionState } from "@/lib/thread-chat-generation/query-repository"
-
-declare global {
-  var __threadChatGenerationAbortControllers:
-    Map | undefined
-}
-
-const controllers =
-  globalThis.__threadChatGenerationAbortControllers ??
-  new Map()
-if (process.env.NODE_ENV !== "production") {
-  globalThis.__threadChatGenerationAbortControllers = controllers
-}
-
-export function registerGenerationController(
-  generationId: string,
-  controller: AbortController
-) {
-  controllers.set(generationId, controller)
-}
-
-export function unregisterGenerationController(
-  generationId: string,
-  controller: AbortController
-) {
-  if (controllers.get(generationId) === controller) {
-    controllers.delete(generationId)
-  }
-}
-
-export function abortGenerationLocally(generationId: string): boolean {
-  const controller = controllers.get(generationId)
-  if (!controller || controller.signal.aborted) return false
-  controller.abort(new DOMException("Generation stopped by user", "AbortError"))
-  return true
-}
-
-function delay(ms: number): Promise {
-  return new Promise((resolve) => setTimeout(resolve, ms))
-}
-
-type GenerationExecutionState = Awaited<
-  ReturnType
->
-
-/** DB 权威状态中只有 current running attempt 仍被允许继续消耗模型执行。 */
-export function shouldContinueGenerationExecution(
-  execution: GenerationExecutionState
-): boolean {
-  return execution?.status === "running" && execution.isCurrent
-}
-
-export function observeGenerationCancellation(
-  generationId: string,
-  controller: AbortController
-) {
-  let stopped = false
-  const done = (async () => {
-    let lastHeartbeat = Date.now()
-    while (!stopped && !controller.signal.aborted) {
-      await delay(GENERATION_CANCEL_POLL_MS)
-      if (stopped || controller.signal.aborted) break
-      try {
-        const execution = await getGenerationExecutionState(generationId)
-        if (!shouldContinueGenerationExecution(execution)) {
-          controller.abort(
-            new DOMException("Generation stopped by server state", "AbortError")
-          )
-          break
-        }
-        if (Date.now() - lastHeartbeat >= GENERATION_HEARTBEAT_MS) {
-          await heartbeatGeneration(generationId)
-          lastHeartbeat = Date.now()
-        }
-      } catch (error) {
-        console.error("[thread-chat-generation] 取消观察器查询失败", {
-          generationId,
-          error,
-        })
-      }
-    }
-  })()
-
-  return {
-    done,
-    stop() {
-      stopped = true
-    },
-  }
-}
diff --git a/lib/thread-chat-generation/finalize-with-retry.ts b/lib/thread-chat-generation/finalize-with-retry.ts
deleted file mode 100644
index e43cf18a..00000000
--- a/lib/thread-chat-generation/finalize-with-retry.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
-  finalizeGeneration,
-  type FinalizeGenerationInput,
-} from "@/lib/thread-chat-generation/finalize"
-
-type FinalizeGeneration = typeof finalizeGeneration
-
-type FinalizeRetryDependencies = {
-  finalize: FinalizeGeneration
-  delay(ms: number): Promise
-}
-
-const defaultDependencies: FinalizeRetryDependencies = {
-  finalize: finalizeGeneration,
-  delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
-}
-
-/** generation 终态最多尝试三次;失败退避不改变输入或终态策略。 */
-export async function finalizeGenerationWithRetry(
-  input: FinalizeGenerationInput,
-  dependencies: FinalizeRetryDependencies = defaultDependencies
-) {
-  let lastError: unknown
-  for (let attempt = 1; attempt <= 3; attempt++) {
-    try {
-      return await dependencies.finalize(input)
-    } catch (error) {
-      lastError = error
-      console.error("[thread-chat-generation] finalize 失败", {
-        generationId: input.generationId,
-        attempt,
-        error,
-      })
-      if (attempt < 3) await dependencies.delay(attempt * 150)
-    }
-  }
-  throw lastError
-}
diff --git a/lib/thread-chat-generation/finalize.ts b/lib/thread-chat-generation/finalize.ts
deleted file mode 100644
index c8765920..00000000
--- a/lib/thread-chat-generation/finalize.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { and, eq, inArray, sql } from "drizzle-orm"
-import type {
-  GenerationBillingStatus,
-  GenerationResultV1,
-} from "@/lib/thread-chat/domain/generation"
-import { ACTIVE_GENERATION_STATUSES } from "@/constants/generation"
-import {
-  chargeUsageOnce,
-  type BillingTransaction,
-  type UsageCostEvidence,
-} from "@/lib/billing/credits"
-import { db } from "@/lib/db"
-import { branchGenerations } from "@/lib/db/schema"
-
-export type FinalizeGenerationUsage = {
-  inputTokens: number
-  outputTokens: number
-  costEvidence?: UsageCostEvidence
-}
-
-export type FinalizeGenerationInput = {
-  generationId: string
-  outcome: "completed" | "stopped" | "failed"
-  result: GenerationResultV1
-  error?: string
-  usage?: FinalizeGenerationUsage
-  usageUnavailable?: boolean
-}
-
-type GenerationBillingIdentity = {
-  id: string
-  userId: string
-  modelId: string
-  threadId: string
-  assistantMessageId: string
-}
-
-const BILLING_STATUS_PROGRESS: Record = {
-  pending: 0,
-  not_billable: 1,
-  usage_unavailable: 2,
-  settled: 3,
-}
-
-function advanceBillingStatus(
-  current: GenerationBillingStatus,
-  candidate: GenerationBillingStatus
-): GenerationBillingStatus {
-  return BILLING_STATUS_PROGRESS[candidate] > BILLING_STATUS_PROGRESS[current]
-    ? candidate
-    : current
-}
-
-async function settleGenerationUsage(
-  tx: BillingTransaction,
-  generation: GenerationBillingIdentity,
-  usage: FinalizeGenerationUsage
-) {
-  await chargeUsageOnce(tx, generation.id, {
-    userId: generation.userId,
-    model: generation.modelId,
-    inputTokens: usage.inputTokens,
-    outputTokens: usage.outputTokens,
-    threadId: generation.threadId,
-    messageId: generation.assistantMessageId,
-    costEvidence: usage.costEvidence,
-  })
-}
-
-export async function finalizeGeneration(input: FinalizeGenerationInput) {
-  return db.transaction(async (tx) => {
-    const locked = await tx.execute(sql`
-      select ${branchGenerations.id}
-      from ${branchGenerations}
-      where ${branchGenerations.id} = ${input.generationId}
-      for update
-    `)
-    if (locked.length === 0) return null
-
-    const [row] = await tx
-      .select()
-      .from(branchGenerations)
-      .where(eq(branchGenerations.id, input.generationId))
-    if (!row) return null
-
-    // 终态不可逆;迟到的权威 usage 仍需补记账,但不得改写用户已看到的结果。
-    if (["completed", "stopped", "failed"].includes(row.status)) {
-      if (!input.usage || row.billingStatus === "settled") return row
-      await settleGenerationUsage(tx, row, input.usage)
-      const [updated] = await tx
-        .update(branchGenerations)
-        .set({ billingStatus: "settled", updatedAt: new Date() })
-        .where(eq(branchGenerations.id, input.generationId))
-        .returning()
-      return updated ?? row
-    }
-
-    let terminalStatus = input.outcome
-    if (row.status === "superseded" || !row.isCurrent) {
-      terminalStatus = "failed"
-    } else if (row.status === "stop_requested") {
-      terminalStatus = "stopped"
-    }
-
-    let billingStatus: GenerationBillingStatus = "not_billable"
-    if (input.usage) {
-      await settleGenerationUsage(tx, row, input.usage)
-      billingStatus = "settled"
-    } else if (input.usageUnavailable) {
-      billingStatus = "usage_unavailable"
-    }
-
-    const now = new Date()
-    if (row.status === "superseded" || !row.isCurrent) {
-      const [updated] = await tx
-        .update(branchGenerations)
-        .set({
-          result: input.result,
-          error: input.error ?? null,
-          billingStatus: advanceBillingStatus(row.billingStatus, billingStatus),
-          finishedAt: row.finishedAt ?? now,
-          updatedAt: now,
-        })
-        .where(eq(branchGenerations.id, input.generationId))
-        .returning()
-      return updated ?? row
-    }
-
-    const [updated] = await tx
-      .update(branchGenerations)
-      .set({
-        status: terminalStatus,
-        result: input.result,
-        error: input.error ?? null,
-        billingStatus,
-        finishedAt: now,
-        updatedAt: now,
-      })
-      .where(
-        and(
-          eq(branchGenerations.id, input.generationId),
-          inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES)
-        )
-      )
-      .returning()
-    return updated ?? row
-  })
-}
diff --git a/lib/thread-chat-generation/message-feedback-repository.ts b/lib/thread-chat-generation/message-feedback-repository.ts
deleted file mode 100644
index 0a101577..00000000
--- a/lib/thread-chat-generation/message-feedback-repository.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { and, eq } from "drizzle-orm"
-import { parseThreadTreeState } from "@/lib/thread-chat/domain/message-graph"
-import type { ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import type {
-  MessageFeedback,
-  MessageFeedbackSummary,
-  SetMessageFeedbackResult,
-} from "@/lib/thread-chat/contracts/message-feedback"
-import { db } from "@/lib/db"
-import {
-  branchGenerations,
-  branchMessageFeedback,
-  branchTrees,
-} from "@/lib/db/schema"
-
-function toSummary(
-  row: typeof branchMessageFeedback.$inferSelect
-): MessageFeedbackSummary {
-  return {
-    treeId: row.treeId,
-    threadId: row.threadId,
-    messageId: row.messageId,
-    feedback: row.feedback,
-    updatedAt: row.updatedAt.toISOString(),
-  }
-}
-
-export async function listMessageFeedbackForTree(
-  userId: string,
-  treeId: string
-): Promise {
-  const rows = await db
-    .select()
-    .from(branchMessageFeedback)
-    .where(
-      and(
-        eq(branchMessageFeedback.userId, userId),
-        eq(branchMessageFeedback.treeId, treeId)
-      )
-    )
-  return rows.map(toSummary)
-}
-
-/** Owner-scoped message feedback replacement; null deletes the current choice. */
-export async function setMessageFeedbackForOwner(input: {
-  userId: string
-  treeId: string
-  threadId: string
-  messageId: string
-  feedback: MessageFeedback | null
-}): Promise {
-  return db.transaction(async (tx) => {
-    const [tree] = await tx
-      .select({ state: branchTrees.state })
-      .from(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-    if (!tree) return { ok: false, reason: "not_found" }
-
-    let state: ThreadTreeState
-    try {
-      state = parseThreadTreeState(tree.state)
-    } catch {
-      return { ok: false, reason: "not_found" }
-    }
-    const message = state.threads[input.threadId]?.messages.find(
-      (candidate) => candidate.id === input.messageId
-    )
-    if (!message) return { ok: false, reason: "not_found" }
-    if (message.role !== "assistant" || message.status !== "done")
-      return { ok: false, reason: "not_completed" }
-
-    const [generation] = await tx
-      .select({ id: branchGenerations.id })
-      .from(branchGenerations)
-      .where(
-        and(
-          eq(branchGenerations.userId, input.userId),
-          eq(branchGenerations.treeId, input.treeId),
-          eq(branchGenerations.threadId, input.threadId),
-          eq(branchGenerations.assistantMessageId, input.messageId),
-          eq(branchGenerations.status, "completed")
-        )
-      )
-      .limit(1)
-    if (!generation) return { ok: false, reason: "missing_generation" }
-
-    const identity = and(
-      eq(branchMessageFeedback.userId, input.userId),
-      eq(branchMessageFeedback.treeId, input.treeId),
-      eq(branchMessageFeedback.threadId, input.threadId),
-      eq(branchMessageFeedback.messageId, input.messageId)
-    )
-    const [current] = await tx
-      .select()
-      .from(branchMessageFeedback)
-      .where(identity)
-    if (current?.feedback === input.feedback)
-      return { ok: true, feedback: toSummary(current) }
-    if (input.feedback === null) {
-      await tx.delete(branchMessageFeedback).where(identity)
-      return { ok: true, feedback: null }
-    }
-
-    const now = new Date()
-    const [saved] = await tx
-      .insert(branchMessageFeedback)
-      .values({
-        userId: input.userId,
-        treeId: input.treeId,
-        threadId: input.threadId,
-        messageId: input.messageId,
-        feedback: input.feedback,
-        updatedAt: now,
-      })
-      .onConflictDoUpdate({
-        target: [
-          branchMessageFeedback.userId,
-          branchMessageFeedback.treeId,
-          branchMessageFeedback.threadId,
-          branchMessageFeedback.messageId,
-        ],
-        set: { feedback: input.feedback, updatedAt: now },
-      })
-      .returning()
-    return { ok: true, feedback: toSummary(saved) }
-  })
-}
diff --git a/lib/thread-chat-generation/query-repository.ts b/lib/thread-chat-generation/query-repository.ts
deleted file mode 100644
index 5696d7b2..00000000
--- a/lib/thread-chat-generation/query-repository.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { and, desc, eq } from "drizzle-orm"
-import type { GenerationSummary } from "@/lib/thread-chat/domain/generation"
-import { db } from "@/lib/db"
-import { branchGenerations } from "@/lib/db/schema"
-
-export type GenerationRow = typeof branchGenerations.$inferSelect
-
-export async function getGenerationForOwner(
-  userId: string,
-  generationId: string
-): Promise {
-  const [row] = await db
-    .select()
-    .from(branchGenerations)
-    .where(
-      and(
-        eq(branchGenerations.id, generationId),
-        eq(branchGenerations.userId, userId)
-      )
-    )
-  return row ?? null
-}
-
-/** 内部执行观察器使用;不构成对用户暴露的数据接口。 */
-export async function getGenerationExecutionState(
-  generationId: string
-): Promise | null> {
-  const [row] = await db
-    .select({
-      status: branchGenerations.status,
-      isCurrent: branchGenerations.isCurrent,
-    })
-    .from(branchGenerations)
-    .where(eq(branchGenerations.id, generationId))
-  return row ?? null
-}
-
-export async function listCurrentGenerationsForTree(
-  userId: string,
-  treeId: string
-): Promise {
-  return db
-    .select()
-    .from(branchGenerations)
-    .where(
-      and(
-        eq(branchGenerations.userId, userId),
-        eq(branchGenerations.treeId, treeId),
-        eq(branchGenerations.isCurrent, true)
-      )
-    )
-    .orderBy(desc(branchGenerations.updatedAt))
-}
-
-export function toGenerationSummary(row: GenerationRow): GenerationSummary {
-  return {
-    id: row.id,
-    treeId: row.treeId,
-    threadId: row.threadId,
-    userMessageId: row.userMessageId,
-    assistantMessageId: row.assistantMessageId,
-    attempt: row.attempt,
-    isCurrent: row.isCurrent,
-    status: row.status,
-    updatedAt: row.updatedAt.toISOString(),
-    result: row.result,
-  }
-}
diff --git a/lib/thread-chat-generation/stale-generation-repository.ts b/lib/thread-chat-generation/stale-generation-repository.ts
deleted file mode 100644
index dcc590c1..00000000
--- a/lib/thread-chat-generation/stale-generation-repository.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { and, eq, inArray, lt } from "drizzle-orm"
-import type { GenerationResultV1 } from "@/lib/thread-chat/domain/generation"
-import { generationResultV1Schema } from "@/lib/thread-chat/contracts/generation-result"
-import {
-  ACTIVE_GENERATION_STATUSES,
-  GENERATION_ERRORS,
-  GENERATION_LEASE_MS,
-  GENERATION_RESULT_VERSION,
-} from "@/constants/generation"
-import { db } from "@/lib/db"
-import { branchGenerations } from "@/lib/db/schema"
-import {
-  getGenerationForOwner,
-  type GenerationRow,
-} from "@/lib/thread-chat-generation/query-repository"
-
-function staleFailureResult(row: GenerationRow): GenerationResultV1 {
-  const partial = row.turnSnapshot.assistantMessage
-  return generationResultV1Schema.parse({
-    version: GENERATION_RESULT_VERSION,
-    generationId: row.id,
-    text: partial.text,
-    status: "error",
-    error: GENERATION_ERRORS.backgroundInterrupted,
-    artifactIds: partial.artifactIds ?? [],
-    artifacts: {},
-    webResearch: partial.webResearch,
-    webResearchTextOffset: partial.webResearchTextOffset,
-    researchRoute: partial.researchRoute,
-    researchPlan: partial.researchPlan,
-  })
-}
-
-/** 将一棵 owner tree 内 lease 过期的活跃 generation 原子收敛为 failed。 */
-export async function failStaleGenerationsForTree(
-  userId: string,
-  treeId: string,
-  now = new Date()
-): Promise {
-  const staleBefore = new Date(now.getTime() - GENERATION_LEASE_MS)
-  const staleRows = await db
-    .select()
-    .from(branchGenerations)
-    .where(
-      and(
-        eq(branchGenerations.userId, userId),
-        eq(branchGenerations.treeId, treeId),
-        inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES),
-        lt(branchGenerations.heartbeatAt, staleBefore)
-      )
-    )
-
-  let changed = 0
-  for (const row of staleRows) {
-    const [updated] = await db
-      .update(branchGenerations)
-      .set({
-        status: "failed",
-        result: staleFailureResult(row),
-        error: GENERATION_ERRORS.backgroundInterrupted,
-        billingStatus: "usage_unavailable",
-        finishedAt: now,
-        updatedAt: now,
-      })
-      .where(
-        and(
-          eq(branchGenerations.id, row.id),
-          inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES),
-          lt(branchGenerations.heartbeatAt, staleBefore)
-        )
-      )
-      .returning({ id: branchGenerations.id })
-    if (updated) changed++
-  }
-  return changed
-}
-
-export async function failStaleGenerationForOwner(
-  userId: string,
-  generationId: string,
-  now = new Date()
-): Promise {
-  const row = await getGenerationForOwner(userId, generationId)
-  if (!row) return null
-  if (
-    ACTIVE_GENERATION_STATUSES.includes(
-      row.status as (typeof ACTIVE_GENERATION_STATUSES)[number]
-    ) &&
-    row.heartbeatAt.getTime() < now.getTime() - GENERATION_LEASE_MS
-  ) {
-    await failStaleGenerationsForTree(userId, row.treeId, now)
-    return getGenerationForOwner(userId, generationId)
-  }
-  return row
-}
diff --git a/lib/thread-chat-generation/start-generation-repository.ts b/lib/thread-chat-generation/start-generation-repository.ts
deleted file mode 100644
index 309a23d5..00000000
--- a/lib/thread-chat-generation/start-generation-repository.ts
+++ /dev/null
@@ -1,406 +0,0 @@
-import { and, eq, inArray, max, sql } from "drizzle-orm"
-import type { ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import { parseThreadTreeState } from "@/lib/thread-chat/domain/message-graph"
-import {
-  prepareRegenerationPatch,
-  type PreparedTurnPatch,
-} from "@/lib/thread-chat/domain/regeneration"
-import type {
-  GenerationTurnIdentity,
-  GenerationTurnSnapshot,
-  ThreadChatGenerationIntent,
-} from "@/lib/thread-chat/domain/generation"
-import { ACTIVE_GENERATION_STATUSES } from "@/constants/generation"
-import { db } from "@/lib/db"
-import { branchGenerations, branchTrees } from "@/lib/db/schema"
-import type { GenerationRow } from "@/lib/thread-chat-generation/query-repository"
-
-export class GenerationRepositoryError extends Error {
-  constructor(
-    readonly code:
-      | "not_found"
-      | "generation_conflict"
-      | "model_mismatch"
-      | "invalid_turn"
-      | "not_latest_turn"
-      | "persistence_failed",
-    message: string
-  ) {
-    super(message)
-    this.name = "GenerationRepositoryError"
-  }
-}
-
-export type StartGenerationInput = GenerationTurnIdentity & {
-  userId: string
-  modelId: string
-  intent: ThreadChatGenerationIntent
-}
-
-export type StartGenerationResult = {
-  created: boolean
-  generation: GenerationRow
-}
-
-export type PrepareGenerationResult =
-  | {
-      created: true
-      generation: GenerationRow
-      state: ThreadTreeState
-      revision: number
-      turnSnapshot: GenerationTurnSnapshot
-      patch?: PreparedTurnPatch
-    }
-  | { created: false; generation: GenerationRow }
-
-function isThreadTreeState(value: unknown): value is ThreadTreeState {
-  if (typeof value !== "object" || value === null) return false
-  const candidate = value as Partial
-  return (
-    typeof candidate.threads === "object" &&
-    candidate.threads !== null &&
-    typeof candidate.artifacts === "object" &&
-    candidate.artifacts !== null &&
-    Array.isArray(candidate.artifactOrder)
-  )
-}
-
-function verifyTurn(
-  stateValue: unknown,
-  input: GenerationTurnIdentity,
-  intent: ThreadChatGenerationIntent
-): GenerationTurnSnapshot {
-  if (!isThreadTreeState(stateValue)) {
-    throw new GenerationRepositoryError(
-      "invalid_turn",
-      "已保存的分支树状态无效"
-    )
-  }
-
-  let state: ThreadTreeState
-  try {
-    state = parseThreadTreeState(stateValue)
-  } catch {
-    throw new GenerationRepositoryError(
-      "invalid_turn",
-      "已保存的分支树消息图无效"
-    )
-  }
-
-  const thread = state.threads[input.threadId]
-  if (!thread) {
-    throw new GenerationRepositoryError("invalid_turn", "目标会话不存在")
-  }
-
-  const assistantMessageIndex = thread.messages.findIndex(
-    (message) => message.id === input.assistantMessageId
-  )
-  const userMessageIndex = thread.messages.findIndex(
-    (message) => message.id === input.userMessageId
-  )
-  const assistantMessage = thread.messages[assistantMessageIndex]
-  const userMessage = thread.messages[userMessageIndex]
-
-  if (
-    assistantMessageIndex < 0 ||
-    userMessageIndex < 0 ||
-    userMessage?.role !== "user" ||
-    assistantMessage?.role !== "assistant" ||
-    assistantMessage.parentMessageId !== userMessage.id ||
-    thread.activeLeafMessageId !== assistantMessage.id ||
-    assistantMessage.generationId !== input.generationId
-  ) {
-    throw new GenerationRepositoryError(
-      "invalid_turn",
-      "生成目标与已持久化的消息占位不一致"
-    )
-  }
-
-  return {
-    intent: structuredClone(intent),
-    threadId: input.threadId,
-    assistantMessageIndex,
-    userMessage: structuredClone(userMessage),
-    assistantMessage: structuredClone(assistantMessage),
-    userParentMessageId: userMessage.parentMessageId,
-    assistantParentMessageId: userMessage.id,
-    activatesAssistantMessageId: assistantMessage.id,
-  }
-}
-
-function generationIntentMatches(
-  stored: ThreadChatGenerationIntent | undefined,
-  received: ThreadChatGenerationIntent
-): boolean {
-  if (!stored || stored.kind !== received.kind) return false
-  switch (received.kind) {
-    case "persisted-turn":
-    case "retry-orphan-user":
-      return true
-    case "regenerate-assistant":
-      return (
-        stored.kind === received.kind &&
-        stored.sourceAssistantMessageId === received.sourceAssistantMessageId
-      )
-    case "edit-last-user":
-      return (
-        stored.kind === received.kind &&
-        stored.sourceUserMessageId === received.sourceUserMessageId &&
-        stored.text === received.text
-      )
-  }
-}
-
-function assertReplayMatches(row: GenerationRow, input: StartGenerationInput) {
-  if (
-    row.userId !== input.userId ||
-    row.treeId !== input.treeId ||
-    row.threadId !== input.threadId ||
-    row.userMessageId !== input.userMessageId ||
-    row.assistantMessageId !== input.assistantMessageId ||
-    row.modelId !== input.modelId ||
-    !generationIntentMatches(row.turnSnapshot.intent, input.intent)
-  ) {
-    throw new GenerationRepositoryError(
-      "generation_conflict",
-      "generation id 已被其他生成请求使用"
-    )
-  }
-}
-
-/**
- * 严格 start transaction。只有它返回 created=true 时调用方才可以发起付费模型请求;
- * 同 generation id 重放返回既有记录,绝不会启动第二次调用。
- */
-export async function prepareGeneration(
-  input: StartGenerationInput
-): Promise {
-  return db.transaction(async (tx) => {
-    const locked = await tx.execute(sql`
-      select ${branchTrees.id}
-      from ${branchTrees}
-      where ${branchTrees.id} = ${input.treeId}
-        and ${branchTrees.userId} = ${input.userId}
-      for update
-    `)
-    if (locked.length === 0) {
-      throw new GenerationRepositoryError("not_found", "分支树不存在")
-    }
-
-    const [tree] = await tx
-      .select({ state: branchTrees.state, revision: branchTrees.revision })
-      .from(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-    if (!tree) {
-      throw new GenerationRepositoryError("not_found", "分支树不存在")
-    }
-
-    const [replayed] = await tx
-      .select()
-      .from(branchGenerations)
-      .where(eq(branchGenerations.id, input.generationId))
-    if (replayed) {
-      assertReplayMatches(replayed, input)
-      return { created: false, generation: replayed }
-    }
-
-    const intent = input.intent
-    let state: ThreadTreeState
-    try {
-      state = parseThreadTreeState(tree.state)
-    } catch {
-      throw new GenerationRepositoryError(
-        "invalid_turn",
-        "已保存的分支树消息图无效"
-      )
-    }
-    const targetThread = state.threads[input.threadId]
-    if (!targetThread)
-      throw new GenerationRepositoryError("invalid_turn", "目标会话不存在")
-    if (targetThread.modelId !== input.modelId)
-      throw new GenerationRepositoryError(
-        "model_mismatch",
-        "请求模型与目标会话不一致,请刷新后重试"
-      )
-
-    if (intent.kind === "persisted-turn") {
-      const [otherActiveGeneration] = await tx
-        .select({ id: branchGenerations.id })
-        .from(branchGenerations)
-        .where(
-          and(
-            eq(branchGenerations.treeId, input.treeId),
-            eq(branchGenerations.threadId, input.threadId),
-            eq(branchGenerations.isCurrent, true),
-            inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES),
-            sql`${branchGenerations.assistantMessageId} <> ${input.assistantMessageId}`
-          )
-        )
-        .limit(1)
-      if (otherActiveGeneration)
-        throw new GenerationRepositoryError(
-          "generation_conflict",
-          "目标会话已有正在执行的生成,请等待完成或明确停止后重试"
-        )
-    }
-
-    let patch: PreparedTurnPatch | undefined
-    let revision = tree.revision
-    if (intent.kind !== "persisted-turn") {
-      const targetIds = [input.assistantMessageId]
-      if (intent.kind === "edit-last-user") targetIds.push(input.userMessageId)
-      if (
-        Object.values(state.threads).some((thread) =>
-          thread.messages.some((message) => targetIds.includes(message.id))
-        )
-      )
-        throw new GenerationRepositoryError(
-          "generation_conflict",
-          "新消息 id 已被其他生成占用"
-        )
-      const sourceMessageId =
-        intent.kind === "regenerate-assistant"
-          ? intent.sourceAssistantMessageId
-          : intent.kind === "edit-last-user"
-            ? intent.sourceUserMessageId
-            : input.userMessageId
-      const sourceExists = state.threads[input.threadId]?.messages.some(
-        (message) => message.id === sourceMessageId
-      )
-      if (!sourceExists)
-        throw new GenerationRepositoryError(
-          "invalid_turn",
-          "生成来源消息不存在"
-        )
-      patch =
-        prepareRegenerationPatch(state, {
-          threadId: input.threadId,
-          userMessageId: input.userMessageId,
-          assistantMessageId: input.assistantMessageId,
-          generationId: input.generationId,
-          intent,
-        }) ?? undefined
-      if (!patch)
-        throw new GenerationRepositoryError(
-          "not_latest_turn",
-          "只能编辑或重新生成当前 active path 的最后一轮"
-        )
-      const thread = state.threads[input.threadId]
-      thread.messages.push(
-        ...patch.addedMessages.map((message) => structuredClone(message))
-      )
-      thread.activeLeafMessageId = patch.nextActiveLeafMessageId
-      const [updatedTree] = await tx
-        .update(branchTrees)
-        .set({
-          state,
-          revision: sql`${branchTrees.revision} + 1`,
-          updatedAt: new Date(),
-        })
-        .where(
-          and(
-            eq(branchTrees.id, input.treeId),
-            eq(branchTrees.userId, input.userId),
-            eq(branchTrees.revision, tree.revision)
-          )
-        )
-        .returning({ revision: branchTrees.revision })
-      if (!updatedTree)
-        throw new GenerationRepositoryError(
-          "generation_conflict",
-          "分支树修订号已变更,请刷新后重试"
-        )
-      revision = updatedTree.revision
-
-      const now = new Date()
-      await tx
-        .update(branchGenerations)
-        .set({
-          isCurrent: false,
-          status: "superseded",
-          finishedAt: now,
-          updatedAt: now,
-        })
-        .where(
-          and(
-            eq(branchGenerations.treeId, input.treeId),
-            eq(branchGenerations.threadId, input.threadId),
-            eq(branchGenerations.isCurrent, true),
-            inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES)
-          )
-        )
-    }
-
-    const turnSnapshot = verifyTurn(state, input, input.intent)
-    const [attemptRow] = await tx
-      .select({ value: max(branchGenerations.attempt) })
-      .from(branchGenerations)
-      .where(
-        and(
-          eq(branchGenerations.treeId, input.treeId),
-          eq(branchGenerations.threadId, input.threadId),
-          eq(branchGenerations.assistantMessageId, input.assistantMessageId)
-        )
-      )
-    const attempt = (attemptRow?.value ?? 0) + 1
-    const now = new Date()
-
-    if (intent.kind === "persisted-turn")
-      await tx
-        .update(branchGenerations)
-        .set({
-          isCurrent: false,
-          status: "superseded",
-          finishedAt: now,
-          updatedAt: now,
-        })
-        .where(
-          and(
-            eq(branchGenerations.treeId, input.treeId),
-            eq(branchGenerations.threadId, input.threadId),
-            eq(branchGenerations.assistantMessageId, input.assistantMessageId),
-            eq(branchGenerations.isCurrent, true)
-          )
-        )
-
-    const [created] = await tx
-      .insert(branchGenerations)
-      .values({
-        id: input.generationId,
-        userId: input.userId,
-        treeId: input.treeId,
-        threadId: input.threadId,
-        userMessageId: input.userMessageId,
-        assistantMessageId: input.assistantMessageId,
-        attempt,
-        isCurrent: true,
-        status: "running",
-        modelId: input.modelId,
-        assistantMessageIndex: turnSnapshot.assistantMessageIndex,
-        turnSnapshot,
-        heartbeatAt: now,
-        updatedAt: now,
-      })
-      .returning()
-
-    return {
-      created: true,
-      generation: created,
-      state,
-      revision,
-      turnSnapshot,
-      ...(patch ? { patch } : {}),
-    }
-  })
-}
-
-export async function startGeneration(
-  input: StartGenerationInput
-): Promise {
-  return prepareGeneration(input)
-}
diff --git a/lib/thread-chat-generation/tree-repository.ts b/lib/thread-chat-generation/tree-repository.ts
deleted file mode 100644
index 58e9f91c..00000000
--- a/lib/thread-chat-generation/tree-repository.ts
+++ /dev/null
@@ -1,307 +0,0 @@
-import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm"
-import { ACTIVE_GENERATION_STATUSES } from "@/constants/generation"
-import { TREE_TITLE_FALLBACK } from "@/constants/thread-chat"
-import {
-  activeLeafTurn,
-  assistantTurnAlternatives,
-  parseThreadTreeState,
-} from "@/lib/thread-chat/domain/message-graph"
-import type { ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import type {
-  SwitchActiveLeafFailureReason,
-  SwitchActiveLeafRequest,
-  SwitchActiveLeafSuccessResponse,
-} from "@/lib/thread-chat/contracts/switch-active-leaf"
-import { db } from "@/lib/db"
-import { branchGenerations, branchTrees } from "@/lib/db/schema"
-
-export class TreeCommandError extends Error {
-  constructor(
-    readonly code: SwitchActiveLeafFailureReason,
-    message: string,
-    readonly currentRevision?: number
-  ) {
-    super(message)
-    this.name = "TreeCommandError"
-  }
-}
-
-export type DeleteOwnedTreeResult =
-  "deleted" | "not_found" | "generation_running"
-
-export type OwnedTreeSnapshot = Pick<
-  typeof branchTrees.$inferSelect,
-  "state" | "customTitle" | "revision"
->
-
-export type SaveOwnedTreeResult =
-  | { kind: "saved"; revision: number }
-  | { kind: "conflict"; revision: number }
-  | { kind: "not_found" }
-
-export interface OwnedTreeSummary {
-  id: string
-  title: string
-  updatedAt: Date
-  threadCount: number
-}
-
-/** 会话列表用的 owner-scoped 轻量投影;不把整棵 JSON state 带出仓储。 */
-export async function listOwnedTreeSummaries(
-  userId: string
-): Promise {
-  return db
-    .select({
-      id: branchTrees.id,
-      title: sql`coalesce(${branchTrees.customTitle}, ${branchTrees.title}, ${TREE_TITLE_FALLBACK})`,
-      updatedAt: branchTrees.updatedAt,
-      // 历史毒行的 threads 不是对象时返回 0,不能让一行数据打挂整个列表。
-      threadCount: sql`(case when jsonb_typeof(${branchTrees.state} -> 'threads') = 'object' then (select count(*) from jsonb_object_keys(${branchTrees.state} -> 'threads')) else 0 end)::int`,
-    })
-    .from(branchTrees)
-    .where(eq(branchTrees.userId, userId))
-    .orderBy(desc(branchTrees.updatedAt))
-    .limit(100)
-}
-
-/** GET 精确 URL 的迁移入口:普通 owner 读取,或原子认领一棵历史无主树。 */
-export async function loadOwnedOrClaimLegacyTree(input: {
-  userId: string
-  treeId: string
-}): Promise {
-  return db.transaction(async (tx) => {
-    const selection = {
-      state: branchTrees.state,
-      customTitle: branchTrees.customTitle,
-      revision: branchTrees.revision,
-    }
-    const [owned] = await tx
-      .select(selection)
-      .from(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-    if (owned) return owned
-
-    const [claimed] = await tx
-      .update(branchTrees)
-      .set({ userId: input.userId })
-      .where(and(eq(branchTrees.id, input.treeId), isNull(branchTrees.userId)))
-      .returning(selection)
-    return claimed ?? null
-  })
-}
-
-/** owner + revision CAS 的整树写入;新树只允许 baseRevision=0 时绑定当前用户。 */
-export async function saveOwnedTree(input: {
-  userId: string
-  treeId: string
-  state: ThreadTreeState
-  title: string | null
-  baseRevision: number
-}): Promise {
-  const now = new Date()
-  return db.transaction(async (tx) => {
-    const [updated] = await tx
-      .update(branchTrees)
-      .set({
-        state: input.state,
-        title: input.title,
-        revision: sql`${branchTrees.revision} + 1`,
-        updatedAt: now,
-      })
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId),
-          eq(branchTrees.revision, input.baseRevision)
-        )
-      )
-      .returning({ revision: branchTrees.revision })
-    if (updated) return { kind: "saved", revision: updated.revision }
-
-    const [existing] = await tx
-      .select({ revision: branchTrees.revision })
-      .from(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-    if (existing) return { kind: "conflict", revision: existing.revision }
-    if (input.baseRevision !== 0) return { kind: "not_found" }
-
-    const [inserted] = await tx
-      .insert(branchTrees)
-      .values({
-        id: input.treeId,
-        userId: input.userId,
-        state: input.state,
-        title: input.title,
-        revision: 1,
-        updatedAt: now,
-      })
-      .onConflictDoNothing({ target: branchTrees.id })
-      .returning({ revision: branchTrees.revision })
-    return inserted
-      ? { kind: "saved", revision: inserted.revision }
-      : { kind: "not_found" }
-  })
-}
-
-/** 已校验标题的 owner-scoped 用户命名写入;不触碰机器派生 title。 */
-export async function renameOwnedTree(input: {
-  userId: string
-  treeId: string
-  customTitle: string
-}): Promise {
-  const [updated] = await db
-    .update(branchTrees)
-    .set({ customTitle: input.customTitle })
-    .where(
-      and(
-        eq(branchTrees.id, input.treeId),
-        eq(branchTrees.userId, input.userId)
-      )
-    )
-    .returning({ id: branchTrees.id })
-  return Boolean(updated)
-}
-
-/**
- * 删除与 generation start 共用 branch_trees 行锁:两者并发时,只可能先删除并让
- * start 得到 not_found,或先创建 generation 并让删除得到 generation_running。
- */
-export async function deleteOwnedTreeIfIdle(input: {
-  userId: string
-  treeId: string
-}): Promise {
-  return db.transaction(async (tx) => {
-    const locked = await tx.execute(sql`
-      select ${branchTrees.id}
-      from ${branchTrees}
-      where ${branchTrees.id} = ${input.treeId}
-        and ${branchTrees.userId} = ${input.userId}
-      for update
-    `)
-    if (locked.length === 0) return "not_found"
-
-    const [activeGeneration] = await tx
-      .select({ id: branchGenerations.id })
-      .from(branchGenerations)
-      .where(
-        and(
-          eq(branchGenerations.userId, input.userId),
-          eq(branchGenerations.treeId, input.treeId),
-          inArray(branchGenerations.status, ACTIVE_GENERATION_STATUSES)
-        )
-      )
-      .limit(1)
-    if (activeGeneration) return "generation_running"
-
-    const [deleted] = await tx
-      .delete(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-      .returning({ id: branchTrees.id })
-    return deleted ? "deleted" : "not_found"
-  })
-}
-
-export async function switchActiveLeafForOwner(
-  input: SwitchActiveLeafRequest & {
-    userId: string
-    treeId: string
-  }
-): Promise {
-  return db.transaction(async (tx) => {
-    const locked = await tx.execute(sql`
-      select ${branchTrees.id}
-      from ${branchTrees}
-      where ${branchTrees.id} = ${input.treeId}
-        and ${branchTrees.userId} = ${input.userId}
-      for update
-    `)
-    if (locked.length === 0)
-      throw new TreeCommandError("not_found", "分支树不存在")
-
-    const [row] = await tx
-      .select({ state: branchTrees.state, revision: branchTrees.revision })
-      .from(branchTrees)
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId)
-        )
-      )
-    if (!row) throw new TreeCommandError("not_found", "分支树不存在")
-    if (row.revision !== input.baseRevision)
-      throw new TreeCommandError(
-        "tree_revision_conflict",
-        "该对话已在其他页面更新",
-        row.revision
-      )
-
-    let state: ThreadTreeState
-    try {
-      state = parseThreadTreeState(row.state)
-    } catch {
-      throw new TreeCommandError("invalid_turn", "分支树消息结构无效")
-    }
-    const thread = state.threads[input.threadId]
-    const currentTurn = thread ? activeLeafTurn(thread) : null
-    if (!thread || !currentTurn?.assistantMessage)
-      throw new TreeCommandError("invalid_turn", "当前会话没有可切换的最新回复")
-
-    const alternatives = assistantTurnAlternatives(
-      thread,
-      currentTurn.assistantMessage.id
-    )
-    const target = alternatives.find(
-      (message) => message.id === input.assistantMessageId
-    )
-    const targetHasMessageChildren = thread.messages.some(
-      (message) => message.parentMessageId === target?.id
-    )
-    if (!target || targetHasMessageChildren)
-      throw new TreeCommandError(
-        "invalid_turn",
-        "目标不是最新一轮的可切换回复版本"
-      )
-
-    thread.activeLeafMessageId = target.id
-    const [updated] = await tx
-      .update(branchTrees)
-      .set({
-        state,
-        revision: sql`${branchTrees.revision} + 1`,
-        updatedAt: new Date(),
-      })
-      .where(
-        and(
-          eq(branchTrees.id, input.treeId),
-          eq(branchTrees.userId, input.userId),
-          eq(branchTrees.revision, input.baseRevision)
-        )
-      )
-      .returning({ revision: branchTrees.revision })
-    if (!updated)
-      throw new TreeCommandError(
-        "tree_revision_conflict",
-        "该对话已在其他页面更新"
-      )
-
-    return {
-      revision: updated.revision,
-      thread: { id: thread.id, activeLeafMessageId: target.id },
-    }
-  })
-}
diff --git a/lib/thread-chat/application/compile-thread-chat-messages.ts b/lib/thread-chat/application/compile-thread-chat-messages.ts
deleted file mode 100644
index 417d4e3b..00000000
--- a/lib/thread-chat/application/compile-thread-chat-messages.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { activeMessagePath } from "@/lib/thread-chat/domain/message-graph"
-import { collectInherited } from "@/lib/thread-chat/domain/selectors"
-import type { Message, ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import { INHERITED_CHAR_BUDGET } from "@/constants/thread-chat"
-import { serializeMessageForModel } from "@/lib/thread-chat/application/serialize-message-for-model"
-import {
-  applyInheritedBudget,
-  omittedNoticeText,
-} from "@/lib/thread-chat/application/prompt-policy"
-
-/** 发给 /api/chat 的最小消息形状(结构匹配 AI SDK UIMessage)。 */
-export interface UIMessageLike {
-  id: string
-  role: "user" | "assistant"
-  parts: { type: "text"; text: string }[]
-}
-
-function includable(message: Message, serialized: string | null): boolean {
-  if (message.status === "error") return false
-  return message.role === "user" || serialized !== null
-}
-
-/**
- * 从已提交的服务端树编译模型上下文:当前 Thread 跟 active path,
- * 祖先 Thread 跟 child.forkFromMsgId 的精确不可变来源路径。
- */
-export function compileThreadChatMessages(input: {
-  state: ThreadTreeState
-  threadId: string
-  excludeAssistantMessageId: string
-}): UIMessageLike[] {
-  const thread = input.state.threads[input.threadId]
-  if (!thread) return []
-  const messages: UIMessageLike[] = []
-  const inherited: UIMessageLike[] = []
-
-  for (const message of collectInherited(input.state, thread)) {
-    const text = serializeMessageForModel(input.state, message)
-    if (!includable(message, text) || text === null) continue
-    inherited.push({
-      id: `inh-${message.id}`,
-      role: message.role,
-      parts: [{ type: "text", text }],
-    })
-  }
-  const { kept, omitted } = applyInheritedBudget(
-    inherited,
-    (message) => message.parts[0].text,
-    INHERITED_CHAR_BUDGET
-  )
-  if (omitted > 0)
-    messages.push({
-      id: "inh-omitted",
-      role: "user",
-      parts: [{ type: "text", text: omittedNoticeText(omitted) }],
-    })
-  messages.push(...kept)
-
-  for (const message of activeMessagePath(thread)) {
-    if (message.id === input.excludeAssistantMessageId) continue
-    const text = serializeMessageForModel(input.state, message)
-    if (!includable(message, text) || text === null) continue
-    messages.push({
-      id: message.id,
-      role: message.role,
-      parts: [{ type: "text", text }],
-    })
-  }
-  return messages
-}
diff --git a/lib/thread-chat/application/merge-generation-result.ts b/lib/thread-chat/application/merge-generation-result.ts
deleted file mode 100644
index c187c6f1..00000000
--- a/lib/thread-chat/application/merge-generation-result.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import type {
-  Artifact,
-  Message,
-  ThreadTreeState,
-} from "@/lib/thread-chat/domain/types"
-import type {
-  GenerationResultV1,
-  GenerationTurnSnapshot,
-} from "@/lib/thread-chat/domain/generation"
-
-export type MergeGenerationResultInput = {
-  threadId: string
-  assistantMessageId: string
-  generationId: string
-  turnSnapshot?: GenerationTurnSnapshot
-  result: GenerationResultV1
-}
-
-function messageReferenceCounts(state: ThreadTreeState): Map {
-  const counts = new Map()
-  for (const thread of Object.values(state.threads)) {
-    for (const message of thread.messages) {
-      for (const artifactId of message.artifactIds ?? []) {
-        counts.set(artifactId, (counts.get(artifactId) ?? 0) + 1)
-      }
-    }
-  }
-  return counts
-}
-
-export function restoreTurnSnapshot(
-  thread: ThreadTreeState["threads"][string],
-  snapshot: GenerationTurnSnapshot
-): { userMessage: Message; assistantMessage: Message } | null {
-  let userMessage = thread.messages.find(
-    (message) => message.id === snapshot.userMessage.id
-  )
-  if (!userMessage) {
-    userMessage = {
-      ...structuredClone(snapshot.userMessage),
-      parentMessageId: snapshot.userParentMessageId,
-    }
-    thread.messages.push(userMessage)
-  }
-  if (userMessage.role !== "user") return null
-
-  let assistantMessage = thread.messages.find(
-    (message) => message.id === snapshot.assistantMessage.id
-  )
-  if (!assistantMessage) {
-    assistantMessage = {
-      ...structuredClone(snapshot.assistantMessage),
-      parentMessageId: snapshot.assistantParentMessageId,
-    }
-    thread.messages.push(assistantMessage)
-  }
-  if (assistantMessage.role !== "assistant") return null
-  return { userMessage, assistantMessage }
-}
-
-/**
- * current generation patch 的幂等合并。只覆盖 generation-owned 字段,目标消息的
- * forks/quote 等并发用户编辑保持不变;不同 generationId 的晚到 patch 被 CAS 丢弃。
- */
-export function mergeGenerationResult(
-  state: ThreadTreeState,
-  input: MergeGenerationResultInput
-): ThreadTreeState {
-  if (input.result.generationId !== input.generationId) return state
-  const sourceThread = state.threads[input.threadId]
-  if (!sourceThread) return state
-
-  const next = structuredClone(state)
-  const thread = next.threads[input.threadId]
-  let messageIndex = thread.messages.findIndex(
-    (message) => message.id === input.assistantMessageId
-  )
-  if (messageIndex === -1) {
-    if (!input.turnSnapshot) return state
-    const repaired = restoreTurnSnapshot(thread, input.turnSnapshot)
-    if (!repaired || repaired.assistantMessage.id !== input.assistantMessageId)
-      return state
-    messageIndex = thread.messages.findIndex(
-      (message) => message.id === input.assistantMessageId
-    )
-  }
-  const message = thread.messages[messageIndex]
-  if (!message || message.role !== "assistant") return state
-  if (message.generationId && message.generationId !== input.generationId)
-    return state
-
-  const oldArtifactIds = message.artifactIds ?? []
-  const referenceCounts = messageReferenceCounts(next)
-  const newArtifactIds = [...new Set(input.result.artifactIds)]
-  const newArtifacts: Record = {}
-  for (const id of newArtifactIds) {
-    const artifact = input.result.artifacts[id]
-    if (artifact)
-      newArtifacts[id] = {
-        ...structuredClone(artifact),
-        sourceThreadId: input.threadId,
-        sourceMessageId: input.assistantMessageId,
-      }
-  }
-
-  for (const oldId of oldArtifactIds) {
-    if (!newArtifacts[oldId] && (referenceCounts.get(oldId) ?? 0) <= 1) {
-      delete next.artifacts[oldId]
-    }
-  }
-  Object.assign(next.artifacts, newArtifacts)
-  next.artifactOrder = next.artifactOrder.filter(
-    (id, index, all) =>
-      next.artifacts[id] !== undefined && all.indexOf(id) === index
-  )
-  for (const id of newArtifactIds) {
-    if (newArtifacts[id] && !next.artifactOrder.includes(id)) {
-      next.artifactOrder.push(id)
-    }
-  }
-
-  message.text = input.result.text
-  message.status = input.result.status
-  message.error = input.result.error
-  message.generationId = input.generationId
-  message.backgroundGeneration = undefined
-  message.artifactIds = newArtifactIds.length ? newArtifactIds : undefined
-  message.markdownGeneration = undefined
-  message.webResearch = input.result.webResearch
-  message.webResearchTextOffset = input.result.webResearchTextOffset
-  message.researchRoute = input.result.researchRoute
-  message.researchPlan = input.result.researchPlan
-  return next
-}
diff --git a/lib/thread-chat/application/project-generation-result.ts b/lib/thread-chat/application/project-generation-result.ts
deleted file mode 100644
index f04e62e1..00000000
--- a/lib/thread-chat/application/project-generation-result.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import type { UIMessage } from "ai"
-import type { Artifact } from "@/lib/thread-chat/domain/types"
-import type {
-  GenerationResultV1,
-  GenerationUsageMetadata,
-} from "@/lib/thread-chat/domain/generation"
-import { generationResultV1Schema } from "@/lib/thread-chat/contracts/generation-result"
-import {
-  GENERATION_ERRORS,
-  GENERATION_RESULT_VERSION,
-} from "@/constants/generation"
-import {
-  MARKDOWN_ARTIFACT_TOOL_NAME,
-  markdownArtifactInputSchema,
-} from "@/lib/chat/markdown-artifact"
-import type { ResearchPlan, ResearchRoute } from "@/lib/chat/research-router"
-import {
-  webResearchSourcesFromOutput,
-  type WebResearchActivity,
-} from "@/lib/chat/web-research-activity"
-
-type ProjectTerminalStatus = "completed" | "stopped" | "failed"
-
-export type ProjectGenerationResultInput = {
-  generationId: string
-  threadId: string
-  assistantMessageId: string
-  responseMessage: Pick
-  terminalStatus: ProjectTerminalStatus
-  error?: string
-  researchRoute?: ResearchRoute
-  researchPlan?: ResearchPlan
-  usage?: GenerationUsageMetadata
-}
-
-export type ProjectGenerationResultOutput = {
-  result: GenerationResultV1
-  hasDisplayableOutput: boolean
-}
-
-function isRecord(value: unknown): value is Record {
-  return typeof value === "object" && value !== null
-}
-
-function stableHash(value: string): string {
-  let hash = 0x811c9dc5
-  for (let i = 0; i < value.length; i++) {
-    hash ^= value.charCodeAt(i)
-    hash = Math.imul(hash, 0x01000193)
-  }
-  return (hash >>> 0).toString(36)
-}
-
-export function generationArtifactId(
-  generationId: string,
-  toolCallId: string
-): string {
-  return `ga_${generationId}_${stableHash(toolCallId)}`
-}
-
-function toolName(part: Record): string | null {
-  if (part.type === "dynamic-tool") {
-    return typeof part.toolName === "string" ? part.toolName : null
-  }
-  if (typeof part.type !== "string" || !part.type.startsWith("tool-"))
-    return null
-  return part.type.slice("tool-".length)
-}
-
-function optionalString(value: unknown): string | undefined {
-  return typeof value === "string" && value.trim() ? value.trim() : undefined
-}
-
-export function projectGenerationResult({
-  generationId,
-  threadId,
-  assistantMessageId,
-  responseMessage,
-  terminalStatus,
-  error,
-  researchRoute: knownRoute,
-  researchPlan: knownPlan,
-  usage,
-}: ProjectGenerationResultInput): ProjectGenerationResultOutput {
-  const textParts: string[] = []
-  const artifacts: Record = {}
-  const artifactIds: string[] = []
-  const researchByCall = new Map()
-  let webResearchTextOffset: number | undefined
-  let receivedTextLength = 0
-  let researchRoute = knownRoute
-  let researchPlan = knownPlan
-
-  for (const rawPart of responseMessage.parts) {
-    if (!isRecord(rawPart) || typeof rawPart.type !== "string") continue
-    const part: Record = rawPart
-    if (part.type === "text" && typeof part.text === "string") {
-      textParts.push(part.text)
-      receivedTextLength += part.text.length
-      continue
-    }
-    if (part.type === "data-research-route" && isRecord(part.data)) {
-      researchRoute = part.data as ResearchRoute
-      continue
-    }
-    if (part.type === "data-research-plan" && isRecord(part.data)) {
-      researchPlan = part.data as unknown as ResearchPlan
-      continue
-    }
-
-    const name = toolName(part)
-    const toolCallId = optionalString(part.toolCallId)
-    if (!name || !toolCallId) continue
-    const input = part.input
-
-    if (name === MARKDOWN_ARTIFACT_TOOL_NAME) {
-      const parsed = markdownArtifactInputSchema.safeParse(input)
-      if (!parsed.success) continue
-      const id = generationArtifactId(generationId, toolCallId)
-      artifacts[id] = {
-        id,
-        sourceThreadId: threadId,
-        sourceMessageId: assistantMessageId,
-        kind: "markdown",
-        title: parsed.data.title,
-        content: parsed.data.content,
-      }
-      if (!artifactIds.includes(id)) artifactIds.push(id)
-      continue
-    }
-
-    if (name === "webSearch" || name === "readUrl") {
-      webResearchTextOffset ??= receivedTextLength
-      const inputRecord = isRecord(input) ? input : {}
-      const output = part.output
-      researchByCall.set(toolCallId, {
-        toolCallId,
-        kind: name === "webSearch" ? "search" : "read",
-        status: "complete",
-        query:
-          name === "webSearch" ? optionalString(inputRecord.query) : undefined,
-        url: name === "readUrl" ? optionalString(inputRecord.url) : undefined,
-        sources:
-          name === "webSearch"
-            ? webResearchSourcesFromOutput(output)
-            : [],
-      })
-    }
-  }
-
-  const text = textParts.join("")
-  const webResearch = [...researchByCall.values()]
-  const hasDisplayableOutput =
-    text.trim().length > 0 ||
-    artifactIds.length > 0 ||
-    webResearch.length > 0 ||
-    researchPlan !== undefined
-
-  let status: GenerationResultV1["status"] = "done"
-  let resultError: string | undefined
-  if (terminalStatus === "failed") {
-    status = "error"
-    resultError = error || GENERATION_ERRORS.streamFailed
-  } else if (terminalStatus === "stopped") {
-    status = "error"
-    resultError = GENERATION_ERRORS.stopped
-  } else if (terminalStatus === "completed" && !hasDisplayableOutput) {
-    status = "error"
-    resultError = GENERATION_ERRORS.emptyResponse
-  }
-
-  return {
-    hasDisplayableOutput,
-    result: generationResultV1Schema.parse({
-      version: GENERATION_RESULT_VERSION,
-      generationId,
-      text,
-      status,
-      ...(resultError ? { error: resultError } : {}),
-      artifactIds,
-      artifacts,
-      ...(webResearch.length > 0 ? { webResearch } : {}),
-      ...(webResearchTextOffset !== undefined ? { webResearchTextOffset } : {}),
-      ...(researchRoute ? { researchRoute } : {}),
-      ...(researchPlan ? { researchPlan } : {}),
-      ...(usage ? { usage } : {}),
-    }),
-  }
-}
diff --git a/lib/thread-chat/application/reconcile-turns.ts b/lib/thread-chat/application/reconcile-turns.ts
deleted file mode 100644
index c4df9018..00000000
--- a/lib/thread-chat/application/reconcile-turns.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { parseThreadTreeState } from "@/lib/thread-chat/domain/message-graph"
-import type { Message, ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import { GENERATION_ERRORS } from "@/constants/generation"
-import {
-  isActiveGenerationStatus,
-  type GenerationForReconcile,
-  type GenerationSummary,
-  type ReconciledThreadChatTree,
-  type RecoverableTurn,
-} from "@/lib/thread-chat/domain/generation"
-import {
-  mergeGenerationResult,
-  restoreTurnSnapshot,
-} from "@/lib/thread-chat/application/merge-generation-result"
-
-const recoverableKey = (threadId: string, userMessageId: string) =>
-  `${threadId}:${userMessageId}`
-
-function parentUser(
-  messages: readonly Message[],
-  assistant: Message
-): Message | undefined {
-  return messages.find(
-    (message) =>
-      message.id === assistant.parentMessageId && message.role === "user"
-  )
-}
-
-export class InvalidCompletedMessageGenerationLinkError extends Error {
-  constructor(readonly messageId: string) {
-    super(`Completed assistant message ${messageId} has no generation link`)
-    this.name = "InvalidCompletedMessageGenerationLinkError"
-  }
-}
-
-/** 完成消息可以不冗余 generationId,但服务端必须能反查其完成执行。 */
-export function assertCompletedMessageGenerationLinks(
-  state: ThreadTreeState,
-  generations: readonly GenerationSummary[]
-): void {
-  const completedByMessage = new Set()
-  const completedByGeneration = new Set()
-  for (const generation of generations) {
-    if (
-      !generation.isCurrent ||
-      generation.status !== "completed" ||
-      generation.result?.status !== "done"
-    )
-      continue
-    const messageKey = `${generation.threadId}:${generation.assistantMessageId}`
-    completedByMessage.add(messageKey)
-    completedByGeneration.add(`${messageKey}:${generation.id}`)
-  }
-
-  for (const thread of Object.values(state.threads)) {
-    for (const message of thread.messages) {
-      if (message.role !== "assistant" || message.status !== "done") continue
-      const messageKey = `${thread.id}:${message.id}`
-      const linked = message.generationId
-        ? completedByGeneration.has(`${messageKey}:${message.generationId}`)
-        : completedByMessage.has(messageKey)
-      if (!linked)
-        throw new InvalidCompletedMessageGenerationLinkError(message.id)
-    }
-  }
-}
-
-/**
- * 用 tree + current generation sidecar 得到唯一的加载投影。
- * 函数不修改输入,也不把协调结果反写 DB。
- */
-export function reconcileThreadChatTurns(input: {
-  state: ThreadTreeState
-  generations: readonly GenerationForReconcile[]
-}): ReconciledThreadChatTree {
-  let state = parseThreadTreeState(input.state)
-  const currentGenerations = input.generations.filter(
-    (generation) => generation.isCurrent
-  )
-
-  for (const generation of currentGenerations) {
-    if (generation.result) {
-      state = mergeGenerationResult(state, {
-        threadId: generation.threadId,
-        assistantMessageId: generation.assistantMessageId,
-        generationId: generation.id,
-        turnSnapshot: generation.turnSnapshot,
-        result: generation.result,
-      })
-      continue
-    }
-
-    if (!isActiveGenerationStatus(generation.status)) continue
-    const thread = state.threads[generation.threadId]
-    if (!thread) continue
-    let assistant = thread.messages.find(
-      (message) => message.id === generation.assistantMessageId
-    )
-    if (!assistant) {
-      const restored = restoreTurnSnapshot(thread, generation.turnSnapshot)
-      assistant = restored?.assistantMessage
-      if (
-        restored &&
-        (thread.activeLeafMessageId === null ||
-          thread.activeLeafMessageId === restored.userMessage.id)
-      )
-        thread.activeLeafMessageId = restored.assistantMessage.id
-    }
-    if (assistant?.role === "assistant") {
-      assistant.generationId = generation.id
-      assistant.backgroundGeneration = true
-      if (assistant.status !== "streaming") assistant.status = "pending"
-      assistant.error = undefined
-    }
-  }
-
-  const activeByAssistant = new Map(
-    currentGenerations
-      .filter((generation) => isActiveGenerationStatus(generation.status))
-      .map((generation) => [
-        `${generation.threadId}:${generation.assistantMessageId}`,
-        generation,
-      ])
-  )
-  const currentByUser = new Map(
-    currentGenerations.map((generation) => [
-      `${generation.threadId}:${generation.userMessageId}`,
-      generation,
-    ])
-  )
-  const recoverableByTurn = new Map()
-
-  for (const thread of Object.values(state.threads)) {
-    for (const message of thread.messages) {
-      if (
-        message.role !== "assistant" ||
-        (message.status !== "pending" && message.status !== "streaming")
-      )
-        continue
-
-      if (activeByAssistant.has(`${thread.id}:${message.id}`)) continue
-      const user = parentUser(thread.messages, message)
-      if (!user) continue
-      const generation = currentByUser.get(`${thread.id}:${user.id}`)
-      message.status = "error"
-      message.error = GENERATION_ERRORS.backgroundInterrupted
-      message.backgroundGeneration = undefined
-      recoverableByTurn.set(recoverableKey(thread.id, user.id), {
-        threadId: thread.id,
-        userMessageId: user.id,
-        assistantMessageId: message.id,
-        reason: generation ? "interrupted_generation" : "missing_generation",
-      })
-    }
-
-    const activeLeaf = thread.messages.find(
-      (message) => message.id === thread.activeLeafMessageId
-    )
-    if (activeLeaf?.role !== "user") continue
-    const hasAssistantChild = thread.messages.some(
-      (message) =>
-        message.role === "assistant" &&
-        message.parentMessageId === activeLeaf.id
-    )
-    if (
-      !hasAssistantChild &&
-      !currentByUser.has(`${thread.id}:${activeLeaf.id}`)
-    )
-      recoverableByTurn.set(recoverableKey(thread.id, activeLeaf.id), {
-        threadId: thread.id,
-        userMessageId: activeLeaf.id,
-        reason: "missing_assistant",
-      })
-  }
-
-  return {
-    state,
-    recoverableTurns: [...recoverableByTurn.values()],
-  }
-}
diff --git a/lib/thread-chat/application/serialize-message-for-model.ts b/lib/thread-chat/application/serialize-message-for-model.ts
deleted file mode 100644
index 37f6e1c2..00000000
--- a/lib/thread-chat/application/serialize-message-for-model.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { Message, ThreadTreeState } from "@/lib/thread-chat/domain/types"
-
-/**
- * 把领域消息编译为模型可见文本。Artifact 不保存 AI SDK tool parts,因此用明确边界
- * 回放标题与原始内容,让“修改刚才的 Markdown”等追问仍有完整 grounding。
- */
-export function serializeMessageForModel(
-  state: ThreadTreeState,
-  message: Message
-): string | null {
-  const sections: string[] = []
-  const body = message.quote?.text
-    ? `就我划选的这段话:「${message.quote.text}」——${message.text}`
-    : message.text
-  if (body.trim()) sections.push(body)
-
-  for (const artifactId of message.artifactIds ?? []) {
-    const artifact = state.artifacts[artifactId]
-    if (!artifact) continue
-    if (artifact.kind !== "markdown") continue
-    sections.push(
-      `[Markdown Artifact: ${artifact.title}]\n${artifact.content}\n[/Markdown Artifact]`
-    )
-  }
-
-  const serialized = sections.join("\n\n").trim()
-  return serialized || null
-}
diff --git a/lib/thread-chat/contracts/generation-identity.ts b/lib/thread-chat/contracts/generation-identity.ts
deleted file mode 100644
index 18a6277f..00000000
--- a/lib/thread-chat/contracts/generation-identity.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { z } from "zod"
-import { threadChatGenerationIntentSchema } from "@/lib/thread-chat/contracts/generation-intent"
-
-/** Thread Chat 生成请求定位持久化 turn 的唯一运行时契约。 */
-export const threadChatGenerationIdentitySchema = z.object({
-  anchorText: z.string().nullable().optional(),
-  treeId: z.string().uuid(),
-  threadId: z.string().min(1),
-  userMessageId: z.string().min(1),
-  assistantMessageId: z.string().min(1),
-  generationId: z.string().uuid(),
-  intent: threadChatGenerationIntentSchema,
-})
-
-export type ThreadChatGenerationIdentity = z.infer<
-  typeof threadChatGenerationIdentitySchema
->
diff --git a/lib/thread-chat/contracts/generation-intent.ts b/lib/thread-chat/contracts/generation-intent.ts
deleted file mode 100644
index 594ca514..00000000
--- a/lib/thread-chat/contracts/generation-intent.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { z } from "zod"
-
-/**
- * Thread Chat generation 命令的唯一运行时契约。
- * TypeScript 类型必须由该 schema 推导,避免客户端类型与 API 校验漂移。
- */
-export const threadChatGenerationIntentSchema = z.discriminatedUnion("kind", [
-  z.object({ kind: z.literal("persisted-turn") }),
-  z.object({
-    kind: z.literal("regenerate-assistant"),
-    sourceAssistantMessageId: z.string().min(1),
-  }),
-  z.object({ kind: z.literal("retry-orphan-user") }),
-  z.object({
-    kind: z.literal("edit-last-user"),
-    sourceUserMessageId: z.string().min(1),
-    text: z.string().trim().min(1),
-  }),
-])
-
-export type ThreadChatGenerationIntent = z.infer<
-  typeof threadChatGenerationIntentSchema
->
diff --git a/lib/thread-chat/contracts/generation-result.ts b/lib/thread-chat/contracts/generation-result.ts
deleted file mode 100644
index fd6c3eb2..00000000
--- a/lib/thread-chat/contracts/generation-result.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { z } from "zod"
-import { GENERATION_RESULT_VERSION } from "@/constants/generation"
-import {
-  researchPlanSchema,
-  researchRouteSchema,
-} from "@/lib/chat/research-contract"
-
-const artifactSchema = z.object({
-  id: z.string(),
-  title: z.string(),
-  kind: z.enum(["code", "note", "markdown"]),
-  lang: z.string().optional(),
-  content: z.string(),
-  sourceThreadId: z.string(),
-  sourceMessageId: z.string(),
-})
-
-const webResearchSourceSchema = z.object({
-  title: z.string(),
-  url: z.string(),
-})
-
-const webResearchActivitySchema = z.object({
-  toolCallId: z.string(),
-  kind: z.enum(["search", "read"]),
-  status: z.enum(["running", "complete"]),
-  query: z.string().optional(),
-  url: z.string().optional(),
-  sources: z.array(webResearchSourceSchema),
-})
-
-const generationUsageMetadataSchema = z.object({
-  inputTokens: z.number(),
-  outputTokens: z.number(),
-  totalTokens: z.number().optional(),
-})
-
-/** generation result V1 的唯一运行时与 TypeScript 契约。 */
-export const generationResultV1Schema = z.object({
-  version: z.literal(GENERATION_RESULT_VERSION),
-  generationId: z.string(),
-  text: z.string(),
-  status: z.enum(["pending", "streaming", "done", "error"]),
-  error: z.string().optional(),
-  artifactIds: z.array(z.string()),
-  artifacts: z.record(z.string(), artifactSchema),
-  webResearch: z.array(webResearchActivitySchema).optional(),
-  webResearchTextOffset: z.number().optional(),
-  researchRoute: researchRouteSchema.optional(),
-  researchPlan: researchPlanSchema.optional(),
-  usage: generationUsageMetadataSchema.optional(),
-})
-
-export type GenerationResultV1 = z.infer
diff --git a/lib/thread-chat/contracts/message-action-failure.ts b/lib/thread-chat/contracts/message-action-failure.ts
deleted file mode 100644
index 9c419f2d..00000000
--- a/lib/thread-chat/contracts/message-action-failure.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { z } from "zod"
-
-/** Thread Chat 消息命令可返回的稳定失败码。 */
-export const messageActionFailureCodeSchema = z.enum([
-  "not_found",
-  "invalid_id",
-  "invalid_request",
-  "invalid_generation_identity",
-  "invalid_thread_model",
-  "invalid_turn",
-  "not_latest_turn",
-  "generation_conflict",
-  "model_mismatch",
-  "tree_revision_conflict",
-  "revision_required",
-  "persistence_failed",
-  "unauthorized",
-  "network_error",
-])
-
-export const messageActionFailureResponseSchema = z.object({
-  error: z.object({
-    code: messageActionFailureCodeSchema,
-    message: z.string().min(1),
-  }),
-})
-
-export type MessageActionFailureCode = z.infer<
-  typeof messageActionFailureCodeSchema
->
-export type MessageActionFailureResponse = z.infer<
-  typeof messageActionFailureResponseSchema
->
diff --git a/lib/thread-chat/contracts/message-feedback.ts b/lib/thread-chat/contracts/message-feedback.ts
deleted file mode 100644
index 2576f1cf..00000000
--- a/lib/thread-chat/contracts/message-feedback.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { z } from "zod"
-
-/**
- * 消息反馈值与写入请求的唯一运行时契约。
- * TypeScript 类型必须由 schema 推导,避免领域类型与 API 校验漂移。
- */
-export const messageFeedbackSchema = z.enum(["positive", "negative"])
-
-export const setMessageFeedbackRequestSchema = z.object({
-  threadId: z.string().min(1),
-  feedback: messageFeedbackSchema.nullable(),
-})
-
-export const messageFeedbackSummarySchema = z.object({
-  treeId: z.string().min(1),
-  threadId: z.string().min(1),
-  messageId: z.string().min(1),
-  feedback: messageFeedbackSchema,
-  updatedAt: z.string().min(1),
-})
-
-export const setMessageFeedbackFailureReasonSchema = z.enum([
-  "not_found",
-  "not_completed",
-  "missing_generation",
-])
-
-export const messageFeedbackErrorCodeSchema = z.enum([
-  "unauthorized",
-  "invalid_id",
-  "invalid_feedback",
-  "not_found",
-  "message_not_completed",
-  "missing_generation_link",
-])
-
-export const setMessageFeedbackSuccessResponseSchema = z.object({
-  feedback: messageFeedbackSummarySchema.nullable(),
-})
-
-export const setMessageFeedbackErrorResponseSchema = z.object({
-  error: z.object({
-    code: messageFeedbackErrorCodeSchema,
-    message: z.string().min(1),
-  }),
-})
-
-export type MessageFeedback = z.infer
-export type SetMessageFeedbackRequest = z.infer<
-  typeof setMessageFeedbackRequestSchema
->
-export type MessageFeedbackSummary = z.infer<
-  typeof messageFeedbackSummarySchema
->
-export type SetMessageFeedbackFailureReason = z.infer<
-  typeof setMessageFeedbackFailureReasonSchema
->
-export type MessageFeedbackErrorCode = z.infer<
-  typeof messageFeedbackErrorCodeSchema
->
-export type SetMessageFeedbackSuccessResponse = z.infer<
-  typeof setMessageFeedbackSuccessResponseSchema
->
-export type SetMessageFeedbackResult =
-  | { ok: true; feedback: MessageFeedbackSummary | null }
-  | { ok: false; reason: SetMessageFeedbackFailureReason }
-
-type MessageFeedbackHttpError = {
-  status: number
-  error: {
-    code: MessageFeedbackErrorCode
-    message: string
-  }
-}
-
-/** 路由阶段与仓储失败原因到公开 HTTP 错误的唯一映射。 */
-export const MESSAGE_FEEDBACK_HTTP_ERRORS = {
-  unauthorized: {
-    status: 401,
-    error: { code: "unauthorized", message: "请先登录" },
-  },
-  invalid_id: {
-    status: 400,
-    error: { code: "invalid_id", message: "消息身份无效" },
-  },
-  invalid_feedback: {
-    status: 400,
-    error: {
-      code: "invalid_feedback",
-      message: "threadId 与 feedback 必须有效",
-    },
-  },
-  not_found: {
-    status: 404,
-    error: { code: "not_found", message: "消息不存在" },
-  },
-  not_completed: {
-    status: 409,
-    error: {
-      code: "message_not_completed",
-      message: "只有已完成的 AI 回复可以评价",
-    },
-  },
-  missing_generation: {
-    status: 409,
-    error: {
-      code: "missing_generation_link",
-      message: "已完成回复缺少生成记录",
-    },
-  },
-} as const satisfies Record<
-  | "unauthorized"
-  | "invalid_id"
-  | "invalid_feedback"
-  | SetMessageFeedbackFailureReason,
-  MessageFeedbackHttpError
->
diff --git a/lib/thread-chat/contracts/save-tree.ts b/lib/thread-chat/contracts/save-tree.ts
deleted file mode 100644
index 2afcacbf..00000000
--- a/lib/thread-chat/contracts/save-tree.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { z } from "zod"
-import { treeRevisionSchema } from "@/lib/thread-chat/contracts/tree-revision"
-
-/**
- * 整树 PUT 的命令信封。state 的完整消息图约束由领域 parser 负责;
- * 本契约只组合 HTTP 信封与 CAS revision,避免复制领域 schema。
- */
-export const saveTreeRequestSchema = z.object({
-  state: z.unknown(),
-  title: z.unknown().optional(),
-  baseRevision: treeRevisionSchema,
-})
-
-export const treeWriteRevisionErrorCodeSchema = z.enum([
-  "tree_revision_conflict",
-  "revision_required",
-])
-
-export const saveTreeErrorCodeSchema = z.enum([
-  "invalid_tree_state",
-  ...treeWriteRevisionErrorCodeSchema.options,
-])
-
-export const saveTreeSuccessResponseSchema = z.object({
-  ok: z.literal(true),
-  revision: treeRevisionSchema,
-})
-
-export const saveTreeErrorResponseSchema = z.object({
-  error: z.object({
-    code: saveTreeErrorCodeSchema,
-    message: z.string().min(1),
-    currentRevision: treeRevisionSchema.optional(),
-  }),
-})
-
-export type SaveTreeRequest = z.infer
-export type SaveTreeErrorCode = z.infer
-export type TreeWriteRevisionErrorCode = z.infer<
-  typeof treeWriteRevisionErrorCodeSchema
->
-
-export const SAVE_TREE_ERROR_STATUS = {
-  invalid_tree_state: 400,
-  tree_revision_conflict: 409,
-  revision_required: 428,
-} as const satisfies Record
-
-export const SAVE_TREE_REVISION_ERRORS = {
-  tree_revision_conflict: {
-    code: "tree_revision_conflict",
-    message: "该对话已在其他页面更新",
-  },
-  revision_required: {
-    code: "revision_required",
-    message: "消息图存盘必须携带 baseRevision",
-  },
-} as const satisfies Record<
-  TreeWriteRevisionErrorCode,
-  { code: TreeWriteRevisionErrorCode; message: string }
->
diff --git a/lib/thread-chat/contracts/switch-active-leaf.ts b/lib/thread-chat/contracts/switch-active-leaf.ts
deleted file mode 100644
index be28f697..00000000
--- a/lib/thread-chat/contracts/switch-active-leaf.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { z } from "zod"
-import { treeRevisionSchema } from "@/lib/thread-chat/contracts/tree-revision"
-
-export const switchActiveLeafRequestSchema = z.object({
-  threadId: z.string().trim().min(1),
-  assistantMessageId: z.string().trim().min(1),
-  baseRevision: treeRevisionSchema,
-})
-
-export const switchActiveLeafFailureReasonSchema = z.enum([
-  "not_found",
-  "tree_revision_conflict",
-  "invalid_turn",
-])
-
-export const switchActiveLeafErrorCodeSchema = z.enum([
-  "unauthorized",
-  "invalid_id",
-  "invalid_request",
-  ...switchActiveLeafFailureReasonSchema.options,
-])
-
-export const switchActiveLeafSuccessResponseSchema = z.object({
-  revision: treeRevisionSchema,
-  thread: z.object({
-    id: z.string().min(1),
-    activeLeafMessageId: z.string().min(1),
-  }),
-})
-
-export const switchActiveLeafErrorResponseSchema = z.object({
-  error: z.object({
-    code: switchActiveLeafErrorCodeSchema,
-    message: z.string().min(1),
-    currentRevision: treeRevisionSchema.optional(),
-  }),
-})
-
-export type SwitchActiveLeafRequest = z.infer<
-  typeof switchActiveLeafRequestSchema
->
-export type SwitchActiveLeafFailureReason = z.infer<
-  typeof switchActiveLeafFailureReasonSchema
->
-export type SwitchActiveLeafErrorCode = z.infer<
-  typeof switchActiveLeafErrorCodeSchema
->
-export type SwitchActiveLeafSuccessResponse = z.infer<
-  typeof switchActiveLeafSuccessResponseSchema
->
-
-export const SWITCH_ACTIVE_LEAF_ERROR_STATUS = {
-  unauthorized: 401,
-  invalid_id: 400,
-  invalid_request: 400,
-  not_found: 404,
-  tree_revision_conflict: 409,
-  invalid_turn: 400,
-} as const satisfies Record
-
-export const SWITCH_ACTIVE_LEAF_ROUTE_ERRORS = {
-  unauthorized: { code: "unauthorized", message: "请先登录" },
-  invalid_id: { code: "invalid_id", message: "treeId 必须是 UUID" },
-  invalid_request: { code: "invalid_request", message: "版本切换参数无效" },
-} as const satisfies Record<
-  "unauthorized" | "invalid_id" | "invalid_request",
-  { code: SwitchActiveLeafErrorCode; message: string }
->
diff --git a/lib/thread-chat/contracts/title-request.ts b/lib/thread-chat/contracts/title-request.ts
deleted file mode 100644
index 8648963e..00000000
--- a/lib/thread-chat/contracts/title-request.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/** 主线与分支共用的标题生成请求契约。 */
-export type ThreadTitleInput =
-  | {
-      kind: "main"
-      question: string
-    }
-  | {
-      kind: "branch"
-      anchorText: string
-      question: string
-      answer: string
-    }
-
-/**
- * 只接受带显式 kind 的统一标题请求;旧分支请求体不会被隐式兼容。
- */
-export function parseThreadTitleInput(input: unknown): ThreadTitleInput | null {
-  if (!input || typeof input !== "object") return null
-  const record = input as Record
-
-  if (record.kind === "main" && typeof record.question === "string") {
-    return record.question.trim()
-      ? { kind: "main", question: record.question }
-      : null
-  }
-
-  if (
-    record.kind === "branch" &&
-    typeof record.anchorText === "string" &&
-    typeof record.question === "string" &&
-    typeof record.answer === "string" &&
-    record.anchorText.trim() &&
-    record.question.trim()
-  ) {
-    return {
-      kind: "branch",
-      anchorText: record.anchorText,
-      question: record.question,
-      answer: record.answer,
-    }
-  }
-
-  return null
-}
diff --git a/lib/thread-chat/contracts/tree-revision.ts b/lib/thread-chat/contracts/tree-revision.ts
deleted file mode 100644
index 2b3cf8d3..00000000
--- a/lib/thread-chat/contracts/tree-revision.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { z } from "zod"
-
-/** 树写命令使用的非负 CAS 修订号。 */
-export const treeRevisionSchema = z.number().int().nonnegative()
-
-export type TreeRevision = z.infer
diff --git a/lib/thread-chat/domain/generation.ts b/lib/thread-chat/domain/generation.ts
deleted file mode 100644
index 1df6e95b..00000000
--- a/lib/thread-chat/domain/generation.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { Message, ThreadTreeState } from "@/lib/thread-chat/domain/types"
-import type {
-  GENERATION_BILLING_STATUSES,
-  GENERATION_STATUSES,
-} from "@/constants/generation"
-import { ACTIVE_GENERATION_STATUSES } from "@/constants/generation"
-import type { GenerationResultV1 } from "@/lib/thread-chat/contracts/generation-result"
-import type { ThreadChatGenerationIntent } from "@/lib/thread-chat/contracts/generation-intent"
-
-export type GenerationStatus = (typeof GENERATION_STATUSES)[number]
-export type GenerationBillingStatus =
-  (typeof GENERATION_BILLING_STATUSES)[number]
-
-export type RecoverableTurnReason =
-  "missing_assistant" | "missing_generation" | "interrupted_generation"
-
-export interface RecoverableTurn {
-  threadId: string
-  userMessageId: string
-  assistantMessageId?: string
-  reason: RecoverableTurnReason
-}
-
-export type { ThreadChatGenerationIntent } from "@/lib/thread-chat/contracts/generation-intent"
-
-export type GenerationTurnIdentity = {
-  treeId: string
-  threadId: string
-  userMessageId: string
-  assistantMessageId: string
-  generationId: string
-}
-
-/** 服务端从严格 schema-v2 树中验证后的最小 turn 快照,用于消息被并发快照删掉时读修复。 */
-export type GenerationTurnSnapshot = {
-  intent: ThreadChatGenerationIntent
-  threadId: string
-  assistantMessageIndex: number
-  userMessage: Message
-  assistantMessage: Message
-  userParentMessageId: string | null
-  assistantParentMessageId: string
-  activatesAssistantMessageId: string
-}
-
-export type { GenerationResultV1 } from "@/lib/thread-chat/contracts/generation-result"
-
-export type GenerationUsageMetadata = NonNullable<
-  import("@/lib/thread-chat/contracts/generation-result").GenerationResultV1["usage"]
->
-
-export type GenerationSummary = {
-  id: string
-  treeId: string
-  threadId: string
-  userMessageId: string
-  assistantMessageId: string
-  attempt: number
-  isCurrent: boolean
-  status: GenerationStatus
-  updatedAt: string
-  result?: GenerationResultV1 | null
-}
-
-export interface GenerationForReconcile extends GenerationSummary {
-  turnSnapshot: GenerationTurnSnapshot
-}
-
-export interface ReconciledThreadChatTree {
-  state: ThreadTreeState
-  recoverableTurns: RecoverableTurn[]
-}
-
-export function isActiveGenerationStatus(
-  status: GenerationStatus
-): status is "running" | "stop_requested" {
-  return ACTIVE_GENERATION_STATUSES.includes(
-    status as (typeof ACTIVE_GENERATION_STATUSES)[number]
-  )
-}
diff --git a/lib/thread-chat/domain/regeneration.ts b/lib/thread-chat/domain/regeneration.ts
deleted file mode 100644
index 1d4facf4..00000000
--- a/lib/thread-chat/domain/regeneration.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { activeLeafTurn } from "./message-graph"
-import type { Message, ThreadTreeState } from "./types"
-import type { ThreadChatGenerationIntent } from "./generation"
-
-export interface PrepareRegenerationInput {
-  threadId: string
-  userMessageId: string
-  assistantMessageId: string
-  generationId: string
-  intent: Exclude
-}
-
-export interface PreparedTurnPatch {
-  threadId: string
-  addedMessages: readonly Message[]
-  nextActiveLeafMessageId: string
-  supersededGenerationId?: string
-}
-
-function messageIdExists(state: ThreadTreeState, messageId: string): boolean {
-  return Object.values(state.threads).some((thread) =>
-    thread.messages.some((message) => message.id === messageId)
-  )
-}
-
-function pendingAssistant(input: {
-  id: string
-  parentMessageId: string
-  generationId: string
-}): Message {
-  return {
-    id: input.id,
-    parentMessageId: input.parentMessageId,
-    role: "assistant",
-    text: "",
-    forks: [],
-    generationId: input.generationId,
-    status: "pending",
-  }
-}
-
-/**
- * 为最新一轮构造只追加的变体 patch。返回 null 表示来源不是最新轮、
- * ID 冲突或结构不合法;函数不修改 state/source node/Artifact。
- */
-export function prepareRegenerationPatch(
-  state: ThreadTreeState,
-  input: PrepareRegenerationInput
-): PreparedTurnPatch | null {
-  const thread = state.threads[input.threadId]
-  if (!thread) return null
-  const latest = activeLeafTurn(thread)
-  if (!latest) return null
-
-  if (
-    messageIdExists(state, input.assistantMessageId) ||
-    (input.intent.kind === "edit-last-user" &&
-      messageIdExists(state, input.userMessageId))
-  )
-    return null
-
-  if (input.intent.kind === "regenerate-assistant") {
-    if (
-      latest.assistantMessage?.id !== input.intent.sourceAssistantMessageId ||
-      latest.userMessage.id !== input.userMessageId
-    )
-      return null
-    return {
-      threadId: input.threadId,
-      addedMessages: [
-        pendingAssistant({
-          id: input.assistantMessageId,
-          parentMessageId: latest.userMessage.id,
-          generationId: input.generationId,
-        }),
-      ],
-      nextActiveLeafMessageId: input.assistantMessageId,
-      ...(latest.assistantMessage.generationId &&
-      (latest.assistantMessage.status === "pending" ||
-        latest.assistantMessage.status === "streaming")
-        ? { supersededGenerationId: latest.assistantMessage.generationId }
-        : {}),
-    }
-  }
-
-  if (input.intent.kind === "retry-orphan-user") {
-    if (latest.userMessage.id !== input.userMessageId) return null
-    if (
-      latest.assistantMessage &&
-      (latest.assistantMessage.text.trim() !== "" ||
-        (latest.assistantMessage.artifactIds?.length ?? 0) > 0 ||
-        latest.assistantMessage.status !== "error")
-    )
-      return null
-    return {
-      threadId: input.threadId,
-      addedMessages: [
-        pendingAssistant({
-          id: input.assistantMessageId,
-          parentMessageId: latest.userMessage.id,
-          generationId: input.generationId,
-        }),
-      ],
-      nextActiveLeafMessageId: input.assistantMessageId,
-    }
-  }
-
-  if (
-    latest.userMessage.id !== input.intent.sourceUserMessageId ||
-    input.intent.text.trim() === ""
-  )
-    return null
-  const editedUser: Message = {
-    id: input.userMessageId,
-    parentMessageId: latest.userMessage.parentMessageId,
-    role: "user",
-    text: input.intent.text.trim(),
-    forks: [],
-    ...(latest.userMessage.quote
-      ? { quote: structuredClone(latest.userMessage.quote) }
-      : {}),
-  }
-  return {
-    threadId: input.threadId,
-    addedMessages: [
-      editedUser,
-      pendingAssistant({
-        id: input.assistantMessageId,
-        parentMessageId: editedUser.id,
-        generationId: input.generationId,
-      }),
-    ],
-    nextActiveLeafMessageId: input.assistantMessageId,
-    ...(latest.assistantMessage?.generationId &&
-    (latest.assistantMessage.status === "pending" ||
-      latest.assistantMessage.status === "streaming")
-      ? { supersededGenerationId: latest.assistantMessage.generationId }
-      : {}),
-  }
-}
diff --git a/lib/thread-chat/domain/types.ts b/lib/thread-chat/domain/types.ts
index 51b6baef..f028e6e3 100644
--- a/lib/thread-chat/domain/types.ts
+++ b/lib/thread-chat/domain/types.ts
@@ -10,10 +10,13 @@ import type { TextAnchor } from "@/lib/thread-chat/domain/text-anchor"
 import type { WebResearchActivity } from "@/lib/chat/web-research-activity"
 import type { ResearchPlan, ResearchRoute } from "@/lib/chat/research-router"
 import type { THREAD_TREE_SCHEMA_VERSION } from "@/constants/thread-chat"
-export type {
-  MessageFeedback,
-  MessageFeedbackSummary,
-} from "@/lib/thread-chat/contracts/message-feedback"
+export type MessageFeedback = "positive" | "negative"
+
+export interface MessageFeedbackSummary {
+  messageId: string
+  feedback: MessageFeedback
+  updatedAt: string
+}
 
 export type Role = "user" | "assistant"
 export type ArtifactKind = "code" | "note" | "markdown"
diff --git a/openspec/changes/define-thread-chat-domain-model/.openspec.yaml b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/.openspec.yaml
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/.openspec.yaml
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/.openspec.yaml
diff --git a/openspec/changes/define-thread-chat-domain-model/backend-verification.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/backend-verification.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/backend-verification.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/backend-verification.md
diff --git a/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/cleanup-evidence.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/cleanup-evidence.md
new file mode 100644
index 00000000..67d3d466
--- /dev/null
+++ b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/cleanup-evidence.md
@@ -0,0 +1,7 @@
+# Domain cleanup evidence
+
+- 旧 `branch_trees`、`branch_generations`、conversation/workspace Schema 和整树写入路径已删除;源码检索未发现旧权威入口。
+- 本地 `thread-chat` 与隔离 `thread-chat-test` 均从空 `thread_chat` schema 执行 `db:push`;当前领域表仅为 `projects`、`threads`、`messages`、`message_runs`、`artifacts`、`message_feedback`。
+- 领域、Repository、Application、Fake AI Runtime、API 与客户端自动测试共 87 项通过。
+- `pnpm typecheck`、lint、production build 与 OpenSpec strict validation 通过。
+- Ego Browser 已验证服务端 Project ID 跳转、SSE 完成、Artifact Summary 与 Artifact-by-ID Drawer;完整交互与 UI parity 证据见 Client/API change 的 `e2e-evidence.md`。
diff --git a/openspec/changes/define-thread-chat-domain-model/design.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/design.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design.md
diff --git a/openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/enter-thread-chat-from-home.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/design/enter-thread-chat-from-home.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/enter-thread-chat-from-home.md
diff --git a/openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/load-thread-messages-by-sequence.md
diff --git a/openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/regenerate-replacement-assistant-message.md
diff --git a/openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/resume-running-message-after-refresh.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/design/resume-running-message-after-refresh.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/design/resume-running-message-after-refresh.md
diff --git a/openspec/changes/define-thread-chat-domain-model/proposal.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/proposal.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/proposal.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/proposal.md
diff --git a/openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/specs/domain/spec.md
similarity index 100%
rename from openspec/changes/define-thread-chat-domain-model/specs/domain/spec.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/specs/domain/spec.md
diff --git a/openspec/changes/define-thread-chat-domain-model/tasks.md b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/tasks.md
similarity index 93%
rename from openspec/changes/define-thread-chat-domain-model/tasks.md
rename to openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/tasks.md
index 4c343f6b..ac4c5165 100644
--- a/openspec/changes/define-thread-chat-domain-model/tasks.md
+++ b/openspec/changes/archive/2026-08-25-define-thread-chat-domain-model/tasks.md
@@ -66,15 +66,15 @@
 
 ## 7. E2E 通过后的旧后端退役
 
-- [ ] 7.1 等待 `design-thread-chat-client-api` 完成 API、前端接入与 Ego Browser E2E;在此前保留旧实现代码,但不建立新旧数据双写。
-- [ ] 7.2 E2E 通过后删除旧 `branch_trees.state`、`branch_generations`、消息图 active leaf 与旧整体保存的后端权威职责。
-- [ ] 7.3 从 Drizzle Schema 删除不再使用的旧表/列,并对本地与隔离测试数据库执行 `pnpm db:push`;无需迁移或保留旧数据。
-- [ ] 7.4 再次执行后端、API、前端集成、build、E2E 与 OpenSpec 严格校验,确认代码中不存在旧写入路径。
-- [ ] 7.5 对照 domain spec 的 Requirement/Scenario 记录自动测试或手动验收证据。
-- [ ] 7.6 在全部门槛满足后先归档 `define-thread-chat-domain-model`,将增量 domain spec 合入正式 `openspec/specs/domain/spec.md`。
+- [x] 7.1 等待 `design-thread-chat-client-api` 完成 API、前端接入与 Ego Browser E2E;在此前保留旧实现代码,但不建立新旧数据双写。
+- [x] 7.2 E2E 通过后删除旧 `branch_trees.state`、`branch_generations`、消息图 active leaf 与旧整体保存的后端权威职责。
+- [x] 7.3 从 Drizzle Schema 删除不再使用的旧表/列,并对本地与隔离测试数据库执行 `pnpm db:push`;无需迁移或保留旧数据。
+- [x] 7.4 再次执行后端、API、前端集成、build、E2E 与 OpenSpec 严格校验,确认代码中不存在旧写入路径。
+- [x] 7.5 对照 domain spec 的 Requirement/Scenario 记录自动测试或手动验收证据。
+- [x] 7.6 在全部门槛满足后先归档 `define-thread-chat-domain-model`,将增量 domain spec 合入正式 `openspec/specs/domain/spec.md`。
 
 ## 8. 归档后提炼永久领域架构
 
-- [ ] 8.1 从已归档 design 提炼稳定目标架构到 `openspec/specs/domain/architecture.md`,只保留实体、关系、ER 图、不变量、最终 DB Schema 与模块边界。
-- [ ] 8.2 在正式 `openspec/specs/domain/spec.md` 中链接 `architecture.md`,明确冲突时可验证 Requirement 优先。
-- [ ] 8.3 校验永久架构文档与正式 spec、实际 Drizzle Schema 和模块结构一致,并把后续同步更新列为验收项。
+- [x] 8.1 从已归档 design 提炼稳定目标架构到 `openspec/specs/domain/architecture.md`,只保留实体、关系、ER 图、不变量、最终 DB Schema 与模块边界。
+- [x] 8.2 在正式 `openspec/specs/domain/spec.md` 中链接 `architecture.md`,明确冲突时可验证 Requirement 优先。
+- [x] 8.3 校验永久架构文档与正式 spec、实际 Drizzle Schema 和模块结构一致,并把后续同步更新列为验收项。
diff --git a/openspec/changes/design-thread-chat-client-api/.openspec.yaml b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/.openspec.yaml
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/.openspec.yaml
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/.openspec.yaml
diff --git a/openspec/changes/design-thread-chat-client-api/backend-api-verification.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/backend-api-verification.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/backend-api-verification.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/backend-api-verification.md
diff --git a/openspec/changes/design-thread-chat-client-api/design.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/design.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design.md
diff --git a/openspec/changes/design-thread-chat-client-api/design/api-contracts.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/api-contracts.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/design/api-contracts.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/api-contracts.md
diff --git a/openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/new-project-first-message-lifecycle.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/design/new-project-first-message-lifecycle.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/new-project-first-message-lifecycle.md
diff --git a/openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/open-existing-project-lifecycle.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/design/open-existing-project-lifecycle.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/open-existing-project-lifecycle.md
diff --git a/openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/thread-message-loading.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/design/thread-message-loading.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/design/thread-message-loading.md
diff --git a/openspec/changes/design-thread-chat-client-api/e2e-evidence.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/e2e-evidence.md
similarity index 89%
rename from openspec/changes/design-thread-chat-client-api/e2e-evidence.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/e2e-evidence.md
index be49952f..26e09a8b 100644
--- a/openspec/changes/design-thread-chat-client-api/e2e-evidence.md
+++ b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/e2e-evidence.md
@@ -3,6 +3,12 @@
 日期:2026-08-25
 环境:`thread-chat-test` PostgreSQL、Next.js `http://localhost:4040`、Ego Browser task space `11`、`1674 × 963` viewport。
 
+## 最终清理复验
+
+- Domain change 归档后重新执行 87/87 自动测试、`pnpm typecheck` 与默认 Turbopack `pnpm build`,全部通过。
+- Ego Browser 在隔离 `thread-chat-test` + Fake AI Runtime 上再次验证 `/new` 创建、服务端 `projectId` URL、SSE 完成与硬刷新恢复;刷新前后 prompt/reply 均存在,URL 不含旧 tree 路径。
+- 清理前的完整交互矩阵与 UI parity 证据保留如下;清理只替换权威数据路径并删除退役代码,未改变既有 UI 输出。
+
 ## 确定性 AI Runtime
 
 - 本地 E2E 没有调用真实模型。服务端仅在 `NODE_ENV !== production` 且
diff --git a/openspec/changes/design-thread-chat-client-api/frontend-state-verification.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/frontend-state-verification.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/frontend-state-verification.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/frontend-state-verification.md
diff --git a/openspec/changes/design-thread-chat-client-api/proposal.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/proposal.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/proposal.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/proposal.md
diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/specs/thread-chat-client-state/spec.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/specs/thread-chat-client-state/spec.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/specs/thread-chat-client-state/spec.md
diff --git a/openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/specs/thread-chat-command-api/spec.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/specs/thread-chat-command-api/spec.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/specs/thread-chat-command-api/spec.md
diff --git a/openspec/changes/design-thread-chat-client-api/tasks.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/tasks.md
similarity index 95%
rename from openspec/changes/design-thread-chat-client-api/tasks.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/tasks.md
index 60f6a8cf..e211b14f 100644
--- a/openspec/changes/design-thread-chat-client-api/tasks.md
+++ b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/tasks.md
@@ -100,14 +100,14 @@
 
 ## 10. 旧客户端退役与归档
 
-- [ ] 10.1 E2E 通过后将 `/thread-chat/new`、`/thread-chat/{projectId}` 和 Project 列表切到新权威路径;开发期间不维护 feature flag 或双写。
-- [ ] 10.2 删除客户端 treeId、新实体 UUID、整树 version 订阅、旧 ThreadTree 写回和 monolithic chat-controller 权威职责;保留仍被新组合根复用的纯 UI 部件。
-- [ ] 10.3 通知 domain change 执行旧后端/Schema 退役,并等待其完成全量测试和先行归档。
-- [ ] 10.4 domain change 归档后,执行 `pnpm typecheck`、全部自动测试、`pnpm build`、Ego Browser E2E 和 OpenSpec 严格校验。
-- [ ] 10.5 对照两个增量 spec 的 Requirement/Scenario 汇总实现证据,在全部门槛满足后归档 `design-thread-chat-client-api`。
+- [x] 10.1 E2E 通过后将 `/thread-chat/new`、`/thread-chat/{projectId}` 和 Project 列表切到新权威路径;开发期间不维护 feature flag 或双写。
+- [x] 10.2 删除客户端 treeId、新实体 UUID、整树 version 订阅、旧 ThreadTree 写回和 monolithic chat-controller 权威职责;保留仍被新组合根复用的纯 UI 部件。
+- [x] 10.3 通知 domain change 执行旧后端/Schema 退役,并等待其完成全量测试和先行归档。
+- [x] 10.4 domain change 归档后,执行 `pnpm typecheck`、全部自动测试、`pnpm build`、Ego Browser E2E 和 OpenSpec 严格校验。
+- [x] 10.5 对照两个增量 spec 的 Requirement/Scenario 汇总实现证据,在全部门槛满足后归档 `design-thread-chat-client-api`。
 
 ## 11. 归档后沉淀稳定客户端与 API 文档
 
-- [ ] 11.1 从已归档 design 提炼客户端架构到 `openspec/specs/thread-chat-client-state/architecture.md`,保留 Store、Runtime、Provider、Command、Hook 与异步加载边界。
-- [ ] 11.2 将落地后的 API 合同提炼到 `openspec/specs/thread-chat-command-api/api-contracts.md`,并校验它与共享 Schema 和实际 Route Handler 一致。
-- [ ] 11.3 在两个正式 `spec.md` 中链接对应说明文档,明确冲突时可验证 Requirement 优先,并把后续同步更新列为验收项。
+- [x] 11.1 从已归档 design 提炼客户端架构到 `openspec/specs/thread-chat-client-state/architecture.md`,保留 Store、Runtime、Provider、Command、Hook 与异步加载边界。
+- [x] 11.2 将落地后的 API 合同提炼到 `openspec/specs/thread-chat-command-api/api-contracts.md`,并校验它与共享 Schema 和实际 Route Handler 一致。
+- [x] 11.3 在两个正式 `spec.md` 中链接对应说明文档,明确冲突时可验证 Requirement 优先,并把后续同步更新列为验收项。
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/README.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/README.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/README.md
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/artifact-drawer.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/breadcrumb-back.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/empty-project.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/empty-project.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/empty-project.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/empty-project.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/fork-composer.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/fork-composer.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/fork-composer.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/fork-composer.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/header-child-selector.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/header-child-selector.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/header-child-selector.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/header-child-selector.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/single-column.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/single-column.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/single-column.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/single-column.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/switched-slot.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/switched-slot.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/switched-slot.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/switched-slot.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/thread-switcher.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/three-columns-nested-fork.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/three-columns-nested-fork.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/three-columns-nested-fork.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/three-columns-nested-fork.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/two-columns.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/two-columns.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-baseline/two-columns.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-baseline/two-columns.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/README.md
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-implementation/README.md
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/README.md
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/artifact-drawer-normalized.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/empty-new-normalized.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/empty-new-normalized.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-implementation/empty-new-normalized.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/empty-new-normalized.png
diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-implementation/three-columns-normalized.png b/openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/three-columns-normalized.png
similarity index 100%
rename from openspec/changes/design-thread-chat-client-api/ui-parity-implementation/three-columns-normalized.png
rename to openspec/changes/archive/2026-08-25-design-thread-chat-client-api/ui-parity-implementation/three-columns-normalized.png
diff --git a/openspec/specs/domain/architecture.md b/openspec/specs/domain/architecture.md
new file mode 100644
index 00000000..6b1d32b5
--- /dev/null
+++ b/openspec/specs/domain/architecture.md
@@ -0,0 +1,62 @@
+# Thread Chat 领域架构
+
+本文记录已经落地的稳定目标架构。规范性行为以同目录 [spec.md](./spec.md) 的 Requirement/Scenario 为准;若二者冲突,以可验证规范为准。
+
+## 聚合与关系
+
+```mermaid
+erDiagram
+    USER ||--o{ PROJECT : owns
+    PROJECT ||--|{ THREAD : contains
+    THREAD o|--o{ THREAD : parent_of
+    THREAD ||--o{ MESSAGE : contains
+    MESSAGE o|--o| MESSAGE : replaces
+    MESSAGE ||--o| MESSAGE_RUN : executes
+    PROJECT ||--o{ ARTIFACT : owns
+    MESSAGE ||--o{ ARTIFACT : produces
+    MESSAGE ||--o| MESSAGE_FEEDBACK : receives
+```
+
+- `Project` 是 owner scope、共享资源范围、Thread 族群和永久删除边界;每个 Project 恰有一个 Root Thread。
+- `Thread` 是统一实体。`parentThreadId = null` 表示 Root;非空表示 Branch。Branch 同时冻结 `sourceMessageId`、`forkSourceSnapshot` 和 `baseContext`。
+- `Message` 属于一个 Thread,以服务端分配的唯一单调 `sequence` 排序。finalized 内容不可覆盖;Edit/Regenerate 追加 replacement 并 supersede 旧消息。
+- `BaseContextV1` 只保存有序 `messageIds`。Fork 时由服务端计算并冻结,不复制 parts 或 Artifact 正文。
+- 每条 assistant Message 恰有一条 `MessageRun`,user Message 没有 Run。状态为 `queued → running → completed | failed | stopped`,也允许 queued 直接进入 failed/stopped。
+- `Artifact` 归属于 Project,并以 `sourceMessageId` 保留 provenance;跨 Project 默认隔离。
+- `MessageFeedback` 仅属于合格的 assistant Message,值为 positive/negative,清除反馈会删除该记录。
+
+## 核心不变量
+
+1. Project、Parent Thread、source Message、Child Thread、Artifact 和反馈的 owner scope 必须一致;跨表关系在同一事务内锁定并校验。
+2. Project 只能有一个 Root;Thread 拓扑无环;Root 不含 Fork facts,Branch 必须完整包含全部 Fork facts。
+3. 相同 Thread 的 `sequence` 唯一且大于零;客户端时间和角色交替都不是排序事实。
+4. finalized Message 的 role、parts、sequence 与来源关系不可变;单条 Message 不 hard delete。
+5. replacement 必须同 Thread、同 role,旧 Message 最多有一个直接 replacement;默认时间线排除 superseded Message。
+6. queued/running/failed/stopped assistant 不具备 Fork Prompt 资格;既有 BaseContext 对后来 superseded 的 Message ID 仍保持有效。
+7. Run 的 checkpoint、`eventSequence`、heartbeat、stop request 和 terminal 时间均持久化;SSE 断开不等于 Stop。
+8. Project 删除通过外键级联清理 Threads、Messages、Runs、Artifacts 与 Feedback;不迁移旧表、不双写。
+
+## 最终 PostgreSQL Schema
+
+| 表 | 关键字段与约束 |
+|---|---|
+| `projects` | `owner_user_id`;标题/Target/Instruction;`artifact_change_sequence >= 0`;owner + update 游标索引 |
+| `threads` | `project_id`;自引用 parent;source、snapshot、baseContext 完整性 CHECK;每 Project 单一 Root 部分唯一索引 |
+| `messages` | `thread_id + sequence` 唯一;role/positive sequence CHECK;replacement 唯一;有效时间线索引 |
+| `message_runs` | `assistant_message_id` 唯一;状态 CHECK;checkpoint/eventSequence/heartbeat/stop/terminal;queued scanner 索引 |
+| `artifacts` | `project_id`、`source_message_id`、kind/title/content;Project 与 source 索引 |
+| `message_feedback` | `message_id` 唯一;`owner_user_id`;positive/negative CHECK |
+
+数据库 CHECK/外键负责单行、存在性、唯一性和级联;同 Project、角色资格、无环、finalized 不可变等跨行规则由 Repository/Application 在事务内负责。空库 `db:push` 是 Schema 验收项。
+
+## 模块边界
+
+```text
+lib/thread-chat/domain/                 纯实体、不变量、状态机、Prompt/BaseContext 规则
+lib/thread-chat/application/            Commands、Queries、事务编排、MessageRun runner
+lib/thread-chat/application/ports/      AI Runtime 等可替换端口
+lib/thread-chat/infrastructure/         PostgreSQL repositories、真实/Fake AI adapter
+lib/thread-chat/api/                    共享契约与服务端 transport(不重定义领域关系)
+```
+
+后续修改任何实体关系、Schema 或模块职责时,必须同时更新并验证正式 spec、本文、Drizzle Schema、Repository/Application 测试与 OpenSpec strict validation。
diff --git a/openspec/specs/domain/spec.md b/openspec/specs/domain/spec.md
index 42d3e2f1..9cbe6c3a 100644
--- a/openspec/specs/domain/spec.md
+++ b/openspec/specs/domain/spec.md
@@ -2,48 +2,215 @@
 
 ## Purpose
 
-定义 Thread Chat 的统一领域语言:树、线程、分叉、消息、生成、产物与标题的边界,使产品、设计和实现使用同一组术语沟通。
+定义 Thread Chat 的 Project、Thread、Message、MessageRun、BaseContext 与 Artifact 领域边界及可验证不变量。
+
+稳定架构说明见 [architecture.md](./architecture.md)。若说明文档与本规范冲突,以本规范中的可验证 Requirement 与 Scenario 为准;任何实现变更都 MUST 同步校验规范、架构文档、Drizzle Schema 与模块边界。
 
 ## Requirements
 
 ### Requirement: 使用统一的核心术语
-
-系统及项目文档 SHALL 使用以下术语:
-
-- **Thread Tree**:一个独立的树形工作区,拥有唯一的根线程与其全部后代。
-- **Thread**:Thread Tree 中的一个对话节点,也是界面中的一栏;它拥有自己的消息序列、模型选择和标题。
-- **MainThread**:Thread Tree 中唯一的根 Thread。
-- **ForkedThread**:由一次 Fork 创建的非根 Thread;它可以继续产生后代 Thread。
-- **Fork**:从某条消息的选区创建 ForkedThread 的关系与动作,不是 Thread 的同义词。
-- **Message**:属于一个 Thread 的用户或助手消息节点。
-- **Generation**:生成一条助手 Message 的一次模型执行尝试。
-- **Artifact**:由某条 Message 产生并持久化的独立内容。
-- **Title**:用于识别 Thread 或 Thread Tree 的人类可读标签。
+系统 MUST 使用以下术语表达目标模型:
+
+- **Project**:一整项可持续工作的内容聚合边界,直接归属于当前用户,拥有一个 Root Thread、全部后代 Thread,以及这些 Thread 共享的 Memory、Instruction、Target、Files 和 Artifacts。当前 UI 的一个“对话列表项”对应一个 Project。
+- **Thread**:Project 中一列可独立继续的线性对话。Root Thread 与 Branch Thread 只是关系角色,不是不同实体类型。
+- **Fork**:从已有 Thread 的确定 Message 创建 Child Thread 的原子操作。
+- **Message**:Thread 中具有稳定 ID、角色、内容和服务端 sequence 的消息实体。
+- **MessageRun**:仅为 assistant Message 持久化的后台生成记录;它属于运行基础设施,不是可独立导航的聊天内容实体。
+- **BaseContext**:Fork 时由服务端冻结的有序 Message ID 列表,表示 Child Thread 继承的有效 Prompt 历史。
+- **ForkSourceSnapshot**:Fork 时冻结的来源定位与引用展示信息。
+- **Project Resource**:在同一 Project 的全部 Thread 间共享、但不跨 Project 自动共享的 Memory、Instruction、Target、File 或 Artifact。
+- **Target**:Project 希望实现的目标集合,用于表达该 Project 的终极目标,以及为实现终极目标而设定的短期目标和中期目标。Target 属于 Project 级共享信息,不表示某个 Thread 的临时任务,也不等同于单条用户消息中的请求。
+- **Artifact**:归属于 Project,并保留来源 Message 身份的持久化产物。
+- **Title**:Project 或 Branch Thread 的短标题。
+
+目标实现 MUST 由 Project 接替当前 Thread Tree 的聚合职责,并将 Thread、Message 和 Artifact 保存为可独立寻址的实体。目标规范、数据库、API 和客户端实体模型 MUST 使用 Project 表达整簇 Thread 的聚合边界,且 MUST NOT 继续把 Thread Tree、MainThread、ForkedThread、独立 ThreadFork、Turn、Generation 或 Message Variant 作为目标领域实体。
+
+#### Scenario: 将一簇分叉对话称为 Project
+- **WHEN** 用户查看由一个 Root Thread 和多列 Branch Thread 组成的工作项
+- **THEN** 系统 MUST 将整体领域实体称为 Project,并将每一列称为 Thread
+
+#### Scenario: 从关系推导 Thread 角色
+- **WHEN** 一个 Thread 没有 Parent Thread
+- **THEN** 系统 MUST 将其视为 Root Thread
+- **AND** 当一个 Thread 具有 Parent Thread 时,系统 MUST 将其视为 Branch Thread
 
 #### Scenario: 描述非根线程
-
-- **WHEN** 产品或代码需要描述由选区创建的对话节点
-- **THEN** 使用 ForkedThread 描述该节点,并使用 Fork 描述其创建关系
-
-### Requirement: 维护线程树的层级不变量
-
-每个 Thread Tree SHALL 恰有一个 MainThread。每个 ForkedThread SHALL 有一个父 Thread 和一个来源 Fork;任意 ForkedThread 都可以作为新的 Fork 的来源。Thread 是统一节点类型,MainThread 与 ForkedThread 是其不同领域角色,而非两套不相容的会话模型。角色由树的根、父子关系和 Fork 来源定义;本规范不规定其在持久化状态中的具体字段或标识符表示。
+- **WHEN** 产品或代码需要描述由 Fork 创建的非根对话列
+- **THEN** 系统 MUST 将该节点称为 Child Thread 或按相对角色称为 Branch Thread
+- **AND** 必须使用 Fork 描述创建动作,不得把 ForkedThread 作为独立实体类型
+
+### Requirement: 维护 Project 的 Thread 拓扑不变量
+系统 MUST 使用 Project 与 Thread 关系维护分叉拓扑:
+
+- 每个 Project MUST 归属于且仅归属于一个用户;其他访问关系不属于本 change。
+- 每个 Project MUST 有且仅有一个没有 Parent Thread 的 Root Thread。
+- 每个 Thread MUST 归属于且仅归属于一个 Project。
+- 每个非 Root Thread MUST 具有同一 Project 内的 Parent Thread、确定的来源 Message、ForkSourceSnapshot 和 BaseContext。
+- Child Thread MUST 可以继续产生自己的 Child Thread,形成任意深度的有向无环层级。
+- 系统 MUST 阻止跨 Project 的 Parent Thread、来源 Message 或 Fork 关系,并阻止形成环。
+- Fork MUST 由服务端原子创建 Child Thread 及全部来源事实;客户端不得构造 Child Thread 新 ID 或 BaseContext。
+
+#### Scenario: 创建 Project 根 Thread
+- **WHEN** 系统创建一个新 Project
+- **THEN** 系统 MUST 在同一事务建立该 Project 唯一的 Root Thread
+- **AND** Root Thread MUST 不包含 Parent Thread 或 Fork 来源
 
 #### Scenario: 创建嵌套分叉
+- **WHEN** 用户从 Branch Thread 的一条合格 Message 再次 Fork
+- **THEN** 系统 MUST 在同一 Project 中创建新的 Child Thread
+- **AND** 新 Thread MUST 指向该 Branch Thread 与确定来源 Message
 
-- **WHEN** 用户从一个 ForkedThread 中的消息创建新的 Fork
-- **THEN** 系统创建新的 ForkedThread,并将该消息所在 Thread 记录为其父 Thread
+#### Scenario: 拒绝跨 Project Fork
+- **WHEN** Fork 来源 Thread 或来源 Message 不属于目标 Project
+- **THEN** 系统 MUST 拒绝请求且不创建任何部分数据
 
 #### Scenario: 拒绝 Fork 与拓扑矛盾的状态
+- **WHEN** 非 Root Thread 缺少 Parent Thread、来源 Message、ForkSourceSnapshot 或 BaseContext,或者关系会形成环
+- **THEN** 系统 MUST 拒绝该 Project 状态
 
-- **WHEN** 保存的 ForkedThread 缺少父 Thread 或来源 Fork
-- **THEN** 系统拒绝该 Thread Tree 状态
+#### Scenario: Fork 原子失败
+- **WHEN** ForkSourceSnapshot、BaseContext 或 Child Thread 中任一项无法持久化
+- **THEN** 系统 MUST 回滚整个 Fork 操作
 
 ### Requirement: 明确标题的归属与优先级
+系统 MUST 区分 Project 标题与 Branch Thread 标题:
+
+1. Project 标题描述整项工作,同时作为 Project 列表项和 Root Thread 列头的展示标题。
+2. Branch Thread 标题描述局部主题,仅覆盖对应列的标题展示。
+3. 用户账户展示名称不得替代 Project 或 Thread 标题。
 
-Title SHALL 描述其目标 Thread。MainThread 的自动 Title 同时作为 Thread Tree 的默认导航标题;ForkedThread 的 Title 描述对应列。用户为 Thread Tree 设置的自定义 Title SHALL 在树级导航和 MainThread 列头展示中优先于自动 Title。
+#### Scenario: 展示 Root Thread 标题
+- **WHEN** 客户端展示 Project 的 Root Thread
+- **THEN** 客户端 MUST 使用 Project 标题作为该列标题
 
 #### Scenario: 主线自动标题与用户重命名并存
+- **WHEN** Project 已生成自动标题,且用户随后设置自定义 Project 标题
+- **THEN** Project 列表和 Root Thread 列头 MUST 展示自定义标题
+- **AND** 系统 MUST 保留自动标题作为机器派生信息
+
+#### Scenario: 展示 Branch Thread 标题
+- **WHEN** 客户端展示具有局部标题的 Branch Thread
+- **THEN** 客户端 MUST 使用该 Thread 标题
+- **AND** 不得因此修改 Project 标题
+
+### Requirement: 使用 Project 作为对话列表与共享资源边界
+“新对话”操作 MUST 创建新的 Project 与唯一 Root Thread;“对话列表” MUST 列出当前用户拥有或可访问的 Project。
+
+Project 的 Memory、Instruction、Target、Files 和 Artifacts MUST 可供该 Project 的全部 Thread 使用,并 MUST NOT 仅因多个 Project 属于同一用户而自动跨 Project 共享。
+
+#### Scenario: 创建新对话
+- **WHEN** 用户点击“新对话”
+- **THEN** 系统 MUST 创建新的 Project 与唯一 Root Thread
+- **AND** 导航到以服务端 Project ID 标识的 ThreadChat 页面
+
+#### Scenario: 展示对话列表
+- **WHEN** 用户打开“对话列表”
+- **THEN** 系统 MUST 展示当前用户拥有或可访问的 Projects
+
+#### Scenario: Thread 使用 Project 资源
+- **WHEN** Project 中任一 Thread 构建允许使用 Project 上下文的请求
+- **THEN** 系统 MUST 允许它引用该 Project 的 Memory、Instruction、Target、Files 和 Artifacts
+
+#### Scenario: Project 资源隔离
+- **WHEN** 另一个 Project 的 Thread 未获得显式授权
+- **THEN** 系统 MUST NOT 自动向它提供当前 Project 的共享资源
+
+### Requirement: 维护 Thread 的线性消息顺序
+系统 MUST 将每个 Thread 内的 Message 保存为严格线性的追加序列。服务端 MUST 为新 Message 分配在该 Thread 内单调递增且唯一的 `sequence`;客户端 MUST 使用 `sequence` 而不是客户端时间、角色交替或前后消息指针恢复稳定顺序。
+
+系统 MUST 允许连续多条 user Message,且 MUST NOT 将 `user → assistant` 一一配对作为数据库不变量。“只允许编辑当前最后一条有效 user Message”是 MVP 应用策略,不代表 user Message 必须拥有配对的 assistant Message。
+
+#### Scenario: 连续发送多条 user Message
+- **WHEN** 用户在 assistant 尚未产生最终回复前又发送一条 user Message
+- **THEN** 系统 MUST 将两条 user Message 保存为同一 Thread 内具有不同 sequence 的独立 Message
+- **AND** 不得仅因角色没有交替而拒绝或重排它们
+
+#### Scenario: 按服务端顺序读取消息
+- **WHEN** 客户端读取一个 Thread 的消息
+- **THEN** 系统 MUST 按 sequence 升序返回有效时间线
+- **AND** 相同 Thread 内不得存在重复 sequence
+
+### Requirement: 使用不可变 Message replacement
+Message 内容在创建完成或 finalized 后 MUST NOT 原地改写。Edit 和 Regenerate MUST 创建具有新 ID 与新 sequence 的 replacement Message,通过 `replacesMessageId` 指向旧 Message,并将旧 Message 标记为 `superseded`。
+
+默认时间线 MUST 隐藏 superseded Message,但持久化层 MUST 保留其 ID、sequence、内容和来源关系。MVP MUST NOT 提供 Message Variant,也 MUST NOT hard delete 单条 Message;只有永久删除整个 Project 时,系统才 MUST 统一清理 Project 下的 Thread、Message、MessageRun 和附属资源。
+
+#### Scenario: Regenerate assistant 回复
+- **WHEN** 用户对当前可重新生成的 assistant Message 执行 Regenerate
+- **THEN** 系统 MUST 创建新的 assistant Message 与新的 MessageRun
+- **AND** 旧 assistant Message MUST 被标记为 superseded,但其 ID、sequence 和内容 MUST 保持不变
+
+#### Scenario: 编辑最后一条 user Message
+- **WHEN** 用户编辑当前 Thread 最后一条有效 user Message
+- **THEN** 系统 MUST 创建 replacement user Message
+- **AND** 原 Message 及所有 sequence 更大、依赖原内容的有效 Message MUST 退出默认时间线但继续保留
+- **AND** replacement user Message 及后续新回复 MUST 以新 sequence 追加到 Thread 尾部
+
+#### Scenario: 拒绝编辑历史 user Message
+- **WHEN** 用户尝试编辑并非当前 Thread 最后一条有效 user Message 的消息
+- **THEN** 系统 MUST 按 MVP 策略拒绝操作,并提示使用 Fork 保留另一条历史
+
+#### Scenario: 永久删除 Project
+- **WHEN** 已获授权的用户确认永久删除整个 Project
+- **THEN** 系统 MUST 将该 Project 的共享资源、Thread、Message 和 MessageRun 作为完整边界清理
+
+### Requirement: 冻结 Fork 的消息身份上下文
+Fork 时,服务端 MUST 根据来源 Thread 在 Fork 点之前的有效 Prompt 历史生成不可变 BaseContext。BaseContext MUST 包含 schema 版本和按 Prompt 顺序排列的 `messageIds`,不得复制 Message Parts,也不得由客户端提交或重建。
+
+进入 BaseContext 或作为 Fork source 的 Message MUST 已 finalized 且具备 Prompt 资格。有效 user Message 可以进入;仅 completed assistant Message 可以进入并作为来源;queued、running、failed 或 stopped assistant Message 不得进入,也不得作为 Fork source。Parent 后续发生 replacement、追加或归档时,既有 Child Thread 的 BaseContext MUST 保持不变。
+
+#### Scenario: 从 completed assistant Message Fork
+- **WHEN** 用户从 finalized 且 completed 的有效 assistant Message 发起 Fork
+- **THEN** 服务端 MUST 在同一 Project 创建 Child Thread
+- **AND** BaseContext MUST 冻结到来源 Message 为止的有序有效 Message ID
+
+#### Scenario: 生成期间不允许 Fork
+- **WHEN** 当前 Thread 最后一条 assistant Message 的 MessageRun 处于 queued 或 running
+- **THEN** 客户端 MUST 隐藏或禁用 Fork
+- **AND** 服务端 MUST 拒绝绕过客户端发起的 Fork 请求
+
+#### Scenario: Parent Message 后续被 replacement
+- **WHEN** Child Thread 的 BaseContext 引用了之后被 superseded 的 Parent Message
+- **THEN** Child Thread MUST 继续按原 Message ID 解析冻结历史
+- **AND** replacement Message MUST NOT 自动替换 BaseContext 中的 ID
+
+#### Scenario: 客户端试图提交 BaseContext
+- **WHEN** Fork 请求包含客户端构造的 BaseContext 或待创建 Child Thread ID
+- **THEN** 服务端 MUST 忽略或拒绝这些字段,并只使用服务端计算和生成的值
+
+### Requirement: 持久化 assistant MessageRun
+每条 assistant Message MUST 具有且仅具有一条持久化 MessageRun,user Message MUST NOT 具有 MessageRun。MessageRun MUST 至少表达 queued、running、completed、failed 和 stopped 状态,并保存恢复流式展示所需的运行进度。
+
+浏览器刷新、关闭或流连接断开 MUST 只终止该客户端订阅,不得自动停止后台 MessageRun。客户端重新加载 Thread 时,系统 MUST 通过 assistant Message 及其 MessageRun 恢复最终内容、生成中、失败或停止状态。
+
+#### Scenario: 创建 assistant Message
+- **WHEN** 服务端接受一次需要 AI 回复的生成命令
+- **THEN** 服务端 MUST 在同一原子边界创建 assistant Message 与唯一 MessageRun
+
+#### Scenario: 刷新后恢复运行状态
+- **WHEN** assistant Message 的 MessageRun 仍为 queued 或 running 且用户刷新页面
+- **THEN** 客户端 MUST 加载该状态并恢复生成展示与事件订阅
+- **AND** 刷新不得创建第二条 MessageRun
+
+#### Scenario: 队列启动失败
+- **WHEN** MessageRun 在进入 running 前无法启动
+- **THEN** 系统 MUST 允许它从 queued 转为 failed
+
+### Requirement: 维护 Project 共享的 Artifact
+Artifact MUST 归属于且仅归属于一个 Project,并 MUST 保留产生它的来源 Message 身份。该 Project 的全部 Thread MUST 能按权限引用 Artifact;其他 Project MUST NOT 仅因归属于同一用户而自动获得访问权。
+
+BaseContext MUST 通过 Message ID 间接保留 Artifact 的来源语义,不得复制大型 Artifact 内容。MVP MUST NOT 因本重构引入 ArtifactVersion、通用资源图或内容寻址存储。
+
+#### Scenario: 同 Project 的 Thread 使用 Artifact
+- **WHEN** Project 中一个 Thread 的 Message 产生 Markdown Artifact
+- **THEN** 同 Project 的其他 Thread MUST 能按权限引用该 Artifact
+- **AND** 系统 MUST 保留 sourceMessageId 作为来源
+
+#### Scenario: Fork 历史包含 Artifact
+- **WHEN** BaseContext 引用的 Message 产生过 Artifact
+- **THEN** 系统 MUST 能通过 Message ID 解析其 Project Artifact 关联
+- **AND** BaseContext 不得复制 Artifact 正文
 
-- **WHEN** MainThread 已生成自动 Title,且用户随后为其 Thread Tree 设置自定义 Title
-- **THEN** 树级导航和 MainThread 列头展示用户自定义 Title,同时保留自动 Title 作为机器派生信息
+#### Scenario: 隔离其他 Project
+- **WHEN** 另一个 Project 未获得显式授权
+- **THEN** 系统 MUST NOT 向它暴露当前 Project 的 Artifact
diff --git a/openspec/specs/thread-chat-client-state/architecture.md b/openspec/specs/thread-chat-client-state/architecture.md
new file mode 100644
index 00000000..4474ddf4
--- /dev/null
+++ b/openspec/specs/thread-chat-client-state/architecture.md
@@ -0,0 +1,53 @@
+# Thread Chat 客户端架构
+
+本文记录已经落地的稳定客户端边界。规范性行为以同目录 [spec.md](./spec.md) 为准;冲突时以可验证 Requirement/Scenario 为准。
+
+## 数据流与依赖方向
+
+```mermaid
+flowchart LR
+    UI[UI Components] --> Hooks[Selector / Command Hooks]
+    Hooks --> Commands[Application Commands]
+    Commands --> API[ThreadChatApiCapabilities]
+    API --> DTO[Strictly validated DTO / SSE]
+    DTO --> Actions[Semantic Store Actions]
+    Hooks --> Actions
+    Actions --> Stores[App Store / Project Store]
+    Stores --> Selectors[Pure Selectors]
+    Selectors --> Hooks
+```
+
+客户端只保存一份服务端事实。组件不直接 fetch、不创建服务端实体 ID、不维护可写 ThreadTree,也不直接调用 Zustand `setState`。Transport 负责会话、编码、严格 Schema 校验和错误映射;Application Commands 编排异步操作;Store Actions 只做同步语义合并。
+
+## Store
+
+- `ThreadChatAppStore`:Provider-scoped;保存 Project Catalog、分页请求状态和 AppShell UI,不保存全局 `selectedProjectId`。
+- `ThreadChatProjectStore`:以 `projectId` 隔离;保存 `entities`、`runs`、`requests`、`readModels` 与 `workbench.ui`。
+- normalizer 维护 `messagesById + messageIdsByThreadId`,校验归属、去重并按服务端 `sequence` 排序;finalized 实体不可被旧 DTO 或乱序事件覆盖。
+- Workbench UI 使用纯客户端 `ColumnSlotId`;列宽、折叠、焦点、placement、Canvas pin、overlay 和 Snapshot 都不进入领域 API。
+
+## Runtime 与 Provider
+
+- `ThreadChatAppRuntime` 组合 App Store、Catalog Commands、Transport、Navigation 与 `ProjectRuntimeRegistry`。
+- Registry 保证一个 Provider 生命周期内同一 `projectId` 只有一个 `ThreadChatProjectRuntime`,并负责 seeded handoff、lease/release 与统一 destroy。
+- `ThreadChatProjectRuntime` 组合 Project Store、Commands、ThreadMessageLoader、ArtifactLoader 和 GenerationCoordinator。
+- `ThreadChatAppProvider`、`ThreadChatProjectProvider`、`NewProjectDraftProvider` 均按 React/Next.js 生命周期创建实例,禁止服务端模块级可变 Store 跨请求共享。
+
+## 异步边界
+
+| 组件 | 去重键 | 职责 |
+|---|---|---|
+| ThreadMessageLoader | `threadId` | Promise 去重、跨 Thread 并行、局部 loading/error、destroy Abort |
+| ArtifactLoader | `artifactId` | 按需加载正文与缓存;Thread 加载不触发 Artifact 正文请求 |
+| GenerationCoordinator | `assistantMessageId` | 单连接、snapshot 合并、严格 eventSequence、断线重连、取消订阅 |
+| Workbench persistence | `projectId` | 防抖保存/校验 Snapshot;恢复列、焦点、折叠、列宽,不恢复草稿/滚动 |
+
+刷新、路由切换或 Runtime destroy 只取消客户端订阅,不调用 Stop。只有显式 Stop Command 才请求服务端停止 Run。
+
+## `/new` 与 Project 生命周期
+
+`/thread-chat/new` 只持有无实体 draft。首次发送获得 CreationBundle 后,App Runtime 先把服务端 Project/Root/Messages/Run 合入 seeded Project Runtime并启动事件协调,再通过集中 route builder replace 到 `/thread-chat/{projectId}`。目标 Provider acquire 同一 Runtime 并跳过重复 Bootstrap,因此没有空白帧或第二条 Run。
+
+已有 Project 页面先 acquire Runtime,再 Bootstrap 轻量 topology 与 Root bundle;其他 Branch Message 和 Artifact 正文按需并行加载。UI 继续复用原有组件、CSS、Header、多栏、Fork、breadcrumb、Artifact Drawer 与分割线交互。
+
+后续修改 Store shape、Runtime 组合、Loader/Coordinator 或 UI 接缝时,必须同步更新正式 spec、本文、共享 DTO、接口注入测试、Testing Library 测试与 Ego Browser UI parity 验收。
diff --git a/openspec/specs/thread-chat-client-state/spec.md b/openspec/specs/thread-chat-client-state/spec.md
new file mode 100644
index 00000000..66262be2
--- /dev/null
+++ b/openspec/specs/thread-chat-client-state/spec.md
@@ -0,0 +1,298 @@
+# thread-chat-client-state Specification
+
+## Purpose
+
+定义 Thread Chat 客户端的 Store、Runtime、Provider、Command、Hook、加载与 UI 状态边界。
+
+稳定说明见 [architecture.md](./architecture.md)。若说明文档与本规范冲突,以本规范中的可验证 Requirement 与 Scenario 为准;任何实现变更都 MUST 同步校验正式规范、说明文档、共享 Schema、Route/Runtime 实现与验收测试。
+
+## Requirements
+
+### Requirement: 统一客户端状态架构术语
+客户端规范、设计和实现 MUST 使用以下术语,并 MUST NOT 用同一个 `Action` 同时指代本地状态转换和跨边界业务流程:
+
+- **Store State**:Zustand Store 保存的数据,包括服务端确认实体、运行态、请求状态和本地 UI 状态。
+- **Store Action**:与对应 Store 或 slice 共置、通过 Zustand `set`/`setState` 执行一次受控 State Transition 的函数。Store Action MUST NOT 调用后端 API、管理路由或建立事件连接。
+- **Application Command**:位于 Store 外、代表一个用户或生命周期业务意图的可测试流程。它 MAY 调用 API、协调订阅与路由,但 MUST 通过 Store Action 提交状态变化,不得自行调用 `set`/`setState`。
+- **Selector Hook**:通过细粒度 selector 订阅 Store,并向 UI 暴露 State 或衍生 ViewModel 的读取 Hook。
+- **Command Hook**:把作用域 ID 与 Application Command 或纯本地 Store Action 绑定为 UI 事件接口的 Hook;它不得包含业务规则或直接修改 State。
+- **Lifecycle Hook**:负责 Bootstrap、按需加载、生成订阅和本地偏好持久化等 React 生命周期接入的 Hook;真正流程仍委托给 Application Command 或 coordinator。
+
+本文中的“状态变更”表示通过 `set`/`setState` 产生新状态并通知订阅者,不表示对现有 State 对象做任意原地修改。
+
+#### Scenario: API 成功后提交状态
+- **WHEN** Application Command 收到并校验合法服务端 DTO
+- **THEN** 它 MUST 调用语义明确的 Store Action 原子合并 DTO
+- **AND** Application Command、Transport 和 React 组件 MUST NOT 绕过 Store Action 直接调用 `set`/`setState`
+
+#### Scenario: 纯本地 UI 操作
+- **WHEN** 用户只改变可见列、画布位置或 overlay 等本地 UI State
+- **THEN** Command Hook MAY 直接调用对应 UI slice 的 Store Action
+- **AND** 不得为了纯本地 State Transition 建立没有跨边界职责的 Application Command
+
+### Requirement: 使用规范化客户端实体模型
+客户端 MUST 以 Project、Thread、Message 和 Artifact 的服务端 DTO 作为已确认内容事实,并 MUST 以 `assistantMessageId` 为关联键保存 AssistantRunState。客户端 MUST NOT 把整棵 Thread 拓扑、Message 列表和运行态重新组合成可整体写回的 `ThreadTreeState`。
+
+Thread 的 Root/Branch、children、depth 和 breadcrumb MUST 由 `parentThreadId` 派生;Message 默认时间线 MUST 由 `supersededAt IS NULL` 和 `sequence ASC` 派生。BaseContext MUST 保持为服务端内部事实,不得进入普通客户端实体状态或由客户端重建。
+
+#### Scenario: 合并 ProjectBootstrap
+- **WHEN** 客户端收到包含 Project、Thread topology、Root Message、AssistantRunState 和 Project Artifact Summary 的合法 ProjectBootstrap
+- **THEN** 客户端 MUST 按实体 ID 规范化合并这些 DTO 与读模型
+- **AND** 不得保存第二份整树权威快照
+- **AND** Bootstrap 中的 Message 只通过 tool result 的 `artifactId` 引用 Artifact,不得附带 Artifact 正文
+
+#### Scenario: 派生 Branch 关系
+- **WHEN** 一个 Thread 的 `parentThreadId` 指向同 Project 的另一个 Thread
+- **THEN** selector MUST 将它作为该 Parent 的 Child/Branch Thread 展示
+- **AND** 客户端不得要求服务端同时返回可独立修改的 `children` 数组
+
+### Requirement: 按生命周期划分 Zustand Store
+客户端 MUST 使用一个轻量 ProjectCatalogStore 管理 Project 列表,并 MUST 为每个打开的 `projectId` 创建独立 ThreadChatStore。ThreadChatStore MUST 以高内聚 slice 区分已确认 entities、服务端统计 read models、加载/命令状态、生成流状态和本地 workbench UI 状态;它们可以位于同一个 Zustand store,但不得互相复制权威数据。
+
+ProjectCatalogStore MUST NOT 保存 Thread 或 Message。Project-scoped Store 在离开对应 Project 页面后 MUST 可被销毁,不得让多个 Project 的 Message 长期堆积在无边界全局 Store 中。
+
+#### Scenario: 打开两个 Project
+- **WHEN** 用户先后打开 Project A 和 Project B
+- **THEN** 两个 Project 的 ThreadChatStore MUST 具有隔离的实体、生成和工作台状态
+- **AND** ProjectCatalogStore MUST 只保留两者的轻量列表信息
+
+#### Scenario: 高频流事件只更新相关订阅者
+- **WHEN** 某条 assistant Message 收到生成增量
+- **THEN** Store MUST 只改变该 `assistantMessageId` 对应的生成流状态
+- **AND** 只订阅其他 Thread 或 Project 元数据的组件 MUST NOT 因全局 version 而被强制重算
+
+### Requirement: 区分服务端事实、运行态和本地 UI 态
+Project、Thread、Message、Artifact、Project Artifact Summary 和 MessageRun 终态 MUST 以服务端响应为权威。运行中的 checkpoint 和 eventSequence MUST 保存于生成 slice;visibleThreadIds、Column Slot、列宽、画布位置、composer 草稿、overlay 和选中 Artifact MUST 保存为本地 UI 态。
+
+客户端 MAY 将设备相关 workbench UI 态持久化到 localStorage,但 MUST NOT 将它提交为 Project 内容。客户端 MUST NOT 为乐观展示伪造 Project、Thread、Message 或 MessageRun ID。
+
+#### Scenario: 发送期间展示等待态
+- **WHEN** 用户提交命令但服务端尚未返回新实体
+- **THEN** 客户端 MUST 使用本地 submitting/busy 状态展示等待
+- **AND** 不得向 entities slice 插入 `temp-*`、`main` 或客户端 UUID 形式的待创建实体
+
+#### Scenario: 恢复工作台偏好
+- **WHEN** ProjectBootstrap 成功且浏览器存在该 Project 的列布局偏好
+- **THEN** 客户端 MUST 过滤其中已经不存在的 Thread ID、重复 Slot/Thread、非法宽度和无效 Canvas pin 后恢复布局
+- **AND** 本地布局不得覆盖服务端返回的 Thread topology
+
+### Requirement: 使用稳定 Column Slot 表达物理列
+客户端 MUST 使用纯本地 `ColumnSlotId` 表示 Root 右侧物理列,并 MUST 将 Slot 当前展示的 `threadId` 与 Slot 身份分离。Root MUST 使用独立固定列身份,不得进入 Branch Slot 数组。
+
+切换一个物理列所展示的 Thread 时,客户端 MUST 保留该 Slot 的 `slotId`、物理位置、折叠态和显式列宽,只替换 `threadId`。`ColumnSlotId` 不是服务端实体 ID,MUST NOT 进入 Project、Thread、Message、Fork 或 Generation API 请求。
+
+#### Scenario: 本列切换 Thread
+- **WHEN** 用户通过现有列 Header 切换器把 Slot S 从 Thread A 切换到 Thread B
+- **THEN** Store MUST 保持 S 的 `slotId`、宽度、折叠态和位置不变,并将 S 的 `threadId` 更新为 B
+- **AND** 不得因为切换内容而重建物理列或把 A 的宽度改绑到 Thread B 实体
+
+#### Scenario: 恢复重复 Thread 的非法 Snapshot
+- **WHEN** 本地 Snapshot 中两个 Slot 指向同一个 Thread 或复用了同一个 Slot ID
+- **THEN** 客户端 MUST 按稳定顺序只保留第一个合法 Slot
+- **AND** 不得在同一工作台恢复两列相同 Thread
+
+### Requirement: 支持分栏分割线拖拽
+客户端 MUST 保留现有相邻展开列之间的分割线拖拽能力。分割线 MUST 同时调整左右两个物理列,并遵守当前 UI 的最小列宽约束;Root 宽度 MUST 归属于固定 Root 列,Branch 宽度 MUST 归属于稳定 Column Slot,而不是 Thread Entity。
+
+Pointer Move 期间的坐标、临时宽度和 Pointer Capture MUST 保持为 Resizer Hook 或组件局部瞬时状态,不得逐帧写入 Zustand。Pointer Up、键盘步进或双击复位时,客户端 MUST 通过一次 Store Action 原子提交受影响列的最终宽度;提交后的宽度 MUST 进入工作台 Snapshot,滚动位置和拖拽瞬时状态不得持久化。该操作 MUST NOT 调用服务端 API。
+
+#### Scenario: 拖拽相邻列分割线
+- **WHEN** 用户拖动两个展开列之间的分割线并释放 Pointer
+- **THEN** Store MUST 在一次 State Transition 中提交左右两列最终宽度
+- **AND** 不得在每次 Pointer Move 时修改 Zustand 或触发服务端请求
+
+#### Scenario: 切换 Thread 后保留列宽
+- **WHEN** 用户调整 Slot S 的宽度后,通过现有 Header 切换器把 S 从 Thread A 切换到 Thread B
+- **THEN** Slot S MUST 保留原宽度
+- **AND** Thread A 与 Thread B Entity 均不得保存该宽度
+
+#### Scenario: 双击分割线恢复自动宽度
+- **WHEN** 用户双击现有分割线复位
+- **THEN** 客户端 MUST 清除受影响物理列的显式宽度并恢复当前自动均分行为
+- **AND** 下一次工作台 Snapshot MUST 保存复位后的状态
+
+### Requirement: 刷新后恢复 Project 工作台视图
+客户端 MUST 按 `projectId` 将带 `schemaVersion` 的工作台视图投影防抖保存到设备 localStorage。刷新并完成 ProjectBootstrap 后,客户端 MUST 恢复合法的列槽、每列当前 Thread、折叠态、物理列宽、焦点列、列数偏好、放置模式、Columns/Canvas 模式和 Canvas pin。
+
+工作台 Snapshot MUST NOT 直接序列化整个 UI Store,也 MUST NOT 包含服务端实体、Message/Run、滚动位置、DOM 引用、弹层坐标、动画、文本选区、临时 Switcher/Help 打开态、请求状态、命令状态、流缓冲、Generation 连接或 Composer 草稿。
+
+#### Scenario: 刷新恢复多栏视图
+- **WHEN** 用户在 Project P 中打开多个 Branch、调整列宽、折叠一列并刷新页面
+- **THEN** Bootstrap 成功后客户端 MUST 恢复刷新前仍然合法的 Slot、Thread、列宽、折叠态和焦点
+- **AND** 每列滚动位置 MUST 使用当前 UI 默认行为,不得从 Snapshot 恢复
+
+#### Scenario: Snapshot 损坏或版本未知
+- **WHEN** localStorage 不可用、Snapshot 无法解析或 `schemaVersion` 不受支持
+- **THEN** 客户端 MUST 安全回退为只显示 Root 且聚焦 Root 的当前默认视图
+- **AND** 不得影响 ProjectBootstrap、Message 展示或 Generation 恢复
+
+### Requirement: 通过 selector 生成 UI ViewModel
+客户端 MUST 使用纯 selector 从 entities、服务端 read models、AssistantRunState 和 UI state 生成 ThreadColumnView、ThreadColumnHeaderView、ProjectTreeRows、MessageView、ForkAvailability 和 ProjectHeaderView 等 ViewModel。派生结果 MUST NOT 作为另一份可独立修改的领域状态持久化。
+
+Selector MUST 至少覆盖 Root Thread、Child Thread、depth、breadcrumb、有效 Message 时间线、Message 运行展示、Artifact 来源、Fork 可用性、可见列组合、物理 Slot、页面统计和列 Header 操作信息。
+
+#### Scenario: replacement 后重算消息视图
+- **WHEN** Store 合并一条 replacement Message 并将来源 Message 标记为 superseded
+- **THEN** Thread Message selector MUST 隐藏来源 Message并按 sequence 展示 replacement
+- **AND** 不得通过手动修改 UI Message 数组维持第二份时间线
+
+#### Scenario: 组合 ThreadColumnView
+- **WHEN** UI 请求一个 Thread 的列视图
+- **THEN** selector MUST 组合 Thread、有效 Messages、相关 AssistantRunState、Message 中的 Artifact 引用、breadcrumb 和操作可用性
+- **AND** 该 ViewModel 不得成为 API 写入对象
+
+#### Scenario: 打开 Artifact Drawer
+- **WHEN** 用户打开 Message tool result 中 `artifactId` 指向的 Artifact
+- **THEN** Lifecycle/Command Hook MUST 按 ID 调用 Artifact Query,并将成功结果缓存到当前 Project Store
+- **AND** Message 组件不得自行请求或复制 Artifact 正文
+
+#### Scenario: 组合每列 Header ViewModel
+- **WHEN** UI 请求 Root 或某个 Branch Slot 的 Header
+- **THEN** selector MUST 派生标题、Root/Branch、depth、breadcrumbs、直接 Child 数量与列表、Fork 来源可用性以及 switch/collapse 能力
+- **AND** `directChildren` MUST 只包含直接 Child;不得把它作为可修改 children 数组写回 Thread Entity
+
+#### Scenario: 组合页面统计
+- **WHEN** UI 请求 ProjectHeaderView
+- **THEN** `threadCount` 与 `branchCount` MUST 从完整 Thread topology 派生,Artifact 与 Markdown 总数 MUST 从服务端 Artifact Summary 派生
+- **AND** 不得使用局部 `artifactsById` 数量冒充 Project Artifact 总数
+
+#### Scenario: 多个 Run 的 Artifact Summary 乱序到达
+- **WHEN** 当前 Summary 的 `changeSequence=8`,随后收到另一个 Run 携带的 `changeSequence=7`
+- **THEN** Store MUST 忽略该旧 Summary 并继续展示 sequence 8 的统计
+- **AND** `changeSequence` MUST NOT 被提交给任何 Project、Thread、Message 或 Generation Command
+
+### Requirement: 使用 Application Command 执行业务意图
+UI 组件和 React Hook MUST NOT 直接调用 `fetch`、构造 Prompt、创建实体 ID或修改服务端实体。跨越 API、事件连接或路由的用户意图 MUST 进入可独立测试的 Application Command,由 Command 调用 API capability、校验结果并通过 Store Action 原子合并 Store。
+
+Application Command MUST 至少覆盖:加载/创建 Project、加载 Thread、发送首条消息、发送后续消息、Fork、Edit、Regenerate、Stop、feedback、更新 Project 元数据以及生成恢复。
+
+#### Scenario: Fork Command 成功
+- **WHEN** UI 调用 Fork Application Command 并传入已有 sourceThreadId、sourceMessageId 和选区信息
+- **THEN** Command MUST 调用服务端 Fork API 并通过 Store Action 合并服务端返回的 Child Thread
+- **AND** UI 层 MAY 在成功后打开新 Thread,但不得自行计算 BaseContext 或 Child Thread ID
+
+#### Scenario: Application Command 失败
+- **WHEN** 服务端返回结构化领域错误
+- **THEN** Command MUST 保持既有已确认 entities 不变并通过 Store Action 更新相应命令错误状态
+- **AND** Hook MUST 向 UI 暴露可展示的失败结果
+
+### Requirement: 使用 Hooks 连接 Store、Command 与 UI
+客户端 MUST 将 Hook 分为 Selector Hook、Command Hook 和 Lifecycle Hook。Selector Hook MUST 通过细粒度 selector 订阅 Store;Command Hook MUST 调用 Application Command 或纯本地 Store Action;Lifecycle Hook MUST 只负责把 React 生命周期接入 Bootstrap、按需加载、生成订阅和本地偏好持久化流程。
+
+普通衍生数据 MUST NOT 通过 `useEffect + setState` 镜像 Store。单个 Message 组件 MUST NOT 自行发起 Message 或 Generation 查询。
+
+#### Scenario: Thread 组件读取数据
+- **WHEN** ThreadColumn 渲染指定 `threadId`
+- **THEN** 它 MUST 通过 Selector Hook 获得 ThreadColumnView
+- **AND** 不得接收整棵 Project 状态后自行遍历并维护副本
+
+#### Scenario: 页面恢复生成订阅
+- **WHEN** 生命周期 Hook 发现已加载 Message 中存在 queued/running AssistantRunState
+- **THEN** 它 MUST 交给统一 generation coordinator 从 eventSequence 恢复订阅
+- **AND** 每个 Message 组件不得建立重复订阅
+
+### Requirement: 支持无实体 ID 的新 Project 入口
+`/thread-chat/new` MUST 表示本地新 Project 草稿入口,不得把 `new` 当作 Project ID。页面 MUST 在没有 Project/Thread 实体的情况下复用空白聊天 UI;用户第一次发送前不得写入 Project 列表或创建假的 Root Thread。
+
+第一次发送成功后,客户端 MUST 合并服务端原子返回的 Project、Root Thread、user Message、assistant Message 和 AssistantRunState,并 MUST 使用集中式路由构造器根据 `project.id` 得到目标 Project URL,再通过 `router.replace` 进入该页面。失败时 MUST 留在 `/thread-chat/new` 并保留草稿。
+
+#### Scenario: 打开后直接离开 new 页面
+- **WHEN** 用户打开 `/thread-chat/new` 但未发送任何内容便离开
+- **THEN** 系统 MUST NOT 创建 Project、Thread、Message 或 MessageRun
+
+#### Scenario: 首次发送成功
+- **WHEN** 用户在 `/thread-chat/new` 发送第一条有效 Message
+- **THEN** 客户端 MUST 调用创建 Project 的 Application Command,并通过 Store Action 使用服务端返回的实体初始化 Project Store
+- **AND** URL MUST 被替换为客户端路由构造器根据服务端 `project.id` 生成的目标 Project URL
+
+#### Scenario: CreationBundle 无空白帧交接
+- **WHEN** `/new` 创建命令返回合法 CreationBundle
+- **THEN** 客户端 MUST 先用 CreationBundle 建立 bootstrap-ready 的 seeded ProjectRuntime,再执行 `router.replace(threadChatRoutes.project(bundle.project.id))`
+- **AND** 目标 ProjectProvider MUST acquire 同一个 seeded Runtime 并跳过第二次 ProjectBootstrap
+- **AND** `/new` 当前页面 MUST 保持渲染到目标 Project Route 可提交,不得插入空白 Project、第二次 Bootstrap Loading 或清空后等待的 Composer 帧
+- **AND** 客户端 MUST NOT 从 CreationBundle 读取页面 URL
+
+#### Scenario: AI 事件早于目标 ProjectProvider 挂载
+- **WHEN** seeded Runtime 在路由交接完成前收到 A1 的 snapshot、delta 或 terminal 事件
+- **THEN** GenerationCoordinator MUST 把事件合并到该 seeded Runtime
+- **AND** 目标 ProjectProvider 接管后 MUST 直接展示同一 Store 的最新状态,不得重新创建 Run 或丢弃已到达事件
+
+#### Scenario: 首次发送失败
+- **WHEN** 创建命令在服务端提交前失败
+- **THEN** 客户端 MUST 保留本地输入并允许重试
+- **AND** 不得向 ProjectCatalogStore 增加未确认 Project
+
+### Requirement: 采用轻量 Bootstrap 与按 Thread 加载
+进入持久化 Project 时,客户端 MUST 首先加载 ProjectBootstrap。Bootstrap MUST 包含 Project、全量轻量 Thread topology、Project Artifact Summary、Root Thread 的有效 Message 和相关 AssistantRunState,但 MUST NOT 包含所有 Branch Message、BaseContext 或 Artifact 正文。
+
+其他 Thread 的 Message MUST 在首次打开时按 Thread 加载。MVP 客户端 MUST 支持一次合并最多 200 条按 sequence 升序排列的有效 Message,并 MUST 保留服务端返回的 `hasOlderMessages` 信息,但不要求实现复杂的树内自动分页替换。
+
+P0 的 Markdown tool result MUST 只向客户端提供 `artifactId` 引用。只有用户打开 Artifact Drawer 时,客户端才 MUST 通过 Artifact Query 按 ID 加载正文;加载 ProjectBootstrap、ThreadMessageBundle 或 topology 不得提前下载 Markdown 正文。
+
+客户端 MUST 使用 ProjectRuntime 级 `ThreadMessageLoader` 管理 `threadId → in-flight Promise/AbortController`。Promise、AbortController 和 in-flight Map MUST NOT 进入 Zustand;Store 只保存每个 Thread 的 `LoadState`、窗口边界和已合并实体。同一 Thread 的并发 ensure MUST 复用同一个 Promise,不同 Thread MUST 可以并行加载。
+
+#### Scenario: 初次进入已有 Project
+- **WHEN** 用户打开 `/thread-chat/{projectId}`
+- **THEN** 客户端 MUST 先合并 ProjectBootstrap 并展示 Root Thread
+- **AND** 不得为了渲染 topology 下载全部 Branch Message
+
+#### Scenario: 刷新恢复多个 Branch Column
+- **WHEN** Bootstrap 后恢复出的工作台 Snapshot 包含多个尚未 ready 的 Branch Thread
+- **THEN** Lifecycle MUST 非阻塞地并行调用每个 Branch 的 `ensureThreadMessages`
+- **AND** Root MUST 立即渲染;不得等待所有 Branch 请求完成后才展示页面
+- **AND** 每个 Branch MUST 独立进入 loading、ready 或 error,一个失败不得阻塞其他列
+
+#### Scenario: Workbench Snapshot 不是 Message Cache
+- **WHEN** 页面刷新后 localStorage 存在多个 Branch Slot,但新的 ProjectRuntime 只有 Root MessageBundle
+- **THEN** 客户端 MUST 使用 Snapshot 恢复列布局,并分别 ensure 每个 Branch MessageBundle
+- **AND** 不得从 Snapshot 构造 Message、Run 或 ready 状态
+
+#### Scenario: 没有 Workbench Snapshot
+- **WHEN** ProjectBootstrap 成功但当前设备没有合法 Workbench Snapshot
+- **THEN** 客户端 MUST 使用当前默认视图,只展示且聚焦 Root
+- **AND** 不得主动加载任何未打开 Branch 的 Message
+
+#### Scenario: ProjectRuntime 中已有 Message Cache
+- **WHEN** 同一 ProjectRuntime 内某个 Branch 的 ThreadMessageWindow 已是 ready 后再次打开该 Thread
+- **THEN** 客户端 MUST 直接复用已合并实体和窗口状态
+- **AND** 不得重复请求该 Thread 的 MessageBundle
+
+#### Scenario: 首次打开 Branch Thread
+- **WHEN** 用户打开尚未加载 Message 的 Branch Thread
+- **THEN** 客户端 MUST 只请求该 Thread 的 Message bundle 并合并结果
+- **AND** 重复打开已加载 Thread 不得自动重复请求
+
+#### Scenario: 同一 Thread 并发 ensure
+- **WHEN** 两个 Lifecycle/Command Hook 在第一个请求完成前同时 ensure 同一 threadId
+- **THEN** ThreadMessageLoader MUST 返回同一个 in-flight Promise
+- **AND** 服务端只能收到一个对应 Message Query
+
+#### Scenario: Slot 切换后旧 Thread 响应迟到
+- **WHEN** Slot S 在 Thread A 请求期间切换到 Thread B,随后 A 的 Bundle 到达
+- **THEN** Bundle MUST 只按 threadId 合并到 A 的实体索引和请求状态
+- **AND** S MUST 继续展示 B;不得让迟到响应把 Slot 切回 A
+
+#### Scenario: ProjectRuntime 销毁
+- **WHEN** ProjectProvider 卸载并销毁 ProjectRuntime
+- **THEN** ThreadMessageLoader MUST Abort 该 Runtime 的全部在飞 Message Query
+- **AND** Abort MUST NOT 写入可重试 error,也不得影响服务端 MessageRun
+
+#### Scenario: 关闭 Column 时请求仍在进行
+- **WHEN** 用户关闭或切换正在加载的 Branch Column,但 ProjectRuntime 仍存活
+- **THEN** Loader MUST 允许请求完成并按 threadId 缓存合法结果
+- **AND** Column 操作不得把请求结果合并到当前 Slot 的新 Thread
+
+### Requirement: 恢复服务端 MessageRun
+客户端 MUST 使用 `assistantMessageId + eventSequence` 识别和恢复生成。queued/running 的展示 MUST 先使用服务端 checkpoint,再从最后 eventSequence 之后订阅;completed MUST 使用 finalized Message.parts;failed/stopped MUST 展示终态且不得自动重启。
+
+刷新、路由切换或取消订阅 MUST NOT 发送 Stop。只有明确的用户 Stop Application Command 可以请求服务端停止 MessageRun。
+
+#### Scenario: 刷新后恢复 running Message
+- **WHEN** ProjectBootstrap 或 Thread bundle 返回 running AssistantRunState
+- **THEN** 客户端 MUST 立即展示 checkpoint 并从返回的 eventSequence 之后恢复订阅
+- **AND** 不得创建新的 assistant Message 或 MessageRun
+
+#### Scenario: 收到 completed 事件
+- **WHEN** generation coordinator 收到合法 completed 终态
+- **THEN** 客户端 MUST 合并服务端给出的 finalized Message 和终态 Run
+- **AND** 必须清除对应的临时流缓冲
diff --git a/openspec/specs/thread-chat-command-api/api-contracts.md b/openspec/specs/thread-chat-command-api/api-contracts.md
new file mode 100644
index 00000000..eb78cf63
--- /dev/null
+++ b/openspec/specs/thread-chat-command-api/api-contracts.md
@@ -0,0 +1,57 @@
+# Thread Chat `/api/v1` 合同
+
+本文是已落地 Route Handler 与共享 Zod Schema 的稳定索引。规范性行为以同目录 [spec.md](./spec.md) 为准;精确字段以 `lib/thread-chat/api/contracts.ts` 的 strict schemas 为可执行合同。
+
+## 通用规则
+
+- 所有领域 ID 是服务端 UUID、不透明字符串;客户端只构造页面 URL,不构造实体 ID,DTO 不返回 `canonicalUrl`。
+- JSON request/response 使用 strict Zod Schema,未知字段拒绝。actor 只来自 Session;所有 Query/Command 都执行 owner scope 与归档/业务资格校验。
+- user parts 只接受允许的 AI SDK v7 user part;Markdown tool output 只保存 `{ artifactId }`,不嵌入正文。
+- Command 在单事务内提交完整原子结果;错误为 `{ error: { code, message, details? } }`,使用一致 HTTP 映射。
+
+## Query API
+
+| Method | Path | Request | Success |
+|---|---|---|---|
+| GET | `/api/v1/projects` | `status=active|archived|all`、`limit`、opaque `cursor` | `ListProjectsResult` |
+| GET | `/api/v1/projects/{projectId}/bootstrap` | path ID | Project、全量轻量 topology、Artifact Summary、唯一 Root bundle |
+| GET | `/api/v1/threads/{threadId}/messages` | `limit<=200`、`beforeSequence` | `ThreadMessageBundle` 与窗口边界 |
+| GET | `/api/v1/artifacts/{artifactId}` | path ID | 按 ID 返回完整 `Artifact` |
+
+Bootstrap/MessageBundle/SSE 不返回 BaseContext、Prompt History、未打开 Branch 消息或 Artifact 正文。Artifact Summary 的 `changeSequence` 和 totals 由服务端计算。
+
+## Command API
+
+| Method | Path | Body | Success |
+|---|---|---|---|
+| POST | `/api/v1/projects` | `{ parts, requestedModelId? }` | CreationBundle:Project、Root、user、assistant、queued Run |
+| PATCH/DELETE | `/api/v1/projects/{projectId}` | metadata patch / none | Project / 204 |
+| POST | `/api/v1/projects/{projectId}/archive|unarchive` | empty | Project |
+| PATCH | `/api/v1/threads/{threadId}` | `{ customTitle }` | Branch Thread;Root metadata 被拒绝 |
+| POST | `/api/v1/threads/{threadId}/archive|unarchive` | empty | Branch Thread |
+| POST | `/api/v1/threads/{threadId}/messages` | `{ parts, requestedModelId? }` | user + assistant + queued Run |
+| POST | `/api/v1/threads/{threadId}/forks` | source Message、anchor/quote;不含 Child ID/BaseContext | 新 Child Thread |
+| POST | `/api/v1/messages/{messageId}/edits` | `{ parts, requestedModelId? }` | ReplacementBundle |
+| POST | `/api/v1/messages/{messageId}/regenerations` | `{ requestedModelId? }` | ReplacementBundle |
+| PUT | `/api/v1/messages/{messageId}/feedback` | `{ value: positive|negative|null }` | Feedback DTO |
+| POST | `/api/v1/assistant-messages/{id}/stop` | empty | 最新 AssistantRunState |
+
+P0 不提供 user-only append、客户端 BaseContext、通用 Idempotency-Key、双写或 feature flag。
+
+## SSE
+
+`GET /api/v1/assistant-messages/{assistantMessageId}/events?afterEventSequence=N` 返回 `text/event-stream`。每次连接首个业务事件固定为持久化 `run.snapshot`;其后只接受严格递增且大于恢复游标的事件:
+
+- `run.delta`:最新 checkpoint 增量/视图;
+- `run.completed`:finalized Message、terminal Run、最新 Artifact Summary;
+- `run.failed`:结构化错误与 terminal Run;
+- `run.stopped`:terminal Run;
+- snapshot/completed 不复制 Artifact 正文。
+
+断开 SSE、刷新或切换页面不停止后台执行。queued/running 刷新后复用同一 `assistantMessageId` 与 MessageRun;显式 Stop API 才写入 stop request。
+
+## 错误与维护
+
+共享错误码覆盖认证、权限、not found、strict validation、归档状态、领域资格、冲突和内部错误;服务端错误堆栈不进入响应。`ThreadChatApiCapabilities` 是 Web UI 与未来 adapter 的 transport-neutral 边界。
+
+后续增删任何字段、Route、事件或错误码时,必须同时更新正式 spec、本文、`contracts.ts`、`capabilities.ts`、`routes.ts`、Route Handler、JSON Transport、合同/权限/原子性/SSE 测试与 OpenSpec strict validation。
diff --git a/openspec/specs/thread-chat-command-api/spec.md b/openspec/specs/thread-chat-command-api/spec.md
new file mode 100644
index 00000000..ddc13aba
--- /dev/null
+++ b/openspec/specs/thread-chat-command-api/spec.md
@@ -0,0 +1,215 @@
+# thread-chat-command-api Specification
+
+## Purpose
+
+定义 Thread Chat `/api/v1` 的共享契约、查询、命令、SSE、权限与错误语义。
+
+稳定说明见 [api-contracts.md](./api-contracts.md)。若说明文档与本规范冲突,以本规范中的可验证 Requirement 与 Scenario 为准;任何实现变更都 MUST 同步校验正式规范、说明文档、共享 Schema、Route/Runtime 实现与验收测试。
+
+## Requirements
+
+### Requirement: 使用版本化 API 和服务端实体身份
+ThreadChat 后端 MUST 在 `/api/v1` 下提供版本化 Query、Command 和事件接口。客户端 MUST 只提交正在操作的既有资源 ID;Project、Thread、Message 和 MessageRun 的新 ID MUST 由服务端生成并在成功响应中返回。
+
+服务端 MUST 从认证 Session 确定 actor,MUST 对每个 Project、Thread 和 Message 操作执行归属或访问授权校验。普通 JSON 成功响应 MUST 返回 `{ data: T }` 中的权威 DTO,错误 MUST 返回 `{ error: { code, message, details? } }`;204 和事件流除外。请求 Object MUST 严格校验未声明字段。MVP 普通追加、Fork 和生成命令 MUST NOT 要求 Project/Thread revision 或 If-Match。
+
+#### Scenario: 客户端提交待创建实体 ID
+- **WHEN** 创建或 Fork 请求包含 newProjectId、newThreadId、newMessageId 或 newMessageRunId
+- **THEN** 服务端 MUST 忽略或拒绝这些字段
+- **AND** 成功结果中的实体 ID MUST 来自服务端
+
+#### Scenario: 访问其他用户的 Project
+- **WHEN** actor 对目标 Project 没有访问权
+- **THEN** 服务端 MUST 返回 403 或不泄漏存在性的 404
+- **AND** 不得返回 Project topology、Message 或运行状态
+
+### Requirement: 提供 Project 列表与元数据命令
+服务端 MUST 提供列出、更新、归档和永久删除 Project 的能力。列表 MUST 为轻量 ProjectSummary,按 `updatedAt DESC, id DESC` 稳定排序,不得包含 Thread topology 或 Message 正文。
+
+Project metadata 更新 MUST 允许独立修改 customTitle、Target 和 Instruction;未提供字段 MUST 保持不变,显式 null MUST 按字段契约清空。MVP 对 metadata 使用服务端最后写入生效,不引入通用 revision。
+
+#### Scenario: 列出 Project
+- **WHEN** 客户端请求 `GET /api/v1/projects`
+- **THEN** 服务端 MUST 只返回 actor 可访问的 ProjectSummary
+- **AND** 每项 MUST 至少包含 id、展示标题、archivedAt、updatedAt 和轻量统计
+
+#### Scenario: 更新 Project Target
+- **WHEN** 客户端请求 `PATCH /api/v1/projects/{projectId}` 并提交合法 Target
+- **THEN** 服务端 MUST 更新该 Project 的终极、短期和中期目标集合
+- **AND** 不得修改 Thread、Message 或其他 Project
+
+#### Scenario: 永久删除 Project
+- **WHEN** 已授权 actor 明确请求永久删除 Project
+- **THEN** 服务端 MUST 按领域规范清理其 Thread、Message、MessageRun 和附属资源
+- **AND** 后续 Bootstrap MUST 返回 not_found
+
+### Requirement: 原子创建首个 Project 对话
+服务端 MUST 允许客户端用一条命令提交首条 user Message,并在同一数据库事务创建 Project、唯一 Root Thread、user Message、assistant Message 和 queued MessageRun。事务成功前不得唤醒模型执行;任一实体写入失败 MUST 回滚全部创建。
+
+请求 MUST 包含符合 AI SDK v7 UIMessage.parts 的 `initialMessage.parts`,并 MAY 包含 `requestedModelId`。请求不得包含新实体 ID。响应 MUST 返回全部创建实体、初始 ProjectArtifactSummary 和 AssistantRunState。服务端 MUST NOT 返回或构造 Web 页面 URL;页面路由由客户端根据响应中的 `project.id` 决定。
+
+#### Scenario: 首次发送成功
+- **WHEN** 客户端请求 `POST /api/v1/projects` 并提交合法 initialMessage
+- **THEN** 服务端 MUST 返回 201 及 Project、Root Thread、U1、A1 和 queued Run
+- **AND** 初始 ProjectArtifactSummary MUST 是 `{ changeSequence: 0, total: 0, byKind: {} }`
+- **AND** 数据库中不得存在缺少 Root Thread 或缺少 A1 Run 的部分 Project
+
+#### Scenario: 创建响应与 Web 路由解耦
+- **WHEN** 服务端成功创建首个 Project 对话
+- **THEN** CreationBundle MUST 包含可作为资源身份的 `project.id`
+- **AND** CreationBundle MUST NOT 包含 `canonicalUrl`、`pageUrl` 或其他客户端页面路径
+- **AND** Web 客户端 MUST 使用集中式路由构造器决定导航目标
+
+#### Scenario: 首次发送校验失败
+- **WHEN** initialMessage.parts 为空、非法或不符合允许的 UIMessage part 协议
+- **THEN** 服务端 MUST 返回 validation_error
+- **AND** 不得创建任何 Project 数据
+
+### Requirement: 提供 ProjectBootstrap Query
+服务端 MUST 提供 `GET /api/v1/projects/{projectId}/bootstrap`。响应 MUST 包含 Project DTO、全量轻量 ThreadTopologyItem、ProjectArtifactSummary、唯一 Root Thread 的 MessageBundle,以及恢复这些 Message 所需的 AssistantRunState。Message 中可以包含 Artifact ID 引用,但 Bootstrap 不得返回 Artifact 正文。
+
+Bootstrap MUST NOT 返回 BaseContext、所有 Branch Message、全部 Project File/Artifact 正文或服务端 Prompt。Thread topology MUST 足以由客户端派生 Root、Child、depth 和 breadcrumb。
+
+ProjectArtifactSummary MUST 统计该 Project 的全部 Artifact,并 MUST 至少返回服务端单调 `changeSequence`、总数和按稳定 `kind` 聚合的数量。它不得退化为客户端已经按 ID 加载的 Artifact 数量;`total` 必须等于各 kind 计数之和。`changeSequence` 只用于拒绝乱序旧统计,不得成为 Command 请求参数或写入前置条件。
+
+#### Scenario: 加载现有 Project
+- **WHEN** actor 请求可访问 Project 的 Bootstrap
+- **THEN** 服务端 MUST 返回且只返回一个 Root Thread,并返回全部轻量 topology
+- **AND** Root MessageBundle MUST 按 sequence 升序排列有效 Message
+- **AND** ProjectArtifactSummary MUST 覆盖该 Project 全量 Artifact
+
+#### Scenario: Project 不存在
+- **WHEN** projectId 不存在或不可访问
+- **THEN** 服务端 MUST 返回 not_found
+- **AND** 不得创建空 Project 作为降级结果
+
+#### Scenario: 未加载 Branch Artifact 仍计入统计
+- **WHEN** Project 的 Branch Thread 中存在 3 个 Markdown Artifact,但 Bootstrap 不返回该 Branch 的 Message
+- **THEN** `artifactSummary.byKind.markdown` MUST 仍然等于 3
+- **AND** Root bundle MUST NOT 因此返回这 3 个 Artifact 的正文
+
+### Requirement: 按 Thread 提供 MessageBundle
+服务端 MUST 提供 `GET /api/v1/threads/{threadId}/messages`,并 MUST 通过 Thread 所属 Project 校验访问权。响应 MUST 返回有效 Message 与相关 AssistantRunState;默认最多返回最新 200 条,再按 sequence 升序输出。
+
+响应 MUST 包含 `hasOlderMessages` 和可供未来向前加载的边界 sequence。MVP 客户端可以不请求更早页面,但 API 不得让调用方通过下载整棵 Project 才能读取一个 Thread。
+
+P0 中,Markdown tool result MUST 在符合 AI SDK v7 的 Message part 中保存 `artifactId`,不得复制 Markdown 正文。客户端需要展示正文时 MUST 通过独立 Artifact Query 按 ID 加载。
+
+#### Scenario: Thread 少于 200 条有效 Message
+- **WHEN** 客户端读取包含 80 条有效 Message 的 Thread
+- **THEN** 服务端 MUST 返回全部 80 条并设置 `hasOlderMessages=false`
+
+#### Scenario: Thread 超过 200 条有效 Message
+- **WHEN** 客户端未指定边界读取包含超过 200 条有效 Message 的 Thread
+- **THEN** 服务端 MUST 返回最新 200 条并按 sequence 升序排列
+- **AND** 必须设置 `hasOlderMessages=true` 和更早页面边界
+
+### Requirement: 提供 Thread 元数据命令
+服务端 MUST 允许授权 actor 更新 Branch Thread 的 customTitle,并 MUST 提供归档和取消归档 Thread 的显式命令。Root Thread 的展示标题 MUST 继续来自 Project,客户端不得通过 Thread metadata 命令为 Root 建立第二套标题权威。
+
+归档 Thread MUST 保留其 Message、Child Thread、Fork 来源和 BaseContext;它只改变默认导航可见性,不得等同于永久删除。
+
+#### Scenario: 重命名 Branch Thread
+- **WHEN** 客户端请求 `PATCH /api/v1/threads/{threadId}` 并提交合法 customTitle
+- **THEN** 服务端 MUST 更新该 Branch Thread 标题并返回最新 Thread DTO
+- **AND** 不得修改 Project 标题
+
+#### Scenario: 归档 Thread
+- **WHEN** 客户端请求 `POST /api/v1/threads/{threadId}/archive`
+- **THEN** 服务端 MUST 设置 archivedAt 并保留完整 Thread 内容和后代关系
+
+### Requirement: 原子发送后续消息并启动生成
+服务端 MUST 提供在既有 Thread 中发送 user Message 的常用原子命令。该命令 MUST 在同一事务创建 user Message、assistant Message 和 queued MessageRun,并返回三者;模型执行只能在事务提交后启动。
+
+Path MUST 包含已有 `threadId`;Body MUST 只包含合法 user `parts` 和可选 `requestedModelId`。服务端 MUST 根据当前有效历史构造 Prompt,客户端不得提交 Prompt History、BaseContext、待创建 ID 或整棵 Project 状态。
+
+P0 MUST NOT 额外提供“只创建 user Message、不创建 assistant Message 与 MessageRun”的发送命令。该限制只是当前 API 能力边界,不得被实现为 user/assistant 必须角色交替的数据库约束。
+
+P0 MUST NOT 额外提供“只创建 user Message、不创建 assistant Message 与 MessageRun”的发送命令。该限制只是当前 API 能力边界,不得被实现为 user/assistant 必须角色交替的数据库约束。
+
+#### Scenario: 在 Root Thread 发送消息
+- **WHEN** 客户端请求 `POST /api/v1/threads/{threadId}/messages` 并提交合法 user parts
+- **THEN** 服务端 MUST 分配新的 sequence 并返回 user Message、assistant Message 和 queued Run
+- **AND** 响应中的所有新 ID MUST 由服务端生成
+
+#### Scenario: 同 Thread 已有运行中生成
+- **WHEN** 当前产品策略不允许同一 Thread 并发生成且已有 queued/running Run
+- **THEN** 服务端 MUST 返回 `thread_generation_in_progress`
+- **AND** 不得依赖客户端 busy 状态作为唯一保护
+
+### Requirement: 原子创建 Fork Thread
+服务端 MUST 提供从已有 sourceThreadId 和 sourceMessageId 创建 Child Thread 的命令。请求 MAY 包含用户选区 anchor/quote,但 MUST NOT 包含 BaseContext、ForkSourceSnapshot 的权威字段或 Child Thread ID。
+
+服务端 MUST 验证来源资格、同 Project 关系和无环约束,在一个事务中计算并持久化 BaseContext、ForkSourceSnapshot 和 Child Thread,然后返回新 Thread DTO。
+
+#### Scenario: 从 completed assistant Message Fork
+- **WHEN** 客户端请求 `POST /api/v1/threads/{sourceThreadId}/forks` 且 sourceMessage 合格
+- **THEN** 服务端 MUST 返回 201 和服务端创建的 Child Thread
+- **AND** Child Thread 必须属于同一 Project并冻结到来源 Message 的 BaseContext
+
+#### Scenario: 从 running assistant Message Fork
+- **WHEN** sourceMessage 的 Run 为 queued 或 running
+- **THEN** 服务端 MUST 返回 `fork_source_not_finalized`
+- **AND** 不得创建部分 Child Thread
+
+### Requirement: 使用 replacement 命令实现 Edit 和 Regenerate
+服务端 MUST 分别提供 Edit 最后一条有效 user Message 和 Regenerate 当前可重新生成 assistant Message 的命令。两种命令 MUST 创建服务端 ID 的 replacement Message;Regenerate 以及 Edit 后的新回答 MUST 同时创建新的 queued MessageRun,且不得覆盖旧 Message.parts 或 sequence。
+
+Edit 请求 MUST 只提交 sourceUserMessageId、新 parts 和可选 requestedModelId。Regenerate 请求 MUST 只提交 sourceAssistantMessageId 和可选 requestedModelId。响应 MUST 返回被 superseded 的 Message ID、全部新增 Message 和新的 AssistantRunState。
+
+#### Scenario: Regenerate 当前 assistant Message
+- **WHEN** 客户端请求 `POST /api/v1/messages/{assistantMessageId}/regenerations` 且来源可重新生成
+- **THEN** 服务端 MUST 返回 replacement assistant Message 和新的 queued Run
+- **AND** 来源 Message 的内容与 sequence 必须保持不变
+
+#### Scenario: Edit 最后一条 user Message
+- **WHEN** 客户端请求 `POST /api/v1/messages/{userMessageId}/edits` 并提交新 parts
+- **THEN** 服务端 MUST 返回 replacement user Message、replacement assistant Message、queued Run 和被 superseded 的后缀 ID
+
+#### Scenario: Edit 历史 user Message
+- **WHEN** sourceUserMessageId 不是当前 Thread 最后一条有效 user Message
+- **THEN** 服务端 MUST 返回 `fork_required`
+- **AND** 不得修改任何既有 Message
+
+### Requirement: 通过 assistantMessageId 管理生成生命周期
+服务端 MUST 允许客户端使用 assistantMessageId 查询、通过 SSE 订阅并停止对应 MessageRun,而不要求客户端理解内部 MessageRun ID。SSE 事件流 MUST 使用严格递增的 eventSequence,并 MUST 支持 `afterEventSequence` 恢复。每次连接 MUST 先返回当前持久化 checkpoint snapshot 及其 cursor;若仍在运行,后续 live event MUST 严格大于该 cursor。旧 token delta MAY 不逐条重放,但恢复结果不得丢失已经持久化的生成内容。
+
+Stop MUST 是显式 Command;连接关闭、页面刷新或取消订阅不得自动停止 Run。终态事件 MUST 携带或允许随后取得 finalized Message 和终态 AssistantRunState。`run.snapshot` 与 `run.completed` MUST 携带当前 ProjectArtifactSummary,使客户端能够在刷新、重连和 Artifact 生成完成后校正 Project 页面统计;客户端不得依赖事件次数自行累加。
+
+#### Scenario: 恢复 running 事件流
+- **WHEN** 客户端请求 assistant Message 事件且提交 `afterEventSequence=42`
+- **THEN** 服务端 MUST 先发送不旧于该游标的当前 checkpoint snapshot
+- **AND** 后续 live event MUST 大于 snapshot cursor
+- **AND** 不得重新启动第二个 MessageRun
+
+#### Scenario: 显式停止生成
+- **WHEN** 客户端请求 `POST /api/v1/assistant-messages/{assistantMessageId}/stop`
+- **THEN** 服务端 MUST 对 queued/running Run 记录 stop 请求并返回最新 AssistantRunState
+- **AND** 重复 Stop MUST 返回相同终态或当前状态而不创建新 Run
+
+#### Scenario: 仅断开订阅
+- **WHEN** 浏览器关闭事件连接但未发送 Stop
+- **THEN** MessageRun MUST 继续由服务端执行
+
+### Requirement: 按 ID 加载 Artifact
+服务端 MUST 提供 `GET /api/v1/artifacts/{artifactId}`,并 MUST 通过 Artifact 所属 Project 校验访问权。Message、ThreadMessageBundle、ProjectBootstrap 和生成事件 MUST 只通过 `artifactId` 引用 Artifact,不得复制 Markdown 正文。
+
+#### Scenario: 打开 Markdown Artifact
+- **WHEN** 用户打开 Message tool result 引用的 Markdown Artifact
+- **THEN** 客户端 MUST 使用 `artifactId` 请求 Artifact Query
+- **AND** 服务端 MUST 返回对应 Artifact 的完整内容
+
+#### Scenario: 未打开 Artifact
+- **WHEN** 客户端只加载 ProjectBootstrap 或 ThreadMessageBundle,但用户没有打开 Artifact
+- **THEN** 服务端 MUST NOT 返回 Artifact 正文
+
+### Requirement: 提供 Message feedback 命令
+服务端 MUST 允许客户端按 assistantMessageId 设置 positive、negative 或 null feedback。只有 finalized 且允许评价的 assistant Message 可以接受 feedback;命令不得修改 Message 内容或 MessageRun。
+
+#### Scenario: 设置正向反馈
+- **WHEN** 客户端请求 `PUT /api/v1/messages/{assistantMessageId}/feedback` 并提交 `positive`
+- **THEN** 服务端 MUST 返回该 Message 最新 feedback
+
+#### Scenario: 评价 user Message
+- **WHEN** 目标 Message 不是 assistant Message
+- **THEN** 服务端 MUST 返回 `message_not_feedback_eligible`

From 3238a5b65b9d4c4b06fed60604287a1be8908162 Mon Sep 17 00:00:00 2001
From: zilin <276161014@qq.com>
Date: Tue, 25 Aug 2026 21:42:06 +0800
Subject: [PATCH 19/23] fix(thread-chat): synchronize model selection

---
 .../normalized/project-view-model.ts          |  28 +++-
 .../normalized/thread-chat-new.tsx            |  28 ++--
 .../normalized/thread-chat-project.tsx        | 132 +++++++++++++-----
 tests/client/providers.test.tsx               |  45 ++++++
 4 files changed, 182 insertions(+), 51 deletions(-)

diff --git a/app/thread-chat/normalized/project-view-model.ts b/app/thread-chat/normalized/project-view-model.ts
index 894670d0..cdbf6bf8 100644
--- a/app/thread-chat/normalized/project-view-model.ts
+++ b/app/thread-chat/normalized/project-view-model.ts
@@ -24,6 +24,22 @@ export interface ArtifactHint {
   sourceMessageId?: string
 }
 
+export function withThreadModel(
+  state: ThreadTreeState,
+  threadId: string,
+  modelId: string
+): ThreadTreeState {
+  const thread = state.threads[threadId]
+  if (!thread || thread.modelId === modelId) return state
+  return {
+    ...state,
+    threads: {
+      ...state.threads,
+      [threadId]: { ...thread, modelId },
+    },
+  }
+}
+
 export function textFromParts(parts: readonly unknown[] | null | undefined) {
   return (parts ?? [])
     .flatMap((part) => {
@@ -106,7 +122,8 @@ function threadDepths(state: ThreadChatProjectStore) {
     memo.set(threadId, depth)
     return depth
   }
-  for (const threadId of Object.keys(state.entities.threadsById)) visit(threadId)
+  for (const threadId of Object.keys(state.entities.threadsById))
+    visit(threadId)
   return memo
 }
 
@@ -131,9 +148,7 @@ function modelForThread(state: ThreadChatProjectStore, threadId: string) {
 }
 
 function sourceAnchor(quote: string | undefined): TextAnchor | undefined {
-  return quote
-    ? { quote: { exact: quote, prefix: "", suffix: "" } }
-    : undefined
+  return quote ? { quote: { exact: quote, prefix: "", suffix: "" } } : undefined
 }
 
 export function projectLegacyTreeView(
@@ -253,7 +268,10 @@ export function projectLegacyTreeView(
     artifacts[hint.id] = {
       id: hint.id,
       title: loaded?.title ?? hint.title,
-      kind: loaded?.kind === "code" || loaded?.kind === "note" ? loaded.kind : "markdown",
+      kind:
+        loaded?.kind === "code" || loaded?.kind === "note"
+          ? loaded.kind
+          : "markdown",
       content: loaded ? artifactContent(loaded.content) : "",
       sourceThreadId: sourceMessage?.threadId ?? hint.sourceThreadId ?? "",
       sourceMessageId: loaded?.sourceMessageId ?? hint.sourceMessageId ?? "",
diff --git a/app/thread-chat/normalized/thread-chat-new.tsx b/app/thread-chat/normalized/thread-chat-new.tsx
index c4afcb1a..bf693098 100644
--- a/app/thread-chat/normalized/thread-chat-new.tsx
+++ b/app/thread-chat/normalized/thread-chat-new.tsx
@@ -1,7 +1,7 @@
 "use client"
 
 import dynamic from "next/dynamic"
-import { useMemo, useRef, useState } from "react"
+import { useEffect, useMemo, useRef, useState } from "react"
 import { DEFAULT_THREAD_CHAT_MODEL_ID } from "@/constants/model"
 import {
   useNewProjectDraftStore,
@@ -21,6 +21,7 @@ import {
 } from "../orchestration/overlays/workspace-toast"
 import { ArtifactDrawer } from "../orchestration/artifacts/artifact-drawer"
 import { ProjectList } from "./project-list"
+import { withThreadModel } from "./project-view-model"
 
 const NEW_PROJECT_VIEW = emptySeedState()
 const EMPTY_ACTION_VIEW = {
@@ -45,15 +46,11 @@ export function ThreadChatNew() {
   const submit = useSubmitNewProjectDraft()
   const status = useNewProjectDraftStore((state) => state.status)
   const error = useNewProjectDraftStore((state) => state.error)
-  const setDraftParts = useNewProjectDraftStore(
-    (state) => state.setDraftParts
-  )
+  const setDraftParts = useNewProjectDraftStore((state) => state.setDraftParts)
   const setRequestedModelId = useNewProjectDraftStore(
     (state) => state.setRequestedModelId
   )
-  const [modelId, setModelId] = useState(
-    DEFAULT_THREAD_CHAT_MODEL_ID
-  )
+  const [modelId, setModelId] = useState(DEFAULT_THREAD_CHAT_MODEL_ID)
   const [hintDismissed, setHintDismissed] = useState(false)
   const [viewMode, setViewMode] = useState<"columns" | "canvas">("columns")
   const [forceCols, setForceCols] = useState(null)
@@ -61,6 +58,21 @@ export function ThreadChatNew() {
     "replace"
   )
   const [canvasStore] = useState(() => createThreadStore(emptySeedState()))
+  const projected = useMemo(
+    () => withThreadModel(NEW_PROJECT_VIEW, "main", modelId),
+    [modelId]
+  )
+  useEffect(
+    () =>
+      canvasStore.subscribe(() =>
+        setModelId(canvasStore.getState().threads.main.modelId)
+      ),
+    [canvasStore]
+  )
+  useEffect(
+    () => canvasStore.setThreadModel("main", modelId),
+    [canvasStore, modelId]
+  )
   const canvasViewState = useMemo(() => ({ pins: new Map() }), [])
   const { windowWidth } = useColumnViewport()
   const {
@@ -117,7 +129,7 @@ export function ThreadChatNew() {
           colsRef={colsRef}
           renderThread={() => (
              message.id)
     activePathByThreadId.set(thread.id, ids)
-    const latestUser = thread.messages.findLast((message) => message.role === "user")
+    const latestUser = thread.messages.findLast(
+      (message) => message.role === "user"
+    )
     const latestAssistant = thread.messages.findLast(
       (message) => message.role === "assistant"
     )
@@ -130,7 +124,6 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
   const state = useThreadChatStore((snapshot) => snapshot)
   const bootstrapReady = state.requests.bootstrap.status === "ready"
   useProjectWorkbench(runtime, bootstrapReady)
-  const projected = useMemo(() => projectLegacyTreeView(state), [state])
   const [rootModelOverride, setRootModelId] = useState(null)
   const root = Object.values(state.entities.threadsById).find(
     (thread) => thread.parentThreadId === null
@@ -143,6 +136,10 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
     : undefined
   const rootModelId =
     rootModelOverride ?? latestRootModelId ?? DEFAULT_THREAD_CHAT_MODEL_ID
+  const projected = useMemo(() => {
+    const view = projectLegacyTreeView(state)
+    return root ? withThreadModel(view, root.id, rootModelId) : view
+  }, [root, rootModelId, state])
   const projectedStore = useProjectedThreadStore(
     state,
     projected,
@@ -169,7 +166,8 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
       onPinsChange: (pins: ReadonlyMap) => {
         const current = runtime.store.getState().ui.canvasPins
         for (const threadId of Object.keys(current))
-          if (!pins.has(threadId)) runtime.store.getState().setCanvasPin(threadId, null)
+          if (!pins.has(threadId))
+            runtime.store.getState().setCanvasPin(threadId, null)
         for (const [threadId, point] of pins)
           runtime.store.getState().setCanvasPin(threadId, point)
       },
@@ -210,7 +208,12 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
     ].filter((threadId): threadId is string => Boolean(threadId))
     for (const threadId of visible)
       void runtime.commands.ensureThreadMessages(threadId)
-  }, [bootstrapReady, runtime, state.entities.threadsById, state.ui.columnSlots])
+  }, [
+    bootstrapReady,
+    runtime,
+    state.entities.threadsById,
+    state.ui.columnSlots,
+  ])
 
   useEffect(() => {
     const expanded = state.ui.columnSlots.filter((slot) => !slot.folded)
@@ -286,7 +289,10 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
   const messageCommands = useMemo(
     () => ({
       async retryAssistant(threadId, assistantMessageId) {
-        await runtime.commands.regenerateMessage(assistantMessageId, rootModelId)
+        await runtime.commands.regenerateMessage(
+          assistantMessageId,
+          rootModelId
+        )
         const error = scopeError(
           runtime.store.getState(),
           `regenerate:${assistantMessageId}`
@@ -313,7 +319,10 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
           [{ type: "text", text }],
           rootModelId
         )
-        const error = scopeError(runtime.store.getState(), `edit:${userMessageId}`)
+        const error = scopeError(
+          runtime.store.getState(),
+          `edit:${userMessageId}`
+        )
         return error
           ? { ok: false, code: "network_error", message: error }
           : {
@@ -332,7 +341,8 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
       },
       async submitFeedback(threadId, messageId, feedback) {
         await runtime.commands.setFeedback(messageId, feedback)
-        const value = runtime.store.getState().entities.feedbackByMessageId[messageId]
+        const value =
+          runtime.store.getState().entities.feedbackByMessageId[messageId]
         return value?.value
           ? {
               messageId,
@@ -364,7 +374,9 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
       hint?: PlacementHint,
       question?: string
     ) => {
-      const before = new Set(Object.keys(runtime.store.getState().entities.threadsById))
+      const before = new Set(
+        Object.keys(runtime.store.getState().entities.threadsById)
+      )
       const sourceSlotId = slotIdForThread(selection.threadId)
       await runtime.commands.forkThread({
         sourceSlotId,
@@ -397,15 +409,29 @@ export function ThreadChatProject({ projectId }: { projectId: string }) {
       if (question?.trim()) await send(created.id, question.trim())
       if (state.ui.viewMode === "canvas") {
         setFocusNode({ id: created.id, n: ++focusSequence.current })
-        showToast(`已开启分支 · ${projected.threads[created.id]?.title ?? "新分支"}`)
+        showToast(
+          `已开启分支 · ${projected.threads[created.id]?.title ?? "新分支"}`
+        )
       } else {
         flash(created.id)
       }
     },
-    [flash, maxExpanded, projected.threads, runtime, send, showToast, slotIdForThread, state.ui.viewMode]
+    [
+      flash,
+      maxExpanded,
+      projected.threads,
+      runtime,
+      send,
+      showToast,
+      slotIdForThread,
+      state.ui.viewMode,
+    ]
   )
 
-  if (state.requests.bootstrap.status === "idle" || state.requests.bootstrap.status === "loading")
+  if (
+    state.requests.bootstrap.status === "idle" ||
+    state.requests.bootstrap.status === "loading"
+  )
     return (
       
对话加载中…
@@ -435,7 +461,10 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { const hintNode = hintVisible ? ( setHintDismissed(true)} /> ) : null - const branchCount = Math.max(0, Object.keys(state.entities.threadsById).length - 1) + const branchCount = Math.max( + 0, + Object.keys(state.entities.threadsById).length - 1 + ) const markdownCount = state.readModels.artifactSummary?.byKind.markdown ?? 0 const activeArtifactLoadState = activeArtifactId ? state.requests.artifactById[activeArtifactId] @@ -446,7 +475,8 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { if (mode.kind === "column") { const slot = state.ui.columnSlots[mode.vpIndex] if (!slot) return - if (row.id === rootThread?.id) runtime.store.getState().closeColumn(slot.slotId) + if (row.id === rootThread?.id) + runtime.store.getState().closeColumn(slot.slotId) else runtime.store.getState().switchColumnThread(slot.slotId, row.id) flash(row.id) return @@ -480,7 +510,9 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { onOpenHelp={openHelpPanel} onShowColumns={() => runtime.store.getState().setViewMode("columns")} onShowCanvas={() => runtime.store.getState().setViewMode("canvas")} - onForceCols={(count) => runtime.store.getState().setForceColumnCount(count)} + onForceCols={(count) => + runtime.store.getState().setForceColumnCount(count) + } onPlacementModeChange={(mode) => { runtime.store.getState().setPlacementMode(mode) if (mode === "replace") @@ -500,13 +532,18 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { flashId={flashId} colsRef={colsRef} onExpandStrip={(threadId) => { - const slot = runtime.store.getState().ui.columnSlots.find( - (candidate) => candidate.threadId === threadId - ) - if (slot) runtime.store.getState().setColumnFolded(slot.slotId, false) + const slot = runtime.store + .getState() + .ui.columnSlots.find( + (candidate) => candidate.threadId === threadId + ) + if (slot) + runtime.store.getState().setColumnFolded(slot.slotId, false) }} onCommitWidths={(patch) => { - const widthsBySlot: Partial> = {} + const widthsBySlot: Partial< + Record<"root" | string, number | null> + > = {} for (const [threadId, width] of Object.entries(patch)) widthsBySlot[slotIdForThread(threadId)] = width runtime.store.getState().commitColumnWidths(widthsBySlot) @@ -520,7 +557,10 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { runtime.store.getState().commitColumnWidths(reset) }} renderThread={(threadId, viewportIndex) => { - const slotId = viewportIndex < 0 ? "root" : state.ui.columnSlots[viewportIndex]?.slotId + const slotId = + viewportIndex < 0 + ? "root" + : state.ui.columnSlots[viewportIndex]?.slotId if (!slotId) return null const columnView = selectThreadColumnView(state, slotId) const headerView = selectThreadColumnHeaderView(state, slotId) @@ -530,7 +570,9 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { ) : columnView.status === "error" ? ( @@ -545,8 +587,13 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { openThread(target, threadId)} onOpenArtifact={(artifactId) => { void runtime.commands.ensureArtifact(artifactId) @@ -576,10 +623,15 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { openColumnSwitcher(viewportIndex, button) } onOpenSubtree={(button) => openSubtree(threadId, button)} - onCollapse={() => runtime.store.getState().closeColumn(slotId)} + onCollapse={() => + runtime.store.getState().closeColumn(slotId) + } busy={busy} onRetry={(message) => - void runtime.commands.regenerateMessage(message.id, rootModelId) + void runtime.commands.regenerateMessage( + message.id, + rootModelId + ) } onStop={() => { const id = activeAssistantId(threadId) @@ -587,7 +639,9 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { }} composerPrefill={ projected.threads[threadId]?.messages.length === 0 - ? kickoffQuestion(projected.threads[threadId]?.anchorText ?? "") + ? kickoffQuestion( + projected.threads[threadId]?.anchorText ?? "" + ) : undefined } onModelChange={setRootModelId} @@ -628,7 +682,9 @@ export function ThreadChatProject({ projectId }: { projectId: string }) { slots={state.ui.viewMode === "canvas" ? EMPTY_SLOTS : slots} mode={state.ui.placementMode} maxExpanded={maxExpanded} - lastActiveOf={(threadId) => projected.threads[threadId]?.lastActive ?? 0} + lastActiveOf={(threadId) => + projected.threads[threadId]?.lastActive ?? 0 + } /> {treeList && ( diff --git a/tests/client/providers.test.tsx b/tests/client/providers.test.tsx index 2f66928c..9582e532 100644 --- a/tests/client/providers.test.tsx +++ b/tests/client/providers.test.tsx @@ -13,6 +13,16 @@ vi.mock("next/navigation", () => ({ usePathname: () => navigationMocks.pathname, })) +vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + } +) +HTMLElement.prototype.scrollIntoView = vi.fn() + import { useAppShellCommands, useNewProjectDraftStore, @@ -34,6 +44,7 @@ import { userMessageDTOFixture, } from "../fixtures/thread-chat-api-fixtures" import { createTestApi } from "./test-api" +import { ThreadChatNew } from "@/app/thread-chat/normalized/thread-chat-new" const bootstrap = { project: projectDTOFixture, @@ -55,6 +66,40 @@ beforeEach(() => { }) describe("ThreadChat Providers and hooks", () => { + it("/new 切换模型后更新选择器并把模型提交给创建命令", async () => { + const createProject = vi.fn().mockResolvedValue(creationBundleDTOFixture) + const user = userEvent.setup() + + render( + + + + + + ) + + const selector = screen.getByRole("combobox", { name: "选择对话模型" }) + await user.click(selector) + await user.click( + screen.getByRole("option", { name: "UMAPIS · GPT-5.6 Sol" }) + ) + expect(selector.textContent).toContain("UMAPIS · GPT-5.6 Sol") + + await user.type( + screen.getByPlaceholderText("继续在主线提问…"), + "验证模型切换" + ) + await user.click(screen.getByRole("button", { name: "发送" })) + await waitFor(() => expect(createProject).toHaveBeenCalledTimes(1)) + expect(createProject).toHaveBeenCalledWith({ + parts: [{ type: "text", text: "验证模型切换" }], + requestedModelId: "umapis-gpt-5.6-sol", + }) + }) + it("ProjectProvider 每个 projectId 复用唯一 Runtime,并由 Hook 细粒度读取", async () => { const bootstrapProject = vi.fn().mockResolvedValue(bootstrap) const user = userEvent.setup() From 794c99e9c0f95b286598752c5c9e5574443fb74c Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 21:50:05 +0800 Subject: [PATCH 20/23] Document repository delivery standards --- AGENTS.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e4306bc5..702c97cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,3 +15,30 @@ project-wide instructions. `CLAUDE.md` is the single source of truth for shared development commands, workflow rules, architecture, and implementation notes. If an instruction in this file conflicts with `CLAUDE.md`, follow this file. + +## 交付物卫生规则 + +### 注释 + +- 注释只写「从代码本身看不出来的为什么」(non-obvious why) +- 禁止出现:修改历史、曾经的做法、"原本/之前/改为"、本次对话中讨论过的内容 +- 不为「没有做的东西」写注释或测试, + 唯一例外:该空缺会被后来者误认为 bug 并试图"修复"时, + 才允许一句话说明这是有意的(intentionally absent) + +### PR / Commit + +- 标题与描述只描述最终行为:这个 diff 让系统变成了什么 +- 禁止出现:被否决的方案、中间尝试、「应要求移除了 X」之类的痕迹 +- 用户的反馈已经体现在 diff 里,不需要文字复述 + +### 文档 / UI 文案 + +- 只服务产品和读者,禁止写入思考过程、实现理由、调试记录、下一步计划 +- 内容只保留最终状态,不带讨论痕迹 + +### 提交前自检(每行新增文本都要过一遍) + +问:「一个从没看过这场对话的工程师,只看最终代码, +这行文字还提供增量信息吗?」 +答「否」→ 删掉。 From 5c591e2c492bbebda40c719b11eb4d42a79624ab Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 22:02:04 +0800 Subject: [PATCH 21/23] docs: clarify user-facing architecture language --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1a7af2f5..d3531c54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 所有输出内容必须使用中文(代码、文件路径、命令等技术内容除外)。 +面向用户解释架构或状态变化时,使用“前端状态”“界面状态”“转换结果”等直白说法,禁止使用术语“投影”。 + ## Commands Package manager is **pnpm** (pnpm-lock.yaml / pnpm-workspace.yaml). From a326dfdcb386eaad36a0a4352e9757ca6e802ff6 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Tue, 25 Aug 2026 22:03:36 +0800 Subject: [PATCH 22/23] docs: standardize state mapping terminology --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index d3531c54..eaebeedd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 所有输出内容必须使用中文(代码、文件路径、命令等技术内容除外)。 -面向用户解释架构或状态变化时,使用“前端状态”“界面状态”“转换结果”等直白说法,禁止使用术语“投影”。 +面向用户解释架构或状态变化时,这类从后端状态转换为前端显示状态的过程统一称为“状态映射”,不要使用晦涩的同义术语。 ## Commands From 149983aacd3949b494a2c49b1864e14b2b7b5cf9 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Wed, 26 Aug 2026 14:49:09 +0800 Subject: [PATCH 23/23] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=8E=20new?= =?UTF-8?q?=20=E5=88=B0=E7=94=A8=E6=88=B7=E5=8F=91=E5=87=BA=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E6=B6=88=E6=81=AF=E3=80=81=E5=88=9B=E5=BB=BA=20projec?= =?UTF-8?q?t=20=E4=B9=8B=E5=90=8E=EF=BC=8C=E4=BC=9A=E7=AB=8B=E5=88=BB?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=90=8E=E5=8F=B0=E7=94=9F=E6=88=90=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../normalized/project-view-model.ts | 12 ++- constants/generation.ts | 2 +- lib/thread-chat/client/project-store.ts | 55 ++++++++++++- lib/thread-chat/client/types.ts | 1 + tests/client/store.test.ts | 77 +++++++++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) diff --git a/app/thread-chat/normalized/project-view-model.ts b/app/thread-chat/normalized/project-view-model.ts index cdbf6bf8..1d6a62c8 100644 --- a/app/thread-chat/normalized/project-view-model.ts +++ b/app/thread-chat/normalized/project-view-model.ts @@ -89,13 +89,21 @@ function statusOf( text: string ): { status?: MessageStatus; error?: string; backgroundGeneration?: boolean } { const run = state.runs.byAssistantMessageId[messageId] + const resumed = state.runs.resumedAssistantMessageIds[messageId] === true if (!run) return state.entities.messagesById[messageId]?.finalizedAt ? { status: "done" } : { status: "pending" } - if (run.status === "queued") return { status: "pending" } + if (run.status === "queued") + return { + status: "pending", + ...(resumed ? { backgroundGeneration: true } : {}), + } if (run.status === "running") - return { status: "streaming", backgroundGeneration: true } + return { + status: "streaming", + ...(resumed ? { backgroundGeneration: true } : {}), + } if (run.status === "completed" || (run.status === "stopped" && text)) return { status: "done" } return { diff --git a/constants/generation.ts b/constants/generation.ts index 7d956261..348b9e33 100644 --- a/constants/generation.ts +++ b/constants/generation.ts @@ -37,4 +37,4 @@ export const GENERATION_ERRORS = { streamFailed: "生成失败,请重试。", } as const -export const GENERATION_BACKGROUND_LABEL = "正在后台生成,完成后显示" +export const GENERATION_BACKGROUND_LABEL = "正在后台继续生成" diff --git a/lib/thread-chat/client/project-store.ts b/lib/thread-chat/client/project-store.ts index 0ec00321..105f1ab8 100644 --- a/lib/thread-chat/client/project-store.ts +++ b/lib/thread-chat/client/project-store.ts @@ -30,6 +30,7 @@ function initialState(): ThreadChatProjectState { runs: { byAssistantMessageId: {}, streamBuffersByAssistantMessageId: {}, + resumedAssistantMessageIds: {}, }, requests: { bootstrap: { status: "idle" }, @@ -92,6 +93,33 @@ function withMessages( } } +function markResumedRuns( + resumedAssistantMessageIds: Record, + runs: readonly AssistantRunState[] +): Record { + return { + ...resumedAssistantMessageIds, + ...Object.fromEntries( + runs + .filter((run) => run.status === "queued" || run.status === "running") + .map((run) => [run.assistantMessageId, true as const]) + ), + } +} + +function clearResumedRun( + resumedAssistantMessageIds: Record, + run: AssistantRunState +): Record { + if (run.status === "queued" || run.status === "running") + return resumedAssistantMessageIds + if (!resumedAssistantMessageIds[run.assistantMessageId]) + return resumedAssistantMessageIds + const next = { ...resumedAssistantMessageIds } + delete next[run.assistantMessageId] + return next +} + function isValidWidth(width: number | null): boolean { return ( width === null || (Number.isFinite(width) && width >= 120 && width <= 2_000) @@ -277,7 +305,13 @@ export function createThreadChatProjectStore(input: { ...merged.entities, project: bootstrap.project, }, - runs: merged.runs, + runs: { + ...merged.runs, + resumedAssistantMessageIds: markResumedRuns( + merged.runs.resumedAssistantMessageIds, + bootstrap.initialThread.assistantRuns + ), + }, requests: { ...state.requests, bootstrap: { status: "ready" }, @@ -322,7 +356,13 @@ export function createThreadChatProjectStore(input: { ) return { entities: merged.entities, - runs: merged.runs, + runs: { + ...merged.runs, + resumedAssistantMessageIds: markResumedRuns( + merged.runs.resumedAssistantMessageIds, + bundle.assistantRuns + ), + }, requests: { ...state.requests, threadMessagesById: { @@ -431,6 +471,7 @@ export function createThreadChatProjectStore(input: { ) return { runs: { + ...state.runs, byAssistantMessageId: { ...state.runs.byAssistantMessageId, [assistantMessageId]: { @@ -471,6 +512,7 @@ export function createThreadChatProjectStore(input: { return { entities, runs: { + ...state.runs, byAssistantMessageId: normalizeRuns({ current: state.runs.byAssistantMessageId, incoming: [run], @@ -484,6 +526,10 @@ export function createThreadChatProjectStore(input: { assistantMessageId !== run.assistantMessageId ) ), + resumedAssistantMessageIds: clearResumedRun( + state.runs.resumedAssistantMessageIds, + run + ), }, readModels: event.type === "run.snapshot" || event.type === "run.completed" @@ -507,6 +553,10 @@ export function createThreadChatProjectStore(input: { incoming: [run], messagesById: state.entities.messagesById, }), + resumedAssistantMessageIds: clearResumedRun( + state.runs.resumedAssistantMessageIds, + run + ), }, })) }, @@ -527,6 +577,7 @@ export function createThreadChatProjectStore(input: { } return { runs: { + ...state.runs, byAssistantMessageId: { ...state.runs.byAssistantMessageId, [assistantMessageId]: { ...run, checkpointParts }, diff --git a/lib/thread-chat/client/types.ts b/lib/thread-chat/client/types.ts index c4810b3d..3e68444b 100644 --- a/lib/thread-chat/client/types.ts +++ b/lib/thread-chat/client/types.ts @@ -112,6 +112,7 @@ export interface StreamBuffer { export interface ThreadChatRunsState { byAssistantMessageId: Record streamBuffersByAssistantMessageId: Record + resumedAssistantMessageIds: Record } export interface ThreadChatRequestsState { diff --git a/tests/client/store.test.ts b/tests/client/store.test.ts index 2d0345b3..a53d916d 100644 --- a/tests/client/store.test.ts +++ b/tests/client/store.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest" import { createThreadChatAppStore } from "@/lib/thread-chat/client/app-store" import { createThreadChatProjectStore } from "@/lib/thread-chat/client/project-store" +import { projectLegacyTreeView } from "@/app/thread-chat/normalized/project-view-model" import { selectForkAvailability, selectThreadColumnView, @@ -40,6 +41,82 @@ const branchThree = { } describe("ThreadChat client stores", () => { + it("区分本页新生成与页面加载后继续生成", () => { + const runningRun = { + ...assistantRunDTOFixture, + status: "running" as const, + } + const freshStore = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + freshStore.getState().mergeCreationBundle(creationBundleDTOFixture) + freshStore.getState().applyRunEvent({ + type: "run.snapshot", + cursor: 0, + run: runningRun, + message: assistantMessageDTOFixture, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + }) + expect( + projectLegacyTreeView(freshStore.getState()).threads[ + rootThreadDTOFixture.id + ].messages.at(-1) + ).toMatchObject({ status: "streaming" }) + expect( + projectLegacyTreeView(freshStore.getState()).threads[ + rootThreadDTOFixture.id + ].messages.at(-1)?.backgroundGeneration + ).toBeUndefined() + + const resumedStore = createThreadChatProjectStore({ + projectId: projectDTOFixture.id, + }) + resumedStore.getState().mergeBootstrap({ + project: projectDTOFixture, + threadTopology: [rootThreadDTOFixture], + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + initialThread: { + threadId: rootThreadDTOFixture.id, + messages: [userMessageDTOFixture, assistantMessageDTOFixture], + assistantRuns: [runningRun], + hasOlderMessages: false, + oldestReturnedSequence: 1, + newestReturnedSequence: 2, + }, + }) + expect( + projectLegacyTreeView(resumedStore.getState()).threads[ + rootThreadDTOFixture.id + ].messages.at(-1) + ).toMatchObject({ + status: "streaming", + backgroundGeneration: true, + }) + + resumedStore.getState().applyRunEvent({ + type: "run.completed", + eventSequence: 1, + run: { + ...runningRun, + status: "completed", + checkpointParts: [{ type: "text", text: "done" }], + eventSequence: 1, + finishedAt: "2026-08-25T00:01:00.000Z", + }, + message: { + ...assistantMessageDTOFixture, + parts: [{ type: "text", text: "done" }], + finalizedAt: "2026-08-25T00:01:00.000Z", + }, + artifactSummary: { changeSequence: 0, total: 0, byKind: {} }, + }) + expect( + resumedStore.getState().runs.resumedAssistantMessageIds[ + assistantMessageDTOFixture.id + ] + ).toBeUndefined() + }) + it("App Store 合并稳定排序的摘要且不保存 selectedProjectId", () => { const store = createThreadChatAppStore() expect(store.getState().catalog.loadState.status).toBe("idle")

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ℜ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 HNh^ZFW literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/collapsed-column.png new file mode 100644 index 0000000000000000000000000000000000000000..dacf6a94d21414f7239c76144935d531a5ae37a7 GIT binary patch literal 175285 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%0DYcei!96ilLAI+{AwammQv_^ac<8J+sl)! zv7Gdu<=9hw6&Z6(s_ z1>8Z}c<(=$Uk@aoY%WIHlpW*bj8h?DjP}$u^N^Q-7qPhqO%S{+E1fX5Ma&&$jyQN+ z03ZdJn>aQYP3BQ8G7%ROX~__ik(oR}Lx*9Ecl*ZuS-^>7b!**g)1{!GS{W{5KP(kJ zB2q{XVt3g@q_%^1@d)h*6@y&bM|CCKY3ICve$jbj7jG6+J%Zc9B4S5vrnrQJsEH}` zfHFJY1E-_Loex1#meAlJqZR9--BIc9`~Cy?sht%QI z(BPBLc>eO~b@)2oPqUXz7diHA$X>nUfBBjna%lp!G1aZ#K7TbyR{uaV37pFv?c=b! zpL0F^^y3H2RX_5xV0y?F7Rfem`~l?~2@QRF6x$qYi$*e?J^7y-*OYhn-n|3q3JZwJ zG$q&AP4sIzrKR=TW0pE9ow4cZ%?``POZYR=_zy8vhEy)*j{wdivOhx=TGqXHmq(4& zktcvteCIjTml56D)9c>dWwQ*EgdP~gLnWg63`mEW#jDSr5f2qL^PKW~VwsQTnx4LK zZe#ukH>f<)CV$KXf}x?No|SLk(JFZ=0S*3Q;N#Ni3~CPY?X!-t*@hi24)Rl6+bb(w zjy-a7Wt@$d#`8w%#{vM!HdrVgjno=-l_$c-1!hADOotj98yBau-`<|Ye+ZB}=gH=^(V`@1RR}@ap!&)YD9~;trKgkK;Jx{|VI^?ad>khG zui@puq*Yh(z$2|(g_WT$jQK|$y+>JpdCnvbvc?2(hFuGY!U(=T&^KAOqeYpLlp znsHet=Ly-(L?ncTg(-!_C9qE!VT*^14>h${G-8G(6q>FQl0U7-4adGMulkPWS?P2# z`DiGxJ?`;RsowSwrFhf><>v0OD;QUDa&oZS^!=Mhz%_M7Q+n-xEs|3QNOX<5%N&N+ zf0p1r2ucgPpQuq%)G(bpvU*>GJaq}+*>Wh>KJ=HyB)^17l$?=JdrX)ItrJJb9QnrDbYRct33U7ovpgPADUl_V7^9LdkneuIwocmrZyzv zIE0H{adC6onT)4o#<%Ij6c%f3KnTj|hrhJ8w#K4oZ7H*vGgYrhr^&$VS}Flcq!Y|PNuE|XAMPAtk?vh`ooyL}p-v!cF(2l1 z6iX*u#ijX@n0WEhMQEK8e&u&R?Qn+_C}(^7&IEX*`s&E@n&-jMl1f? z(hUrZlvGtu=MF|nEz;gf?=vMf>zuODbJO#{^1fI_P8Je!GeP^ayXrG>d2wms4PH9u zLCALOlKyD_mX8OwbzZaO+ZP4h<3_4@zhH4wcnU8t`4J2^5%FEj5#W4-G7SZNwOz-h z+f3H4`gSOHNPtBTyuG?PM)gXk#p#q*RW@lcGfUW;U;?CIrsMXa@F8Ri3WdFdtB!VvCl5eyCm10oEsdRdkvJZPb`r~X$;}U_nCFao z({SlkLTL9p=Mu64;$Ar;_YP$vp`Esi<4t=wqas7zF^c)dL(qJ_va?HiKu+!!Qhrjf z)Muqh@+dni;>}4*WZMW4Xd)bzc1%lm`8T2!vw*U)HqA1$eY6k0#?_$;*?W;$_9xf1 zjf^;*Y94SB6Z2S4e?LYiWHXt@o1cFabq%`WAdY09S(}+D&}r;iRyBu-uc`pquVS}O zD8QN%$%W`jSIz^LBa>aG&-2{1F{C4}_59x%WP1f7gehoaqubQu)M^* z@Q~Zh#~=Rs@fh1npe4>7cchvAMdB$TAt7Wsf^g}=0r%jZ<0Twc)BK?QEnTL{e zfsdRQTL8f)XlYpm)<`qzM(U;}8l)+`OWs)Jl*AYj`Ks-!$8szV1S0n?5O~6z*LXKv)C$!~=%d*&h#n=wP@Hh1aFa;W zHr?VC(O4dY(aBpupvo(inxuG?a_T~mPl7{8d&7>$n_441!s9Dtt+iKLd?w$x%l_mk zoQjzRNU##{@j30+>A&kqmkvI@wS?O%Dl8t#wN2^h)cLWux5uTic5OHD92B67hu#cjPVWpBSXDR|A*^4jSKHq*YXhCqH?aq&nK zAt^pOi`9{RQeLMX3dw0gO*CZC!QS3xBqTOAQ^6D=IlxRB zs{XY8Os_chkOwAv4>EOwNb#`o8!6Vt%gl9jrWx-Zx-1VRUQ27TUhOelDqP8&Q`>hE zv%Vf(uuG(DU*WJjIU9F7?t%Hq+iP=-Qk9<#Yk{^1^Eck1Tf8yMc4MGN0eOyWqNT2j zGoyy}aG{OkP9o5`rQwV+sh8utgg+ZJJd-3DC!e7b`q=YtJ-x40!VQtq z-BOYwV&yK!bzl@ICvoW95Un@V5tzYu+(#2&b+&Vv!|e#pKONn z_;KbTVMf(RiPgx+2p8At_S$%R>&gudUG0z0@Or!F`ifw3E{4r(a04 zF3zD}6co&KdZu#FrQ2Lg06jBKn};>{2y2$W1M~}YD&>$W^T89k>*B@ z$(w1x7NAn}`>YN41bl#2N~`0WOiGQ99w|4cCEt(kXP%?9xjpkjI!gm$1f8@t?x{f2 z5}CAaFQ^c?SiBs?DH>wGG^X?QC=7 z1sWGWBBy8j$_395H=3o^DW|Vb5TSUwqg7#-20T{_0U;NSXf#{rqp_WQ*2JTg*}SRkWzzBI{}9( zKz5%8pt%d((O<7$yS6kCk2<=DB4Eu;{Hw8%`pU+Joba<-3$_b=1<`TiNhae=*KW0e z(=_i;akE6%XO>>2@DmojY9Y~sPp)N!#BH&U*>xIzkVXXTElc}mow}yA7kODRXr@}H zz#;ye>g6DyE@i%~hAtZ!u(XQ}pC^}1@G3N>{DJjxr)Z=}_q4z2$ooeAnMq+&Y#zCw6$(sR$k0~iwYX=d1$)+k~2xZ*}q zUc^&$y3xG-gKguSGr}X8=XLzWrK!hOVW=cY7ag~#=zc&@^Ok&+L(k;Nr#{2**PdP@ z=Xe_wbORWsnguGo8#r~5@~JG&Lt8(7d;m?hCF^G-T|Fkr2*hta3i|UQHzbB&`rbUP zVP~a3Je_+Lt;Eq2OP4cR^4>ZrqegMV#b7GQ$s!pM%iQSU8K`Y}dbqb)M2d%vfI-~Q zQN}TXS>Io4W0JRfY`!wa-=EFh%j?S6Sir)VS26j$YN<^CDF;eRQw9aou#(V-i~E{o zdV0FE8+7~_E!&lrmIfBxV2&{!?rKL4H+hNZLx=n+2)E}YgZ6j^1Ny0oxT4Kthy3p| zlhXx^os~zPXmm(E)5XQ0(B01OnaAF1_KpyWcDrS@ z-uTvQeX?IyA$Ih+#n7N%5`-qTS;o%HnBucnG6ZnYuZdr~j!}l)UoAxeV7k;0NWSV| zuZDDS?n#!0SwTq&;lVX;nEVdL zlM|F{Vc9PUe?wZ2Z7s2Es?lXwI27Q#%!M zdQ(%glaW0s-#z77xK0oqY%d8}sG~~3ST?ov3K(FlY5nV{xx%?1ie{spbS-1!<{9&` z;vAsL;86KDo9JuoERR;}>F5kU_Khk%h#hsjFz0>hLb7KY8HeBsqgMZF8QKJ=dLmYR z05MQ!7qjE}71GskZ``n4i7KAE#@bfxiRsAgAop3|3L)VVcD1Kf{Q`h0AXciAyxrb9 z*OJnmIZi+_7+kQ_GrMU>odQnzlBLxX)(6=}p){EO{_<&ZJ1$4|Z68bW`GKbEJULCR zKZg&tmrxGghUUhl#raL+jm~;ce?F~e5|f`#ictD}R#}h6(+!y|Wi%?BObvB~p?Xe2 z%xOIzKmEk4`>V~IL`?C1$6(`ki<=yf1|~|p`e9@7#}BD@OIkJA=QWW*-4>@OXa`MQ zU2oguCV9Mq_PeD^Tu;w)?!t7B-nZB1^BF>tY#(b?)j&a`yF|j_f?qfZUn&ia_1JK^ zVVfqp#n#*=7>okjaG$*U)F`4qafUGPCkZEmw zWag(1?bd2h!V+P9)5DK6tvyEa4HFXGFP1PTHiuBw+irZO@clxOu2ebFmYLIX#F0)% zPkS5RtH%8Ho`B2CZ35PY+Nq|FojKLNjxR!V5~xCsc%{>CS)k787yi1YQ>7Q2l>R$qAV*4Si@7b# z2;1|4g{C^v)@}JY)7oEXXl2%`b*7s@yLJ~65WCGQA*>u}l$@9d_!4Cozt0J^Zgk-| zTUz~nU9&|2L7%%g`mL(i;L;@pc^DX9x>{7S#~$z_YnUjUBVYM8U(!=k;*|{5qMl@p zW8OIZ;_ES2FMAvMHLw#wcEG=oZFhw`z_Arj4de0Lr_Eo~N;a)SI@jI|=IJr0RzCHL z8WAjlr%HZgnb^z0dBex|osZ8N^Dgh!NC~PdhtG9=&7K75lRG__j_^XSZ`g`Gv^~-3 z_!Y0=m3GL>$;7)}Kj+Gew7wbY6zb#&r%IQ3QyGaJl%AVLFDl5$+~ln#e<=Y7TZu># zcH4k2W-dIHAbt%E4>#|xROd!HRE;A+5e&0WdMC}HnYw*k_`*pNaE*KHXS?MzZ*$Os z&uUY!F;(XkK0jBax1wU1FfWs>&>9t@oc&?~yng6gYe~uisD_CI?K=O_GW{a&RPQkj76ry5{iX8Xz1_nnP2*3+z*t6?t`)IVj(p36aM2%Ol+a(N`3i|AwD`KEDKi?@)>y1mu zZfo*VW^#yiX}(Ktcla{ToSOPVQ}*d=7YKhUZC9eeBnmdqrf}NRag}CRDboU$7z9Hg zt}p?owld&O5R9qN{!0JVr>d-7Ctf(+e*TL#)q+(Qk ziLI$ggjO+q@ywm5mLJYdI$hcic|2~ATEQ^BHNa$#9_i0T8XBemsbO&>G4Ji?s+nfR z2I?PUtasC?3a*-6C%3#0tr`4vLSV{Cb{1J7#8$tgnP>cx>f>V53~d~bYnWc;jO0fy zzQeXJpBpDBL>oN`49pqTLXvZ5 zCnY$txpl?+7UT>F^>)&)9lgQSp|AkA9gG+q8IFklD2#aCY zyJf7Lsr{W+10gEQbg&2>LFj~!0c|pQdYs*I-@kv43QBUIw&h;p3^%q{=hQ57@biC* zcDNgwB^5in#-xuKc?iD)wVu@Sk^40_bc`|A7v_;&iH3@8uVR(Qhnfnt_;U$!<4E5v zi3%zF=zpT(0J!j;-X7Fo?0CZ9*X?aoqQJ$3O|l%^y_F}ioS|If;Ji;uPR<8$`s2r) z79t)C22}MOAEZ)m$eXkd^|AyM?>!Q#@9fz_$HT zLIJkvDj78RI69dN^9HKDMP%&Wm^h`pT84(yRu?*R)zoH3kalru9HoSOs}XK)Qs$#y#Op{OmaRLq{`9=v$2!qT#7=X}?CcW6?9(cX<4pXy! z_KiN`GBQCWVNdF*!{f{T5|3iTwwuSvzm*6l4J zyOEBWnS^Qp*F4-OHtNmO`sfY&hgl89;*wiyYU@^q3Opr@chFYC8I+$ryS*O)SHd|X zKF47HTyoC<7K0bv-|9U2iWFK20Q3A~uC9W=4l#RuLYWNJLPtfzZyWz(8Jn0`yoe%s zcCdoh%z0rs*I`@afHomM#p0kR5{F8%i$uA=LH@Ptle9k5LbE}!oyqn%tFC*_H*j%p z>J0PuDQry6h#m+(nOlLP4y)lyyZ&Q_5Opv_}qgQ`w<^WRS>gwu+qCK$h7Srl) z&cYyYEB#E)t~OKE&f2(YfeLHWz!2MFNBmSuchVW26mzsGw7J&28wZ)46^TUhBFJ`j z&~5|=^`&b(wqO6z8B|Ha;#wNwyt|`*srjTRL%fHTJ<0gOCZ5CYVy(!*g_2laUc)Yd zOjr(Ng`K9q)9|Hi~+LixcH{#W98~dmlzvoV+E$G0dv)!)~pg{&;1dp z`kwy8i#;jT&`APFDu9G@yPmuq`Nr+I({gcEew{ zA9ZNt_R$-RXJ{7jjr1?`W^4--aJ!bpSikp?>}Gx=%YGKaD85;POaG>+`3h!`y%UA)$KlaRU2YuGr|$Vibv zb7deWIMKZpDs|8&*I6!jO}enKC*Il7k{>2Dqjlcz>Z zz-pGSRbBT`yAI%PRN! z7~y7&f-q)!flT%lk*?0=1AW)DUWlo`N=A1UnyQ*%dFSp9G!2#R0eXNQ2j_-+lIKJ1 z7$|7mS9^Ktg0{CyGmBv=X2F=Ttbgu1o72Jc0%XgF>f8SOZ`bf)^yhic_xQ?mjDc&^fFIQ zcpnUN@5F2-dv1rZx5926?h?QEea~evd3$$OY9nVbk+4J$?O-?Qmc;B|Mb7j0zovjq z82@_-AQ!z_u&1}zJKa%uVC;CQA?ehI%j+>R?gH1KrF#ig4}&I#;cC%L>y-_+$9(UbfreM&j(HHgE@E+ip}B#@ z5<2sqC|g$YxSl|oX#uZ2_Hf^sj5&k|0HMRT@d-WOc~DYO0bBMVJ-wg87sI@-);#wT z0^9tc{+j6bWG<09ILUggQ+=e~Ppd_&6*5BYC9za)1&&2#jlzv^qoGzPw+Gzhm{X8x zZ4^)-lzdLf^3BHs2@hmFY)v0u=I!dU-yDc;>Kye4i^hpL%Q5kn;42xMc}eYi`#z5C zjm=i{Z-%c~^ZVP*RJr?*a61UF=hyiX^Vj3;cjg=6P>EBKAzE^5sl7*N$K-=KkK-B#->>e(bLZUOoA0jg zALd`g(|;9Us#D=cpW*ggid2|rz1FqiLp&S@QLK)$?EFHt6_eKAm zhiB~#3yaClCW@)0PI-~&zKng{0@Gm+hGBuXHm7e^D5Wi_APTcT=)o^F`qTo6h_3Fg zg zVCJq^D~qGO{OC zT~uA2-bw|#+8jMltZdTJMEET@$7Cs?Vx`tR5YbYxGjWWbOWDXewcGhcI4B&gVcEyT z23QOzUwr0;8*&mFtCSfJlYGjKORzH|U`29Lvwo{bU|M%*#jb+7!}gnqoG-Sbe#Vr8%ei0Tik~Pr2{J zczAHCe`hrrcz?*m${Lz!Psox#!^37g6yi_kBI8nlNy7C)hmLrpNtvlieU>dGB)%{( zEUM1Dx8*>`r)E(iWO@w{U6XZa;YOLu@WmG@QP%a@1w3cn7teCST@X%ph<{70{hg&;&;E z98>4?^|{R0kvBv{Hw7qJL(RMLy=ZnBq;ojrB^4ESTZh?un!qdP|oR^$inw}*cjq|8LI47 zhfA6DqtNrRI#R%YV7q2+N@vBJ+tO>?ucoF3Z9vI0ye^vqjfQl)xEwFP;9by;>(pxN z^}|#&<6_0!SaMik2ppzZ)s}kkcR^j|Xbt&;55|LKl8h>xrt+o1d_4V-A%N2pat}i2 zdJy6*Fn&u5&#%yg@fGmAlXts^n!L#vF6(Ky}?b~ zX}oTFoT1tx^HB+5@r9m}sflW#hO94+Bv76E>JPZr zZ|WIzBkYTjM}! z`5U8W!}eHz&<+-W&qS-TAaecvWfk%vSv5n>Qv)M|O8bqst63K=UF?iaRZF}q2^VekZr&r&vu43W6BErW;pz7#FMK^kx@RjuR z^g_f%BxK~InuzkEL;Wi@Yk9xb)aXo9T^xy7ggi%2QQ@sCtJ=IfJC(0CCU<+{u>FrV zGuq$J7M+ljHC}Wp4zDTVd_V4fka1LxHOWI@I-HMV#=NnvA#me4RwwROU zT4;va*oaq76IOeWG1mzP2B0O3D$I>N4~46fha6E z>BXn4_UD<~VGPPu&I>a$g5lru^jfER78jNt4mwzR5hA_!Tt_&)J*V|A3`@y;6`gB~ zRK24a%CUM6b>;G&1LO&>Hk^xk;G$M95z`~rTWoIFd9;IdIG&sx2%9jg+}$S$2YLV;e=dG7m3gUaEYJxswLc%rt#LI((M}DDJeu;*6$_mB_ z>6{?NBdiaVla|M3NV7>(@C+crXE&oO(BZ_*2l>tHBN=IVVm9mKY|{^Gyt+*x&?sk_ zKzmiyKh=8??!Sr|Dy>;O#Ie@-{-nHtjb2_jbxHc~HOn8ota-)MuVjH`KwddR%4IJl zp;4(HKr$kfmV{np_IFy*UAtfA1ZafT@7rT|dzD^QW%P-JVkw8og-E6#$h1XWB zD_b�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 WvL#1?M^}KAHU98mU&fKs*Zu{=`$aGS literal 0 HcmV?d00001 diff --git a/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png b/openspec/changes/design-thread-chat-client-api/ui-parity-baseline/divider-dragged.png new file mode 100644 index 0000000000000000000000000000000000000000..da9f91203031c2b95f09a59717e6b0ae184c5036 GIT binary patch literal 220856 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%0Ad`0c0Rz_b}sQAy$a&q@UqK9-9z<9@f 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