Skip to content

docs(openspec): 收紧当前 Thread 引用并定义低成本 Prompt Cache 架构 - #49

Open
hifizz wants to merge 186 commits into
codex/feat-agent-observability-evaluationfrom
codex/design-thread-chat-prompt-cache
Open

docs(openspec): 收紧当前 Thread 引用并定义低成本 Prompt Cache 架构#49
hifizz wants to merge 186 commits into
codex/feat-agent-observability-evaluationfrom
codex/design-thread-chat-prompt-cache

Conversation

@hifizz

@hifizz hifizz commented Aug 29, 2026

Copy link
Copy Markdown
Owner

概要

本 PR 基于:

  • Base:codex/feat-agent-observability-evaluation@2f3024747ddb72e1e69aa916cb45addb7140f6ab
  • Head:codex/design-thread-chat-prompt-cache@7561193844f5364f74ac071ae8379dada8a9ef84

本轮进一步收紧了 Quote 范围:v1 不支持任意跨 Thread、跨分栏或 @Thread 引用。

统一流程只覆盖:

1. 父 Thread 划选 -> 开新分支
2. 当前 Thread 划选 -> 当前 Thread Composer
3. 当前 Thread Markdown Artifact 批量批注 -> Artifact 来源 Thread Composer

Fork 的父 Thread 选区会自动成为新 Thread 第一轮的 branch-origin Quote。它是 Fork 拓扑的服务端派生数据,不代表系统支持把任意 Thread 内容加入另一个 Thread。

已确认产品决定

  • 新 Quote 来源只允许 completed assistant Message;generating / stopped / failed 全部禁止。
  • 每条用户 Message 最多支持 50 个 Quote
  • 50 是块数量上限;正式模型调用前仍执行模型 Route 相关的 Quote/Input Budget 预检。
  • 普通 Quote 只允许来自目标 Composer 所属当前 Thread。
  • 客户端 Command 不提交 sourceThreadId;服务端加载来源实体并验证它属于目标 Thread。
  • 任意跨 Thread、跨分栏、跨 Project 和 @Thread 引用留到独立 change。
  • 划选后弹窗不输入问题:只创建新 Thread,不创建 B1/BA1,不调用模型;branch-origin Quote Block 显示在新 Thread Composer。
  • 当前 Thread 划选可加入当前输入框,不自动发送。
  • Markdown Artifact 批量批注只能回填 Artifact 来源 Message 所属 Thread 的 Composer;一次发送只触发一次 AI 回复。

缓存与 Claude 成本目标

缓存与模型线路不再要求用户做技术选择,统一采用:

回答质量、工具行为、安全与终态不变差时,选择经过验证的最低真实总成本方案。

真实总成本包括:

未缓存输入
缓存写入
缓存读取
输出
Gateway / Relay 费用
路由漂移导致的缓存失效
  • 第一条 Claude Probe 使用当前实际存在的 UMAPIS Claude Route。
  • 如果不能证明缓存透传、cache Usage 和净成本下降,则保持关闭,不猜测已经省钱。
  • 有 Anthropic 直连凭据时可作参考实验,不要求生产立即切换。
  • 第一阶段使用 Provider 默认短时缓存;支持时按约 5 分钟验证。
  • 1 小时 Extended TTL 默认关闭,只有真实会话间隔和读写费用证明更便宜且通过数据保留检查后才启用。
  • 任何便宜方案只要导致质量、引用理解、工具、安全或终态回归,就不得启用。

目标 Prompt 顺序

稳定 Tool Profile
稳定 Agent Kernel
可选 Project Contract
冻结祖先历史
已完成分支历史
---------------- 缓存边界 ----------------
本轮 Runtime Control
Current User:Quote × 0..50 + comment + Text? + File*

具体 Quote 正文位于当前 User;具体 Anchor 不再进入前置 System Prompt。

以下信息永远不发送给模型:

quoteId / kind
Project / Thread / Message / Artifact ID
TextAnchor
标题 / 脚注 / 列位置
Draft / Command / Request / Trace ID

三个 OpenSpec Capability

specs/
├── thread-chat-message-quotes/spec.md
├── thread-chat-quote-composer/spec.md
└── thread-chat-prompt-cache/spec.md

thread-chat-message-quotes

定义:

  • 当前 Thread Message / Artifact 两类普通 Quote 来源;
  • Fork 自动 branch-origin 唯一父 Thread 来源例外;
  • thread-quote-v1 Parts;
  • completed-only 授权;
  • 50 Quote 与模型输入预算;
  • Edit/Retry/Legacy;
  • Quote-to-model 只发送正文与 comment。

thread-chat-quote-composer

定义:

  • 空问题开分支后 Quote Block 进入新 Thread Composer;
  • 当前 Thread 内引用;
  • 明确禁止任意跨 Thread、跨分栏和 @Thread
  • required branch-origin;
  • 多引用去重、排序、删除;
  • 当前 Thread Markdown 批量批注;
  • Draft 发送前不创建 Message、不调用模型;
  • 一次 Draft 只形成一条 User Message 和一次 assistant attempt。

具体 React 组件、富文本技术、视觉样式、拖拽、来源跳转和 Draft 持久化留到下一阶段前端 Research。

thread-chat-prompt-cache

定义:

  1. 稳定前缀;
  2. 动态尾部;
  3. 非模型元信息;
  4. 主动缓存分区;
  5. Prompt Compiler 与 Prefix Hash;
  6. Tool Profile 与实际模型 Route;
  7. Claude 短缓存 Probe;
  8. 质量硬门禁和真实总成本比较;
  9. cache read/write、TTFT、实际成本与 Agent Eval;
  10. off / observe / enabled 渐进发布。

数据与后端合同

  • MessageSelectionInput 只提交 sourceMessageId + TextAnchor
  • ArtifactSelectionInput 只提交 artifactId + TextAnchor
  • 服务端从目标 Thread 和来源实体推导真实 Project/Thread ID。
  • messages.parts JSONB 继续是 Quote Snapshot 的唯一事实源。
  • threads Fork 字段继续是分支拓扑事实。
  • 第一阶段不新增 Quote 表、不增加顶层 MessageDTO.quotes、不执行数据库迁移。
  • quoteContentToModelText() / quoteTextToModelText() 使用版本化确定性 JSON 格式。
  • compilePromptBase() + finalizeGenerationPrompt() 保护 inherited / branch history。
  • ResolvedChatModel 暴露真实 Adapter、Gateway、upstream、routeId 与缓存能力。

文件

docs/prompt-cache/01-research.md
openspec/changes/optimize-thread-chat-prompt-cache/
├── .openspec.yaml
├── proposal.md
├── design.md
├── tasks.md
└── specs/
    ├── thread-chat-message-quotes/spec.md
    ├── thread-chat-quote-composer/spec.md
    └── thread-chat-prompt-cache/spec.md

范围边界

  • 这是规划与合同 PR,不修改业务代码;
  • 不执行数据库迁移;
  • 不实现具体 Composer 前端;
  • 不支持任意跨 Thread / @thread
  • 不承诺任意模型、任意代理或任意首次分叉 100% 命中;
  • 缓存收益不得覆盖回答质量、安全、权限、终态和数据保留硬失败。

验证

  • Head:7561193844f5364f74ac071ae8379dada8a9ef84
  • OpenSpec strict validation:success
  • PR 当前可合并;Vercel 状态与本规划文档改动无关,未据此推断部署失败原因。

@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
thread-chatbot Error Error Sep 1, 2026 12:52am UTC

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: fda879c2-1bee-4160-93b1-eb90c957fb78

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@hifizz hifizz changed the title docs(openspec): 细化 Thread Chat Prompt Cache 优化方案 docs(openspec): 定义多引用消息与 Thread Chat Prompt Cache 架构 Aug 31, 2026
@hifizz hifizz changed the title docs(openspec): 定义多引用消息与 Thread Chat Prompt Cache 架构 docs(openspec): 统一 Quote Draft、多引用消息与 Prompt Cache 架构 Aug 31, 2026
@hifizz hifizz changed the title docs(openspec): 统一 Quote Draft、多引用消息与 Prompt Cache 架构 docs(openspec): 收紧当前 Thread 引用并定义低成本 Prompt Cache 架构 Aug 31, 2026
@hifizz
hifizz changed the base branch from codex/feat-agent-observability-evaluation to codex/research-project-workspace-design September 1, 2026 03:34
@hifizz
hifizz changed the base branch from codex/research-project-workspace-design to codex/feat-agent-observability-evaluation September 1, 2026 03:36

@hifizz hifizz left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛔ Blocking Review(结论等同 REQUEST_CHANGES)

GitHub 不允许 PR 作者账号对自己的 PR 提交 REQUEST_CHANGES;因此以 blocking comment 提交。审查结论:当前 Head 不应合并。

按 thermo-nuclear code-quality bar,这个 Head 目前既不可合并,也不可通过“补几个导出、加 compatibility alias”来修平。

[P0] 当前 PR 没有一份可执行的单一合同

PR 描述仍声称“规划与合同 PR、不修改业务代码”,但当前 Head 已是 126 个文件、约 1.6 万行新增、186 个提交,且 Prompt Cache / Apply / Final Verification 都在 typecheck 阶段失败。更根本的问题是,同一个概念被保留了多套互不兼容的实现:

  • Quote:thread-quote / quote-selectionquote-resolver / quote-selections,两套 budget、两套 composer draft;
  • Prompt:prompt-compilercompile-prompt-base + finalize-generation-prompt,两套 PromptBase / PromptManifest / hash
  • Route/Cache:providerresolved-chat-model,三套 fallback stream、两套 usage normalizer、两套 tool profile/config。

这些实现不仅重复,而且常量、字段、函数名和语义已经分叉;测试也分别指向不同实现。要求:从干净基线重组或拆 PR,选定每个 bounded context 的唯一 canonical contract,删除废弃平行栈,测试只通过真实生产入口执行。不要用 alias 维持两套模型共存。

[P1] Quote 不是经过服务端验证的来源快照

当前 resolver 只验证来源 ID、Project/Thread、role/status,却直接把客户端提交的 anchor.quote.exact 持久化为 text;它没有把 Anchor 与 message.partsartifact.content 对齐。客户端因此可以把任意文字伪装成某条 completed assistant Message / Artifact 的引用,破坏来源归属、未来跳转和审计语义。Fork origin 也存在同类问题。

应明确建模为 QuoteSelectionRequest -> VerifiedSelection -> QuoteSnapshot:服务端基于版本化的 selectable-content projection 解析/验证 exact、position、prefix/suffix(或 source revision/hash),只有 VerifiedSelection 可以构造持久化 Quote。必须增加伪造 exact、错位 position、错误 context 的拒绝测试。

[P1] JSONB Parser 没有成为持久化边界

messages.parts 仍被数据库层直接断言成可信 UI union,toMessageDTO() 原样返回 JSONB;Parser 只在部分 edit/compiler 路径临时调用,且损坏数据抛普通 Error。这与“版本化 payload、兼容 legacy、所有读取统一解析”的合同相冲突,也让数据损坏可能直接进入 DTO/UI 或变成 500。

数据库表示应保持 unknown/PersistedMessageParts,在 mapper/repository boundary 完成 parse + normalize;应用层只接收已验证类型,损坏持久化数据应稳定映射为 STATE_CONFLICT

[P1] 生产 Compiler 在 B2 以后丢失 legacy branch-origin

实际运行的 prompt-compiler.ts 只对当前 User Message 调用 withLegacyBranchOrigin(),并在存在历史 User 时直接返回。因此旧 ForkedThread 到 B2/B3 时,缺少 origin 的历史 B1 永远不会被补齐,分支焦点从模型上下文消失。应先对整个 active current-thread history 的第一条 User 做 deterministic normalization,再切分 branch history/current user,并增加 B1、B2、B3 回归用例。

[P1] 生产缓存 fallback 没有实现声明的安全降级

generation-plan.ts 使用的 wrapper 不识别 TextStreamParttype: "error";它会把首个 error chunk 当成“已输出”,从而禁止 fallback。错误匹配 regex 还漏掉常见的 400 unknown field cache_control 顺序。处理 error chunk 的实现和测试存在于另外两套未接入模块中。

请保留一套 fallback state machine,并通过真实 prepareGeneration -> streamText -> wrapper 入口验证:首个 cache-control error chunk/throw 只重试一次;首个可见文本或工具副作用之后绝不重试;401/429/普通模型错误不得被吞掉。

[P1] Quote / Runtime envelope 可以被正文关闭

JSON.stringify 会转义引号和换行,但不会转义 < / >;正文包含 </thread_quote> 时,输出中会真实出现额外 closing tag。现有测试恰好放入了该字符串,却没有验证边界仍唯一。runtime_control 使用了同样模式。请改为无歧义的结构化编码(例如转义 <>&、长度前缀或不使用可由正文复现的标签),并断言任意正文不能产生第二个协议边界。

[P1] Tool Profile 与 Cache Usage 都是影子模型,不是 Provider-visible truth

Tool hash 基于手写 descriptor/schema: "...-v1" 标签,而不是实际发送的 name、description、JSON Schema 与顺序;实际路径还丢掉了 fetch-only profile,使 fetch 模式暴露额外 webSearch。Usage normalizer 则按“字段数量最多”选一份对象,不合并标准 usage 的 input/output 与 provider metadata 的 cache read/write/cost;相关测试又覆盖的是另一套 normalizer。

Tool descriptor 应成为构造实际 ToolSet 的唯一来源,并对最终 provider-visible schema 做 canonical hash。Usage 应按字段级优先级合并 allowlisted evidence,保留来源/冲突状态;测试必须导入生产使用的实现。

[P1] Anchor 与输入预算仍允许绕过

prefix/suffix 无长度限制,position 未与 exact 长度或 canonical source 对齐,Quote budget 又只统计 text/comment;客户端可以在数量和模型预算都合法时写入巨大的 Anchor 元数据。另一个风险是所有 Route 默认 128k context、按 3 chars/token 估算,这对中文/代码并非保守上界,不能承担“发送前安全预算”的不变量。

请为 Anchor 元数据和完整持久化 payload 设置独立硬上限,定义 offset 单位并服务端验证;context window 必须来自具体 model route,未知值 fail closed,预算最好使用对应 tokenizer 或真正保守的估算。

在以上结构与不变量收敛之前,我不会继续给命名、局部抽取或格式类意见;目前的主要风险不是代码风格,而是多个互相矛盾的领域真相同时存在。

@@ -0,0 +1,226 @@
import { and, eq, inArray, isNull } from "drizzle-orm"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0][Single source of truth] 这是 quote-resolver.ts 的第二套完整 Quote 解析器,同时导入了 constants/prompt-cache 中不存在的 Quote 常量以及 thread-quote.ts 中不存在的去重 helper。请不要通过补 alias 让两套都能编译;删除废弃实现,并让 schema、resolver、budget 只存在一个 canonical 依赖方向。

return quote
}

export async function resolveQuoteSelections(input: {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Provenance integrity] 这个函数验证了来源实体的归属和状态,却没有验证 Selection 的正文确实来自该实体;后面直接把客户端的 anchor.quote.exact 写成冻结 text。因此任何客户端都能给真实 completed Message/Artifact 伪造引用正文。请先从 row.parts / artifact.content 构造 canonical selectable projection,解析成 VerifiedSelection,再允许创建 QuoteSnapshot


const entityIdSchema = z.uuid()

export const textAnchorSchema = z

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Data contract] prefix/suffix 完全无上限,position 也没有与 exact 长度或来源内容建立不变量;而预算只统计 text/comment。这样可以绕过 Quote/Input budget,把巨大导航元数据写入 messages.parts。请定义 prefix/suffix 上限、offset 单位、完整 payload 上限,并在服务端来源投影上验证 Anchor。

@@ -0,0 +1,462 @@
import {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Legacy semantics] 实际运行路径只把 withLegacyBranchOrigin() 应用于 currentUi[currentUserIndex];当 B2/B3 存在历史 User 时 helper 直接退出,因此旧分支缺失的 origin 不会被补到历史 B1。应在切分 branch history/current user 之前,先规范化整个 current-thread history 的第一条有效 User,并用 B2/B3 用例覆盖。

@@ -0,0 +1,125 @@
export type PromptCacheStreamResult<TChunk, TUsage> = {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Fallback state machine] 生产入口使用的是这个版本,但它不检查 TextStreamPart.type === "error";首个 error chunk 会被标记为已输出并直接透传,缓存参数拒绝不会触发无缓存重试。另两套 wrapper 已实现/测试了不同语义。请只保留一套,并从 prepareGeneration 的真实入口覆盖 error chunk、throw、首个可见输出和工具副作用边界。

}
return [
`<thread_quote format="${THREAD_QUOTE_MODEL_FORMAT_VERSION}">`,
JSON.stringify(normalized),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Protocol boundary] JSON.stringify 不会转义 </>,所以正文中的 </thread_quote> 会原样出现在 envelope 内,产生第二个 closing tag;现有测试包含这个输入但没有断言边界唯一。请使用不可由正文复现的编码(转义 <>&、长度前缀或纯结构化 JSON);runtime_control 也要同步修复。

@@ -1,10 +1,92 @@
import { tool } from "ai"
import { tool, type ToolSet } from "ai"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Tool identity/permission] toolProfile.hash 基于手写影子 descriptor,而不是最终 Provider 可见的 name/description/JSON Schema;实际工具变化时 hash 可以不变。并且这里把 fetch 合并进 web profile,额外暴露了 webSearch,与另一套 thread-fetch-v1 语义冲突。请让 canonical descriptor 同时构造 ToolSet 与 hash,并保持 fetch-only 权限面。

@@ -0,0 +1,207 @@
export type PromptCacheUsageSource =

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1][Evidence model] 生产 normalizer 会从 usage/providerMetadata 中挑“字段最多”的单个 candidate,而不是按字段合并;标准 usage 提供 input/output、provider metadata 提供 cache read/write/cost 时会丢掉一半证据。测试覆盖的是另一份 prompt-cache/usage.ts。请收敛为一个 normalizer,定义字段级来源优先级与冲突规则,并让生产入口测试它。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant