Skip to content

fix(core,opencode): resolve audit CRITICAL/HIGH defects across crash recovery, settlement races, and hot paths - #154

Merged
LeXwDeX merged 1 commit into
devfrom
fix/dag-audit-fixes
Jul 31, 2026
Merged

fix(core,opencode): resolve audit CRITICAL/HIGH defects across crash recovery, settlement races, and hot paths#154
LeXwDeX merged 1 commit into
devfrom
fix/dag-audit-fixes

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Jul 31, 2026

Copy link
Copy Markdown
Owner

背景

一次全代码库审查(DAG 工作流:4 并行 reviewer + 1 仲裁者)报告了 3 个 CRITICAL + 12 个 HIGH 级缺陷,集中在三个域:崩溃恢复数据丢失、资源无界增长、热路径性能退化。本 PR 逐项定位、论证并修复。

每项都经过独立诊断验证,其中 5 项修正了审查报告的论断(详见下方"论断修正")。

CRITICAL 修复

C1 · TUI Worker 空异常处理器packages/opencode/src/cli/tui/worker.ts

全局 uncaughtException/unhandledRejection 处理器为空函数,静默丢弃所有未捕获异常;进程在损坏状态下继续运行,掩盖所有其他缺陷的崩溃信号(表现为 TUI 黑屏/挂死且零日志)。

  • uncaughtException:同步落盘到 Global.Path.log/opencode.logappendFileSync 保证 exit 前写完)→ Rpc.emit("worker.fatal") 通知主进程 → process.exit(1)
  • unhandledRejection:落盘日志但不退出 —— 后台任务的游离 rejection 不等于进程损坏,保留原"保活"意图,只消除静默
  • 加固 util/rpc.ts:原本 worker 内任何 RPC 方法抛错都会变成 unhandledRejection,且主进程侧 Promise 永久 pending(pending Map 泄漏 + 调用方冻结)。现在 listen 捕获错误回传 rpc.result { error }client.call 相应 reject
  • 主进程 cli/cmd/tui.ts 三路兜底:worker.fatal RPC 事件、worker error、worker close,任一触发即 terminate + UI.error + exit(1)stopped 标志保证正常 stop() 不误触发

C2 · LLM 工具结算竞争packages/core/src/session/runner/llm.ts

raceFirst(FiberSet.join, FiberSet.awaitEmpty) 在最后一个工具 fiber 失败时,awaitEmpty 的成功可能赢得竞争,吞掉工具结算失败。机制已从 Effect 源码证实:FiberSet observer 先 backing.delete(使 awaitEmpty 就绪)再 Deferred.doneUnsafe(使 join 就绪),两者同 tick 竞争。

修复:awaitEmpty 胜出后复查 Deferred.isDone(fibers.deferred),有失败则改走 join。保留"任一 fiber 失败立即中止等待"的原语义,确定性关闭吞错窗口。

C3 · LLM 请求构建串行阻塞 — 两处

  • packages/opencode/src/session/prompt.tsEffect.all([...]) 未传 concurrency 选项,Effect 默认顺序执行 —— skills 文件读取、environment DB 查询、instruction glob+HTTP fetch(5s 超时)、MCP 通信、hooks 查询、消息投影这 6 个相互独立的 I/O 被串行化,估算每 turn 额外 100–600ms。同文件同类路径(llm.ts:95、V2 runner)都已正确用 { concurrency: "unbounded" },这是唯一漏掉的热路径
  • packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts:reasoning 的 parseProviderOptions 从循环内串行 await 提升为循环外 Promise.all 批量解析,保持 input 顺序与跨迭代 reasoningMessages 状态

HIGH 修复

# 文件 修复
H1 dag/runtime/recovery.ts 收窄为 catchTag("NotFoundError");意外错误传播而非编造 nodeFailed;移除 [] as never 类型谎言
H2 dag/runtime/loop.ts wake 重投递读取改 catchCause + logWarning,失败不再静默
H3 cli/cmd/run/stream.transport.ts prompt 回合加"连续 8 次(2s)服务端 idle"逃生阈值,正常路径仍事件驱动
H4 dag/runtime/loop.ts dag.fail 移入 evalLock 同一 permit,消除检查→fail 之间的 spawn 窗口
H5 console/.../zen/util/handler.ts 累加权重取模替代 Array(weight).fill 物理展开,槽位布局逐位等价
H6 cli/cmd/run/stream.transport.ts buffered 加 1000 条上限、丢最旧
H7 effect/instance-state.ts 论证为有意设计,仅加注释(见下)
H8 plugin/index.ts void hook.event(...)tryPromise + tapError(logError) + forkDetach,不阻塞总线但 rejection 全部可观测
H9 dag/dag.ts parseWorkflowConfig 加结构守卫(nodes 数组 + 每节点 id/depends_on
H10 acp/service.ts 失败时从缓存删除 in-flight 条目(带同一性校验防误删),下次重试
H11 app/.../event-reducer.ts 每 delta 两次 setStore 用 batch 合并为一次响应式通知
H12 cli/cmd/run/stream.transport.ts recoverQuestion 加 120s 截止 + trace,不再无限轮询 HTTP

论断修正

诊断过程中有 5 项与审查报告的结论不同,按实际根因处理:

  • H1:审查称"SQLite 暂时锁定即触发"不成立 —— DB 错误已被 orDie 转为 defect,Effect.catch 吞不掉。真实问题是宽 catch 折叠所有类型化错误
  • H3:审查建议"补 item.live = true"会引入更糟的新竞态 —— promptAsync 是持久准入,返回时 drain 未启动、会话仍 idle,立即置 live 会让零输出回合提前完成。改为 poll 层逃生阈值
  • H7:无限容量是正确的 —— 驱逐必须由 InstanceStore.disposeDirectory 按目录跨服务协调;单缓存加 LRU/TTL 会独立驱逐产生撕裂的实例状态、释放活跃 fiber 持有的资源。改为把该约束写成注释
  • H9:用结构守卫而非完整严格 Schema —— 后者会拒绝旧持久化行、破坏在途工作流恢复
  • H11text + delta 在 V8/JSC 是 rope 串接,均摊 O(1),O(n²) 论断对现代引擎不成立session-data.ts 未改动;真实开销是每 token 两次 SolidJS 通知
  • 工具结算竞争:V2 Runner 已用 Semaphore.makeUnsafe(1).withPermit + tool.settled 守卫免疫;V1 processor.ts 存在窄窗口(cleanup 250ms 超时后覆盖已完成结果),已加状态守卫 + 同步认领(单线程 fiber 模型下 delete ctx.toolcalls[id] 即互斥,比加锁更轻且无死锁风险)

验证

  • typecheckcore / opencode / app / console-app 四包 tsgo --noEmit 全部 0 错误
  • lint 棘轮:4710 警告(阈值 4716,HEAD 基线 4711)—— 逐文件比对确认净减少,并修掉本次引入的 5 条新警告(unbound-method ×2、no-unsafe-type-assertion ×3、await-thenable ×1)
  • 测试:session-runner 85 + tool-events、DAG recovery/loop/wake/config/validation 133、stream.transport + session-data 43、plugin loader 28、github-copilot 29、processor-effect 15 —— 全部通过,0 失败
  • CodeReview 子代理:16 个文件逐一审查,无 blocker/major/minor/nit
  • L3 深度安全审查:0 findings

门禁

目标分支 dev,需通过 Typecheck(含 lint 棘轮)。合并后 push 到 dev 将触发全量 Unit Tests + E2E 验证。

…recovery, settlement races, and hot paths

- worker.ts: replace empty exception handlers with file logging, worker.fatal RPC notification, and safe exit; main process listens for worker fatal/error/close
- rpc.ts: propagate RPC handler failures to caller instead of unhandledRejection + hung promise
- llm.ts (V2 runner): close awaitToolFibers race where awaitEmpty could swallow the last tool fiber failure
- processor.ts (V1): guard cleanup against overwriting settled tool results; claim toolcalls synchronously
- prompt.ts: parallelize independent request-build I/O with concurrency unbounded
- recovery.ts: narrow session status checker to NotFoundError; propagate unexpected errors instead of inventing node failures
- loop.ts: move orchestrator_unresponsive dag.fail under evalLock; log wake redelivery store failures instead of swallowing
- stream.transport.ts: cap buffered events at 1000, add prompt-turn idle escape threshold, bound recoverQuestion polling to 120s
- convert-to-openai-responses-input.ts: batch reasoning provider-option parsing out of the per-part loop
- handler.ts (zen): cumulative-weight provider pick replaces O(sum-of-weights) array expansion
- plugin/index.ts: observe plugin event hook rejections via forkDetach + logError
- dag.ts: structural guard in parseWorkflowConfig for nodes/id/depends_on invariants
- acp/service.ts: drop failed contextLimit lookups from cache so they retry
- event-reducer.ts: batch per-delta store updates into one reactive notification
- instance-state.ts: document why the ScopedCache is deliberately unbounded
@LeXwDeX
LeXwDeX merged commit 5a100a6 into dev Jul 31, 2026
5 checks passed
@LeXwDeX
LeXwDeX deleted the fix/dag-audit-fixes branch July 31, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant