niceeval 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (548) hide show
  1. package/INDEX.md +25 -2
  2. package/README.md +9 -10
  3. package/README.zh.md +9 -10
  4. package/dist/agents/ai-sdk.cjs.map +1 -1
  5. package/dist/agents/ai-sdk.d.cts +1 -1
  6. package/dist/agents/ai-sdk.d.mts +1 -1
  7. package/dist/agents/ai-sdk.d.ts +1 -1
  8. package/dist/agents/bub-install-spec.cjs +19 -5
  9. package/dist/agents/bub-install-spec.cjs.map +1 -1
  10. package/dist/agents/bub-install-spec.d.cts +9 -2
  11. package/dist/agents/bub-install-spec.d.mts +9 -2
  12. package/dist/agents/bub-install-spec.d.ts +9 -2
  13. package/dist/agents/bub-install-spec.js +14 -12
  14. package/dist/agents/bub.cjs +10 -6
  15. package/dist/agents/bub.cjs.map +1 -1
  16. package/dist/agents/bub.d.cts +2 -2
  17. package/dist/agents/bub.d.mts +2 -2
  18. package/dist/agents/bub.d.ts +2 -2
  19. package/dist/agents/claude-code.cjs.map +1 -1
  20. package/dist/agents/claude-code.d.cts +2 -2
  21. package/dist/agents/claude-code.d.mts +2 -2
  22. package/dist/agents/claude-code.d.ts +2 -2
  23. package/dist/agents/codex.cjs +4 -4
  24. package/dist/agents/codex.cjs.map +1 -1
  25. package/dist/agents/codex.d.cts +3 -3
  26. package/dist/agents/codex.d.mts +3 -3
  27. package/dist/agents/codex.d.ts +3 -3
  28. package/dist/agents/coding-cli-versions.cjs +10 -1
  29. package/dist/agents/coding-cli-versions.cjs.map +1 -1
  30. package/dist/agents/coding-cli-versions.d.cts +8 -3
  31. package/dist/agents/coding-cli-versions.d.mts +8 -3
  32. package/dist/agents/coding-cli-versions.d.ts +8 -3
  33. package/dist/agents/coding-cli-versions.js +18 -12
  34. package/dist/agents/deepseek-harness.cjs +151 -0
  35. package/dist/agents/deepseek-harness.cjs.map +1 -0
  36. package/dist/agents/deepseek-harness.d.cts +14 -0
  37. package/dist/agents/deepseek-harness.d.mts +14 -0
  38. package/dist/agents/deepseek-harness.d.ts +14 -0
  39. package/dist/agents/deepseek-harness.js +7 -0
  40. package/dist/agents/deepseek-harness.js.map +1 -0
  41. package/dist/agents/exact-npm-plugins.cjs +28 -0
  42. package/dist/agents/exact-npm-plugins.cjs.map +1 -0
  43. package/dist/agents/exact-npm-plugins.d.cts +10 -0
  44. package/dist/agents/exact-npm-plugins.d.mts +10 -0
  45. package/dist/agents/exact-npm-plugins.d.ts +10 -0
  46. package/dist/agents/exact-npm-plugins.js +8 -0
  47. package/dist/agents/exact-npm-plugins.js.map +1 -0
  48. package/dist/agents/index.cjs +5 -1
  49. package/dist/agents/index.cjs.map +1 -1
  50. package/dist/agents/index.d.cts +4 -0
  51. package/dist/agents/index.d.mts +4 -0
  52. package/dist/agents/index.d.ts +4 -0
  53. package/dist/agents/index.js +54 -50
  54. package/dist/agents/index.mjs +54 -50
  55. package/dist/agents/omp.cjs +170 -0
  56. package/dist/agents/omp.cjs.map +1 -0
  57. package/dist/agents/omp.d.cts +14 -0
  58. package/dist/agents/omp.d.mts +14 -0
  59. package/dist/agents/omp.d.ts +14 -0
  60. package/dist/agents/omp.js +7 -0
  61. package/dist/agents/omp.js.map +1 -0
  62. package/dist/agents/openclaw.cjs +51 -2
  63. package/dist/agents/openclaw.cjs.map +1 -1
  64. package/dist/agents/openclaw.d.cts +2 -0
  65. package/dist/agents/openclaw.d.mts +2 -0
  66. package/dist/agents/openclaw.d.ts +2 -0
  67. package/dist/agents/types.cjs.map +1 -1
  68. package/dist/agents/types.d.cts +4 -4
  69. package/dist/agents/types.d.mts +4 -4
  70. package/dist/agents/types.d.ts +4 -4
  71. package/dist/agents/ui-message-stream.cjs +24 -7
  72. package/dist/agents/ui-message-stream.cjs.map +1 -1
  73. package/dist/analysis/api.cjs +28 -2
  74. package/dist/analysis/api.cjs.map +1 -1
  75. package/dist/analysis/api.d.cts +22 -1
  76. package/dist/analysis/api.d.mts +22 -1
  77. package/dist/analysis/api.d.ts +22 -1
  78. package/dist/analysis/api.js +8 -6
  79. package/dist/analysis/bindings.cjs +11 -18
  80. package/dist/analysis/bindings.cjs.map +1 -1
  81. package/dist/analysis/bindings.d.cts +177 -49
  82. package/dist/analysis/bindings.d.mts +177 -49
  83. package/dist/analysis/bindings.d.ts +177 -49
  84. package/dist/analysis/contracts.cjs +1 -0
  85. package/dist/analysis/contracts.cjs.map +1 -1
  86. package/dist/analysis/contracts.d.cts +43 -3
  87. package/dist/analysis/contracts.d.mts +43 -3
  88. package/dist/analysis/contracts.d.ts +43 -3
  89. package/dist/analysis/cost-projection.cjs +2 -1
  90. package/dist/analysis/cost-projection.cjs.map +1 -1
  91. package/dist/analysis/cost-projection.d.cts +1 -1
  92. package/dist/analysis/cost-projection.d.mts +1 -1
  93. package/dist/analysis/cost-projection.d.ts +1 -1
  94. package/dist/analysis/cost.cjs +13 -3
  95. package/dist/analysis/cost.cjs.map +1 -1
  96. package/dist/analysis/cost.d.cts +10 -3
  97. package/dist/analysis/cost.d.mts +10 -3
  98. package/dist/analysis/cost.d.ts +10 -3
  99. package/dist/analysis/experiment-groups.cjs +96 -0
  100. package/dist/analysis/experiment-groups.cjs.map +1 -0
  101. package/dist/analysis/experiment-groups.d.cts +9 -0
  102. package/dist/analysis/experiment-groups.d.mts +9 -0
  103. package/dist/analysis/experiment-groups.d.ts +9 -0
  104. package/dist/analysis/experiment-groups.js +10 -0
  105. package/dist/analysis/experiment-groups.js.map +1 -0
  106. package/dist/analysis/index.cjs +4 -1
  107. package/dist/analysis/index.cjs.map +1 -1
  108. package/dist/analysis/index.d.cts +4 -2
  109. package/dist/analysis/index.d.mts +4 -2
  110. package/dist/analysis/index.d.ts +4 -2
  111. package/dist/analysis/index.js +40 -36
  112. package/dist/analysis/index.mjs +40 -36
  113. package/dist/assertions/limits.cjs +4 -0
  114. package/dist/assertions/limits.cjs.map +1 -1
  115. package/dist/assertions/limits.d.cts +4 -0
  116. package/dist/assertions/limits.d.mts +4 -0
  117. package/dist/assertions/limits.d.ts +4 -0
  118. package/dist/assertions/match.cjs +63 -25
  119. package/dist/assertions/match.cjs.map +1 -1
  120. package/dist/assertions/match.d.cts +7 -0
  121. package/dist/assertions/match.d.mts +7 -0
  122. package/dist/assertions/match.d.ts +7 -0
  123. package/dist/assertions/record/attachment.cjs +32 -4
  124. package/dist/assertions/record/attachment.cjs.map +1 -1
  125. package/dist/assertions/record/diff.cjs +10 -4
  126. package/dist/assertions/record/diff.cjs.map +1 -1
  127. package/dist/assertions/record/producer.cjs +101 -4
  128. package/dist/assertions/record/producer.cjs.map +1 -1
  129. package/dist/assertions/record/producer.d.cts +4 -1
  130. package/dist/assertions/record/producer.d.mts +4 -1
  131. package/dist/assertions/record/producer.d.ts +4 -1
  132. package/dist/assertions/runtime.cjs +45 -5
  133. package/dist/assertions/runtime.cjs.map +1 -1
  134. package/dist/cli/record.cjs +113 -8
  135. package/dist/cli/record.cjs.map +1 -1
  136. package/dist/cli/record.d.cts +20 -1
  137. package/dist/cli/record.d.mts +20 -1
  138. package/dist/cli/record.d.ts +20 -1
  139. package/dist/cli/record.js +4 -2
  140. package/dist/cli.cjs +386 -31
  141. package/dist/cli.cjs.map +1 -1
  142. package/dist/cli.d.cts +10 -2
  143. package/dist/cli.d.mts +10 -2
  144. package/dist/cli.d.ts +10 -2
  145. package/dist/context/send-retry.cjs +4 -4
  146. package/dist/context/send-retry.cjs.map +1 -1
  147. package/dist/coordination/host/runtime.cjs +8 -30
  148. package/dist/coordination/host/runtime.cjs.map +1 -1
  149. package/dist/coordination/host/runtime.d.cts +3 -3
  150. package/dist/coordination/host/runtime.d.mts +3 -3
  151. package/dist/coordination/host/runtime.d.ts +3 -3
  152. package/dist/coordination/host/types.cjs.map +1 -1
  153. package/dist/coordination/host/types.d.cts +0 -5
  154. package/dist/coordination/host/types.d.mts +0 -5
  155. package/dist/coordination/host/types.d.ts +0 -5
  156. package/dist/coordination/platform/node.cjs +90 -9
  157. package/dist/coordination/platform/node.cjs.map +1 -1
  158. package/dist/define.cjs +25 -1
  159. package/dist/define.cjs.map +1 -1
  160. package/dist/experiment/host/index.cjs +1 -0
  161. package/dist/experiment/host/index.cjs.map +1 -1
  162. package/dist/i18n/en.cjs +26 -8
  163. package/dist/i18n/en.cjs.map +1 -1
  164. package/dist/i18n/en.d.cts +19 -6
  165. package/dist/i18n/en.d.mts +19 -6
  166. package/dist/i18n/en.d.ts +19 -6
  167. package/dist/index.cjs.map +1 -1
  168. package/dist/index.d.cts +1 -1
  169. package/dist/index.d.mts +1 -1
  170. package/dist/index.d.ts +1 -1
  171. package/dist/o11y/parsers/hermes.cjs +1 -1
  172. package/dist/o11y/parsers/hermes.cjs.map +1 -1
  173. package/dist/o11y/prices.json +1 -1
  174. package/dist/o11y/record/families.cjs +12 -3
  175. package/dist/o11y/record/families.cjs.map +1 -1
  176. package/dist/o11y/record/families.d.cts +8 -8
  177. package/dist/o11y/record/families.d.mts +8 -8
  178. package/dist/o11y/record/families.d.ts +8 -8
  179. package/dist/o11y/record/family-projectors.d.cts +66 -18
  180. package/dist/o11y/record/family-projectors.d.mts +66 -18
  181. package/dist/o11y/record/family-projectors.d.ts +66 -18
  182. package/dist/o11y/record/family-writers.d.cts +11 -3
  183. package/dist/o11y/record/family-writers.d.mts +11 -3
  184. package/dist/o11y/record/family-writers.d.ts +11 -3
  185. package/dist/o11y/record/runner-producer.cjs +14 -4
  186. package/dist/o11y/record/runner-producer.cjs.map +1 -1
  187. package/dist/record/codec/core.d.cts +2 -2
  188. package/dist/record/codec/core.d.mts +2 -2
  189. package/dist/record/codec/core.d.ts +2 -2
  190. package/dist/record/family/catalog.d.cts +44 -12
  191. package/dist/record/family/catalog.d.mts +44 -12
  192. package/dist/record/family/catalog.d.ts +44 -12
  193. package/dist/record/family/index.js +128 -124
  194. package/dist/record/family/observability-v1.cjs +35 -0
  195. package/dist/record/family/observability-v1.cjs.map +1 -0
  196. package/dist/record/family/observability-v1.d.cts +3 -0
  197. package/dist/record/family/observability-v1.d.mts +3 -0
  198. package/dist/record/family/observability-v1.d.ts +3 -0
  199. package/dist/record/family/observability-v1.js +6 -0
  200. package/dist/record/family/observability-v1.js.map +1 -0
  201. package/dist/record/family/observability.cjs +58 -9
  202. package/dist/record/family/observability.cjs.map +1 -1
  203. package/dist/record/family/observability.d.cts +1014 -15
  204. package/dist/record/family/observability.d.mts +1014 -15
  205. package/dist/record/family/observability.d.ts +1014 -15
  206. package/dist/record/family/observability.js +22 -18
  207. package/dist/record/host/runtime.cjs +600 -31
  208. package/dist/record/host/runtime.cjs.map +1 -1
  209. package/dist/record/host/types.cjs.map +1 -1
  210. package/dist/record/host/types.d.cts +33 -9
  211. package/dist/record/host/types.d.mts +33 -9
  212. package/dist/record/host/types.d.ts +33 -9
  213. package/dist/record/maintenance/runtime.cjs +1 -2
  214. package/dist/record/maintenance/runtime.cjs.map +1 -1
  215. package/dist/record/model/core.cjs.map +1 -1
  216. package/dist/record/model/core.d.cts +1 -1
  217. package/dist/record/model/core.d.mts +1 -1
  218. package/dist/record/model/core.d.ts +1 -1
  219. package/dist/record/model/definition.d.cts +6 -6
  220. package/dist/record/model/definition.d.mts +6 -6
  221. package/dist/record/model/definition.d.ts +6 -6
  222. package/dist/record/model/identifiers.cjs +1 -1
  223. package/dist/record/model/identifiers.cjs.map +1 -1
  224. package/dist/record/model/identifiers.d.cts +1 -1
  225. package/dist/record/model/identifiers.d.mts +1 -1
  226. package/dist/record/model/identifiers.d.ts +1 -1
  227. package/dist/record/platform/node.cjs +159 -23
  228. package/dist/record/platform/node.cjs.map +1 -1
  229. package/dist/record/platform/services.cjs.map +1 -1
  230. package/dist/record/platform/services.d.cts +15 -2
  231. package/dist/record/platform/services.d.mts +15 -2
  232. package/dist/record/platform/services.d.ts +15 -2
  233. package/dist/record/reader/errors.cjs +37 -1
  234. package/dist/record/reader/errors.cjs.map +1 -1
  235. package/dist/record/reader/errors.d.cts +49 -1
  236. package/dist/record/reader/errors.d.mts +49 -1
  237. package/dist/record/reader/errors.d.ts +49 -1
  238. package/dist/record/reader/errors.js +22 -12
  239. package/dist/record/reader/format.cjs +76 -17
  240. package/dist/record/reader/format.cjs.map +1 -1
  241. package/dist/record/reader/format.d.cts +11 -4
  242. package/dist/record/reader/format.d.mts +11 -4
  243. package/dist/record/reader/format.d.ts +11 -4
  244. package/dist/record/reader/format.js +2 -0
  245. package/dist/record/reader/index.js +32 -18
  246. package/dist/record/reader/runtime.cjs +64 -0
  247. package/dist/record/reader/runtime.cjs.map +1 -1
  248. package/dist/record/reader/runtime.d.cts +15 -0
  249. package/dist/record/reader/runtime.d.mts +15 -0
  250. package/dist/record/reader/runtime.d.ts +15 -0
  251. package/dist/record/reader/runtime.js +2 -0
  252. package/dist/report/assets/styles.css +53 -29
  253. package/dist/report/built-in/analysis-values.cjs +21 -13
  254. package/dist/report/built-in/analysis-values.cjs.map +1 -1
  255. package/dist/report/built-in/analysis-values.d.cts +11 -22
  256. package/dist/report/built-in/analysis-values.d.mts +11 -22
  257. package/dist/report/built-in/analysis-values.d.ts +11 -22
  258. package/dist/report/built-in/analysis-values.js +4 -6
  259. package/dist/report/built-in/index.cjs +1 -3
  260. package/dist/report/built-in/index.cjs.map +1 -1
  261. package/dist/report/built-in/index.d.cts +1 -1
  262. package/dist/report/built-in/index.d.mts +1 -1
  263. package/dist/report/built-in/index.d.ts +1 -1
  264. package/dist/report/built-in/index.js +4 -8
  265. package/dist/report/built-in/index.mjs +4 -8
  266. package/dist/report/built-in/machine.cjs +0 -2
  267. package/dist/report/built-in/machine.cjs.map +1 -1
  268. package/dist/report/built-in/machine.d.cts +0 -2
  269. package/dist/report/built-in/machine.d.mts +0 -2
  270. package/dist/report/built-in/machine.d.ts +0 -2
  271. package/dist/report/built-in/result-components.cjs +49 -51
  272. package/dist/report/built-in/result-components.cjs.map +1 -1
  273. package/dist/report/built-in/result-components.d.cts +16 -17
  274. package/dist/report/built-in/result-components.d.mts +16 -17
  275. package/dist/report/built-in/result-components.d.ts +16 -17
  276. package/dist/report/built-in/result-components.js +0 -4
  277. package/dist/report/built-in/run-membership-overview.cjs +126 -17
  278. package/dist/report/built-in/run-membership-overview.cjs.map +1 -1
  279. package/dist/report/built-in/run-membership-overview.d.cts +14 -0
  280. package/dist/report/built-in/run-membership-overview.d.mts +14 -0
  281. package/dist/report/built-in/run-membership-overview.d.ts +14 -0
  282. package/dist/report/built-in/run-membership-overview.js +4 -2
  283. package/dist/report/built-in/standard.cjs +80 -45
  284. package/dist/report/built-in/standard.cjs.map +1 -1
  285. package/dist/report/built-in/standard.d.cts +19 -41
  286. package/dist/report/built-in/standard.d.mts +19 -41
  287. package/dist/report/built-in/standard.d.ts +19 -41
  288. package/dist/report/built-in/standard.js +6 -6
  289. package/dist/report/components/attempt-detail/compute.cjs +78 -2
  290. package/dist/report/components/attempt-detail/compute.cjs.map +1 -1
  291. package/dist/report/components/attempt-detail/compute.d.cts +1 -1
  292. package/dist/report/components/attempt-detail/compute.d.mts +1 -1
  293. package/dist/report/components/attempt-detail/compute.d.ts +1 -1
  294. package/dist/report/components/attempt-detail/content.cjs +1 -1
  295. package/dist/report/components/attempt-detail/content.cjs.map +1 -1
  296. package/dist/report/components/entity-lists/compute.cjs +42 -5
  297. package/dist/report/components/entity-lists/compute.cjs.map +1 -1
  298. package/dist/report/components/entity-lists/compute.d.cts +10 -8
  299. package/dist/report/components/entity-lists/compute.d.mts +10 -8
  300. package/dist/report/components/entity-lists/compute.d.ts +10 -8
  301. package/dist/report/components/entity-lists/content.cjs +31 -6
  302. package/dist/report/components/entity-lists/content.cjs.map +1 -1
  303. package/dist/report/components/entity-lists/index.cjs +9 -4
  304. package/dist/report/components/entity-lists/index.cjs.map +1 -1
  305. package/dist/report/components/entity-lists/index.d.cts +4 -7
  306. package/dist/report/components/entity-lists/index.d.mts +4 -7
  307. package/dist/report/components/entity-lists/index.d.ts +4 -7
  308. package/dist/report/components/summaries/compute.cjs +8 -1
  309. package/dist/report/components/summaries/compute.cjs.map +1 -1
  310. package/dist/report/components/summaries/compute.d.cts +9 -1
  311. package/dist/report/components/summaries/compute.d.mts +9 -1
  312. package/dist/report/components/summaries/compute.d.ts +9 -1
  313. package/dist/report/components/summaries/index.cjs +48 -4
  314. package/dist/report/components/summaries/index.cjs.map +1 -1
  315. package/dist/report/components/summaries/index.d.cts +7 -4
  316. package/dist/report/components/summaries/index.d.mts +7 -4
  317. package/dist/report/components/summaries/index.d.ts +7 -4
  318. package/dist/report/definition/cell.cjs +3 -0
  319. package/dist/report/definition/cell.cjs.map +1 -1
  320. package/dist/report/definition/cell.d.cts +2 -0
  321. package/dist/report/definition/cell.d.mts +2 -0
  322. package/dist/report/definition/cell.d.ts +2 -0
  323. package/dist/report/definition/primitives/chart.cjs +8 -5
  324. package/dist/report/definition/primitives/chart.cjs.map +1 -1
  325. package/dist/report/definition/primitives/points-dataset.cjs +1 -0
  326. package/dist/report/definition/primitives/points-dataset.cjs.map +1 -1
  327. package/dist/report/definition/primitives.cjs +15 -2
  328. package/dist/report/definition/primitives.cjs.map +1 -1
  329. package/dist/report/definition/report.cjs +27 -3
  330. package/dist/report/definition/report.cjs.map +1 -1
  331. package/dist/report/definition/report.d.cts +8 -0
  332. package/dist/report/definition/report.d.mts +8 -0
  333. package/dist/report/definition/report.d.ts +8 -0
  334. package/dist/report/execution/machine.cjs +30 -14
  335. package/dist/report/execution/machine.cjs.map +1 -1
  336. package/dist/report/execution/machine.d.cts +20 -10
  337. package/dist/report/execution/machine.d.mts +20 -10
  338. package/dist/report/execution/machine.d.ts +20 -10
  339. package/dist/report/execution/machine.js +6 -6
  340. package/dist/report/host/execute.cjs +25 -16
  341. package/dist/report/host/execute.cjs.map +1 -1
  342. package/dist/report/host/execute.d.cts +4 -0
  343. package/dist/report/host/execute.d.mts +4 -0
  344. package/dist/report/host/execute.d.ts +4 -0
  345. package/dist/report/host/from-record.cjs +22 -5
  346. package/dist/report/host/from-record.cjs.map +1 -1
  347. package/dist/report/host/machine.cjs +53 -11
  348. package/dist/report/host/machine.cjs.map +1 -1
  349. package/dist/report/host/site-runtime.cjs +9 -8
  350. package/dist/report/host/site-runtime.cjs.map +1 -1
  351. package/dist/report/host/site-runtime.d.cts +1 -1
  352. package/dist/report/host/site-runtime.d.mts +1 -1
  353. package/dist/report/host/site-runtime.d.ts +1 -1
  354. package/dist/report/host/static.cjs +56 -9
  355. package/dist/report/host/static.cjs.map +1 -1
  356. package/dist/report/index.cjs.map +1 -1
  357. package/dist/report/index.d.cts +1 -1
  358. package/dist/report/index.d.mts +1 -1
  359. package/dist/report/index.d.ts +1 -1
  360. package/dist/report/model/dataset.cjs +7 -2
  361. package/dist/report/model/dataset.cjs.map +1 -1
  362. package/dist/report/model/locale.cjs +2 -0
  363. package/dist/report/model/locale.cjs.map +1 -1
  364. package/dist/report/model/locale.d.cts +1 -0
  365. package/dist/report/model/locale.d.mts +1 -0
  366. package/dist/report/model/locale.d.ts +1 -0
  367. package/dist/report/model/metrics.cjs +3 -1
  368. package/dist/report/model/metrics.cjs.map +1 -1
  369. package/dist/report/model/pricing.cjs +16 -11
  370. package/dist/report/model/pricing.cjs.map +1 -1
  371. package/dist/report/react/styles.css +53 -29
  372. package/dist/runner/adoption.cjs +1 -0
  373. package/dist/runner/adoption.cjs.map +1 -1
  374. package/dist/runner/attempt.cjs +44 -5
  375. package/dist/runner/attempt.cjs.map +1 -1
  376. package/dist/runner/attempt.d.cts +13 -1
  377. package/dist/runner/attempt.d.mts +13 -1
  378. package/dist/runner/attempt.d.ts +13 -1
  379. package/dist/runner/config-identity.cjs +10 -2
  380. package/dist/runner/config-identity.cjs.map +1 -1
  381. package/dist/runner/config-identity.d.cts +4 -2
  382. package/dist/runner/config-identity.d.mts +4 -2
  383. package/dist/runner/config-identity.d.ts +4 -2
  384. package/dist/runner/feedback/human.cjs +298 -59
  385. package/dist/runner/feedback/human.cjs.map +1 -1
  386. package/dist/runner/feedback/json.cjs +14 -6
  387. package/dist/runner/feedback/json.cjs.map +1 -1
  388. package/dist/runner/feedback/json.d.cts +3 -2
  389. package/dist/runner/feedback/json.d.mts +3 -2
  390. package/dist/runner/feedback/json.d.ts +3 -2
  391. package/dist/runner/feedback/reducer.cjs +2 -0
  392. package/dist/runner/feedback/reducer.cjs.map +1 -1
  393. package/dist/runner/judge-config.cjs +1 -1
  394. package/dist/runner/judge-config.cjs.map +1 -1
  395. package/dist/runner/ledger.cjs +124 -12
  396. package/dist/runner/ledger.cjs.map +1 -1
  397. package/dist/runner/project-current.cjs +13 -5
  398. package/dist/runner/project-current.cjs.map +1 -1
  399. package/dist/runner/run.cjs +564 -256
  400. package/dist/runner/run.cjs.map +1 -1
  401. package/dist/runner/sandbox-pool.cjs +161 -30
  402. package/dist/runner/sandbox-pool.cjs.map +1 -1
  403. package/dist/runner/sandbox-pool.d.cts +34 -2
  404. package/dist/runner/sandbox-pool.d.mts +34 -2
  405. package/dist/runner/sandbox-pool.d.ts +34 -2
  406. package/dist/runner/shared-state-lease.cjs +803 -0
  407. package/dist/runner/shared-state-lease.cjs.map +1 -0
  408. package/dist/runner/shared-state-lease.d.cts +121 -0
  409. package/dist/runner/shared-state-lease.d.mts +121 -0
  410. package/dist/runner/shared-state-lease.d.ts +121 -0
  411. package/dist/runner/shared-state-lease.js +24 -0
  412. package/dist/runner/shared-state-lease.js.map +1 -0
  413. package/dist/runner/teardown-registry.cjs +97 -34
  414. package/dist/runner/teardown-registry.cjs.map +1 -1
  415. package/dist/runner/teardown-registry.d.cts +27 -1
  416. package/dist/runner/teardown-registry.d.mts +27 -1
  417. package/dist/runner/teardown-registry.d.ts +27 -1
  418. package/dist/runner/teardown-registry.js +20 -16
  419. package/dist/runner/types.cjs.map +1 -1
  420. package/dist/runner/types.d.cts +31 -5
  421. package/dist/runner/types.d.mts +31 -5
  422. package/dist/runner/types.d.ts +31 -5
  423. package/dist/sample/analysis.cjs +249 -459
  424. package/dist/sample/analysis.cjs.map +1 -1
  425. package/dist/sample/capability.cjs +138 -7
  426. package/dist/sample/capability.cjs.map +1 -1
  427. package/dist/sample/capability.d.cts +3 -1
  428. package/dist/sample/capability.d.mts +3 -1
  429. package/dist/sample/capability.d.ts +3 -1
  430. package/dist/sample/capability.js +8 -6
  431. package/dist/sandbox/docker-agent-image.cjs +5 -1
  432. package/dist/sandbox/docker-agent-image.cjs.map +1 -1
  433. package/dist/sandbox/docker-agent-image.d.cts +2 -0
  434. package/dist/sandbox/docker-agent-image.d.mts +2 -0
  435. package/dist/sandbox/docker-agent-image.d.ts +2 -0
  436. package/dist/sandbox/docker-agent-image.js +14 -10
  437. package/dist/sandbox/docker-profile/schema.cjs +283 -534
  438. package/dist/sandbox/docker-profile/schema.cjs.map +1 -1
  439. package/dist/sandbox/docker-profile/schema.d.cts +10 -27
  440. package/dist/sandbox/docker-profile/schema.d.mts +10 -27
  441. package/dist/sandbox/docker-profile/schema.d.ts +10 -27
  442. package/dist/sandbox/e2b-agent-template.cjs +5 -2
  443. package/dist/sandbox/e2b-agent-template.cjs.map +1 -1
  444. package/dist/sandbox/index.cjs +4 -2
  445. package/dist/sandbox/index.cjs.map +1 -1
  446. package/dist/sandbox/index.d.cts +1 -1
  447. package/dist/sandbox/index.d.mts +1 -1
  448. package/dist/sandbox/index.d.ts +1 -1
  449. package/dist/sandbox/index.js +120 -116
  450. package/dist/sandbox/index.mjs +120 -116
  451. package/dist/sandbox/keep-registry.cjs +28 -66
  452. package/dist/sandbox/keep-registry.cjs.map +1 -1
  453. package/dist/sandbox/types.cjs.map +1 -1
  454. package/dist/sandbox/types.d.cts +1 -1
  455. package/dist/sandbox/types.d.mts +1 -1
  456. package/dist/sandbox/types.d.ts +1 -1
  457. package/dist/shared/aggregate.cjs +16 -6
  458. package/dist/shared/aggregate.cjs.map +1 -1
  459. package/dist/shared/aggregate.d.cts +12 -4
  460. package/dist/shared/aggregate.d.mts +12 -4
  461. package/dist/shared/aggregate.d.ts +12 -4
  462. package/dist/shared/turn-label.cjs +20 -2
  463. package/dist/shared/turn-label.cjs.map +1 -1
  464. package/dist/shared/turn-label.d.cts +2 -0
  465. package/dist/shared/turn-label.d.mts +2 -0
  466. package/dist/shared/turn-label.d.ts +2 -0
  467. package/dist/shared/turn-label.js +4 -2
  468. package/dist/vendor/remark/remark-gfm.cjs +7 -7
  469. package/dist/vendor/remark/remark-gfm.cjs.map +1 -1
  470. package/dist/vendor/remark/remark-parse.cjs +12 -12
  471. package/dist/vendor/remark/remark-parse.cjs.map +1 -1
  472. package/dist/vendor/remark/remark.cjs +13 -13
  473. package/dist/vendor/remark/remark.cjs.map +1 -1
  474. package/dist/view/server.cjs +6 -0
  475. package/dist/view/server.cjs.map +1 -1
  476. package/docs-site/images/hitl-handshake-zh.svg +3 -3
  477. package/docs-site/zh/examples/ai-agent-application.mdx +1 -1
  478. package/docs-site/zh/examples/coding-agent-extensions.mdx +1 -1
  479. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +4 -3
  480. package/docs-site/zh/examples/integrations/claude-sdk.mdx +3 -3
  481. package/docs-site/zh/examples/integrations/codex-sdk.mdx +5 -5
  482. package/docs-site/zh/examples/integrations/langgraph.mdx +4 -4
  483. package/docs-site/zh/examples/integrations/pi-sdk.mdx +5 -5
  484. package/docs-site/zh/explanation/adapter.mdx +5 -5
  485. package/docs-site/zh/explanation/assert.mdx +7 -7
  486. package/docs-site/zh/explanation/drive.mdx +4 -2
  487. package/docs-site/zh/explanation/evals.mdx +10 -8
  488. package/docs-site/zh/explanation/experiment.mdx +2 -2
  489. package/docs-site/zh/explanation/hitl.mdx +9 -9
  490. package/docs-site/zh/explanation/judge.mdx +5 -5
  491. package/docs-site/zh/explanation/overview.mdx +3 -3
  492. package/docs-site/zh/explanation/runner.mdx +1 -1
  493. package/docs-site/zh/index.mdx +3 -3
  494. package/docs-site/zh/reference/builtin-agents.mdx +13 -13
  495. package/docs-site/zh/reference/capabilities.mdx +1 -1
  496. package/docs-site/zh/reference/cli.mdx +18 -12
  497. package/docs-site/zh/reference/define-agent.mdx +130 -5
  498. package/docs-site/zh/reference/define-config.mdx +2 -2
  499. package/docs-site/zh/reference/define-eval.mdx +4 -4
  500. package/docs-site/zh/reference/events.mdx +11 -11
  501. package/docs-site/zh/reference/official-adapters.mdx +6 -6
  502. package/docs-site/zh/reference/results-data.mdx +6 -0
  503. package/docs-site/zh/troubleshooting/debugging.mdx +1 -1
  504. package/docs-site/zh/troubleshooting/recover-after-kill.mdx +37 -5
  505. package/docs-site/zh/tutorials/accept-results.mdx +72 -0
  506. package/docs-site/zh/tutorials/agent-onboarding.mdx +6 -6
  507. package/docs-site/zh/tutorials/authoring.mdx +9 -13
  508. package/docs-site/zh/tutorials/ci-integration.mdx +6 -6
  509. package/docs-site/zh/tutorials/compare-runs.mdx +61 -0
  510. package/docs-site/zh/tutorials/concurrency.mdx +13 -12
  511. package/docs-site/zh/tutorials/configuration.mdx +4 -6
  512. package/docs-site/zh/tutorials/connect-your-agent.mdx +1 -1
  513. package/docs-site/zh/tutorials/control-run-cost.mdx +58 -0
  514. package/docs-site/zh/tutorials/criteria-files.mdx +2 -2
  515. package/docs-site/zh/tutorials/custom-reports.mdx +2 -2
  516. package/docs-site/zh/tutorials/debug-lifecycle-plan.mdx +46 -0
  517. package/docs-site/zh/tutorials/docker-in-docker.mdx +52 -43
  518. package/docs-site/zh/tutorials/eval-groups.mdx +6 -6
  519. package/docs-site/zh/tutorials/evaluation-kinds.mdx +3 -3
  520. package/docs-site/zh/tutorials/experiments.mdx +2 -3
  521. package/docs-site/zh/tutorials/handle-execution-failures.mdx +18 -10
  522. package/docs-site/zh/tutorials/install-custom-sandbox-agent.mdx +16 -8
  523. package/docs-site/zh/tutorials/nixos-managed-dind.mdx +160 -0
  524. package/docs-site/zh/tutorials/plugins.mdx +14 -0
  525. package/docs-site/zh/tutorials/rerun-and-cache.mdx +4 -2
  526. package/docs-site/zh/tutorials/sandbox-agent.mdx +17 -11
  527. package/docs-site/zh/tutorials/sandbox-providers.mdx +17 -13
  528. package/docs-site/zh/tutorials/sandbox-reuse.mdx +12 -19
  529. package/docs-site/zh/tutorials/select-and-preview.mdx +65 -0
  530. package/docs-site/zh/tutorials/verify-judge.mdx +78 -0
  531. package/docs-site/zh/tutorials/viewing-results.mdx +38 -1
  532. package/docs-site/zh/tutorials/write-experiment.mdx +7 -5
  533. package/docs-site/zh/tutorials/write-send.mdx +9 -8
  534. package/package.json +9 -5
  535. package/dist/report/built-in/overview.cjs +0 -44
  536. package/dist/report/built-in/overview.cjs.map +0 -1
  537. package/dist/report/built-in/overview.d.cts +0 -6
  538. package/dist/report/built-in/overview.d.mts +0 -6
  539. package/dist/report/built-in/overview.d.ts +0 -6
  540. package/dist/report/built-in/overview.js +0 -9
  541. package/dist/report/built-in/overview.js.map +0 -1
  542. package/dist/runner/gate-lease.cjs +0 -283
  543. package/dist/runner/gate-lease.cjs.map +0 -1
  544. package/dist/runner/gate-lease.d.cts +0 -62
  545. package/dist/runner/gate-lease.d.mts +0 -62
  546. package/dist/runner/gate-lease.d.ts +0 -62
  547. package/dist/runner/gate-lease.js +0 -22
  548. package/dist/runner/gate-lease.js.map +0 -1
@@ -33,7 +33,7 @@ const node_os_1 = require("node:os");
33
33
  const contracts_ts_1 = require("../plugin/contracts.cjs");
34
34
  const teardown_registry_ts_1 = require("./teardown-registry.cjs");
35
35
  const lock_ts_1 = require("./lock.cjs");
36
- const gate_lease_ts_1 = require("./gate-lease.cjs");
36
+ const shared_state_lease_ts_1 = require("./shared-state-lease.cjs");
37
37
  const record_ts_1 = require("./record.cjs");
38
38
  const runner_producer_ts_1 = require("../o11y/record/runner-producer.cjs");
39
39
  const sandbox_reuse_ts_1 = require("./sandbox-reuse.cjs");
@@ -48,6 +48,40 @@ function feedbackIdentity(a) {
48
48
  function feedbackWho(a) {
49
49
  return (0, types_ts_1.runWho)({ agentName: a.run.agent.name, model: a.run.model, experimentId: a.run.experimentId });
50
50
  }
51
+ const SHARED_BUILD_FAILURE_DIAGNOSTIC = "sandbox-build-failed";
52
+ function sharedBuildFailureRoot(error) {
53
+ const codeMatch = /\bERR_[A-Z0-9_]+\b/u.exec(error.message);
54
+ const code = codeMatch?.[0] ?? error.code;
55
+ const focused = codeMatch === null
56
+ ? error.message.slice(-600)
57
+ : error.message.slice(codeMatch.index, codeMatch.index + 600);
58
+ return { code, message: focused.trim() || error.message.slice(-600).trim() || error.code };
59
+ }
60
+ function sharedBuildFailureDetail(a, error) {
61
+ const failureId = error.origin.scope === "run"
62
+ ? error.origin.timingNodeId
63
+ : `${error.code}:${a.evalDef.id}`;
64
+ const root = sharedBuildFailureRoot(error);
65
+ let message = root.message;
66
+ const encode = () => JSON.stringify({
67
+ schema: "niceeval.shared-build-failure/v1",
68
+ failureId,
69
+ evalId: a.evalDef.id,
70
+ attemptOrdinal: a.attempt,
71
+ phase: "sandbox.image.build",
72
+ errorCode: root.code,
73
+ message,
74
+ ...(root.code === "ERR_PNPM_IGNORED_BUILDS"
75
+ ? { remediation: "pnpm-allow-builds" }
76
+ : {}),
77
+ });
78
+ let encoded = encode();
79
+ while (new TextEncoder().encode(encoded).byteLength > 1000 && message.length > 0) {
80
+ message = message.slice(0, Math.floor(message.length * 0.75));
81
+ encoded = encode();
82
+ }
83
+ return encoded;
84
+ }
51
85
  function attemptIdentityKey(experimentId, agentName, model, evalId) {
52
86
  return JSON.stringify([experimentId, agentName, model ?? null, evalId]);
53
87
  }
@@ -210,7 +244,7 @@ function runEvals(opts) {
210
244
  return failure === undefined ? [] : [failure];
211
245
  }));
212
246
  if (opts.onCurrentRecordReusePlan !== undefined) {
213
- yield* opts.onCurrentRecordReusePlan({ reused: reusedAttempts.length, reusedFailures });
247
+ yield* opts.onCurrentRecordReusePlan({ reused: reusedAttempts.length, reusedFailures, runIds });
214
248
  }
215
249
  else {
216
250
  // Direct library callers have no feedback coordinator. Preserve their
@@ -395,6 +429,11 @@ function runEvals(opts) {
395
429
  const judgePrecheckFailures = new Map();
396
430
  {
397
431
  const uniquePairs = [...new Map(attempts.map((a) => [(0, fingerprint_ts_1.cacheKey)(a.run, a.evalDef.id), a])).entries()];
432
+ const plannedByPair = new Map();
433
+ for (const attempt of attempts) {
434
+ const pairKey = (0, fingerprint_ts_1.cacheKey)(attempt.run, attempt.evalDef.id);
435
+ plannedByPair.set(pairKey, (plannedByPair.get(pairKey) ?? 0) + 1);
436
+ }
398
437
  const { targets, evalKeys } = judgeProbePlan(uniquePairs.map(([id, a]) => ({
399
438
  id,
400
439
  judge: a.judge,
@@ -410,13 +449,35 @@ function runEvals(opts) {
410
449
  const failedByKey = new Map();
411
450
  for (const target of targets) {
412
451
  const err = yield* (0, judge_ts_1.probeJudgeEffect)(target.judge, opts.signal);
413
- if (err)
452
+ if (err) {
414
453
  failedByKey.set(target.key, err);
454
+ }
415
455
  }
416
456
  for (const [pairKey, key] of evalKeys) {
417
457
  const err = failedByKey.get(key);
418
- if (err !== undefined)
419
- judgePrecheckFailures.set(pairKey, err);
458
+ if (err === undefined)
459
+ continue;
460
+ judgePrecheckFailures.set(pairKey, err);
461
+ const pair = uniquePairs.find(([candidate]) => candidate === pairKey)?.[1];
462
+ if (pair === undefined)
463
+ continue;
464
+ const planned = plannedByPair.get(pairKey) ?? 1;
465
+ // No Attempt exists yet, so this pair-owned warning is the stable
466
+ // machine projection: identity and terminal counts travel in data,
467
+ // without fabricating a locator-addressable eval event.
468
+ (0, sink_ts_1.reportDiagnostic)({
469
+ key: `judge-precheck-failed:${pairKey}`,
470
+ code: "judge-precheck-failed",
471
+ severity: "error",
472
+ message: err,
473
+ data: {
474
+ phase: "judge.precheck",
475
+ ...(pair.run.experimentId === undefined ? {} : { experimentId: pair.run.experimentId }),
476
+ evalId: pair.evalDef.id,
477
+ planned,
478
+ errored: planned,
479
+ },
480
+ });
420
481
  }
421
482
  (0, sink_ts_1.reportPrecheck)({
422
483
  status: failedByKey.size > 0 ? "failed" : "done",
@@ -614,6 +675,8 @@ function runEvals(opts) {
614
675
  const sandboxSem = yield* effect_1.Effect.makeSemaphore(opts.maxConcurrency);
615
676
  // 相同 provider physical identity 共享一个按需池;不从 AgentRun 重选 template。
616
677
  const reusePools = new Map();
678
+ /** A terminal Experiment freezes its own registry before physical stop. */
679
+ const frozenReusePoolRuns = new Set();
617
680
  // A physical Sandbox number is Run-wide, not pool-local: distinct Eval
618
681
  // Groups may materialize identical provider plans concurrently and must not
619
682
  // both report `reuseSandbox: 1` for different instances.
@@ -650,6 +713,9 @@ function runEvals(opts) {
650
713
  if (key === undefined || a.plan._tag !== "Sandbox") {
651
714
  return effect_1.Effect.succeed({ _tag: "Fresh" });
652
715
  }
716
+ if (frozenReusePoolRuns.has(a.run)) {
717
+ return effect_1.Effect.die(new Error("Sandbox reuse registry was frozen before this Attempt could acquire a pool."));
718
+ }
653
719
  let bySpec = reusePools.get(a.run);
654
720
  if (bySpec === undefined) {
655
721
  bySpec = new Map();
@@ -690,20 +756,31 @@ function runEvals(opts) {
690
756
  }
691
757
  return effect_1.Effect.succeed({ _tag: "Reuse", pool });
692
758
  };
693
- // 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。
694
- // 实验级闸让「有共享状态、必须串行」的实验(如跨 eval 累积记忆,maxConcurrency: 1)只在
695
- // 自己内部排队,同批其它实验照常并发。它的名额域跨 Invocation 共用(见 gate-lease.ts
696
- // docs/feature/experiments/architecture.md「并发 Invocation:用例锁」末条),所以名额不是
697
- // 进程内信号量而是磁盘上的逐槽租约——多开不叠加 N,`maxConcurrency: 1` 的临界区声明在
698
- // 多开下同样成立。全局位反过来是每条 Invocation 私有的吞吐旋钮,仍是进程内信号量。
759
+ /**
760
+ * Pool ownership is Experiment-scoped, not Invocation-scoped. The last
761
+ * Attempt settlement freezes this registry before stop starts, so no late
762
+ * acquire can create another physical Sandbox between pool stop and the
763
+ * Experiment hook's checkpoint/teardown work.
764
+ */
765
+ const freezeReusablePoolsForRun = (run) => effect_1.Effect.sync(() => {
766
+ frozenReusePoolRuns.add(run);
767
+ }).pipe(effect_1.Effect.zipRight(effect_1.Effect.forEach([...(reusePools.get(run)?.values() ?? [])], (pool) => pool.freeze(), { concurrency: "unbounded", discard: true })));
768
+ const stopReusablePoolsForRun = (run) => effect_1.Effect.gen(function* () {
769
+ const exits = yield* effect_1.Effect.forEach([...(reusePools.get(run)?.values() ?? [])], (pool) => effect_1.Effect.exit(pool.stop()), { concurrency: "unbounded" });
770
+ const failures = [];
771
+ for (const exit of exits) {
772
+ if (effect_1.Exit.isFailure(exit))
773
+ failures.push(effect_1.Cause.squash(exit.cause));
774
+ }
775
+ if (failures.length > 0) {
776
+ return yield* effect_1.Effect.fail(new AggregateError(failures, "Sandbox reuse pool cleanup failed."));
777
+ }
778
+ });
779
+ // 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。两者都只
780
+ // 属于本 Invocation;跨 Invocation 的共享外部状态由 sharedState 租约另行保护,不能把
781
+ // `maxConcurrency` 误当成跨进程临界区。
699
782
  const globalSem = yield* effect_1.Effect.makeSemaphore(opts.maxConcurrency);
700
- // 实验闸在进程内先垫一层同名额的信号量,再去取租约。两个原因:
701
- // ① 名额在本进程内部交接是即时的(Effect 信号量把 permit 直接递给排队的下一个 attempt),
702
- // 不必等租约轮询的下一个周期——单开是绝大多数场景,不该为跨进程协调付整整一个轮询周期
703
- // 的空转;② 同实验同时去拍磁盘的 attempt 至多 N 个,不让 runs 展开出的一大批兄弟一起
704
- // 轮询。它只会更严、不会更松:permit 数恒等于该实验 resolved 的 N,真正的名额权威仍是
705
- // 租约(跨 Invocation 共用、min-N 收紧)。裸 run(没有 experimentId、没有可共享的名额域)
706
- // 就只有这一层,不产生任何跨进程协调。
783
+ // 实验闸是 Invocation 内信号量:同实验的 attempt 即时交接 permit,同批其它实验不受影响。
707
784
  const gateLocalSems = new Map();
708
785
  for (const run of effectiveAgentRuns) {
709
786
  if (run.maxConcurrency !== undefined) {
@@ -754,7 +831,7 @@ function runEvals(opts) {
754
831
  };
755
832
  const expLifecycles = new Map();
756
833
  for (const a of attempts) {
757
- if (!a.run.setup && !a.run.teardown)
834
+ if (!a.run.setup && !a.run.teardown && !a.run.sharedState)
758
835
  continue;
759
836
  const cell = expLifecycles.get(a.run);
760
837
  if (cell) {
@@ -766,9 +843,18 @@ function runEvals(opts) {
766
843
  expLifecycles.set(a.run, {
767
844
  state: { _tag: "Dormant", pendingAttempts: new Set([a]) },
768
845
  mutex: yield* effect_1.Effect.makeSemaphore(1),
846
+ physicalCleanupFailures: [],
769
847
  });
770
848
  }
771
849
  }
850
+ const recordFreshSandboxCleanupFailure = (run, failure) => {
851
+ if (run.sharedState === undefined)
852
+ return;
853
+ const cell = expLifecycles.get(run);
854
+ if (cell === undefined)
855
+ return;
856
+ cell.physicalCleanupFailures.push(failure);
857
+ };
772
858
  // 实验域诊断累积器(docs/runner.md「实验域诊断持久化」):只接无法归属单 Attempt 的实验
773
859
  // 事实——ctx.diagnostic、teardown failed/late、budget-unenforceable。相同 dedupeKey 只在
774
860
  // 同一个 experimentId 桶内折叠 count;不同 Experiment 各自独立累计,不跨来源合并。裸 run
@@ -818,10 +904,74 @@ function runEvals(opts) {
818
904
  list.push(record);
819
905
  experimentDiagnostics.set(input.experimentId, list);
820
906
  };
907
+ /**
908
+ * Owner tokens are an explicit inspection capability, not ordinary Runner
909
+ * feedback. Keep both the live feedback event and the durable Run diagnostic
910
+ * limited to the key and recovery obligation; `exp --teardown` is the only
911
+ * public surface that renders immutable owner evidence.
912
+ */
913
+ const reportSharedStateRecoveryRequired = (input) => {
914
+ const key = input.run.sharedState?.key ?? "";
915
+ const message = (0, index_ts_1.t)("runner.sharedStateRecoveryRequired", { key }).trimEnd();
916
+ (0, sink_ts_1.reportDiagnostic)({
917
+ key: `state-lease-recovery-required:${key || input.experimentId}`,
918
+ code: "state-lease-recovery-required",
919
+ severity: "warning",
920
+ message,
921
+ data: {
922
+ experimentId: input.experimentId,
923
+ sharedStateKey: key,
924
+ reason: input.reason,
925
+ },
926
+ });
927
+ recordExperimentDiagnostic({
928
+ experimentId: input.run.experimentId,
929
+ code: "state-lease-recovery-required",
930
+ level: "warning",
931
+ message,
932
+ phase: input.phase,
933
+ data: { sharedStateKey: key, reason: input.reason },
934
+ });
935
+ };
936
+ /**
937
+ * A blocked sharedState acquisition is normal contention. It is deliberately
938
+ * an info notice and deliberately does not enter a Run's durable diagnostic
939
+ * list: there is no cleanup obligation and no owner evidence to expose.
940
+ */
941
+ const acquireSharedStateClaim = (run, experimentId) => effect_1.Effect.gen(function* () {
942
+ const sharedState = run.sharedState;
943
+ if (sharedState === undefined) {
944
+ return yield* effect_1.Effect.die(new Error("Attempted to acquire an undeclared sharedState lease."));
945
+ }
946
+ const processIdentity = sharedStateProcessIdentity ?? (yield* (0, shared_state_lease_ts_1.currentProcessIdentityEffect)());
947
+ sharedStateProcessIdentity = processIdentity;
948
+ const acquired = yield* (0, shared_state_lease_ts_1.acquireSharedStateLeaseEffect)(coordinationRoot, sharedState.key, {
949
+ experimentId,
950
+ pid: process.pid,
951
+ host: currentHost,
952
+ processIdentity,
953
+ }, {
954
+ ...(opts.signal === undefined ? {} : { signal: opts.signal }),
955
+ onWaitStart: () => {
956
+ const message = (0, index_ts_1.t)("runner.sharedStateWaiting", { key: sharedState.key }).trimEnd();
957
+ (0, sink_ts_1.reportDiagnostic)({
958
+ key: `state-lease-waiting:${sharedState.key}`,
959
+ code: "state-lease-waiting",
960
+ severity: "info",
961
+ message,
962
+ data: { experimentId, sharedStateKey: sharedState.key },
963
+ });
964
+ },
965
+ });
966
+ return acquired.claim;
967
+ });
821
968
  // 强杀后的收尾兜底(docs/feature/experiments/architecture.md「强杀后的收尾兜底」)的磁盘登记
822
969
  // 挂在本地协调根下,与留存注册表 `.niceeval/sandboxes/` 同一个根(默认 cwd/.niceeval,
823
970
  // 与 attempt.ts 的 `coordinationRoot` 兜底同一口径)。
824
971
  const currentHost = (0, node_os_1.hostname)();
972
+ // Establish process identity lazily: only a configured sharedState opts in
973
+ // to this fail-closed cross-process coordination requirement.
974
+ let sharedStateProcessIdentity;
825
975
  const makeExperimentHookContext = (run, phase) => {
826
976
  const experimentId = run.experimentId;
827
977
  return {
@@ -869,6 +1019,35 @@ function runEvals(opts) {
869
1019
  return setup;
870
1020
  throw new Error("Experiment setup completed without recording its terminal outcome.");
871
1021
  };
1022
+ const releaseSharedStateLease = (run, cell, cleanupSucceeded) => effect_1.Effect.suspend(() => {
1023
+ const claim = cell.sharedStateClaim;
1024
+ return effect_1.Effect.sync(() => claim).pipe(effect_1.Effect.flatMap((heldClaim) => {
1025
+ const claim = heldClaim;
1026
+ if (claim === undefined)
1027
+ return effect_1.Effect.void;
1028
+ const experimentId = run.experimentId ?? run.agent.name;
1029
+ const recoveryRequired = (reason) => effect_1.Effect.sync(() => {
1030
+ reportSharedStateRecoveryRequired({
1031
+ run,
1032
+ experimentId,
1033
+ reason,
1034
+ phase: "experiment.teardown",
1035
+ });
1036
+ });
1037
+ if (!cleanupSucceeded()) {
1038
+ // A retained lease is a durable recovery obligation, not a reason
1039
+ // to leave a daemon heartbeat/timer behind after this Invocation.
1040
+ // `abandon` cannot delete or rewrite the record.
1041
+ cell.sharedStateClaim = undefined;
1042
+ return claim.abandon.pipe(effect_1.Effect.catchAll(() => effect_1.Effect.void), effect_1.Effect.zipRight(recoveryRequired("cleanup-failed")));
1043
+ }
1044
+ // Once release starts its heartbeat is interrupted. Do not retain a
1045
+ // stale in-memory claim after a failed compare-owner mutation; its
1046
+ // durable record is deliberately left for explicit recovery.
1047
+ cell.sharedStateClaim = undefined;
1048
+ return claim.release.pipe(effect_1.Effect.catchAll(() => recoveryRequired("release-failed")));
1049
+ }));
1050
+ });
872
1051
  const runExperimentTeardown = (run, cell) => effect_1.Effect.uninterruptibleMask((restore) => cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
873
1052
  const current = cell.state;
874
1053
  // setup 时点没走到就没有收尾义务;已完成与在飞状态分别复用自己的确定 Effect。
@@ -887,52 +1066,106 @@ function runEvals(opts) {
887
1066
  setup: current.setup,
888
1067
  completion,
889
1068
  };
1069
+ let cleanupSucceeded = true;
890
1070
  const teardown = effect_1.Effect.gen(function* () {
891
- // 强清可能先取得收尾执行权;必须等 setup Effect 结算后再清理。
892
- yield* awaitSetup(current.setup);
893
- if (!run.teardown)
894
- return;
895
- (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "teardown", status: "started" });
896
- const startedAt = Date.now();
897
- const ctx = makeExperimentHookContext(run, "experiment.teardown");
898
- yield* (0, cleanup_timeout_ts_1.cleanupCallback)(() => run.teardown(ctx)).pipe(effect_1.Effect.matchEffect({
899
- onSuccess: () => effect_1.Effect.sync(() => {
900
- (0, sink_ts_1.reportExperimentHook)({
901
- experimentId,
902
- hook: "teardown",
903
- status: "done",
904
- durationMs: Date.now() - startedAt,
905
- });
906
- }),
907
- onFailure: (error) => effect_1.Effect.sync(() => {
908
- (0, sink_ts_1.reportExperimentHook)({
909
- experimentId,
910
- hook: "teardown",
911
- status: "failed",
912
- durationMs: Date.now() - startedAt,
913
- });
914
- // teardown 失败只作运行级诊断,不改任何已产出的 verdict。
915
- const message = (0, index_ts_1.t)("runner.experimentTeardownFailed", {
916
- experimentId,
917
- message: error instanceof Error ? error.message : String(error),
918
- }).trimEnd();
919
- (0, sink_ts_1.reportDiagnostic)({
920
- key: `experiment-teardown-failed:${experimentId}`,
921
- code: "experiment-teardown-failed",
922
- severity: "warning",
923
- message,
924
- data: { experimentId },
925
- });
926
- recordExperimentDiagnostic({
927
- experimentId: run.experimentId,
928
- code: "experiment-teardown-failed",
929
- level: "warning",
930
- message,
931
- phase: "experiment.teardown",
932
- });
933
- }),
934
- }));
935
- // 这个 Experiment 真正走完 teardown(不论成败)的时刻。
1071
+ // A failed setup still has teardown obligations. Capture its outcome
1072
+ // rather than short-circuiting the physical pool/provider cleanup.
1073
+ yield* effect_1.Effect.exit(awaitSetup(current.setup));
1074
+ // Last Attempt settle -> freeze registry -> stop every reusable pool
1075
+ // (Sandbox lifecycle teardown + provider finalizer) -> author hook.
1076
+ // Stop failures are recorded but never skip later cleanup.
1077
+ const pools = yield* effect_1.Effect.exit(freezeReusablePoolsForRun(run).pipe(effect_1.Effect.zipRight(stopReusablePoolsForRun(run))));
1078
+ if (effect_1.Exit.isFailure(pools)) {
1079
+ cleanupSucceeded = false;
1080
+ const message = `Sandbox reuse cleanup failed before Experiment teardown: ${String(effect_1.Cause.squash(pools.cause))}`;
1081
+ (0, sink_ts_1.reportDiagnostic)({
1082
+ key: `sandbox-reuse-cleanup-failed:${experimentId}`,
1083
+ code: "sandbox-reuse-cleanup-failed",
1084
+ severity: "warning",
1085
+ message,
1086
+ data: { experimentId },
1087
+ });
1088
+ recordExperimentDiagnostic({
1089
+ experimentId: run.experimentId,
1090
+ code: "sandbox-reuse-cleanup-failed",
1091
+ level: "warning",
1092
+ message,
1093
+ phase: "experiment.teardown",
1094
+ });
1095
+ }
1096
+ // Fresh Sandbox Scopes close inside their owning Attempt and have
1097
+ // therefore already reached a real terminal state by the time the
1098
+ // final Attempt settles. Runtime converts hook/provider failures into
1099
+ // diagnostics to preserve the remaining finalizers, so consult the
1100
+ // Experiment cell's physical ledger before deciding whether its
1101
+ // sharedState lease can be released.
1102
+ if (cell.physicalCleanupFailures.length > 0) {
1103
+ cleanupSucceeded = false;
1104
+ const details = cell.physicalCleanupFailures
1105
+ .map((failure) => `${failure.stage}: ${failure.error.message}`)
1106
+ .join("; ");
1107
+ const message = `Fresh Sandbox cleanup failed before Experiment teardown: ${details}`;
1108
+ (0, sink_ts_1.reportDiagnostic)({
1109
+ key: `sandbox-fresh-cleanup-failed:${experimentId}`,
1110
+ code: "sandbox-fresh-cleanup-failed",
1111
+ severity: "warning",
1112
+ message,
1113
+ data: { experimentId, failureCount: cell.physicalCleanupFailures.length },
1114
+ });
1115
+ recordExperimentDiagnostic({
1116
+ experimentId: run.experimentId,
1117
+ code: "sandbox-fresh-cleanup-failed",
1118
+ level: "warning",
1119
+ message,
1120
+ phase: "experiment.teardown",
1121
+ });
1122
+ }
1123
+ if (run.teardown) {
1124
+ (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "teardown", status: "started" });
1125
+ const startedAt = Date.now();
1126
+ const ctx = makeExperimentHookContext(run, "experiment.teardown");
1127
+ yield* (0, cleanup_timeout_ts_1.cleanupCallback)(() => run.teardown(ctx)).pipe(effect_1.Effect.matchEffect({
1128
+ onSuccess: () => effect_1.Effect.sync(() => {
1129
+ (0, sink_ts_1.reportExperimentHook)({
1130
+ experimentId,
1131
+ hook: "teardown",
1132
+ status: "done",
1133
+ durationMs: Date.now() - startedAt,
1134
+ });
1135
+ }),
1136
+ onFailure: (error) => effect_1.Effect.sync(() => {
1137
+ cleanupSucceeded = false;
1138
+ (0, sink_ts_1.reportExperimentHook)({
1139
+ experimentId,
1140
+ hook: "teardown",
1141
+ status: "failed",
1142
+ durationMs: Date.now() - startedAt,
1143
+ });
1144
+ // Teardown failure never skips subsequent release handling, but
1145
+ // it makes automatic lease release unsafe.
1146
+ const message = (0, index_ts_1.t)("runner.experimentTeardownFailed", {
1147
+ experimentId,
1148
+ message: error instanceof Error ? error.message : String(error),
1149
+ }).trimEnd();
1150
+ (0, sink_ts_1.reportDiagnostic)({
1151
+ key: `experiment-teardown-failed:${experimentId}`,
1152
+ code: "experiment-teardown-failed",
1153
+ severity: "warning",
1154
+ message,
1155
+ data: { experimentId },
1156
+ });
1157
+ recordExperimentDiagnostic({
1158
+ experimentId: run.experimentId,
1159
+ code: "experiment-teardown-failed",
1160
+ level: "warning",
1161
+ message,
1162
+ phase: "experiment.teardown",
1163
+ });
1164
+ }),
1165
+ }));
1166
+ }
1167
+ // This Experiment has reached its terminal cleanup point whether its
1168
+ // individual cleanup steps succeeded or failed.
936
1169
  experimentCompletedAt.set(experimentId, new Date().toISOString());
937
1170
  }).pipe(effect_1.Effect.ensuring(cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
938
1171
  const state = cell.state;
@@ -946,15 +1179,37 @@ function runEvals(opts) {
946
1179
  if (run.experimentId) {
947
1180
  yield* (0, teardown_registry_ts_1.removeTeardownRegistrationIfPresentEffect)(coordinationRoot, (0, teardown_registry_ts_1.teardownEntryId)(run.experimentId, process.pid)).pipe(effect_1.Effect.ignore);
948
1181
  }
949
- }))));
950
- const settle = effect_1.Effect.exit(restore(teardown)).pipe(effect_1.Effect.flatMap((exit) => effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
951
- yield* restore(settle).pipe(effect_1.Effect.forkIn(invocationScope));
1182
+ }))),
1183
+ // Include both the cleanup body and its lifecycle-registration
1184
+ // finalizer in this decision. A failure while unregistering the
1185
+ // teardown ownership is still an incomplete cleanup and must retain
1186
+ // the sharedState lease; release itself remains outside this check.
1187
+ effect_1.Effect.onExit((exit) => effect_1.Exit.isFailure(exit)
1188
+ ? effect_1.Effect.sync(() => {
1189
+ cleanupSucceeded = false;
1190
+ })
1191
+ : effect_1.Effect.void),
1192
+ // Lease release happens only when all prior cleanup was successful.
1193
+ // Otherwise the durable owner token remains for the public explicit
1194
+ // recovery command; the CLI exit sweep is not allowed to erase it.
1195
+ // Read `cleanupSucceeded` only when this finalizer executes. Passing
1196
+ // its current boolean while constructing the pipeline would capture
1197
+ // the initial `true` and could release after a later teardown failure.
1198
+ effect_1.Effect.ensuring(effect_1.Effect.suspend(() => releaseSharedStateLease(run, cell, () => cleanupSucceeded))));
1199
+ // The initiating waiter may be interrupted, but the one published
1200
+ // cleanup fiber must still advance through every later cleanup stage.
1201
+ // `cleanupCallback` remains explicitly interruptible for its own
1202
+ // bounded timeout; this mask only prevents a caller's cancellation from
1203
+ // skipping pool stop or Experiment teardown entirely.
1204
+ const settle = effect_1.Effect.exit(effect_1.Effect.uninterruptible(teardown)).pipe(effect_1.Effect.flatMap((exit) => effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
1205
+ yield* settle.pipe(effect_1.Effect.forkIn(invocationScope));
952
1206
  return completion;
953
1207
  })).pipe(effect_1.Effect.flatMap((completion) => completion === undefined ? effect_1.Effect.void : restore(effect_1.Deferred.await(completion)))));
954
1208
  /**
955
1209
  * 启动自愈:本实验触发 setup 之前,先核对磁盘上是否有它自己的遗留登记——上一次运行同一
956
1210
  * experimentId 被强杀、来不及删除。同宿主且 pid 已死才是遗留义务;pid 活或异宿主可能是
957
- * 并发 run,不触碰。先原子删登记拿到执行权,再补执行一次它的 teardown(新进程语义:
1211
+ * 并发 run,不触碰。sharedState caller 必须已先取得同 key exact authority;再原子删登记拿到执行权,
1212
+ * 再补执行一次它的 teardown(新进程语义:
958
1213
  * ctx.selectedEvalIds 从登记恢复,不依赖已丢失的 setup 产物)。失败只记诊断,不阻断、
959
1214
  * 不重试本次 run 的调度——recovery 补偿的是上一次的泄漏,不是这一次的前提条件。
960
1215
  */
@@ -1039,123 +1294,174 @@ function runEvals(opts) {
1039
1294
  }
1040
1295
  });
1041
1296
  });
1297
+ // Startup recovery can exist even when every Attempt is carried (or there
1298
+ // are no selected Attempts), so it cannot borrow an ExperimentLifecycleCell.
1299
+ // Each selected run instead gets an independent completion gate; the first
1300
+ // real setup awaits the same gate before it can claim or initialize state.
1301
+ const startupRecoveryCompletions = new Map();
1302
+ const startupRecoveryCompletionsToAwait = [];
1042
1303
  const ensureExperimentSetup = (a) => {
1043
1304
  const cell = expLifecycles.get(a.run);
1044
- return effect_1.Effect.uninterruptibleMask((restore) => cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
1045
- const current = cell.state;
1046
- if (current._tag === "Active" || current._tag === "TearingDown")
1047
- return current.setup;
1048
- if (current._tag === "TornDown")
1049
- return undefined;
1050
- if (current._tag === "UntriggeredComplete") {
1051
- return yield* effect_1.Effect.die(new Error("Experiment lifecycle cannot be triggered after all attempts settled."));
1052
- }
1053
- const run = a.run;
1054
- const experimentId = run.experimentId ?? run.agent.name;
1055
- const completion = yield* effect_1.Deferred.make();
1056
- // 先写 Active、再登记和启动 worker:随后任何 attempt 或强清都会复用同一个 Effect 完成点。
1057
- cell.state = {
1058
- _tag: "Active",
1059
- pendingAttempts: current.pendingAttempts,
1060
- setup: { _tag: "InProgress", completion },
1061
- };
1062
- if (run.teardown) {
1063
- yield* (0, experiment_cleanup_registry_ts_1.registerExperimentTeardown)(experimentId, () => runExperimentTeardown(run, cell));
1064
- }
1065
- let setupStartedAt;
1066
- const setupBody = effect_1.Effect.gen(function* () {
1067
- // 上一进程的遗留收尾先补做,再原子写入本次登记;两步都先于 setup。
1068
- if (run.teardown && run.experimentId) {
1069
- yield* recoverOrphanedTeardownRegistration(run, experimentId);
1070
- yield* (0, teardown_registry_ts_1.writeTeardownRegistrationEffect)(coordinationRoot, {
1071
- experimentId: run.experimentId,
1072
- selectedEvalIds: run.selectedEvalIds,
1073
- pid: process.pid,
1074
- host: currentHost,
1075
- startedAt: new Date().toISOString(),
1076
- }).pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.sync(() => {
1077
- const message = (0, index_ts_1.t)("runner.teardownRegistrationWriteFailed", {
1078
- experimentId,
1079
- message: error instanceof Error ? error.message : String(error),
1080
- }).trimEnd();
1081
- (0, sink_ts_1.reportDiagnostic)({
1082
- key: `teardown-registration-write-failed:${experimentId}`,
1083
- code: "teardown-registration-write-failed",
1084
- severity: "warning",
1085
- message,
1086
- data: { experimentId },
1087
- });
1088
- recordExperimentDiagnostic({
1089
- experimentId: run.experimentId,
1090
- code: "teardown-registration-write-failed",
1091
- level: "warning",
1092
- message,
1093
- phase: "experiment.setup",
1094
- });
1095
- })));
1305
+ return effect_1.Effect.uninterruptibleMask((restore) => {
1306
+ const startupRecovery = startupRecoveryCompletions.get(a.run);
1307
+ return (startupRecovery === undefined ? effect_1.Effect.void : restore(effect_1.Deferred.await(startupRecovery))).pipe(effect_1.Effect.zipRight(cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
1308
+ const current = cell.state;
1309
+ if (current._tag === "Active" || current._tag === "TearingDown")
1310
+ return current.setup;
1311
+ if (current._tag === "TornDown")
1312
+ return undefined;
1313
+ if (current._tag === "UntriggeredComplete") {
1314
+ return yield* effect_1.Effect.die(new Error("Experiment lifecycle cannot be triggered after all attempts settled."));
1096
1315
  }
1097
- if (!run.setup)
1098
- return;
1099
- setupStartedAt = Date.now();
1100
- (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "setup", status: "started" });
1101
- const ctx = makeExperimentHookContext(run, "experiment.setup");
1102
- const value = yield* effect_1.Effect.tryPromise({
1103
- try: () => Promise.resolve().then(() => run.setup(ctx)),
1104
- catch: (error) => error,
1105
- });
1106
- const returned = value;
1107
- if (typeof returned === "function") {
1108
- // tsx 旧式 setup cleanup 只在 author callback 边界适配;主错误说明迁移方向。
1109
- yield* (0, cleanup_timeout_ts_1.cleanupCallback)(returned).pipe(effect_1.Effect.catchAll(() => effect_1.Effect.void));
1110
- return yield* effect_1.Effect.fail(new Error((0, index_ts_1.t)("runner.setupReturnedCleanup", {
1111
- layer: `ExperimentDef.setup (${experimentId})`,
1112
- hint: "ExperimentDef.teardown",
1113
- }).trimEnd()));
1316
+ const run = a.run;
1317
+ const experimentId = run.experimentId ?? run.agent.name;
1318
+ if (run.sharedState && cell.sharedStateClaim === undefined) {
1319
+ cell.sharedStateClaim = yield* restore(acquireSharedStateClaim(run, experimentId));
1114
1320
  }
1115
- (0, sink_ts_1.reportExperimentHook)({
1116
- experimentId,
1117
- hook: "setup",
1118
- status: "done",
1119
- durationMs: Date.now() - (setupStartedAt ?? Date.now()),
1120
- });
1121
- });
1122
- const setupWorker = setupBody.pipe(effect_1.Effect.matchEffect({
1123
- onSuccess: () => replaceSetup(cell, { _tag: "Succeeded" }),
1124
- onFailure: (error) => effect_1.Effect.sync(() => {
1321
+ const completion = yield* effect_1.Deferred.make();
1322
+ // 先写 Active、再登记和启动 worker:随后任何 attempt 或强清都会复用同一个 Effect 完成点。
1323
+ cell.state = {
1324
+ _tag: "Active",
1325
+ pendingAttempts: current.pendingAttempts,
1326
+ setup: { _tag: "InProgress", completion },
1327
+ };
1328
+ if (run.teardown || run.sharedState) {
1329
+ yield* (0, experiment_cleanup_registry_ts_1.registerExperimentTeardown)(experimentId, () => runExperimentTeardown(run, cell));
1330
+ }
1331
+ let setupStartedAt;
1332
+ const setupBody = effect_1.Effect.gen(function* () {
1333
+ // startup gate 已补做已有遗留收尾;sharedState 时此处已经持有同 key exact authority。
1334
+ // 再核对一次可覆盖 gate 之后才出现的孤儿登记,随后原子写入本次登记;两步都先于 setup。
1335
+ if (run.teardown && run.experimentId) {
1336
+ yield* recoverOrphanedTeardownRegistration(run, experimentId);
1337
+ yield* (0, teardown_registry_ts_1.writeTeardownRegistrationEffect)(coordinationRoot, {
1338
+ experimentId: run.experimentId,
1339
+ selectedEvalIds: run.selectedEvalIds,
1340
+ pid: process.pid,
1341
+ host: currentHost,
1342
+ startedAt: new Date().toISOString(),
1343
+ }).pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.sync(() => {
1344
+ const message = (0, index_ts_1.t)("runner.teardownRegistrationWriteFailed", {
1345
+ experimentId,
1346
+ message: error instanceof Error ? error.message : String(error),
1347
+ }).trimEnd();
1348
+ (0, sink_ts_1.reportDiagnostic)({
1349
+ key: `teardown-registration-write-failed:${experimentId}`,
1350
+ code: "teardown-registration-write-failed",
1351
+ severity: "warning",
1352
+ message,
1353
+ data: { experimentId },
1354
+ });
1355
+ recordExperimentDiagnostic({
1356
+ experimentId: run.experimentId,
1357
+ code: "teardown-registration-write-failed",
1358
+ level: "warning",
1359
+ message,
1360
+ phase: "experiment.setup",
1361
+ });
1362
+ })));
1363
+ }
1364
+ if (!run.setup)
1365
+ return;
1366
+ setupStartedAt = Date.now();
1367
+ (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "setup", status: "started" });
1368
+ const ctx = makeExperimentHookContext(run, "experiment.setup");
1369
+ const value = yield* effect_1.Effect.tryPromise({
1370
+ try: () => Promise.resolve().then(() => run.setup(ctx)),
1371
+ catch: (error) => error,
1372
+ });
1373
+ const returned = value;
1374
+ if (typeof returned === "function") {
1375
+ // tsx 旧式 setup cleanup 只在 author callback 边界适配;主错误说明迁移方向。
1376
+ yield* (0, cleanup_timeout_ts_1.cleanupCallback)(returned).pipe(effect_1.Effect.catchAll(() => effect_1.Effect.void));
1377
+ return yield* effect_1.Effect.fail(new Error((0, index_ts_1.t)("runner.setupReturnedCleanup", {
1378
+ layer: `ExperimentDef.setup (${experimentId})`,
1379
+ hint: "ExperimentDef.teardown",
1380
+ }).trimEnd()));
1381
+ }
1125
1382
  (0, sink_ts_1.reportExperimentHook)({
1126
1383
  experimentId,
1127
1384
  hook: "setup",
1128
- status: "failed",
1385
+ status: "done",
1129
1386
  durationMs: Date.now() - (setupStartedAt ?? Date.now()),
1130
1387
  });
1131
- }).pipe(effect_1.Effect.zipRight(replaceSetup(cell, {
1132
- _tag: "Failed",
1133
- error: { ...(0, attempt_ts_1.errorFromThrown)(error, "experiment.setup"), code: "experiment-setup-failed" },
1134
- }))),
1135
- }), effect_1.Effect.onExit((exit) => {
1136
- const recordUnexpectedExit = effect_1.Exit.isFailure(exit)
1137
- ? replaceSetup(cell, {
1388
+ });
1389
+ const setupWorker = setupBody.pipe(effect_1.Effect.matchEffect({
1390
+ onSuccess: () => replaceSetup(cell, { _tag: "Succeeded" }),
1391
+ onFailure: (error) => effect_1.Effect.sync(() => {
1392
+ (0, sink_ts_1.reportExperimentHook)({
1393
+ experimentId,
1394
+ hook: "setup",
1395
+ status: "failed",
1396
+ durationMs: Date.now() - (setupStartedAt ?? Date.now()),
1397
+ });
1398
+ }).pipe(effect_1.Effect.zipRight(replaceSetup(cell, {
1138
1399
  _tag: "Failed",
1139
- error: {
1140
- ...(0, attempt_ts_1.errorFromThrown)(effect_1.Cause.squash(exit.cause), "experiment.setup"),
1141
- code: "experiment-setup-failed",
1142
- },
1143
- })
1144
- : effect_1.Effect.void;
1145
- return recordUnexpectedExit.pipe(effect_1.Effect.zipRight(effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
1146
- }));
1147
- yield* restore(setupWorker).pipe(effect_1.Effect.forkIn(invocationScope));
1148
- return cell.state.setup;
1149
- })).pipe(effect_1.Effect.flatMap((setup) => setup === undefined ? effect_1.Effect.void : restore(awaitSetup(setup)))));
1400
+ error: { ...(0, attempt_ts_1.errorFromThrown)(error, "experiment.setup"), code: "experiment-setup-failed" },
1401
+ }))),
1402
+ }), effect_1.Effect.onExit((exit) => {
1403
+ const recordUnexpectedExit = effect_1.Exit.isFailure(exit)
1404
+ ? replaceSetup(cell, {
1405
+ _tag: "Failed",
1406
+ error: {
1407
+ ...(0, attempt_ts_1.errorFromThrown)(effect_1.Cause.squash(exit.cause), "experiment.setup"),
1408
+ code: "experiment-setup-failed",
1409
+ },
1410
+ })
1411
+ : effect_1.Effect.void;
1412
+ return recordUnexpectedExit.pipe(effect_1.Effect.zipRight(effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
1413
+ }));
1414
+ yield* restore(setupWorker).pipe(effect_1.Effect.forkIn(invocationScope));
1415
+ return cell.state.setup;
1416
+ })).pipe(effect_1.Effect.flatMap((setup) => setup === undefined ? effect_1.Effect.void : restore(awaitSetup(setup))))));
1417
+ });
1150
1418
  };
1151
- // 自愈是「选中实验」的启动期职责,不是首个派发 attempt 的副作用:全携带使 attempts 为空时,
1152
- // 仍必须补上上一进程强杀遗留的收尾。无 teardown 的新定义无法安全补执行,交由 CLI 提醒。
1153
- const recoveredExperimentIds = new Set();
1419
+ const hasOrphanedTeardownRegistration = (run) => effect_1.Effect.suspend(() => {
1420
+ if (!run.experimentId || !run.teardown)
1421
+ return effect_1.Effect.succeed(false);
1422
+ return (0, teardown_registry_ts_1.readTeardownRegistrationsEffect)(coordinationRoot).pipe(effect_1.Effect.map((registrations) => registrations.some(({ entry }) => entry.experimentId === run.experimentId && (0, teardown_registry_ts_1.isOrphanedTeardownRegistration)(entry, currentHost))),
1423
+ // A missing or unreadable registration directory must never invent an
1424
+ // orphan cleanup obligation. The normal setup path still records a
1425
+ // diagnostic if it cannot write this Invocation's own registration.
1426
+ effect_1.Effect.catchAll(() => effect_1.Effect.succeed(false)));
1427
+ });
1428
+ /**
1429
+ * Startup self-heal remains a selected-Experiment responsibility even for a
1430
+ * full-carry / zero-Attempt Invocation. For sharedState it first obtains the
1431
+ * exact current key's authority. Thus an active or recovering generation is
1432
+ * waited on, never implicitly torn down or replaced.
1433
+ */
1434
+ const recoverStartupOrphanedTeardown = (run, experimentId) => effect_1.Effect.gen(function* () {
1435
+ if (!(yield* hasOrphanedTeardownRegistration(run)))
1436
+ return;
1437
+ if (run.sharedState === undefined) {
1438
+ yield* recoverOrphanedTeardownRegistration(run, experimentId);
1439
+ return;
1440
+ }
1441
+ const claim = yield* acquireSharedStateClaim(run, experimentId);
1442
+ yield* recoverOrphanedTeardownRegistration(run, experimentId).pipe(effect_1.Effect.ensuring(claim.release.pipe(effect_1.Effect.catchAll(() => effect_1.Effect.sync(() => {
1443
+ reportSharedStateRecoveryRequired({
1444
+ run,
1445
+ experimentId,
1446
+ reason: "startup-recovery-release-failed",
1447
+ phase: "experiment.teardown",
1448
+ });
1449
+ })))));
1450
+ });
1451
+ const startupRecoveryByExperimentId = new Map();
1154
1452
  for (const run of effectiveAgentRuns) {
1155
- if (!run.experimentId || !run.teardown || recoveredExperimentIds.has(run.experimentId))
1453
+ if (!run.experimentId || !run.teardown)
1156
1454
  continue;
1157
- recoveredExperimentIds.add(run.experimentId);
1158
- yield* recoverOrphanedTeardownRegistration(run, run.experimentId);
1455
+ let completion = startupRecoveryByExperimentId.get(run.experimentId);
1456
+ if (completion === undefined) {
1457
+ const createdCompletion = yield* effect_1.Deferred.make();
1458
+ completion = createdCompletion;
1459
+ startupRecoveryByExperimentId.set(run.experimentId, createdCompletion);
1460
+ startupRecoveryCompletionsToAwait.push(createdCompletion);
1461
+ const recovery = recoverStartupOrphanedTeardown(run, run.experimentId).pipe(effect_1.Effect.exit, effect_1.Effect.flatMap((exit) => effect_1.Deferred.done(createdCompletion, exit)), effect_1.Effect.asVoid);
1462
+ yield* recovery.pipe(effect_1.Effect.forkIn(invocationScope));
1463
+ }
1464
+ startupRecoveryCompletions.set(run, completion);
1159
1465
  }
1160
1466
  const settleExperimentAttempt = (a) => {
1161
1467
  const cell = expLifecycles.get(a.run);
@@ -1569,53 +1875,6 @@ function runEvals(opts) {
1569
1875
  if (claim !== undefined)
1570
1876
  caseClaimsAwaitingPublication.push(claim);
1571
1877
  })));
1572
- /**
1573
- * 实验闸的取位:声明了 `maxConcurrency` 的实验从**跨 Invocation 共用**的逐槽租约取名额
1574
- * (gate-lease.ts);未声明的实验不走租约、不产生任何跨进程协调。取位在全局位之前——等名额
1575
- * 的 attempt 不占别的实验的并发位。返回 undefined 表示等待期间被中断,这条 attempt 就此放弃。
1576
- */
1577
- /**
1578
- * 撞满实验闸名额时报一条 info:名额域跨 Invocation,所以「等」的对象是别的进程,用户无从
1579
- * 从本进程的面板推断。生效名额与本次声明分开报——两者不等就是 min-N 夹低了(别的运行声明了
1580
- * 更小的 maxConcurrency),这是「我明明写了 3 却只跑 1 条」的唯一解释,不说清就只能去翻锁目录。
1581
- * 按实验折叠:同一实验的一堆 attempt 会前后脚撞上同一批持有者,不该刷屏。
1582
- */
1583
- const reportGateLeaseWait = (experimentId, declaredN, holders) => {
1584
- const effectiveN = holders.reduce((n, h) => Math.min(n, h.declaredN), declaredN);
1585
- const who = [...new Set(holders.map((h) => `pid ${h.pid}@${h.host}`))].join(", ") || "another run";
1586
- (0, sink_ts_1.reportDiagnostic)({
1587
- key: `gate-lease-waiting:${experimentId}`,
1588
- code: "gate-lease-waiting",
1589
- severity: "info",
1590
- message: (0, index_ts_1.t)("runner.gateLeaseWaiting", { experimentId, effectiveN, declaredN, holders: who }).trimEnd(),
1591
- data: { experimentId, effectiveN, declaredN },
1592
- });
1593
- };
1594
- const acquireGateLease = (experimentId, maxConcurrency) => effect_1.Effect.uninterruptibleMask((restore) => restore((0, gate_lease_ts_1.acquireGateSlotEffect)(coordinationRoot, experimentId, maxConcurrency, lockIdentity, {
1595
- // 撞满名额只发生在「别的 Invocation 正占着这个实验的位子」——本进程自己的并发早被
1596
- // gateLocalSems 挡在这一层之前了。不报的话面板就只剩 `0 running · N queued` 干等,
1597
- // 看不出在等什么、等谁,也看不出生效名额被对方更小的声明夹低了(min-N)。
1598
- onWaitStart: (holders) => reportGateLeaseWait(experimentId, maxConcurrency, holders),
1599
- })).pipe(effect_1.Effect.map(({ claim, takenOver, takenOverFrom }) => {
1600
- if (takenOver) {
1601
- const message = (0, index_ts_1.t)("runner.coordinationRecovered", { experimentId }).trimEnd();
1602
- (0, sink_ts_1.reportDiagnostic)({
1603
- key: `${types_ts_1.COORDINATION_RECOVERED_CODE}:concurrency-slot:${experimentId}|${claim.slot}`,
1604
- code: types_ts_1.COORDINATION_RECOVERED_CODE,
1605
- severity: "info",
1606
- message,
1607
- data: {
1608
- experimentId,
1609
- resource: "concurrency-slot",
1610
- slot: claim.slot,
1611
- ...(takenOverFrom !== undefined
1612
- ? { previousPid: takenOverFrom.pid, previousHost: takenOverFrom.host }
1613
- : {}),
1614
- },
1615
- });
1616
- }
1617
- return claim;
1618
- })));
1619
1878
  const withGlobalSlot = (haltSignal, use) => effect_1.Effect.uninterruptibleMask((restore) => {
1620
1879
  const state = { held: false };
1621
1880
  const release = effect_1.Effect.suspend(() => state.held
@@ -1641,24 +1900,11 @@ function runEvals(opts) {
1641
1900
  ? restore(use({ release, reacquire }))
1642
1901
  : effect_1.Effect.succeed({ kind: "recheck" })), effect_1.Effect.ensuring(release));
1643
1902
  });
1644
- /**
1645
- * 实验闸(② 道许可)的持有作用域:名额与 attempt **同生命周期**——从这里持有到执行体收尾
1646
- * (teardown 链、沙箱销毁)之后才归还,turn 退避等内部等待一律不释放
1647
- * (docs/runner.md「调度:有界并发」)。撞用例锁挂起是唯一的例外:那时执行体以 `suspend`
1648
- * 正常返回,作用域退出、名额归还,挂起结束后重新取——挂起的用例不占名额,否则同实验的
1649
- * 持锁方(可能就在另一条 Invocation 里)会被自己的等待方饿死。
1650
- */
1903
+ /** Invocation 内实验闸的持有作用域。它只限制此 Invocation 的 attempt,不取得任何
1904
+ * 跨进程资源;共享外部状态必须由 `sharedState` 生命周期租约保护。 */
1651
1905
  const withExperimentGate = (a, use) => {
1652
- const { maxConcurrency, experimentId } = a.run;
1653
1906
  const localSem = gateLocalSems.get(a.run);
1654
- if (maxConcurrency === undefined || localSem === undefined)
1655
- return use;
1656
- if (experimentId === undefined)
1657
- return localSem.withPermits(1)(use);
1658
- const leased = effect_1.Effect.uninterruptibleMask((restore) => effect_1.Effect.raceFirst(restore(acquireGateLease(experimentId, maxConcurrency)).pipe(effect_1.Effect.map((claim) => ({ kind: "acquired", claim }))), restore(haltAwait(a)).pipe(effect_1.Effect.as({ kind: "halted" }))).pipe(effect_1.Effect.flatMap((lease) => lease.kind === "halted"
1659
- ? effect_1.Effect.succeed({ kind: "recheck" })
1660
- : restore(use).pipe(effect_1.Effect.ensuring(lease.claim.release.pipe(effect_1.Effect.ignore))))));
1661
- return localSem.withPermits(1)(leased);
1907
+ return localSem === undefined ? use : localSem.withPermits(1)(use);
1662
1908
  };
1663
1909
  // earlyExit:为每个 key 各建一个 AbortController。某 attempt 通过或 errored 时 abort 它,
1664
1910
  // 让并发进行中的同 key attempt 通过 signal 尽早退出,而不只是等排队的才能被跳过。
@@ -1669,9 +1915,11 @@ function runEvals(opts) {
1669
1915
  evalAbortControllers.set(a.key, new AbortController());
1670
1916
  }
1671
1917
  }
1672
- // 有界派发:forEach worker 数就是 maxConcurrency,不先给全部 Slot 建 fiber。
1673
- // worker 内仍按止损闸、实验闸、全局 permit、用例锁的顺序取得资源;退避可以占住
1674
- // 这个有界 worker,避免为了补位再造一层无界 scheduler。
1918
+ // 所有 Slot 都先成为轻量 coordination fiber。执行资源仍由实验闸、globalSem
1919
+ // provider lane 严格有界;但 sharedState / predecessor / lock 等待绝不能占住一个
1920
+ // 有限 forEach worker,否则同 key waiter 会饿死持有者的后继 Slot,形成环形等待。
1921
+ // 因而这里必须 unbounded:它只放大 suspendable fibers,不放大 Agent、Sandbox 或
1922
+ // provider 的物理并发。
1675
1923
  // runAttemptEffect 只把「执行错误」收进 EvalResult.error(不 fail),
1676
1924
  // 但中断(Ctrl+C / kill)照常向上传播 —— 所以一条挂掉不会中断其它 attempt,而中断能停掉全部。
1677
1925
  //
@@ -1732,7 +1980,8 @@ function runEvals(opts) {
1732
1980
  }
1733
1981
  const budget = a.run.budget;
1734
1982
  if (budget !== undefined) {
1735
- // 只看已完成 attempt 的实测花费(见上方 BudgetState 注释),到顶就跳过新 attempt,
1983
+ // 只看已完成 attempt 的价目表估算成本(见上方 BudgetState 注释;observed
1984
+ // usage.costUSD 不参与),到顶就跳过新 attempt,
1736
1985
  // 没到顶就立即放行——不等待、不做预测性节流。
1737
1986
  const s = budgetState(budgetKey);
1738
1987
  if (s.spent >= budget) {
@@ -1809,6 +2058,15 @@ function runEvals(opts) {
1809
2058
  : undefined;
1810
2059
  if (blockedError !== undefined)
1811
2060
  cancelReuseAttempt(a, true);
2061
+ if (buildFailure !== undefined) {
2062
+ recordExperimentDiagnostic({
2063
+ experimentId: a.run.experimentId,
2064
+ code: SHARED_BUILD_FAILURE_DIAGNOSTIC,
2065
+ level: "error",
2066
+ message: sharedBuildFailureDetail(a, buildFailure),
2067
+ phase: "sandbox.queue",
2068
+ });
2069
+ }
1812
2070
  // A scheduler-admitted Attempt owns a durable identity before any
1813
2071
  // attempt-owned sandbox, Agent, or Eval work begins. Reservation
1814
2072
  // failure is invocation-fatal at the Record publish gate.
@@ -1929,6 +2187,13 @@ function runEvals(opts) {
1929
2187
  // 到达调度器,不走错误通道向上传播——attempt fiber 的 E 保持 never,`errored`
1930
2188
  // 仍是 eval runner 的合法结果而不是调度失败(architecture.md「Effect 边界」)。
1931
2189
  onFailureClass: (declaration) => closeHaltGate(a, declaration),
2190
+ ...(a.run.sharedState !== undefined && poolSelection._tag === "Fresh"
2191
+ ? {
2192
+ onFreshSandboxCleanupFailure: (failure) => {
2193
+ recordFreshSandboxCleanupFailure(a.run, failure);
2194
+ },
2195
+ }
2196
+ : {}),
1932
2197
  onSealedEvaluation: (sealed) => recordCoordinator.noteSealedOrMarkIncomplete(recordedAttempt, sealed),
1933
2198
  });
1934
2199
  if (lease) {
@@ -2062,9 +2327,11 @@ function runEvals(opts) {
2062
2327
  // 「报告回调串行化」不一致。
2063
2328
  yield* reportMutex.withPermits(1)((0, report_ts_1.emitReporterEvent)(reporters, { type: "eval:complete", result }));
2064
2329
  });
2065
- // ③ 全局并发位 → 派发时刻试锁 → preflight → 实验级 setup → body。
2066
- // 独占串行 provider(如 local):同一 provider 名的所有 attempt 共享一把 permit=1 的锁,
2067
- // 包在全局位之外(见上面 exclusiveSemFor 的注释)。
2330
+ // ③ 全局并发位 → sharedState(如声明,先于 Eval lock)→ 派发时刻试锁 → preflight
2331
+ // 实验级 setup provider/Sandbox/body。
2332
+ // 独占串行 provider( local):同一 provider 名的所有 attempt 共享一把 permit=1 的锁。
2333
+ // sharedState 获取与 Experiment setup 都是宿主协调,不能让等待者占着该 Provider lane;
2334
+ // permit 只包真正触及 provider、Sandbox 与 Attempt body 的执行段。
2068
2335
  const exclusiveSem = exclusiveSemFor(a.plan);
2069
2336
  const arriveAtDispatchWave = dispatchWaveArrive.get(a);
2070
2337
  const dispatch = withGlobalSlot(haltAwait(a), (slot) => effect_1.Effect.gen(function* () {
@@ -2073,6 +2340,18 @@ function runEvals(opts) {
2073
2340
  // 不在上面的竞速覆盖范围里(竞速只包全局位),闸可能正是在那段时间落下的。
2074
2341
  if (checkDispatchHalt(a).halted)
2075
2342
  return { kind: "recheck" };
2343
+ // sharedState 的等待不得先占 Eval lock 或全局并发位,也不得运行 setup / 创建
2344
+ // Sandbox。拿到租约后这个 Invocation 仍继续自己在本轮已形成的 plan,不采用别的
2345
+ // Run 在等待期间发布的 attempt。
2346
+ if (a.run.sharedState) {
2347
+ const proceed = yield* preflight;
2348
+ if (!proceed)
2349
+ return { kind: "done" };
2350
+ authorizedReuseAttempts.add(a);
2351
+ yield* slot.release;
2352
+ yield* ensureExperimentSetup(a);
2353
+ yield* slot.reacquire;
2354
+ }
2076
2355
  // 派发时刻取锁:授位之后才试,非阻塞。撞上别人持有的新鲜锁就把这个位子连同实验闸
2077
2356
  // 名额一起还回去(返回 "suspend"),由外层转入 elsewhere 挂起;位子当场空出来,
2078
2357
  // 排队中的下一条没被锁的用例接手。
@@ -2080,15 +2359,17 @@ function runEvals(opts) {
2080
2359
  const outcome = yield* tryAcquireCase(caseState);
2081
2360
  if (outcome.kind === "busy")
2082
2361
  return { kind: "suspend", window: outcome.window };
2083
- if (yield* recordCoordinator.adoptLatePublishedAttempt(a)) {
2362
+ if (!a.run.sharedState && (yield* recordCoordinator.adoptLatePublishedAttempt(a))) {
2084
2363
  return { kind: "done" };
2085
2364
  }
2086
2365
  }
2087
- const proceed = yield* preflight;
2088
- if (!proceed)
2089
- return { kind: "done" };
2090
- authorizedReuseAttempts.add(a);
2091
- if (a.run.setup || a.run.teardown) {
2366
+ if (!a.run.sharedState) {
2367
+ const proceed = yield* preflight;
2368
+ if (!proceed)
2369
+ return { kind: "done" };
2370
+ authorizedReuseAttempts.add(a);
2371
+ }
2372
+ if (!a.run.sharedState && (a.run.setup || a.run.teardown)) {
2092
2373
  // 实验级 setup:第一个通过派发许可的 attempt 真正执行,其余等同一个 memoized
2093
2374
  // Effect completion(作者 setup 失败收进 lc.setupFailed,由 body 合成 errored 结果)。
2094
2375
  // 等它的时候让出全局并发位(docs/runner.md「调度:有界并发」——内部等待一律让位,
@@ -2097,10 +2378,16 @@ function runEvals(opts) {
2097
2378
  yield* ensureExperimentSetup(a);
2098
2379
  yield* slot.reacquire;
2099
2380
  }
2100
- yield* body(slot);
2101
- return { kind: "done" };
2381
+ const physicalBody = body(slot).pipe(effect_1.Effect.as({ kind: "done" }));
2382
+ if (exclusiveSem === undefined)
2383
+ return yield* physicalBody;
2384
+ // 保持原来的物理资源获取顺序:exclusive provider → 全局位 → body。先还全局位
2385
+ // 再等 provider,避免一个已完成 sharedState/setup 协调、却仍在排 exclusive lane 的
2386
+ // attempt 饿死其它 provider;拿到 lane 后才回补全局位,Sandbox 与 body 因而仍在两把
2387
+ // permit 的保护中。中断时两个作用域各自归还自己实际持有的 permit。
2388
+ yield* slot.release;
2389
+ return yield* exclusiveSem.withPermits(1)(slot.reacquire.pipe(effect_1.Effect.zipRight(physicalBody)));
2102
2390
  }));
2103
- const guarded = exclusiveSem ? exclusiveSem.withPermits(1)(dispatch) : dispatch;
2104
2391
  // 许可链的循环外壳:撞锁挂起的用例解决后从 ① 重新走一遍(实验闸名额与全局位都要
2105
2392
  // 重新取,不能拿着别人在等的名额干等)。
2106
2393
  const pipeline = effect_1.Effect.gen(function* () {
@@ -2127,8 +2414,9 @@ function runEvals(opts) {
2127
2414
  accountDispatchHalted(a, halt.gate);
2128
2415
  return;
2129
2416
  }
2130
- // ② 实验闸 → ③ 全局位 → 用例锁preflight → body
2131
- const outcome = yield* withExperimentGate(a, guarded);
2417
+ // ② Invocation 内实验闸 → ③ 全局位 → sharedState / 生命周期协调 Eval lock
2418
+ // exclusive provider permit(如声明)→ body。
2419
+ const outcome = yield* withExperimentGate(a, dispatch);
2132
2420
  if (outcome.kind === "done")
2133
2421
  return;
2134
2422
  // 许可获取被落下的闸打断:许可已随作用域归还,回到 ① 让上面的检查点记账。
@@ -2145,10 +2433,10 @@ function runEvals(opts) {
2145
2433
  const orderedPipeline = predecessor === undefined
2146
2434
  ? wavePipeline
2147
2435
  : predecessor.pipe(effect_1.Effect.flatMap(() => wavePipeline));
2148
- // 实验级 teardown 结算:每个 attempt 收尾(含被 preflight 跳过、被中断的、被用例锁
2436
+ // 实验级 teardown / sharedState lease 结算:每个 attempt 收尾(含被 preflight 跳过、被中断的、被用例锁
2149
2437
  // 被 preflight 跳过、被中断的)都从身份集合移除;最后一个触发 ExperimentDef.teardown。ensuring
2150
2438
  // 在中断路径同样执行,重复 finalizer 也不会让状态下溢。
2151
- const withExpLifecycle = !a.run.setup && !a.run.teardown
2439
+ const withExpLifecycle = !a.run.setup && !a.run.teardown && !a.run.sharedState
2152
2440
  ? orderedPipeline
2153
2441
  : orderedPipeline.pipe(effect_1.Effect.ensuring(settleExperimentAttempt(a).pipe(effect_1.Effect.orDie)));
2154
2442
  // 用例锁释放:这个 key 的全部 attempt(真实派发的与被 late-carry 跳过的)都 settle 后
@@ -2163,7 +2451,7 @@ function runEvals(opts) {
2163
2451
  return withWaveLifecycle;
2164
2452
  const releaseSuccessor = groupedReleases.get(a);
2165
2453
  return withWaveLifecycle.pipe(effect_1.Effect.ensuring(releaseSuccessor));
2166
- }, { concurrency: opts.maxConcurrency, discard: true }).pipe(
2454
+ }, { concurrency: "unbounded", discard: true }).pipe(
2167
2455
  // 中断(用户 Ctrl+C):finalizer 已在中断过程中跑完(容器已停),这里只是把它咽下,
2168
2456
  // 好让流程走到 summarize / onInvocationComplete,用已完成的 results 出一份部分汇总,而不是抛栈。
2169
2457
  // 判据是 `isInterruptedOnly` 而不是 `isInterrupted`:一条 fiber 的缺陷会连带中断兄弟 fiber,
@@ -2180,6 +2468,18 @@ function runEvals(opts) {
2180
2468
  const exit = yield* effect_1.Effect.exit(opts.signal === undefined
2181
2469
  ? dispatchEffect
2182
2470
  : effect_1.Effect.raceFirst(dispatchEffect, interruptOnAbort(opts.signal)));
2471
+ // A full-carry / zero-Attempt invocation has no dispatch fiber that could
2472
+ // await startup recovery. Seal its selected-Experiment obligation here; for
2473
+ // normal runs the per-setup gate above has already awaited the same Deferred.
2474
+ const startupRecoveryExit = yield* effect_1.Effect.exit(opts.signal === undefined
2475
+ ? effect_1.Effect.forEach(startupRecoveryCompletionsToAwait, effect_1.Deferred.await, {
2476
+ concurrency: "unbounded",
2477
+ discard: true,
2478
+ })
2479
+ : effect_1.Effect.raceFirst(effect_1.Effect.forEach(startupRecoveryCompletionsToAwait, effect_1.Deferred.await, {
2480
+ concurrency: "unbounded",
2481
+ discard: true,
2482
+ }), interruptOnAbort(opts.signal)));
2183
2483
  // provenance 在这里齐全:没有任何 attempt 依赖的 key 也照样跑完并留下自己那条记录,
2184
2484
  // 中断路径下同批构建随 signal 收束成 cancelled,不把 run.json 的 sandboxBuilds 落空。
2185
2485
  if (runningBuilds !== undefined) {
@@ -2261,6 +2561,14 @@ function runEvals(opts) {
2261
2561
  return yield* effect_1.Effect.failCause(exit.cause).pipe(effect_1.Effect.ensuring(sweepExperimentTeardowns().pipe(effect_1.Effect.orDie)));
2262
2562
  }
2263
2563
  }
2564
+ if (effect_1.Exit.isFailure(startupRecoveryExit)) {
2565
+ if (effect_1.Cause.isInterruptedOnly(startupRecoveryExit.cause)) {
2566
+ interrupted = true;
2567
+ }
2568
+ else {
2569
+ return yield* effect_1.Effect.failCause(startupRecoveryExit.cause).pipe(effect_1.Effect.ensuring(sweepExperimentTeardowns().pipe(effect_1.Effect.orDie)));
2570
+ }
2571
+ }
2264
2572
  if (interrupted)
2265
2573
  (0, sink_ts_1.reportInterrupted)();
2266
2574
  yield* sweepExperimentTeardowns();