ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

oh-my-claudecode 取消(cancel)后技能活动状态残留:skill-active-state 与 stop hook 的清理断裂分析与修复方案

oh-my-claudecode 取消(cancel)后技能活动状态残留:skill-active-state 与 stop hook 的清理断裂分析与修复方案 oh-my-claudecode 取消cancel后技能活动状态残留skill-active-state 与 stop hook 的清理断裂分析与修复方案【免费下载链接】oh-my-claudecodeTeams-first Multi-agent orchestration for Claude Code项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode导读本文聚焦 oh-my-claudecode以下简称 OMC中一个真实存在的状态管理缺陷当用户通过/oh-my-claudecode:cancel中断会话时cancel 流程会清理 ralph、ultrawork、autopilot、team 等模式的运行状态却遗漏了保护类技能写下的skill-active-state.json导致 Claude Code 的 Stop hook 在取消之后仍然持续输出[SKILL ACTIVE: xxx]强化提示直到触达强化上限5 次或 15 分钟 TTL 自动过期。文档 cancel-skill-active-state-gap.md 完整记录了这一缺陷的复现步骤、根因、临时逃生通道与三种候选修复方案。读完本文你将理解 OMC 技能保护台账的存储模型与双副本不变量、cancel 的清理边界为何出现盲区以及方案 A把skill-active纳入 state 工具模式枚举为什么是比绕过式删除更优雅的工程选择。1. 缺陷概述cancel 清理了“模式状态”却漏掉了“技能活动状态”1.1 Summary两条互相咬合的状态体系OMC 的会话状态管理事实上存在两条独立的台账线模式运行状态mode state由/oh-my-claudecode:cancel显式清理覆盖 ralph、ultrawork、autopilot、team、ralplan、omc-teams、deep-interview 等。技能活动状态skill active state保护类技能如sciomc、skillify、release被 Skill 工具调用时写下的skill-active-state.json。文档明确指出缺陷行为模式cancel-skill-active-state-gap.mdWhen/oh-my-claudecode:cancelis invoked, it clears mode state files for ralph, ultrawork, autopilot, team, etc. — but it doesnotclearskill-active-state.json. This causes the stop hook to keep firing reinforcements after cancel until either the reinforcement limit or the stale TTL expires.也就是说cancel 只是“模式状态”的充分清理者却不是“技能活动状态”的清理者。两条台账一旦脱节stop hook 就会拿着残留的active: true继续拦截“停止”意图。1.2 影响范围与量化后果残留文件使 stop hook 持续输出[SKILL ACTIVE: skill]强化消息提示文案为reinforcement 1/5 → 2/5 → …用户实际被“锁”在该会话中直到强化次数打到上限max_reinforcementsmedium为 5 次或stale_ttl_ms过期medium为 15 分钟。若用户没有手动介入最坏等待时间即 15 分钟。2. 复现路径一份可执行的缺陷验收用例原文档给出三行复现步骤这里结合源码补充每一步背后的预期行为方便你把该文档直接当作缺陷回归用例使用调用一个medium保护级别的技能例如sciomc、skillify或release这些均被注册为medium见 src/hooks/skill-state/index.ts在技能尚未跑完之前立即调用/oh-my-claudecode:cancel观察stop hook 并不因为 cancel 而收敛而是继续以[SKILL ACTIVE: sciomc]阻塞并递增强化计数1/5 → 2/5 → …直到触达 5 次上限或 15 分钟 TTL 自动清账。其中第 3 步的“强化计数递增”与“自动清账”并非幻影行为而是由 src/hooks/skill-state/index.ts 中的checkSkillActiveState()强制保证的每触发一次 stop 事件只要状态非 stale、非超限、且没有正在运行的非 stale agent就把reinforcement_count加一并重新写入两份状态文件同时返回shouldBlock: true。3. 底层机制skill-active-state.json的双副本台账模型要理解“为何残留一个 JSON 就能锁住 stop”需要先读透技能状态文件的存储结构。文档给出了文件位置.omc/state/sessions/{sessionId}/skill-active-state.json但从 src/hooks/skill-state/index.ts 的文件头注释可以确认它其实是v2 混合 schema 的“双副本工作流台账”会话副本.omc/state/sessions/{sessionId}/skill-active-state.json—— 会话本地读取的权威来源根副本.omc/state/skill-active-state.json—— 跨会话聚合读取的权威来源。文件 JSON 结构如下v2 形态workflow-slot 分支 兼容分支并存{ version: 2, active_skills: { canonical workflow skill: { skill_name: ..., started_at: ..., completed_at: null, parent_skill: null, session_id: ..., mode_state_path: ..., initialized_mode: ..., initialized_state_path: ..., initialized_session_state_path: ... } }, support_skill: { active: true, skill_name: plan, started_at: ..., last_checked_at: ..., reinforcement_count: 0, max_reinforcements: 5, stale_ttl_ms: 900000 } }源码中定义了三条硬不变量HARD INVARIANTS它们是理解修复难点的钥匙writeSkillActiveStateCopies()是唯一允许持久化 workflow-slot 状态的助手任何写入、确认confirm、墓碑tombstone、TTL 剪枝与硬清除都必须同时更新两份副本支持技能support-skill的写入也走同一助手确保共享根文件永不丢失active_skills分支会话副本对会话本地读取权威根副本对跨会话聚合权威。同时注意另一关键设计当 resolved 状态为空时无 slot、无 support_skill对应的文件会被删除而非写入空对象——“文件不存在”才是规范的“空状态”。这直接解释了为何修复方案 A让state_clear能按skill-active模式把文件清掉在语义上是自洽的清空即删除无需维护哨兵文件。4. 保护分级与注册表medium的“5 次强化 / 15 分钟 TTL”从哪来文档中反复出现的 “5 reinforcements, 15-min stale TTL” 出自 src/hooks/skill-state/index.ts 的三级配置表heavy额外存在none关闭保护保护级别maxReinforcementsstaleTtlMs说明none00不写状态、不拦截 stoplight35 min简单快捷指令medium515 min评审 / 规划类长流程heavy1030 min长时间运行流程每个技能通过SKILL_PROTECTION注册表映射到级别src/hooks/skill-state/index.ts// Medium protection (review/planning, 5 reinforcements) omc-plan: medium, plan: medium, review: medium, external-context: medium, omc-setup: medium, setup: medium, psm: medium, sciomc: medium, skillify: medium, release: medium,而cancel自身被注册为none同组还有 workflow 类技能 autopilot/ralph/team/ralplan 与只读类技能这说明 cancel 本不该被技能保护拦住被拦住正是“残留账本 兜底策略缺失”耦合出来的结果。getSkillProtection()还做了两条防御src/hooks/skill-state/index.ts仅当技能名带oh-my-claudecode:前缀或被以原始名调用时才计保护避免与用户自建同名项目技能混淆issue #1581RETIRED_SKILL_NAMESultrawork、ccg一律视为none只做清理、永不再武装 stop 拦截。checkSkillActiveState()的完整判定链src/hooks/skill-state/index.ts依次为无活动状态 → 会话隔离校验 → 退役技能豁免 →stale 检查过期即自动清除并放行→强化上限检查打满即清除放行→ 编排器空闲豁免有非 stale 的 running agent 则重置计数放行→ 否则阻塞并把reinforcement_count 1写回双副本。可见“15 分钟 / 5 次”是文档所述阻塞的两道天然熔断也是没有手动清理时用户唯一的等待出口。5. 根因定位state_clear的 mode 枚举没有skill-active5.1 直接根因cancel 技能清理状态时依赖 MCP 工具state_clear其mode参数用z.enum约束。原文档给出缺陷出现时的枚举内容autopilot | team | ralph | ultrawork | ralplan | omc-teams | deep-interview枚举中没有skill-active→state_clear(modeskill-active, ...)在 schema 层就无法通过 → 文件永远不会被 cancel 的“按模式清扫”路径触及 → stop hook 每次读到的都是残留的active: true。5.2 停靠点stop hook 中的 “Priority 2” 调用链技能状态检查在 stop hook 中的真实调用点位于 src/hooks/persistent-mode/index.ts文档标注的index.ts:1170行号已随代码演进偏移当前是 checkPersistentModes 的 “Priority 2”// Priority 2: Skill Active State (issue #1033) // Skills like code-review, plan, tdd, etc. write skill-active-state.json // when invoked via the Skill tool. This prevents premature stops mid-skill. try { const { checkSkillActiveState } await import(../skill-state/index.js); const skillResult checkSkillActiveState(workingDir, sessionId); if (skillResult.shouldBlock) { return { shouldBlock: true, message: skillResult.message, mode: skill-active, metadata: { phase: skill:${skillResult.skillName || unknown} }, }; } } catch { // If skill-state module is unavailable, skip gracefully }这段代码每次 stop 事件都会执行读到活动支持技能 → 返回mode: skill-active的硬阻塞 →createHookOutput()把shouldBlock: true翻译为continue: false即 Claude Code 不允许本次“停止/结束”继续。cancel 之后若没有任何机制提前清掉台账这条路径就会持续命中。5.3 上游防御已存在但当时未覆盖该分支值得对照的是persistent-mode 的 stop hook 对 cancel 其实已有成熟的信号防御isExplicitCancelCommand(stopContext)与isSessionCancelInProgress()检测到取消时会直接返回shouldBlock: falsesrc/hooks/persistent-mode/index.tscancelInProgress也会被传入checkRalphLoop()/checkAutopilot/checkUltrawork()等函数作为“取消窗口内不再重新武装”的信号。唯独 Priority 2 的技能状态检查在当时的实现中未接收该信号——这正是 Option C 的着眼点。6. 逃生通道缺陷当时的工作区解法原文档记录了两种不依赖代码修复的即时解围手段# 方式一手动删除残留账本精确到 session rm .omc/state/sessions/sessionId/skill-active-state.json方式二什么都不做等待内置熔断自行解围——15 分钟 stale TTL 过期或 stop hook 的第 5 次强化触发上限自动clearSkillActiveState()。补充说明由于 v2 台账采用双副本若根副本.omc/state/skill-active-state.json中仍存在跨会话 slot手工只删会话副本即可解除本会话拦截源码中会话本地读取以会话副本为权威src/hooks/skill-state/index.ts且禁止回落到根副本以避免跨会话状态泄漏issue #456。7. 修复方案对比A/B/C 的取舍原文档给出三种候选方案这里逐一结合源码给出落地面与代价分析。Option A —— 把skill-active纳入state_clear的 mode 枚举推荐让 cancel 可以显式调用state_clear(modeskill-active, session_id...)优点使skill-active成为 state 工具族中的一等模式与 ralph、team、autopilot 等模式的治理方式完全一致状态文件的清理含双副本统一走clearStateFileLockedIf/clearSkillActiveState()的加锁路径杜绝绕过式删除带来的竞态。落地证据当前仓库快照中src/tools/state-tools.ts 的STATE_TOOL_MODES已包含skill-active同时被列入STATE_WRITE_MODES而stateClearTool的 schema 正是z.enum(STATE_TOOL_MODES)src/tools/state-tools.ts。git 历史中的提交d31b32c24 fix(state-tools): add skill-active to STATE_TOOL_MODES so cancel can clear it (#2122)即对应此方案的落地。Option B —— cancel 技能在清理段直接补一步在 skills/cancel/SKILL.md 的 “No Active Modes / force-clear” 段落中在模式清理之后追加清除skill-active-state.json的步骤After mode cleanup, also clear skill-active-state.json: state_clear(modeskill-active, session_id)优点无需新增任何基础设施改动最小、见效最快可独立发布。现状佐证cancel 技能的 bash 兜底分支中已经包含直接删除该文件的动作skills/cancel/SKILL.md的 fallback 段会执行rm -f $OMC_STATE/sessions/$SESSION_ID/skill-active-state.json说明项目把“文件删除逃生”定位为兜底而非主路径主路径应优先走 state 工具。Option C —— 在技能状态 stop hook 里识别 cancel 信号在src/hooks/skill-state/index.ts的 stop-hook 检查逻辑即被 Priority 2 调用的checkSkillActiveState阻塞之前先检测 cancel-in-progress 信号仿照checkUltrawork()接收cancelInProgress参数的方式短路放行。优点防御放在“拦截点本身”即使未来出现其他漏清理的状态写入方也能兜住属于纵深防御。代价需要把 cancel 信号从 persistent-mode 的检查入口贯通到 skill-state 模块当前checkSkillActiveState(workingDir, sessionId)的调用签名并未接收该参数改动面略大于 A。结论原文档推荐Option A is the cleanest: it makesskill-activea first-class mode in the state tooling, consistent with how other modes are managed. Option B is a quick fix with no new infrastructure needed.即A 是正解把skill-active提升为一等模式治理语义统一B 是无基建要求的快速修复若追求兜底鲁棒性可在 A/B 之上补充 C 的取消信号短路。8. 修复验证与运维确认清单结合当前仓库可执行的验证路径确认枚举已就绪查看 src/tools/state-tools.ts 中STATE_TOOL_MODES是否包含skill-active当前快照已包含且 git 提交d31b32c24#2122即为此修复运行一次清理冒烟在技能残留场景下执行state_clear(modeskill-active, session_id...)随后用state_read(modeskill-active, session_id...)确认返回 “No state found”且两个路径会话副本 根副本均不再存在活动条目回归 stop 行为残留清除后再次触发停止确认不再出现[SKILL ACTIVE: ...] reinforcement N/5强化文案。注意仓库为只读研究环境以上验证步骤描述的是在你自己的 checkout/运行环境中的操作方式文档与本文均不涉及修改仓库内容。9. 工程启示状态清理边界的“穷举”原则这一缺陷的本质是两种状态台账由不同子系统写入、却由 cancel 这一单一入口负责回收回收方对“自己该管哪些文件”的认知来自state_clear的模式枚举。一旦枚举落后于写入方新增的模式如 skill protection就会出现“写入有门、回收无门”的不对称。工程上可沉淀的三条经验枚举即契约任何按模式批量清理的入口其 mode 枚举必须与全部状态写入方保持单一事实源同步并配契约测试防止漂移硬不变量要可复核双副本同写、空态即删文件这类不变量src/hooks/skill-state/index.ts应当有专门的测试夹具验证“任一副本未更新即为失败”取消信号要在所有拦截点贯通一旦系统有“cancel 时不得重新武装”的信号cancelInProgress它应当贯穿全部 stop-hook 子检查而不只是优先级较高的 ralph/autopilot/ultrawork 分支。10. 相关代码与文档索引缺陷分析与修复方案文档cancel-skill-active-state-gap.md本文主线技能保护注册表与检查逻辑src/hooks/skill-state/index.ts含PROTECTION_CONFIGS、SKILL_PROTECTION、checkSkillActiveStatestop hook 中技能状态调用点Priority 2src/hooks/persistent-mode/index.tsstate 工具模式枚举与state_clear定义src/tools/state-tools.ts、src/tools/state-tools.tscancel 技能清理步骤与兜底删除分支skills/cancel/SKILL.md原文档背景锚点技能保护特性最初来自 issue #1033PR #2099 修复的是 ralph/ultrawork/autopilot 的awaiting_confirmation残留问题属于不同子系统本缺陷不在其覆盖范围内。【免费下载链接】oh-my-claudecodeTeams-first Multi-agent orchestration for Claude Code项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表