Skip to content

fix(eval): case 超时后取消底层 run 并为收尾设置有界等待 - #117

Merged
helsome merged 2 commits into
helsome:mainfrom
xiaodu55:fix/eval-case-timeout-teardown
Sep 18, 2026
Merged

helsome merged 2 commits into
helsome:mainfrom
xiaodu55:fix/eval-case-timeout-teardown

Conversation

@xiaodu55

Copy link
Copy Markdown
Contributor

改了什么

packages/shared/src/evaluation/experiment-service.tsrunCase 收尾路径:

  1. 超时/用户取消时幂等取消底层 runPromise.race 判定为 timeoutabort 后,对准确的 sessionId + runId 调用内核已有的 RunManager.cancelRun(幂等:run 已结算或 id 不匹配时为 no-op)。ExperimentKernel.runs 新增可选 cancelRun,现有 fake kernel 不受影响。
  2. 终态持久化与 runtime 收尾改为有界等待:原 while (kernel.runs.isRunning?.()) 无截止时间,runtime 不退出时 eval 会无限挂起(无法开始下一 case,也无法结束 nightly)。现改为默认 10s 的有界等待(RUNTIME_TEARDOWN_MS,可经 ExperimentServiceOptions.timing.teardownMs 覆盖用于测试/运维)。
  3. 收尾超限时隔离 runtime 并明确失败:等待超限后不再 deleteSession(session 文件仍被活跃 run 持有),run 记录补 RUNTIME_TEARDOWN_TIMEOUT 基础设施错误与 runtime_error 模式,实验置为 failed 并停止——不把下一个 case 跑在仍活跃的上下文上;已产生的产物保持可读。
  4. 单一终态与资源清理timeout/abort 竞态只结算一次;race 结束后立即清除等待 timer 与 abort listener。用户 abort 的 case 不再从统计里悄悄消失:partial 答案/tool 记录以 cancelled run 落库。
  5. fix(runtime): enforce wall-clock budget while the event stream is silent #85 的 runtime wall-clock budget 仍是第一道防线;本 PR 补的是 Eval 层自己的超时/隔离语义(CLI live kernel 未给每个 case 配 budget 时也不挂起)。

为什么改

Issue #115 指出的挂起路径:case timeout 不等于底层 Agent run 被停止;runtime 无终态事件时,超时分支既不取消 run,又进入无上界的 isRunning 轮询。

对应 Issue

Closes #115(仅拆出超时收尾缺陷,不替代 #15 父任务)

做了哪些验证

  • bun test src/evaluation src/kernel221 pass / 0 fail(14 files,Bun 1.3.14 / Windows 10.0.26200)
  • bun test src/storage:10 pass / 0 fail;shared 其余 20 个测试目录合跑 exit 0(无回归)
  • tsc --noEmit(packages/shared):通过
  • 新增 5 个受控 stalled runtime 集成测试(真实 ExperimentService + 受控 kernel + 临时存储,可控时间内结束,不依赖真实等待):
    • 无首输出即停滞 → 仍产出 timeout 结果、取消被调用
    • partial 后停滞 → partial 答案与 tool 记录保留、session 正常清理
    • cancel 不响应 → 实验 failed、不启动下一 case、产物可读、run 带 RUNTIME_TEARDOWN_TIMEOUT
    • 完成与超时竞态 → 只结算一个终态(timeout 胜出,迟到完成不重开 case)
    • 用户 abort → 取消正确 run、该 case 以 cancelled 落库、剩余 case 跳过

已知未完成项 / 后续

  • 未引入新调度框架;取消完全复用现有 RunManager/Pi cancellation。
  • timing 覆盖仅用于测试与特殊运维场景,默认值与 §79 guardrails 一致。
  • 无可见 UI 变化。

- runCase 超时/用户取消时对对应 sessionId+runId 幂等取消(复用 RunManager.cancelRun)
- 终态持久化与 runtime 收尾从无限轮询改为有界等待(默认 10s,可经 timing.teardownMs 覆盖)
- runtime 收尾超限时记录 RUNTIME_TEARDOWN_TIMEOUT 基础设施错误、隔离该 runtime 并将实验置为 failed,不把下一个 case 跑在仍活跃的上下文上
- 超时/abort 竞态只结算一个终态,及时清理 timer 与 abort listener;被取消 case 的 partial 答案与 tool 记录保留入库
- 补充受控 stalled runtime 集成测试:无首输出、partial 后停滞、取消不响应、完成与超时竞态、用户 abort

Closes helsome#115

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

方向和现有测试报告认可,这是内部生命周期增量,不需要真实模型/API key。当前只剩一个核心收尾边界未被覆盖:runCase 在调用有界 waitForIdle() 之前,无上限 await cancelRun()。主干 RunManager.cancelRun 本身会 await runtime.cancel;若取消 promise 一直 pending,就永远到不了新增的 teardown deadline。

请把“发出取消 + 等待 idle”放在同一个有界收尾窗口内(并处理迟到 rejection),补一个 cancelRun 返回永不 resolve 的 Promise 的用例:实验在短测试预算内结束、partial 产物保存、不删除仍被占用的 session、不启动下一 case。现有 cancelResponds=false fake 会立即 resolve,只模拟“返回了但仍 running”,因此漏掉这条路径。

同时在该测试中断言传给 cancelRun 的准确 sessionId/runId。修复后复跑现有 focused suites + typecheck 即可;不要扩大到新框架或完整真实 E2E。这里是可由代码和受控测试验证的缺陷,与 Nightly 缺 Key 无关。

- runCase 中发出 cancelRun 与等待 idle 合并进同一个有界窗口(settleTeardown):
  runtime.cancel 永不返回时总耗时仍受 teardownMs 上限约束,随后走既有隔离路径
- cancelRun 的迟到 rejection 在创建处吞掉,不会以 unhandled rejection 逃逸
- 新增 cancelRun 永不 resolve 的集成用例:实验在短预算内结束、partial 产物落库、
  不删除仍被占用的 session、不启动下一 case,并断言传入 cancelRun 的准确 sessionId/runId

反馈来自 helsome#117 review
@xiaodu55

Copy link
Copy Markdown
Contributor Author

感谢 review,已修复。三点对应如下:

  1. 无上限 await cancelRun:新增 settleTeardown——发出幂等取消与等待 idle 共用同一个 teardownMs 有界窗口;RunManager.cancelRun 内部 await runtime.cancel 永不返回时,总耗时仍被封顶,随后走既有隔离路径(RUNTIME_TEARDOWN_TIMEOUT、不删 session、实验 failed、不启动下一 case)。
  2. 迟到 rejection:在 promise 创建处 .catch(() => undefined) 吞掉,不影响窗口计时,也不会以 unhandled rejection 逃逸。
  3. 新用例bounds the cancellation round-trip itself when the runtime never answers——cancelRun 返回永不 resolve 的 Promise,断言实验在短 teardown 预算内结束、partial 答案保留、session 未删除、a2 未启动,并断言传入 cancelRun 的准确 sessionId/runIdsess-1/run-1)。

验证:bun test src/evaluation src/kernel 222 pass / 0 fail(Bun 1.3.14 / Windows),tsc --noEmit 通过。

@helsome
helsome dismissed their stale review September 18, 2026 11:21

head d3ee5ba 已把 cancelRun round-trip 纳入同一 teardown 有界窗口,并新增永不 resolve 的取消回归;原 blocker 已解除。

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原收尾 blocker 已解除:当前实现把 cancellation promise 与 isRunning idle wait 放进同一个 teardownMs 窗口;cancelRun 永不 resolve 时也会在窗口结束后隔离 runtime,不删占用中的 session、不启动下一 case,并保留 partial 产物。新增测试还断言了准确的 sessionId/runId。作者补充的当前 head 验证为 bun test src/evaluation src/kernel 222 pass / 0 fail(Bun 1.3.14 / Windows),tsc --noEmit 通过;CI 为首次贡献者 action_required,不视为代码失败。本 PR 是内部生命周期修复,不需要真实模型/API key。

@helsome
helsome merged commit 6c9cd82 into helsome:main Sep 18, 2026
suzhiguo7 added a commit to suzhiguo7/folio that referenced this pull request Sep 18, 2026
Reconcile the review follow-up with the CLI/effective-config work that
landed on main (helsome#114/helsome#116, helsome#115/helsome#117).

Conflict resolutions:
- core/evaluation.ts: keep both `execution` (helsome#113 validity) and
  `effectiveConfig` (helsome#114 readback).
- experiment-service.ts: keep the helsome#113 evaluation gating, the helsome#116
  effective-config readback, and the helsome#117 bounded teardown; the helsome#114
  config-apply failure path now stamps `execution: 'not-started'` and
  returns `runtimeUnusable`, so a config failure stays infra-invalid
  instead of being measured as a zero score.
- experiment-service.test.ts: keep both the relay-header test and the
  helsome#115 teardown suite; update the helsome#114 CONFIG_APPLY_FAILED expectation to
  the helsome#113 semantics (validity `invalid`, passRate null).
- scripts/eval/run.ts: keep the live preflight (helsome#113) and the
  effective-config summary (helsome#114); a single normalizeModelSelection
  import, with thinkingLevel from the CLI.

Verification:
- bun test --isolate -> 1539 pass / 8 skip / 0 fail
- bun run typecheck -> all packages exit 0

UI impact: no layout or interaction changes.
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.

[P1][Eval Reliability] Case 超时必须取消并有界收尾,避免等待 isRunning 永久挂起

2 participants