docs(openspec): 收紧当前 Thread 引用并定义低成本 Prompt Cache 架构 - #49
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
hifizz
left a comment
There was a problem hiding this comment.
⛔ 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-selection,quote-resolver/quote-selections,两套 budget、两套 composer draft; - Prompt:
prompt-compiler与compile-prompt-base + finalize-generation-prompt,两套PromptBase / PromptManifest / hash; - Route/Cache:
provider与resolved-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.parts 或 artifact.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 不识别 TextStreamPart 的 type: "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" | |||
There was a problem hiding this comment.
[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: { |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[P1][Data contract] prefix/suffix 完全无上限,position 也没有与 exact 长度或来源内容建立不变量;而预算只统计 text/comment。这样可以绕过 Quote/Input budget,把巨大导航元数据写入 messages.parts。请定义 prefix/suffix 上限、offset 单位、完整 payload 上限,并在服务端来源投影上验证 Anchor。
| @@ -0,0 +1,462 @@ | |||
| import { | |||
There was a problem hiding this comment.
[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> = { | |||
There was a problem hiding this comment.
[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), |
There was a problem hiding this comment.
[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" | |||
There was a problem hiding this comment.
[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 = | |||
There was a problem hiding this comment.
[P1][Evidence model] 生产 normalizer 会从 usage/providerMetadata 中挑“字段最多”的单个 candidate,而不是按字段合并;标准 usage 提供 input/output、provider metadata 提供 cache read/write/cost 时会丢掉一半证据。测试覆盖的是另一份 prompt-cache/usage.ts。请收敛为一个 normalizer,定义字段级来源优先级与冲突规则,并让生产入口测试它。
概要
本 PR 基于:
codex/feat-agent-observability-evaluation@2f3024747ddb72e1e69aa916cb45addb7140f6abcodex/design-thread-chat-prompt-cache@7561193844f5364f74ac071ae8379dada8a9ef84本轮进一步收紧了 Quote 范围:v1 不支持任意跨 Thread、跨分栏或
@Thread引用。统一流程只覆盖:
Fork 的父 Thread 选区会自动成为新 Thread 第一轮的
branch-originQuote。它是 Fork 拓扑的服务端派生数据,不代表系统支持把任意 Thread 内容加入另一个 Thread。已确认产品决定
completedassistant Message;generating / stopped / failed全部禁止。sourceThreadId;服务端加载来源实体并验证它属于目标 Thread。@Thread引用留到独立 change。缓存与 Claude 成本目标
缓存与模型线路不再要求用户做技术选择,统一采用:
真实总成本包括:
目标 Prompt 顺序
具体 Quote 正文位于当前 User;具体 Anchor 不再进入前置 System Prompt。
以下信息永远不发送给模型:
三个 OpenSpec Capability
thread-chat-message-quotes定义:
thread-quote-v1Parts;thread-chat-quote-composer定义:
@Thread;具体 React 组件、富文本技术、视觉样式、拖拽、来源跳转和 Draft 持久化留到下一阶段前端 Research。
thread-chat-prompt-cache定义:
off / observe / enabled渐进发布。数据与后端合同
MessageSelectionInput只提交sourceMessageId + TextAnchor。ArtifactSelectionInput只提交artifactId + TextAnchor。messages.partsJSONB 继续是 Quote Snapshot 的唯一事实源。threadsFork 字段继续是分支拓扑事实。MessageDTO.quotes、不执行数据库迁移。quoteContentToModelText()/quoteTextToModelText()使用版本化确定性 JSON 格式。compilePromptBase()+finalizeGenerationPrompt()保护 inherited / branch history。ResolvedChatModel暴露真实 Adapter、Gateway、upstream、routeId 与缓存能力。文件
范围边界
验证
7561193844f5364f74ac071ae8379dada8a9ef84