Skip to content

fix(reuse): 本地账本不再挡住投递、蒸馏与常驻宿主的事件循环 - #58

Merged
forrestlinfeng merged 2 commits into
mainfrom
fix/ledger-off-delivery-path
Nov 8, 2026
Merged

forrestlinfeng merged 2 commits into
mainfrom
fix/ledger-off-delivery-path

Conversation

@forrestlinfeng

Copy link
Copy Markdown
Contributor

Summary

v1.0.6 的发布 run 在 Linux test 上偶发超时:cursor 的 Recipe、correction 两条用例,以及 opencode 的“会话删除后蒸馏”。这三条都在等后台工作,上限 10 秒,重跑后才通过。

根因

  • 在自托管 runner 上用 8 个并发 job、各跑 3 遍整套测试,并逐步计时:每投递一条复用报告,都要在本地账本记一笔,这需要加载整个 evolver-core。runner 空闲时,新进程首次加载要 3.3 秒,负载下超过 10 秒;写事件本身最多 175ms。
  • 加载夹在投递循环里,带来三个问题:
    1. 占着 outbox 锁:cursor 的 hook 新入队一条报告,新起的 worker 抢不到锁就直接放弃,报告只能等持锁者加载完。
    2. 堵住常驻队列:常驻宿主(opencode、MCP、dsh)的后台任务是串行的,排在后面的蒸馏要等投递任务加载完。
    3. 堵住宿主事件循环:加载是同步执行的大段模块代码,会占住所在线程。常驻宿主和插件共用一个事件循环,所以加载期间宿主和插件都停住。真实用户也会遇到:opencode 或 MCP 进程第一次记账时会卡顿。
  • 另外有一个漏发窗口:抢不到锁的投递会放弃,把报告留给持锁者;如果持锁者已经最后看过一次队列,这份报告就要等下一个事件才会发出。

修复

  • DRAIN 只把报告发到 Hub(postPending)。本地记账(recordReports)在 Hub 投递之后单独做:先看有没有待记的账,没有就不加载;有的话,在锁外加载 evolver-core,再拿锁记账。
  • 常驻进程通过新加的 scheduler.alongside 在独立通道记账,不挡后续任务,idle() 会等两条通道都完成。只活一批任务的 worker 进程仍在批末记账。
  • 常驻宿主的账本改走 worker thread(lib/runtime/ledger-thread.js):
    • 线程入口和调用方是同一个文件,靠 workerData 里的角色标识区分,所以会随静态 import 一起打进包里。
    • 线程 unref,不拖住进程退出。
    • 设置 execArgv: [],不继承宿主的启动参数(例如 --input-type)。
  • outbox 的持锁者放锁后再看一次队列,有新报告就再排一轮。
  • npm root -g 的结果在进程内复用,只缓存成功的结果。

Scope and cuts

  • 被要求的是修掉这三条 Linux 偶发超时。本 PR 修的是插件代码里导致超时的原因,没有调大任何等待上限。覆盖所有用到 environment.js 的宿主:常驻的 opencode、MCP、dsh,以及短命进程的 cursor、claude-code、codex、grok 等。
  • 测试改动(行为没变,按既定设计改写校验):test/hosts/dsh/plugin.test.js 的 “the plugin's unload ends the open sessions…”。
    • 它原先要求卸载中的进程自己投递 aborted 报告,并计一次“没有模型服务”的尝试,这要靠卸载那一刻碰巧还有本地记账在跑。
    • dsh 插件的注释写明 “what they leave is only written, and the next session start delivers and distills it”,现在按这个设计校验:卸载后报告已写入队列,下一个进程启动后投递它并完成蒸馏。
    • “没有模型服务时这次尝试算失败”由 test/hosts/dsh/model.test.js 覆盖。不再使用的 elapseRetryWaits 一并删除。
  • 压测中发现的其他负载敏感用例,不在本 PR:它们在 main 上同样会在压测中失败,在实际 CI 负载下目前没有挡过发布,是否处理由插件维护者决定。
    • grok 的“会话结束在 1.5 秒内答复”:计时包含 hook 进程自身的启动时间。
    • redaction stays linear on long adversarial input:CPU 计时。
    • test/hosts/cursor/hooks.test.js 整个文件超过 120 秒的文件级超时。

Evidence

  • 本机全套 875 条:873 通过、0 失败。所有能打包的宿主(antigravity、claude-code、codex、dsh、grok、minimax、opencode、qoder、workbuddy)的包里都有 lib/runtime/ledger-thread.js。
  • 新增 3 条回归测试,各对应一个真实故障;去掉对应修复后都会失败:
    • test/runtime/file-stores.test.js:持锁者最后一次看完队列后才入队的报告,即使它自己的投递因为锁被占而放弃了,也会被送出。
    • test/app/turn-lifecycle.test.js:本地账本还在加载时,报告已经到了 Hub,下一批任务也能照常运行。
    • test/runtime/local-ledger.test.js:打包后的插件加载一个需要 2 秒的 evolver-core 时,宿主事件循环最长停滞不到 1 秒。不走线程时停滞 2014ms。
  • 用同步阻塞模拟首次加载耗时 11 秒,opencode 的 session-end.test.js:没有线程时,“会话删除后蒸馏”这条复现了 CI 上的超时;用线程后 8/8 通过。
  • 自托管 Linux 压测,8 个并发 job、各跑 3 遍整套测试,修复分支和 main 在同样条件下各跑一次:
    • 三条目标用例在 main 上(run 42446841415),负载重的那台机器 3 个 job 共 9 轮全部失败;在修复分支上(run 42446341927),24 轮里 0 次失败。
    • 剩下的失败只有 grok 的 1.5 秒用例,两边次数和耗时分布都相当(main 9 次,修复分支 10 次),说明这条和本 PR 无关。

Merge Danger

可逆,没有改状态文件格式。outbox 里本地记账相关的字段(localAt 等)含义不变,旧状态可以直接沿用。本地记账会比 Hub 投递晚一点,但仍在同一批后台任务里完成。

🤖 Generated with Claude Code

记一笔本地复用账要加载整个 evolver-core(自托管 runner 空闲时首次约 3.3 秒,负载下十秒以上),
原先它夹在投递循环里:占着 outbox 锁让后到的报告等,堵住常驻进程的串行后台队列让蒸馏等,
而且同步执行的模块加载会占住常驻宿主与插件共用的事件循环。

- 投递只发 Hub;本地记账在 Hub 投递之后单独进行,且只在有待记条目时加载 evolver-core,加载在锁外。
- 常驻进程经 scheduler.alongside 在独立通道记账;常驻宿主的账本跑在 worker thread(同文件,随包打入,
  unref、不继承宿主启动参数);只活一批任务的 worker 进程仍在批末记账。
- 抢不到 outbox 锁的投递把报告留给持锁者:持锁者放锁后再看一次队列,排空期间新到的。
- npm root -g 的结果在进程内复用。
- dsh 卸载用例按既定设计校验(卸载只写入,下一次启动投递并蒸馏),删去不再使用的 elapseRetryWaits。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,未发现已核验问题,但发现需要人工确认的风险。

另有 4 个需要人工确认的风险;黄色 advisory 不阻塞合并,建议开发 agent review advisory 评论并逐条确认。

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-agent-plugin"、pr_number=58、head_sha="754f85272600967eac029de0c2ed89bb28f2b6e4",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 754f85272600

@evomap-aiqa-bot

Copy link
Copy Markdown

Sentinel 保留了 4 个尚未确认的审核候选。这些黄色条目不阻塞合并;红色 P0/P1 与其他独立硬门禁不受影响。 单一猜测、缺少变更行证据或纯 checklist 的候选会统一降为 P3。
开发 agent:请 review 以下 advisory,并逐条给出“已验证无风险 / 已修复 / 转为后续事项”的结论;不要只做整体确认。

  • [P3] worker 在副作用完成后退出会导致 ledger 重试重复入账 — worker 中的 ledger 写入与父线程 outbox 的 localAt 提交不在同一原子边界内。若 recordReuseLocally...(truncated) · lib/runtime/ledger-thread.js·advisory_6ba4f0c0f54d25150ea7bd08`
  • [P3] 异步完成可能覆盖更新后的状态 — 状态链探针:late async completion may overwrite newer local/store state because no request/version/abort guard is ...(truncated) · lib/runtime/store/file-stores.js · slice:whole_pr:state_chain_959b544a096c1139
  • [P3] 旧 worker 的 exit 回调会清空并拒绝替代 worker 的请求 — error 和 exit 共用无 worker 身份校验的 failAll()。旧 worker 的 error 会将 thread 清空并拒绝...(truncated) · lib/runtime/ledger-thread.js · advisory_2e9ed592d01993259dcf4d38
  • [P3] 状态链路需要人工确认 — 状态链探针:hydrate/refresh path writes data/cache state; verify failed or stale responses preserve last-known-goo...(truncated) · lib/runtime/store/file-stores.js · slice:whole_pr:state_chain_14444ff93de8566f

需要完整证据时,可用行末 finding_key 查询 Sentinel PR review context。

由 Sentinel 审核 · 目标提交 754f85272600

处理 #58 的 Sentinel 评审项:error 与 exit 曾共用不认线程身份的清理,旧线程迟到的 exit
会拒绝替代线程的请求并清空它的引用;等待表与清理现按线程实例隔离。补一条测试固定
"线程在写入后停止 → 记为 uncertain、不再重试、下一次由新线程服务"。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,未发现需要报告的问题。

最近一次申诉核验 · resolution_fcf8e18c2ea7296a680f0295aefdf53c · completed

  • slice:whole_pr:state_chain_959b544a096c1139 accept: False positive. On the exact resolution head, drainUnderLock is exclusively executed under withLock(outbox.lock). It merges queued entries with the current outbox, awaits delivery, then writes `{ ...readState(outboxPath, {}), ...changed...(truncated)
  • slice:whole_pr:state_chain_14444ff93de8566f accept: False positive. The exact-head hunk operates on a lock-serialized persistent outbox, not a hydrate/refresh projection with user-visible last-known-good state. outbox.some(isLocalRecordDue) only evaluates `Object.values(readState(...)).som...(truncated)

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-agent-plugin"、pr_number=58、head_sha="ad239392531cfb3ce1b0a8fad0650440b233513d",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 ad239392531c

@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

🟢 Sentinel 风险索引已清空(head ad239392531c)

本 head 最新复跑没有保留黄色 risk / hypothesis。

@forrestlinfeng
forrestlinfeng merged commit 5967706 into main Nov 8, 2026
8 checks passed
@forrestlinfeng
forrestlinfeng deleted the fix/ledger-off-delivery-path branch November 8, 2026 05:02
imxyanua added a commit to imxyanua/evolver-agent-plugin that referenced this pull request Nov 8, 2026
Keep the Kimi native monitor wiring and the ledger delivery changes from upstream PR EvoMap#58.
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