oh-my-openagent prompt-async-gate 路径兼容性修复实录:从 `got undefined` 生产故障到双端回归验证
2026/9/19 1:03:48 网站建设 项目流程

oh-my-openagent prompt-async-gate 路径兼容性修复实录:从got undefined生产故障到双端回归验证

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

本文基于 oh-my-openagent 仓库中 .omo/evidence/20260804-prompt-async-gate-undefined/README.md 及其配套证据 TESTIMONY-USER-SESSION.md,完整还原一次"真实生产会话驱动"的缺陷定位与修复过程:OpenCode 会话中edit/task工具持续抛出The "path" property must be of type string, got undefined,根因是 prompt-async-gate 的路径类型兼容守卫只识别got object一种错误形态。读完本文,你将掌握该门闩(gate)的双路径调度原理、错误形状守卫的判定逻辑、最小修复方式,以及一套"真实 harness 回归 + 单元测试覆盖 + 数据隔离证明"的可复用 QA 方法。

一、事件背景:一次真实发生的got undefined故障

1.1 故障现场

2026 年 8 月 4 日,oh-my-openagent 提交 PR #6583(对应 Issue #6582,提交938d35003),修复 prompt-async-gate 的路径兼容性缺陷。这不是一个合成复现:故障首次出现在真实 OpenCode 用户会话中,会话 ID 为ses_038a0d10dffe7hHQyjy3ndVIgF,运行于 2026-08-03,累计 494 条消息,涉及openproject-updaterSisyphus - ultraworkercodePrometheus - Plan BuilderAtlas - Plan Executorcompaction等多个 agent。实际驱动工具调用的模型栈为重负载9router5.6 terra),故障 agent 为 oh-my-openagent 插件自带的Atlas(运行在$start-work计划执行流程下)。

关键事实链:

  • 故障工具edittask,每次调用都报同一错误;
  • 正常工具readwritebash、glob/grep、lsp_*envsitter_*均不受影响;
  • 失败事件时间戳(ms epoch → UTC)
    • 1785791986035edit作用于.../FakeAppSettings.cs,返回{"status":"error", "input":{"filePath":"...", "oldString":"...undefined"}}
    • 1785792261985edit作用于/tmp/edit_probe.txt,返回{"status":"error", "error":"The \"path\" property must be of type string, got undefined"}
    • 1785791698122+task(多参数)返回同款got undefined错误;
    • 1785795757766edit以最小参数作用于/tmp/edit_probe.txt,仍报got undefined

最小复现的意义在于:/tmp/edit_probe.txt文件真实存在,edit依然失败,说明故障发生在插件调度层而非编辑内容或 schema 校验本身。

1.2 现场诊断手段

会话内诊断(故障 agent 自身的 bash 转录)直接在已安装产物上搜索错误特征:

grep -n "got undefined\|got object\|required error\|AggregateError\|must be of type string" \ /home/allmaker/.cache/opencode/packages/oh-my-openagent@latest/node_modules/oh-my-openagent/dist/index.js # => 11453: return message.includes('The "path" property must be of type string') && message.includes("got object");

在 oh-my-openagent 的 QA 方法论中,这类"指向精确安装产物 + 精确行号"的 grep 是定位已发布 bug 的第一步,证据链完整记录在 TESTIMONY-USER-SESSION.md。

二、根因:错误形状守卫与真实错误形态的错配

2.1 已安装产物的守卫逻辑

安装的插件版本为oh-my-openagent@latestv4.19.4,打包产物dist/index.js:11453中守卫函数如下(修复前状态):

function isObjectPathTypeError(error) { const message = ...; return message.includes('The "path" property must be of type string') && message.includes("got object"); }

该守卫驱动dispatchWithPathCompatibility的重试决策:只有错误消息同时包含The "path" property must be of type stringgot object时,才会认为这是"对象形态的session.promptAsync({path:{id}})需要兼容重试"。

2.2 真实错误形态:got undefined

但真实 harness 产生的是got undefined——即会话路径在调度时已经为 undefined。此时守卫判定失败,dispatchWithPathCompatibility直接重抛原始错误,原始错误冒泡到用户端,表现为edit/task全部不可用。

在 QA 主机上对同一已安装插件产物独立验证:/home/allmaker/.cache/opencode/packages/oh-my-openagent@latest/node_modules/oh-my-openagent/dist/index.js:11453确实仍只包含message.includes("got object")——即修复前的 bug 形态。

2.3 为什么真实会话证据不可替代

测试文件 TESTIMONY-USER-SESSION.md 明确阐述了该证据的独特价值:

  • 它是真实 harness 中的可观测行为:真实模型(5.6 terra)、真实 agent(来自同一插件的Atlas)、真实会话path生命周期,共同产出了got undefined这一变体——这是 SDK/服务端加插件路径的单元 mock 无法覆盖的组合;
  • 精确定位到安装产物与行号;修复方案(got objectgot undefined)正是让dispatchWithPathCompatibility重试这一真实生产错误形状的最小改动;
  • 与 README 中的合成 harness 运行(全新安装 + 真实 OpenCode CLI 干净驱动edit)和单元测试一起,构成"复现 → 根因 → 修复 → 回归覆盖"的完整闭环。

2.4 证据来源(provenance)

会话ses_038a0d10dffe7hHQyjy3ndVIgF的 part 行通过生产 OpenCode 数据库查询取得:SELECT ... FROM part WHERE session_id=? AND data LIKE '%got undefined%'(数据库路径~/.local/share/opencode/opencode.db)。最小自包含的失败用例edit /tmp/edit_probe.txt在文件存在的前提下仍以got undefined失败,进一步证明故障位于插件调度层。

三、修复后的源码剖析:dispatchWithPathCompatibility的双路径调度

修复后的核心实现在 packages/utils/src/prompt-async-gate.ts,同时以镜像形式存在于 packages/omo-opencode/src/shared/prompt-async-gate.ts(后者为生产插件侧的主实现)。

3.1 对象形态路径的判定

type ObjectPathPromptInput = { readonly path?: { readonly id?: string } | string readonly [key: string]: unknown } function hasObjectSessionPath(input: unknown): input is ObjectPathPromptInput & { readonly path: { readonly id: string } } { return typeof input === "object" && input !== null && "path" in input && typeof input.path === "object" && input.path !== null && "id" in input.path && typeof input.path.id === "string" }

hasObjectSessionPath是一个类型守卫:只有当input是对象、包含path键、path本身是对象、且path.id是字符串时,才认为这是"对象形态会话路径"——即session.promptAsync({ path: { id: "ses_..." }, ... })这种调用形态。

3.2 错误形状守卫(最小修复点)

function isObjectPathTypeError(error: unknown): boolean { const message = error instanceof Error ? error.message : typeof error === "string" ? error : "" return message.includes('The "path" property must be of type string') && (message.includes("got object") || message.includes("got undefined")) }

修复内容就是最后一行的|| message.includes("got undefined")。该函数同时兼容Error实例与裸字符串错误,并容忍空消息。

3.3 兼容重试调度器

async function dispatchWithPathCompatibility<TInput>( dispatch: (dispatchInput: TInput) => Promise<unknown>, input: TInput, ): Promise<unknown> { try { return await dispatch(input) } catch (error) { if (!isObjectPathTypeError(error) || !hasObjectSessionPath(input)) { throw error } const retryInput = { ...input, path: input.path.id, } as TInput return dispatch(retryInput) } }

逻辑十分清晰:

  1. 先以原始input调用底层dispatch
  2. 若抛出错误且同时满足"path 类型错误"(含got objectgot undefined)与"输入为对象形态路径",则将path{ id: "ses_..." }摊平为字符串"ses_..."后重试一次;
  3. 其余任何错误(非 path 类型错误、或输入本身不是对象形态路径)一律原样重抛,不掩盖真实失败。

这一"先试对象形态、失败再降级为字符串形态"的机制,兼容了 OpenCode SDK 不同版本对session.promptAsync/session.prompt入参的差异。

3.4 在主调度流程中的接入点

dispatchInternalPrompt是门闩对外暴露的唯一公共调度入口,其内部按mode"async"/"sync")分别绑定session.promptAsync/session.prompt,并按queueBehaviordefer/ 队列 / 直接)走三条路径,三条路径最终都经过dispatchWithPathCompatibility包装:

  • defer模式:dispatchAfterSessionIdledispatch: (dispatchInput) => dispatchWithPathCompatibility(dispatch, dispatchInput)(见 prompt-async-gate.ts);
  • 队列模式:enqueueInternalPromptdispatch: async (_dispatchInput) => dispatchWithPathCompatibility(dispatch, input)
  • 直接模式:同样经dispatchWithPathCompatibility包装(L281-L297)。

这意味着无论调用方走哪条注入路径,路径兼容重试都生效。另外,当resolved.route === "live"且发生发送前连接失败(isPreSendConnectionFailure)时,还会回退到调用方传入的originalSession上的原始promptAsync/prompt再次尝试——这是与路径兼容正交的另一层容错。

3.5 门闩的宏观定位(ADR 背景)

prompt-async-gate 的完整设计见 docs/reference/prompt-async-gate-rfc.md(ADR,v4.2.0 引入):其起因是 Issue #4012 的重复流式输出——OMO 的 13+ 内部 hook 调用方(后台任务父唤醒、运行时回退重试、模型建议重试、团队邮箱实时投递、会话恢复续写、todo 续接、CLI run 恢复、Claude Code hook 注入、同步/后台子 agent prompt 等)各自判断空闲/完成/错误边缘,可能对同一会话重复注入 prompt。门闩以Map<sessionID, Reservation>的模块级保留表保证"每个会话同一时刻只有一个注入赢家",默认 post-dispatch hold 为DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS = 2_000ms(v4.2.3 起由 250 ms 提升 8 倍),默认调度超时DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS = 30_000ms。原始session.prompt/session.promptAsync调用被 packages/omo-opencode/src/shared/prompt-async-route-audit.test.ts 的 TypeScript AST 审计禁止(非正则,可捕获解构、括号访问、可选链、别名/断言访问等绕过形态)。

四、回归测试:双包镜像覆盖三种调度场景

4.1 测试位置与断言

修复配套的单元测试分别位于:

  • packages/utils/src/prompt-async-gate-path-compat.test.ts;
  • packages/omo-opencode/src/shared/prompt-async-gate-path-compat.test.ts(生产侧镜像)。

测试桩createPathSensitivePrompt(errorKind)构造一个 mock prompt:只要收到非字符串path就抛出TypeError: The "path" property must be of type string, got ${errorKind},并记录每次调用的入参。三个用例分别覆盖:

  1. 同步prompt拒绝对象形态路径:期望dispatchInternalPrompt({ mode: "sync", ... })两次调用底层 prompt,第一次入参path: { id: "ses_sync_path_compat" },第二次降级为"ses_sync_path_compat",最终result.status === "dispatched"
  2. 异步promptAsync拒绝对象形态路径:同上语义,走mode: "async"
  3. 异步promptAsync拒绝并报got undefined:即本 PR 修复的真实生产错误形状,期望同样重试成功。

关键断言为calls.map((call) => call.path)的两次调用形态:[{ id }, "id"],直接验证"先对象后字符串"的降级顺序。

4.2 运行结果

README 记录的回归运行结果:

$ bun test src/prompt-async-gate-path-compat.test.ts # packages/utils 3 pass, 0 fail, 6 expect() calls $ bun test src/shared/prompt-async-gate-path-compat.test.ts # packages/omo-opencode 3 pass, 0 fail, 6 expect() calls

两个包各 3 个用例、6 次expect(),全部通过。新用例精确断言isObjectPathTypeError接受生产错误消息原文The "path" property must be of type string, got undefined

五、真实 harness 验证:驱动真实 OpenCode CLI 走通 edit 工具

5.1 环境与命令

合成 harness 在全新插件构建上运行,环境如下:

  • OpenCode:1.18.11
  • 模型:9router/Light
  • QA 目录:/tmp/oh-my-openagent
  • 真实 OpenCode 数据库会话数(QA 前后):25/25

驱动命令(真实安装的 OpenCode CLI、SDK/插件工具调度、edit工具路径全链路):

opencode run "Use the edit tool to append a comment '// QA: tool dispatch test' to packages/utils/src/prompt-async-gate.ts. Confirm TOOL_QA_OK." -m 9router/Light --format json --dir /tmp/oh-my-openagent --print-logs

5.2 观测到的结构化事件

  • step_start
  • tool_usetool: "read"state.status: "completed"
  • tool_usetool: "edit"state.status: "completed"filediff.additions: 1filediff.deletions: 0
  • 最终textTOOL_QA_OK
  • 最终step_finishreason: "stop"

全程未出现任何path类型错误。QA 标记随后立即回滚,避免污染跟踪源码:

git checkout packages/utils/src/prompt-async-gate.ts

原始完整 JSON 输出捕获于/tmp/opencode-qa-output.log

六、隔离性/回归证明:数据库会话数不变

QA 运行前后分别查询真实 OpenCode 数据库的会话表:

$ opencode db "SELECT count(*) AS cnt FROM session" 25 $ opencode db "SELECT count(*) AS cnt FROM session" # after QA 25

结论:真实 DB 会话数保持25不变;QA 使用仓库本地目录,且标记回滚后未改动任何受跟踪源文件。这证明验证过程本身无副作用,是"数据隔离 + 工作区隔离"双保险的完整示范。

七、遗留事项(Caveat)

仓库级构建虽然到达了生成的插件 bundle,但在无关的build:senpi-plugin元数据生成阶段停止(packages/omo-senpi/plugin/extensions/omo.js.meta.json缺失)。README 明确说明:这不影响真实安装的 OpenCode CLI 运行结果,也不影响上述针对性单元测试。这一细节也提醒读者:在评估 QA 证据时,应区分"被验证子系统"与"无关的构建阻断项"。

八、经验总结与复用建议

  1. 错误形状守卫要覆盖所有真实生产变体isObjectPathTypeError从只认got object扩展为got object || got undefined,是"最小且充分"的修复。任何基于错误消息字符串的兼容层,都应从真实日志中收集全部变体后再收敛判定条件。
  2. 合成 harness 与真实会话证据互补:README(合成、可控、可重复)与 TESTIMONY(真实、不可伪造、暴露 mock 覆盖不到的 SDK 路径)各自回答不同问题;发布前两者都应保留。
  3. 验证必须可隔离:QA 前后opencode db会话数不变 +git checkout回滚标记,是"证明验证本身干净"的模板。
  4. 测试断言要验证调度顺序calls.map((call) => call.path)断言[{ id }, "id"],比只断言最终成功更能防止未来有人破坏降级重试逻辑。

参考路径速查

  • 证据文档:.omo/evidence/20260804-prompt-async-gate-undefined/README.md、.omo/evidence/20260804-prompt-async-gate-undefined/TESTIMONY-USER-SESSION.md
  • 修复实现:packages/utils/src/prompt-async-gate.ts(isObjectPathTypeError/hasObjectSessionPath/dispatchWithPathCompatibility/dispatchInternalPrompt)、packages/omo-opencode/src/shared/prompt-async-gate.ts
  • 回归测试:packages/utils/src/prompt-async-gate-path-compat.test.ts、packages/omo-opencode/src/shared/prompt-async-gate-path-compat.test.ts
  • 设计 ADR:docs/reference/prompt-async-gate-rfc.md
  • 变更记录:CHANGELOG.md(PR #6583 合并记录9ffcab37f

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询