Skip to content

fix(research): 合成阶段取消挂起并持续占用同一证券的研究任务 #273

Description

@Guo-Yixin

问题与影响

Deep Research 已进入合成阶段时,如果 beforeSynthesis 身份检查或 ResearchSynthesizer.synthesize() 不响应取消信号、返回的 Promise 长时间不结算,ResearchService.cancel(runId) 会一直等待。

调用路径:ResearchService.cancel → controller.abort() → await active.done → ResearchService.execute → ResearchRunner.run → await beforeSynthesis / synthesizer.synthesize。当前编排层直接等待后两者,没有独立的取消结算边界。

实际影响:

  • 即使持久化/展示状态已变为 cancelled,取消请求本身仍未返回,底层研究编排仍未结束。
  • active 中该证券的 reservation 只在 execute().finally() 清理;旧合成不结算时,用户重新启动同一证券研究仍收到 RESEARCH_RUN_ACTIVE。
  • 取消后迟到的 recovery.onAgentRun 仍可能追加旧运行的检查点事件。
  • 合成阶段的取消 catch 把全部计划能力写为失败,可能让同一已成功能力同时出现在 completed / failed 列表。
  • 在进入 synthesizing 的异步状态迁移期间取消,也可能继续调用合成器。

这是“编排层停止等待、释放任务并隔离迟到工作”的正确性缺陷;不把取消信号已发送等同于取消已完成。

复现

基线:上游 main ba5dcdfd31b162f5edb8b908f7f099a560389326(2026-09-30 排查时仍为该提交)。
实际验证环境:Bun 1.4.2(744846f84)/ Windows 11(Microsoft Windows 10.0.22631)。

最小服务场景:

  1. 使用实际 ResearchService、ResearchRunner、CapabilityExecutor 和临时目录中的 JsonFileStore / ResearchReportRepository;注册一个成功的 company.profile 能力。
  2. 注入受控合成器:首次 synthesize() 返回一个不会响应 AbortSignal 的 deferred Promise;等待合成器确实进入后再取消。
  3. 启动 NVDA.US 研究并执行 service.cancel(run.id)。在旧合成 Promise 尚未结算时检查 cancel 是否返回、同一证券能否再次 start。
  4. 旧实现的 cancel 超过测试的 1 秒 watchdog 仍未结算;reservation 仍占用。同一证券无法重启,直到旧合成自行结束。
  5. 另分别在合成前检查中阻塞、在 synthesizing 状态回调中立即 abort,核对取消是否仍会等待或继续调用合成器。

回归测试在 PR #272 的 packages/shared/src/research/synthesis-cancellation.test.ts;其中包含迟到 resolve/reject、磁盘检查点和真实 Bun 子进程边界。复现不需要模型或 Provider 凭证。

以下为可重复的基线对照步骤(在已有依赖的仓库执行;新 worktree 需准备依赖):

git fetch https://github.com/Guo-Yixin/folio.git codex/research-cancel-synthesis
git worktree add --detach ../folio-synthesis-repro ba5dcdfd31b162f5edb8b908f7f099a560389326
git -C ../folio-synthesis-repro checkout 3a137d8d87e491c7eacb6cffb332f55dc4882e78 -- packages/shared/src/research/synthesis-cancellation.test.ts
cd ../folio-synthesis-repro
bun install --frozen-lockfile
bun test packages/shared/src/research/synthesis-cancellation.test.ts --isolate

对照方式:只把新测试放到未修复 main,保留 main 的生产代码。上述 install 为独立准备依赖的步骤;本地实际验证复用了已安装依赖,没有声称执行过该 install。

实际同测试/同 Bun 对照结果:

  • 未修复 main:2 pass / 6 fail,包括 Cancellation is still waiting for synthesis 及取消后仍调用模型的断言失败。
  • PR fix(research): 合成阶段取消及时结算并隔离迟到回调 #272 修复后:8 pass / 0 fail / 35 expect() calls。
  • 修复后 --rerun-each 5:40 pass / 0 fail / 175 expect() calls。
  • bun test packages/shared/src/research --isolate:116 pass / 0 fail / 403 expect() calls。

1 秒是回归测试的有界等待 watchdog,不是新增产品超时策略或性能 SLA。验证属于实际服务/磁盘路径的受控集成和真实子进程 smoke,未声称是真实模型 / live Provider / Electron UI E2E。

预期行为与验收

  • 底层合成或合成前检查不响应 AbortSignal 时,编排层收到取消后仍能结束等待、持久化取消终态并释放该证券的 active reservation。
  • cancel 返回后,同一证券可立即启动新的独立研究;新任务不受旧合成迟到完成影响。
  • 已取消后不再启动合成器;合成前检查迟到完成也不能继续调用模型。
  • 旧合成迟到 resolve / reject 不发布旧报告,不改回旧任务状态;迟到 recovery 回调不追加旧检查点事件。
  • 已成功能力仍属于 completed;取消不把这些能力重新标为 failed。
  • 正常成功、普通失败和取消路径均移除本次 abort listener;迟到 rejection 被观察,不产生未处理异常。
  • 提供基线失败/修复通过的回归,以及真实服务、磁盘和受控子进程边界证据。

拟修范围与边界

对应修复已经提交为 PR #272,本 Issue 是补建的独立 Bug 记录,不声称此前已完成 Issue 认领沟通。

只修改 packages/shared/src/research/runner.ts 并新增 synthesis-cancellation.test.ts:

  • 合成前检查和合成调用与 abort Promise 竞争结算,在调用前检查已取消状态;
  • 结束后清理 listener,观察迟到异常;
  • 取消后隔离 recovery 回调并保留真实能力结果。

公共 ResearchSynthesizer / ResearchService 接口、恢复协议、预算策略、Provider 接线、金融计算、UI 外观及文案保持原有范围。底层信号仍传递给合成器;编排层无法强行终止任意第三方进程或网络请求,其资源释放由合成器负责。受控子进程在测试中显式清理。

重复排查与所有权

2026-09-30 已检索该仓库开放/关闭 Issue 与 PR 的 research、synthesis、合成、取消、cancel 等相关项,并核对相近问题:

在已核对条目中未发现覆盖该缺陷的独立 Issue 或他人修复 PR。由 @Guo-Yixin 认领并继续处理 PR #272 的 review;认领以评论和实际 Assignee / claimed 状态为准。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

claimedClaimed by a contributor and currently in progress

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions