发现于 #3293 的核验过程(那张单的前提已失效,详见其上的报告)。这是同一个文件里残留 的一处手写 wire 类型,与 #3293 已解决的那两个是同一失败类,但因为本地名字和 spec 名字不同 ,scripts/check-spec-symbol-derivation.mjs 这类基于名字碰撞的守卫对它完全没有抓手。
事实 packages/plugin-chatbot/src/usePendingActions.ts:69-80(origin/main 00b9451d8)手写:
export interface ApproveOutcome { id : string ; status : 'executed' | 'failed' | string ; result ?: unknown ; error ?: string ; [ k : string ] : unknown ; } export interface RejectOutcome { id : string ; status : 'rejected' | string ; } spec 侧已经声明了这两个响应(objectstack origin/main6029cc1bb,packages/spec/src/api/protocol.zod.ts:1453-1462,类型导出在 :1680-1681):
export const ApproveAiPendingActionResponseSchema = lazySchema ( ( ) => z . object ( { status : z . enum ( [ 'executed' , 'failed' ] ) , result : z . unknown ( ) . optional ( ) , error : z . string ( ) . optional ( ) , } ) ) ; export const RejectAiPendingActionResponseSchema = lazySchema ( ( ) => z . object ( { status : z . literal ( 'rejected' ) , id : z . string ( ) , } ) ) ; 漂移有三处,其中一处是当下就在跑 的错类型 ApproveOutcome.id 是必填,但 approve 响应根本不带 id。 spec 的 approve 响应只有 status/result/error;reject 响应才有 id。而 useHitlInChat.ts:237 把原始 payload 直接断言过去:
onDecided?.(toolCallId, payload as ApproveOutcome | RejectOutcome);
于是交给 onDecided 的对象在编译期承诺 id: string,运行期是 undefined。onDecided 是 plugin-chatbot 的公开回调 (src/index.tsx:348-349 导出这两个类型),外部消费者读 outcome.id 拿到 undefined 而编译器不报。这一条不是休眠的。
| string 吸收字面量。'executed' | 'failed' | string 就是 string,注解不携带任何信息 —— 与 refactor(data-objectstack,plugin-chatbot,plugin-list)!: 台账燃尽批次 6 —— 12 个符号不再顶着 spec 的名字 (#3160) #3220 从同文件另两个类型上删掉的 Drift 1 一模一样。下游后果在 useHitlInChat.ts:217-235:代码手写 status 字符串比较,并且 else 兜底分支把认不出的 status 当成成功 (succeeded = true),进而继续对话。若用闭合枚举,这个兜底分支要么可证死、要么必须显式处理。(诚实标注:今天服务端只返回代码已处理的那几个值,所以这一条的行为 后果目前不可达;可达的是类型层面已经零信息。)
[k: string]: unknown 索引签名。 objectstack#4075 那个机制:有它在,任何结构比较都答"一致",无论抄本漂多远 —— 也就是说,就算给这两个类型补一条 parity 测试,索引签名也会让它恒绿。
为什么现有守卫抓不到 check-spec-symbol-derivation.mjs 判的是"本地声明占用了 spec 导出的名字 "。ApproveOutcome ≠ ApproveAiPendingActionResponse,名字不碰撞,守卫按设计放行。这正是名字型守卫的残留洞:换个名字手抄 对它隐形。#3720 是同类(本地名字下手写第三套拼写)。
建议修法(契约优先,零新依赖) plugin-chatbot 已经依赖 @objectstack/spec(package.json 里 ^17.0.0-rc.5,#3220 引入),所以派生是免费的:把两个类型改成从 spec 派生(z.infer/re-export),删掉 | string 与索引签名;id 按 spec 归位到 reject 一侧。随后 useHitlInChat.ts:217 的 else 兜底需要重新裁决 —— 是显式处理未知 status,还是按闭合枚举证死。
请 PM triage 分级:第 1 条我判为具体缺陷(公开 API 类型承诺了 wire 不返回的字段,今天就在跑),第 2、3 条按现状更接近潜伏/观察类。
Refs: #3293 (核验来源)、#3220 (同文件另两个类型的处置范式)、#3720 (同类)、objectstack#4075、objectstack#4115。
发现于 #3293 的核验过程(那张单的前提已失效,详见其上的报告)。这是同一个文件里残留的一处手写 wire 类型,与 #3293 已解决的那两个是同一失败类,但因为本地名字和 spec 名字不同,
scripts/check-spec-symbol-derivation.mjs这类基于名字碰撞的守卫对它完全没有抓手。事实
packages/plugin-chatbot/src/usePendingActions.ts:69-80(origin/main00b9451d8)手写:spec 侧已经声明了这两个响应(objectstack
origin/main6029cc1bb,packages/spec/src/api/protocol.zod.ts:1453-1462,类型导出在:1680-1681):漂移有三处,其中一处是当下就在跑的错类型
ApproveOutcome.id是必填,但 approve 响应根本不带id。 spec 的 approve 响应只有status/result/error;reject 响应才有id。而useHitlInChat.ts:237把原始 payload 直接断言过去:onDecided?.(toolCallId, payload as ApproveOutcome | RejectOutcome);于是交给
onDecided的对象在编译期承诺id: string,运行期是undefined。onDecided是plugin-chatbot的公开回调(src/index.tsx:348-349导出这两个类型),外部消费者读outcome.id拿到 undefined 而编译器不报。这一条不是休眠的。| string吸收字面量。'executed' | 'failed' | string就是string,注解不携带任何信息 —— 与 refactor(data-objectstack,plugin-chatbot,plugin-list)!: 台账燃尽批次 6 —— 12 个符号不再顶着 spec 的名字 (#3160) #3220 从同文件另两个类型上删掉的 Drift 1 一模一样。下游后果在useHitlInChat.ts:217-235:代码手写status字符串比较,并且else兜底分支把认不出的 status 当成成功(succeeded = true),进而继续对话。若用闭合枚举,这个兜底分支要么可证死、要么必须显式处理。(诚实标注:今天服务端只返回代码已处理的那几个值,所以这一条的行为后果目前不可达;可达的是类型层面已经零信息。)[k: string]: unknown索引签名。 objectstack#4075 那个机制:有它在,任何结构比较都答"一致",无论抄本漂多远 —— 也就是说,就算给这两个类型补一条 parity 测试,索引签名也会让它恒绿。为什么现有守卫抓不到
check-spec-symbol-derivation.mjs判的是"本地声明占用了 spec 导出的名字"。ApproveOutcome≠ApproveAiPendingActionResponse,名字不碰撞,守卫按设计放行。这正是名字型守卫的残留洞:换个名字手抄对它隐形。#3720 是同类(本地名字下手写第三套拼写)。建议修法(契约优先,零新依赖)
plugin-chatbot已经依赖@objectstack/spec(package.json里^17.0.0-rc.5,#3220 引入),所以派生是免费的:把两个类型改成从 spec 派生(z.infer/re-export),删掉| string与索引签名;id按 spec 归位到 reject 一侧。随后useHitlInChat.ts:217的else兜底需要重新裁决 —— 是显式处理未知 status,还是按闭合枚举证死。请 PM triage 分级:第 1 条我判为具体缺陷(公开 API 类型承诺了 wire 不返回的字段,今天就在跑),第 2、3 条按现状更接近潜伏/观察类。
Refs: #3293(核验来源)、#3220(同文件另两个类型的处置范式)、#3720(同类)、objectstack#4075、objectstack#4115。