问题与影响
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。
预期行为与验收
拟修范围与边界
对应修复已经提交为 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 状态为准。
问题与影响
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仍可能追加旧运行的检查点事件。synthesizing的异步状态迁移期间取消,也可能继续调用合成器。这是“编排层停止等待、释放任务并隔离迟到工作”的正确性缺陷;不把取消信号已发送等同于取消已完成。
复现
基线:上游 main
ba5dcdfd31b162f5edb8b908f7f099a560389326(2026-09-30 排查时仍为该提交)。实际验证环境:Bun 1.4.2(744846f84)/ Windows 11(Microsoft Windows 10.0.22631)。
最小服务场景:
company.profile能力。synthesize()返回一个不会响应 AbortSignal 的 deferred Promise;等待合成器确实进入后再取消。NVDA.US研究并执行service.cancel(run.id)。在旧合成 Promise 尚未结算时检查 cancel 是否返回、同一证券能否再次 start。回归测试在 PR #272 的
packages/shared/src/research/synthesis-cancellation.test.ts;其中包含迟到 resolve/reject、磁盘检查点和真实 Bun 子进程边界。复现不需要模型或 Provider 凭证。以下为可重复的基线对照步骤(在已有依赖的仓库执行;新 worktree 需准备依赖):
对照方式:只把新测试放到未修复 main,保留 main 的生产代码。上述 install 为独立准备依赖的步骤;本地实际验证复用了已安装依赖,没有声称执行过该 install。
实际同测试/同 Bun 对照结果:
Cancellation is still waiting for synthesis及取消后仍调用模型的断言失败。--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。
预期行为与验收
拟修范围与边界
对应修复已经提交为 PR #272,本 Issue 是补建的独立 Bug 记录,不声称此前已完成 Issue 认领沟通。
只修改
packages/shared/src/research/runner.ts并新增synthesis-cancellation.test.ts:公共 ResearchSynthesizer / ResearchService 接口、恢复协议、预算策略、Provider 接线、金融计算、UI 外观及文案保持原有范围。底层信号仍传递给合成器;编排层无法强行终止任意第三方进程或网络请求,其资源释放由合成器负责。受控子进程在测试中显式清理。
重复排查与所有权
2026-09-30 已检索该仓库开放/关闭 Issue 与 PR 的 research、synthesis、合成、取消、cancel 等相关项,并核对相近问题:
在已核对条目中未发现覆盖该缺陷的独立 Issue 或他人修复 PR。由 @Guo-Yixin 认领并继续处理 PR #272 的 review;认领以评论和实际 Assignee / claimed 状态为准。