
oh-my-openagent Atlas Hook 缺失 worktree_path 崩溃修复从 boulder.json 根因分析到纵深防御的执行计划【免费下载链接】oh-my-openagentOmO: Drop your tokens. Ultrawork. Done.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent本文基于 oh-my-openagent 仓库中work-with-pr技能评估工作区产出的一份真实修复执行计划完整剖析 Atlas hook 在boulder.json缺少worktree_path/active_plan字段时进程崩溃的根因与调用链并结合当前仓库源码验证该计划每一步的落点从readBoulderState()的校验缺陷、getPlanProgress()的防御缺失到setTimeout中悬浮 Promise 导致的未处理拒绝最终给出源头失败、边界拦截、异步兜底的三层防御修复方案与 CI 验证流程。该计划文件的原始位置是 execution-plan.md其所在评估工作区还配套了 code-changes.md、pr-description.md 和 verification-strategy.md构成了一个完整的 PR 工作流产物集。背景Atlas Hook 与 boulder.json 状态文件在 oh-my-openagent 中Atlas 是一套 hook 机制负责在会话空闲session idle时检测巨石任务boulder的计划进度并在未完成时自动注入续跑提示continuation让 agent 沿计划继续工作。其状态持久化在项目的boulder.json文件中由readBoulderState(directory)读取核心字段包括active_plan当前计划文件路径是计算进度的入口plan_name计划名称worktree_path可选的 git worktree 路径用于把计划文件定位到 worktree 副本session_ids/session_origins关联会话及其来源direct/appendedworks当前仓库已演进出的多任务结构按work_id组织的任务状态集合。路径拼装逻辑在 getBoulderFilePath 中完成。这份执行计划要解决的正是当boulder.json被手动编辑、损坏或写入不完整时缺少worktree_path、active_plan等字段Atlas hook 链路上的哪一环会崩溃、为什么崩溃、以及如何系统性修复。Bug 分析三层缺陷叠加出崩溃缺陷一readBoulderState()校验不足unsafe cast 直通下游计划文档指出的根因是readBoulderState()解析boulder.json时校验过于宽松原文引用的问题代码形态为const parsed JSON.parse(content) if (!parsed || typeof parsed ! object || Array.isArray(parsed)) return null if (!Array.isArray(parsed.session_ids)) parsed.session_ids [] return parsed as BoulderState // -- unsafe cast, no field validation即只修复了session_ids却不校验active_plan、plan_name、worktree_path。畸形文件如{}或缺少关键字段会被当作合法的BoulderState直通下游携带active_plan: undefined进入 hook 链路。对照当前仓库实现可以确认这一判断的准确性现在的readBoulderState()位于 read-state.ts由 packages/omo-opencode/src/features/boulder-state/storage.ts 从oh-my-opencode/boulder-state包统一再导出。它在原文档基础上已经增加了空对象拦截const parsed JSON.parse(content) if (!parsed || typeof parsed ! object || Array.isArray(parsed) || Object.keys(parsed).length 0) { return null }并通过 normalizeState 对session_ids、session_origins、task_sessions、各 work 的会话字段做了归一化同时用try/catch包住整个解析过程JSON 解析失败直接返回null。但对active_plan/worktree_path字段级缺失的校验依然没有——一个{session_ids: [abc]}这样的文件仍会以active_plan: undefined被返回。这正是计划文档 Step 1 的价值所在。当前代码中部分缓解措施是 getBoulderWorks 在走legacy mirror分支时会检查!state.active_plan || !state.plan_name || !state.started_at并返回空数组但这条防线只覆盖 works 视图不覆盖直接消费state.active_plan的调用点。缺陷二getPlanProgress(undefined)触发 TypeError 崩溃路径计划文档给出的崩溃链是boulder.json因手动编辑、损坏或部分写入而缺少必需字段readBoulderState()返回带active_plan: undefined的BoulderState多个调用点把boulderState.active_plan传给进度解析函数文档点名的调用点包括src/hooks/atlas/idle-event.ts:72位于setTimeout回调内——未处理的拒绝src/hooks/atlas/resolve-active-boulder-session.ts:21src/hooks/atlas/tool-execute-after.ts:74getPlanProgress()内部对undefined路径调用existsSync(undefined)抛出TypeError: The path argument must be of type string。在当前仓库中这条链路仍然成立且崩溃点可以更精确地定位resolve-active-boulder-session.ts 在确认会话归属后直接以resolveBoulderPlanPath(input.directory, nextBoulderState)的结果调用getPlanProgress()resolveBoulderPlanPath 第一行就对state.active_plan执行isAbsolute(trackedPath)active_plan为undefined时这里就会抛 TypeError值得注意的是它对worktree_path是宽容的——第 20 行state.worktree_path?.trim()用 truthiness 判断缺失时优雅回退到主目录下的计划路径。也就是说worktree_path本身被妥善处理真正致命的是同一状态里的active_plan缺失getPlanProgress 自身没有任何入参防御export function getPlanProgress(planPath: string): PlanProgress { if (!existsSync(planPath)) { // planPath 为 undefined 时在此抛 TypeError return { total: 0, completed: 0, isComplete: false } } ... }文件不存在时它能优雅返回零进度但路径参数非字符串这一输入错误没有任何拦截。缺陷三setTimeout异步回调中的悬浮 Promise计划文档强调的次生问题最具隐蔽性sessionState.pendingRetryTimer setTimeout(async () { // ... no try/catch wrapper const currentBoulder readBoulderState(ctx.directory) const currentProgress getPlanProgress(currentBoulder.active_plan) // CRASH if active_plan undefined // ... }, RETRY_DELAY_MS)setTimeout的async回调会创建一个无人等待的 Promise回调内任何抛错都变成未处理的 Promise 拒绝unhandled rejection直接把进程拖垮——而且发生在定时器延迟触发之后离最初的 idle 事件很远极难定位。值得记录的是当前仓库中这段scheduleRetry实现已重构到 idle-continuation.ts已经体现了计划中 Step 2 的修复形态——回调体完整包在try/catch中入口先做lifecycleActive、失败次数上限、stall 状态、最终波次审批等多重短路检查随后对readBoulderState的结果做了if (!currentBoulder) return空值判断第 188-194 行catch 分支里记录日志、递增promptFailureCount并递归重排重试形成带退避的失败自愈循环。对照 idle-event.ts 主流程中scheduleRetry的三处调度点后台任务运行时、注入冷却期内可以看出这条异步链路的错误隔离已经比较完整。这提示读者计划文档描述的是修复前状态仓库后续演进已经把异步层兜底这一层补上了但源头校验与边界守卫两层仍值得按计划在boulder-state包内落实。六步修复计划从源头到 PR 的完整路线Step 1强化readBoulderState()的字段校验目标文件文档原始路径src/features/boulder-state/storage.ts当前落点 read-state.ts在session_ids归一化之后补上active_plan与plan_name必需字段的校验校验worktree_path只能是undefined或字符串拒绝null、数字等类型污染对缺少必需字段的状态整体返回null让上游按无活跃 boulder处理。这个设计原则在现有代码里已有先例可循read-state.test.ts 中已存在#given malformed state json #when reading state #then null is returned的畸形 JSON 用例字段级校验用例可以与其并列沿用项目既有的#given ... #when ... #then ...命名规范。Step 2为setTimeout回调补上 try/catch目标文件文档路径src/hooks/atlas/idle-event.tssetTimeout 逻辑当前在 idle-continuation.ts将重试回调主体包入 try/catch并使用 atlas hook 的 logger 记录错误现有 catch 分支的写法log(\[${HOOK_NAME}] Failed during boulder continuation retry, { sessionID, error: loggedError }) 即是标准范式。如前所述当前仓库该处已具备此保护实施前应先核实现状避免重复改动。Step 3getPlanProgress增加防御性早退目标文件当前 plan-progress.ts在函数入口对非字符串planPath早退返回零进度对象。这是边界守卫层——即使上游校验被绕过进度解析器自己也不会把TypeError抛给调用方与existsSync失败时的优雅降级行为保持一致。Step 4测试补齐文档点名的测试文件及当前仓库对应物src/features/boulder-state/storage.test.ts→ 覆盖缺失/畸形字段的用例当前对应 packages/boulder-state/src/read-state.test.tssrc/hooks/atlas/index.test.ts→ 覆盖boulder 缺少worktree_path时 atlas hook 仍正常工作的场景当前对应 packages/omo-opencode/src/hooks/atlas/index.test.ts同目录下的 idle-event.test.ts 和 resolve-active-boulder-session.test.ts 也是相关用例的合理落点。Step 5运行 CI 检查计划文档给出的验证命令项目使用 bun 作为运行时与测试框架见根目录 bun.lock 与 bunfig.tomlbun run typecheck bun test src/features/boulder-state/storage.test.ts # 当前对应 packages/boulder-state/src/read-state.test.ts bun test src/hooks/atlas/index.test.ts # 当前对应 packages/omo-opencode/src/hooks/atlas/index.test.ts bun test # 全量测试Step 6创建 PR分支fix/atlas-hook-missing-worktree-path目标分支dev合入前确认 CI 全绿。方法论总结三层纵深防御模式这份执行计划的价值不仅在于修掉一个崩溃更在于它示范了对外部持久化状态不可信这一前提的系统性应对可以拆成三层复用源头失败fail at the source读取器对不可信 JSON 做完整字段校验畸形数据在入口就变成null而不是带undefined字段流入全局。对应 Step 1落点在 read-state.ts边界守卫guard at the boundary公开 API如getPlanProgress对自身入参做防御性早退不因某个调用方失手而抛原生 TypeError。对应 Step 3落点在 plan-progress.ts异步层兜底isolate the async layer所有setTimeout/定时触发的异步回调必须有 try/catch 与失败计数/退避机制把未处理拒绝变成可观测、可自愈的重试。对应 Step 2当前实现可见 idle-continuation.ts其失败计数与退避常量定义在 idle-constants.ts。三层中任何一层失守时其余两层仍能阻止进程崩溃——这正是该计划文档在Bug Analysis → Step-by-Step Plan结构中反复强调 try/catch、早退与返回null的原因。延伸阅读路径状态读取与归一化实现packages/boulder-state/src/storage/read-state.ts路径解析与 worktree 回退逻辑packages/boulder-state/src/storage/path.ts计划进度解析packages/boulder-state/src/storage/plan-progress.tsAtlas idle 事件主流程packages/omo-opencode/src/hooks/atlas/idle-event.ts会话归属解析packages/omo-opencode/src/hooks/atlas/resolve-active-boulder-session.ts状态包文档packages/boulder-state/AGENTS.md本计划的完整评估工作区含代码变更、PR 描述与验证策略.agents/skills/work-with-pr-workspace/iteration-1/eval-2/without_skill/outputs/【免费下载链接】oh-my-openagentOmO: Drop your tokens. Ultrawork. Done.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考