Repository navigation
fix(research): 合成阶段取消挂起并持续占用同一证券的研究任务 #273
Description
问题与影响
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)。
最小服务场景:
- 使用实际 ResearchService、ResearchRunner、CapabilityExecutor 和临时目录中的 JsonFileStore / ResearchReportRepository;注册一个成功的
company.profile能力。 - 注入受控合成器:首次
synthesize()返回一个不会响应 AbortSignal 的 deferred Promise;等待合成器确实进入后再取消。 - 启动
NVDA.US研究并执行service.cancel(run.id)。在旧合成 Promise 尚未结算时检查 cancel 是否返回、同一证券能否再次 start。 - 旧实现的 cancel 超过测试的 1 秒 watchdog 仍未结算;reservation 仍占用。同一证券无法重启,直到旧合成自行结束。
- 另分别在合成前检查中阻塞、在 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 等相关项,并核对相近问题:
- [Reliability] Make long-running research jobs crash-safe and resumable #18(已关闭)/ PR feat: 为 Deep Research 增加持久化检查点与崩溃恢复 #80:崩溃持久化与恢复,本 Issue 不重新认领或关闭它。
- [Deep Research] Fatal worker errors do not stop sibling capability executions #205 / PR fix(research): fail fast on fatal worker errors #206:capability fetching 阶段的 fatal worker fail-stop;本 Issue 是后续 synthesis 的用户取消。两者均涉及 runner.ts,若先合入需 rebase 并复验。
- [Agent Runtime] Local cancellation does not abort in-flight capability execution #201 / PR fix(agent): propagate local run cancellation to capabilities #202:LocalRuntimeAdapter 的 capability 取消传播,属于另一条调用链。
- fix(providers): ProviderRouter 外部取消在异步路由等待期间丢失,仍会调用 provider #207 / PR fix(providers): ProviderRouter 外部取消在异步路由等待期间丢失,仍会调用 provider #208:ProviderRouter 路由等待期间的已取消检查,属于 Provider 层。
- fix(eval): 用户取消的运行在可观测性评估里被记成 failed,污染质量统计与 skipped 计数 #254 / PR fix(eval): 可观测性链路把用户取消的运行记成 cancelled,不再记成任务失败 #255、[Trace] Cancelled Agent runs are rendered as running in the timeline #199 / PR fix(trace): preserve cancelled run state in timeline #200:取消状态在评估/trace 的投影,本 Issue 修实际执行结算与 reservation。
- 已合并 PR fix(shared): enforce capability timeout for uncooperative runs #183:CapabilityExecutor 的不合作能力超时/取消,未覆盖独立的 beforeSynthesis / synthesize 等待。
- PR fix(research): 合成阶段取消及时结算并隔离迟到回调 #272 是本 Bug 已提交的修复,不是另一位贡献者的重复实现。
在已核对条目中未发现覆盖该缺陷的独立 Issue 或他人修复 PR。由 @Guo-Yixin 认领并继续处理 PR #272 的 review;认领以评论和实际 Assignee / claimed 状态为准。
Activity
Guo-Yixin commentedon Sep 30, 2026
/claim
我来处理本 Issue,范围限定为 ResearchRunner 的 beforeSynthesis / synthesize 取消结算、同一证券 reservation 释放及迟到回调隔离,不扩大到 #18、#205、#201 的整体验收。
对应修复已提交:PR #272。本次补建 Bug 记录并完成认领;PR 将以 Closes #273 关联,继续处理该修复的 review。
已验证同一回归:未修复 main 2 pass / 6 fail,修复后 8 pass / 0 fail,重复运行 40 pass / 0 fail;完整环境、命令、集成边界及未运行项见 PR 测试报告。
github-actions commentedon Sep 30, 2026
@Guo-Yixin 已认领该 Issue,并已自动设为 Assignee。提交 PR 时请在描述中写
Closes #273。