niceeval 0.13.2-canary.35 → 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 (485) hide show
  1. package/INDEX.md +24 -2
  2. package/dist/agents/bub-install-spec.cjs +19 -5
  3. package/dist/agents/bub-install-spec.cjs.map +1 -1
  4. package/dist/agents/bub-install-spec.d.cts +9 -2
  5. package/dist/agents/bub-install-spec.d.mts +9 -2
  6. package/dist/agents/bub-install-spec.d.ts +9 -2
  7. package/dist/agents/bub-install-spec.js +14 -12
  8. package/dist/agents/bub.cjs +10 -6
  9. package/dist/agents/bub.cjs.map +1 -1
  10. package/dist/agents/bub.d.cts +2 -2
  11. package/dist/agents/bub.d.mts +2 -2
  12. package/dist/agents/bub.d.ts +2 -2
  13. package/dist/agents/claude-code.cjs.map +1 -1
  14. package/dist/agents/claude-code.d.cts +2 -2
  15. package/dist/agents/claude-code.d.mts +2 -2
  16. package/dist/agents/claude-code.d.ts +2 -2
  17. package/dist/agents/codex.cjs +4 -4
  18. package/dist/agents/codex.cjs.map +1 -1
  19. package/dist/agents/codex.d.cts +3 -3
  20. package/dist/agents/codex.d.mts +3 -3
  21. package/dist/agents/codex.d.ts +3 -3
  22. package/dist/agents/deepseek-harness.cjs +50 -2
  23. package/dist/agents/deepseek-harness.cjs.map +1 -1
  24. package/dist/agents/deepseek-harness.d.cts +2 -0
  25. package/dist/agents/deepseek-harness.d.mts +2 -0
  26. package/dist/agents/deepseek-harness.d.ts +2 -0
  27. package/dist/agents/exact-npm-plugins.cjs +28 -0
  28. package/dist/agents/exact-npm-plugins.cjs.map +1 -0
  29. package/dist/agents/exact-npm-plugins.d.cts +10 -0
  30. package/dist/agents/exact-npm-plugins.d.mts +10 -0
  31. package/dist/agents/exact-npm-plugins.d.ts +10 -0
  32. package/dist/agents/exact-npm-plugins.js +8 -0
  33. package/dist/agents/exact-npm-plugins.js.map +1 -0
  34. package/dist/agents/openclaw.cjs +51 -2
  35. package/dist/agents/openclaw.cjs.map +1 -1
  36. package/dist/agents/openclaw.d.cts +2 -0
  37. package/dist/agents/openclaw.d.mts +2 -0
  38. package/dist/agents/openclaw.d.ts +2 -0
  39. package/dist/agents/types.cjs.map +1 -1
  40. package/dist/agents/types.d.cts +2 -2
  41. package/dist/agents/types.d.mts +2 -2
  42. package/dist/agents/types.d.ts +2 -2
  43. package/dist/agents/ui-message-stream.cjs +24 -7
  44. package/dist/agents/ui-message-stream.cjs.map +1 -1
  45. package/dist/analysis/api.cjs +19 -1
  46. package/dist/analysis/api.cjs.map +1 -1
  47. package/dist/analysis/api.d.cts +2 -2
  48. package/dist/analysis/api.d.mts +2 -2
  49. package/dist/analysis/api.d.ts +2 -2
  50. package/dist/analysis/bindings.cjs +11 -18
  51. package/dist/analysis/bindings.cjs.map +1 -1
  52. package/dist/analysis/bindings.d.cts +177 -49
  53. package/dist/analysis/bindings.d.mts +177 -49
  54. package/dist/analysis/bindings.d.ts +177 -49
  55. package/dist/analysis/contracts.cjs +1 -0
  56. package/dist/analysis/contracts.cjs.map +1 -1
  57. package/dist/analysis/contracts.d.cts +43 -3
  58. package/dist/analysis/contracts.d.mts +43 -3
  59. package/dist/analysis/contracts.d.ts +43 -3
  60. package/dist/analysis/cost-projection.cjs +2 -1
  61. package/dist/analysis/cost-projection.cjs.map +1 -1
  62. package/dist/analysis/cost-projection.d.cts +1 -1
  63. package/dist/analysis/cost-projection.d.mts +1 -1
  64. package/dist/analysis/cost-projection.d.ts +1 -1
  65. package/dist/analysis/cost.cjs +13 -3
  66. package/dist/analysis/cost.cjs.map +1 -1
  67. package/dist/analysis/cost.d.cts +10 -3
  68. package/dist/analysis/cost.d.mts +10 -3
  69. package/dist/analysis/cost.d.ts +10 -3
  70. package/dist/analysis/experiment-groups.cjs +96 -0
  71. package/dist/analysis/experiment-groups.cjs.map +1 -0
  72. package/dist/analysis/experiment-groups.d.cts +9 -0
  73. package/dist/analysis/experiment-groups.d.mts +9 -0
  74. package/dist/analysis/experiment-groups.d.ts +9 -0
  75. package/dist/analysis/experiment-groups.js +10 -0
  76. package/dist/analysis/experiment-groups.js.map +1 -0
  77. package/dist/analysis/index.cjs +4 -1
  78. package/dist/analysis/index.cjs.map +1 -1
  79. package/dist/analysis/index.d.cts +4 -2
  80. package/dist/analysis/index.d.mts +4 -2
  81. package/dist/analysis/index.d.ts +4 -2
  82. package/dist/analysis/index.js +40 -36
  83. package/dist/analysis/index.mjs +40 -36
  84. package/dist/assertions/limits.cjs +4 -0
  85. package/dist/assertions/limits.cjs.map +1 -1
  86. package/dist/assertions/limits.d.cts +4 -0
  87. package/dist/assertions/limits.d.mts +4 -0
  88. package/dist/assertions/limits.d.ts +4 -0
  89. package/dist/assertions/match.cjs +63 -25
  90. package/dist/assertions/match.cjs.map +1 -1
  91. package/dist/assertions/match.d.cts +7 -0
  92. package/dist/assertions/match.d.mts +7 -0
  93. package/dist/assertions/match.d.ts +7 -0
  94. package/dist/assertions/record/attachment.cjs +32 -4
  95. package/dist/assertions/record/attachment.cjs.map +1 -1
  96. package/dist/assertions/record/diff.cjs +10 -4
  97. package/dist/assertions/record/diff.cjs.map +1 -1
  98. package/dist/assertions/record/producer.cjs +101 -4
  99. package/dist/assertions/record/producer.cjs.map +1 -1
  100. package/dist/assertions/record/producer.d.cts +4 -1
  101. package/dist/assertions/record/producer.d.mts +4 -1
  102. package/dist/assertions/record/producer.d.ts +4 -1
  103. package/dist/assertions/runtime.cjs +45 -5
  104. package/dist/assertions/runtime.cjs.map +1 -1
  105. package/dist/cli/record.cjs +113 -8
  106. package/dist/cli/record.cjs.map +1 -1
  107. package/dist/cli/record.d.cts +20 -1
  108. package/dist/cli/record.d.mts +20 -1
  109. package/dist/cli/record.d.ts +20 -1
  110. package/dist/cli/record.js +4 -2
  111. package/dist/cli.cjs +386 -31
  112. package/dist/cli.cjs.map +1 -1
  113. package/dist/cli.d.cts +10 -2
  114. package/dist/cli.d.mts +10 -2
  115. package/dist/cli.d.ts +10 -2
  116. package/dist/context/send-retry.cjs +4 -4
  117. package/dist/context/send-retry.cjs.map +1 -1
  118. package/dist/coordination/host/runtime.cjs +8 -30
  119. package/dist/coordination/host/runtime.cjs.map +1 -1
  120. package/dist/coordination/host/runtime.d.cts +3 -3
  121. package/dist/coordination/host/runtime.d.mts +3 -3
  122. package/dist/coordination/host/runtime.d.ts +3 -3
  123. package/dist/coordination/host/types.cjs.map +1 -1
  124. package/dist/coordination/host/types.d.cts +0 -5
  125. package/dist/coordination/host/types.d.mts +0 -5
  126. package/dist/coordination/host/types.d.ts +0 -5
  127. package/dist/coordination/platform/node.cjs +90 -9
  128. package/dist/coordination/platform/node.cjs.map +1 -1
  129. package/dist/define.cjs +25 -1
  130. package/dist/define.cjs.map +1 -1
  131. package/dist/experiment/host/index.cjs +1 -0
  132. package/dist/experiment/host/index.cjs.map +1 -1
  133. package/dist/i18n/en.cjs +19 -6
  134. package/dist/i18n/en.cjs.map +1 -1
  135. package/dist/i18n/en.d.cts +12 -4
  136. package/dist/i18n/en.d.mts +12 -4
  137. package/dist/i18n/en.d.ts +12 -4
  138. package/dist/index.cjs.map +1 -1
  139. package/dist/index.d.cts +1 -1
  140. package/dist/index.d.mts +1 -1
  141. package/dist/index.d.ts +1 -1
  142. package/dist/o11y/prices.json +1 -1
  143. package/dist/o11y/record/families.cjs +12 -3
  144. package/dist/o11y/record/families.cjs.map +1 -1
  145. package/dist/o11y/record/families.d.cts +8 -8
  146. package/dist/o11y/record/families.d.mts +8 -8
  147. package/dist/o11y/record/families.d.ts +8 -8
  148. package/dist/o11y/record/family-projectors.d.cts +66 -18
  149. package/dist/o11y/record/family-projectors.d.mts +66 -18
  150. package/dist/o11y/record/family-projectors.d.ts +66 -18
  151. package/dist/o11y/record/family-writers.d.cts +11 -3
  152. package/dist/o11y/record/family-writers.d.mts +11 -3
  153. package/dist/o11y/record/family-writers.d.ts +11 -3
  154. package/dist/o11y/record/runner-producer.cjs +14 -4
  155. package/dist/o11y/record/runner-producer.cjs.map +1 -1
  156. package/dist/record/codec/core.d.cts +2 -2
  157. package/dist/record/codec/core.d.mts +2 -2
  158. package/dist/record/codec/core.d.ts +2 -2
  159. package/dist/record/family/catalog.d.cts +44 -12
  160. package/dist/record/family/catalog.d.mts +44 -12
  161. package/dist/record/family/catalog.d.ts +44 -12
  162. package/dist/record/family/index.js +128 -124
  163. package/dist/record/family/observability-v1.cjs +35 -0
  164. package/dist/record/family/observability-v1.cjs.map +1 -0
  165. package/dist/record/family/observability-v1.d.cts +3 -0
  166. package/dist/record/family/observability-v1.d.mts +3 -0
  167. package/dist/record/family/observability-v1.d.ts +3 -0
  168. package/dist/record/family/observability-v1.js +6 -0
  169. package/dist/record/family/observability-v1.js.map +1 -0
  170. package/dist/record/family/observability.cjs +58 -9
  171. package/dist/record/family/observability.cjs.map +1 -1
  172. package/dist/record/family/observability.d.cts +1014 -15
  173. package/dist/record/family/observability.d.mts +1014 -15
  174. package/dist/record/family/observability.d.ts +1014 -15
  175. package/dist/record/family/observability.js +22 -18
  176. package/dist/record/host/runtime.cjs +600 -31
  177. package/dist/record/host/runtime.cjs.map +1 -1
  178. package/dist/record/host/types.cjs.map +1 -1
  179. package/dist/record/host/types.d.cts +33 -9
  180. package/dist/record/host/types.d.mts +33 -9
  181. package/dist/record/host/types.d.ts +33 -9
  182. package/dist/record/maintenance/runtime.cjs +1 -2
  183. package/dist/record/maintenance/runtime.cjs.map +1 -1
  184. package/dist/record/model/core.cjs.map +1 -1
  185. package/dist/record/model/core.d.cts +1 -1
  186. package/dist/record/model/core.d.mts +1 -1
  187. package/dist/record/model/core.d.ts +1 -1
  188. package/dist/record/model/definition.d.cts +6 -6
  189. package/dist/record/model/definition.d.mts +6 -6
  190. package/dist/record/model/definition.d.ts +6 -6
  191. package/dist/record/model/identifiers.cjs +1 -1
  192. package/dist/record/model/identifiers.cjs.map +1 -1
  193. package/dist/record/model/identifiers.d.cts +1 -1
  194. package/dist/record/model/identifiers.d.mts +1 -1
  195. package/dist/record/model/identifiers.d.ts +1 -1
  196. package/dist/record/platform/node.cjs +159 -23
  197. package/dist/record/platform/node.cjs.map +1 -1
  198. package/dist/record/platform/services.cjs.map +1 -1
  199. package/dist/record/platform/services.d.cts +15 -2
  200. package/dist/record/platform/services.d.mts +15 -2
  201. package/dist/record/platform/services.d.ts +15 -2
  202. package/dist/record/reader/errors.cjs +37 -1
  203. package/dist/record/reader/errors.cjs.map +1 -1
  204. package/dist/record/reader/errors.d.cts +49 -1
  205. package/dist/record/reader/errors.d.mts +49 -1
  206. package/dist/record/reader/errors.d.ts +49 -1
  207. package/dist/record/reader/errors.js +22 -12
  208. package/dist/record/reader/format.cjs +76 -17
  209. package/dist/record/reader/format.cjs.map +1 -1
  210. package/dist/record/reader/format.d.cts +11 -4
  211. package/dist/record/reader/format.d.mts +11 -4
  212. package/dist/record/reader/format.d.ts +11 -4
  213. package/dist/record/reader/format.js +2 -0
  214. package/dist/record/reader/index.js +32 -18
  215. package/dist/record/reader/runtime.cjs +64 -0
  216. package/dist/record/reader/runtime.cjs.map +1 -1
  217. package/dist/record/reader/runtime.d.cts +15 -0
  218. package/dist/record/reader/runtime.d.mts +15 -0
  219. package/dist/record/reader/runtime.d.ts +15 -0
  220. package/dist/record/reader/runtime.js +2 -0
  221. package/dist/report/assets/styles.css +53 -29
  222. package/dist/report/built-in/analysis-values.cjs +0 -8
  223. package/dist/report/built-in/analysis-values.cjs.map +1 -1
  224. package/dist/report/built-in/analysis-values.d.cts +0 -13
  225. package/dist/report/built-in/analysis-values.d.mts +0 -13
  226. package/dist/report/built-in/analysis-values.d.ts +0 -13
  227. package/dist/report/built-in/analysis-values.js +4 -6
  228. package/dist/report/built-in/index.cjs +1 -3
  229. package/dist/report/built-in/index.cjs.map +1 -1
  230. package/dist/report/built-in/index.d.cts +1 -1
  231. package/dist/report/built-in/index.d.mts +1 -1
  232. package/dist/report/built-in/index.d.ts +1 -1
  233. package/dist/report/built-in/index.js +4 -8
  234. package/dist/report/built-in/index.mjs +4 -8
  235. package/dist/report/built-in/machine.cjs +0 -2
  236. package/dist/report/built-in/machine.cjs.map +1 -1
  237. package/dist/report/built-in/machine.d.cts +0 -2
  238. package/dist/report/built-in/machine.d.mts +0 -2
  239. package/dist/report/built-in/machine.d.ts +0 -2
  240. package/dist/report/built-in/result-components.cjs +25 -73
  241. package/dist/report/built-in/result-components.cjs.map +1 -1
  242. package/dist/report/built-in/result-components.d.cts +3 -19
  243. package/dist/report/built-in/result-components.d.mts +3 -19
  244. package/dist/report/built-in/result-components.d.ts +3 -19
  245. package/dist/report/built-in/result-components.js +0 -4
  246. package/dist/report/built-in/run-membership-overview.cjs +30 -17
  247. package/dist/report/built-in/run-membership-overview.cjs.map +1 -1
  248. package/dist/report/built-in/run-membership-overview.d.cts +14 -0
  249. package/dist/report/built-in/run-membership-overview.d.mts +14 -0
  250. package/dist/report/built-in/run-membership-overview.d.ts +14 -0
  251. package/dist/report/built-in/run-membership-overview.js +4 -2
  252. package/dist/report/built-in/standard.cjs +80 -46
  253. package/dist/report/built-in/standard.cjs.map +1 -1
  254. package/dist/report/built-in/standard.d.cts +19 -42
  255. package/dist/report/built-in/standard.d.mts +19 -42
  256. package/dist/report/built-in/standard.d.ts +19 -42
  257. package/dist/report/built-in/standard.js +6 -6
  258. package/dist/report/components/attempt-detail/compute.cjs +78 -2
  259. package/dist/report/components/attempt-detail/compute.cjs.map +1 -1
  260. package/dist/report/components/attempt-detail/compute.d.cts +1 -1
  261. package/dist/report/components/attempt-detail/compute.d.mts +1 -1
  262. package/dist/report/components/attempt-detail/compute.d.ts +1 -1
  263. package/dist/report/components/attempt-detail/content.cjs +1 -1
  264. package/dist/report/components/attempt-detail/content.cjs.map +1 -1
  265. package/dist/report/components/entity-lists/compute.cjs +2 -2
  266. package/dist/report/components/entity-lists/compute.cjs.map +1 -1
  267. package/dist/report/components/entity-lists/compute.d.cts +2 -2
  268. package/dist/report/components/entity-lists/compute.d.mts +2 -2
  269. package/dist/report/components/entity-lists/compute.d.ts +2 -2
  270. package/dist/report/components/entity-lists/index.cjs +6 -2
  271. package/dist/report/components/entity-lists/index.cjs.map +1 -1
  272. package/dist/report/components/entity-lists/index.d.cts +3 -6
  273. package/dist/report/components/entity-lists/index.d.mts +3 -6
  274. package/dist/report/components/entity-lists/index.d.ts +3 -6
  275. package/dist/report/components/summaries/compute.cjs +8 -1
  276. package/dist/report/components/summaries/compute.cjs.map +1 -1
  277. package/dist/report/components/summaries/compute.d.cts +9 -1
  278. package/dist/report/components/summaries/compute.d.mts +9 -1
  279. package/dist/report/components/summaries/compute.d.ts +9 -1
  280. package/dist/report/components/summaries/index.cjs +48 -4
  281. package/dist/report/components/summaries/index.cjs.map +1 -1
  282. package/dist/report/components/summaries/index.d.cts +7 -4
  283. package/dist/report/components/summaries/index.d.mts +7 -4
  284. package/dist/report/components/summaries/index.d.ts +7 -4
  285. package/dist/report/definition/primitives/chart.cjs +8 -5
  286. package/dist/report/definition/primitives/chart.cjs.map +1 -1
  287. package/dist/report/definition/primitives/points-dataset.cjs +1 -0
  288. package/dist/report/definition/primitives/points-dataset.cjs.map +1 -1
  289. package/dist/report/definition/primitives.cjs +6 -0
  290. package/dist/report/definition/primitives.cjs.map +1 -1
  291. package/dist/report/definition/report.cjs +27 -3
  292. package/dist/report/definition/report.cjs.map +1 -1
  293. package/dist/report/definition/report.d.cts +8 -0
  294. package/dist/report/definition/report.d.mts +8 -0
  295. package/dist/report/definition/report.d.ts +8 -0
  296. package/dist/report/execution/machine.cjs +30 -14
  297. package/dist/report/execution/machine.cjs.map +1 -1
  298. package/dist/report/execution/machine.d.cts +20 -10
  299. package/dist/report/execution/machine.d.mts +20 -10
  300. package/dist/report/execution/machine.d.ts +20 -10
  301. package/dist/report/execution/machine.js +6 -6
  302. package/dist/report/host/execute.cjs +25 -16
  303. package/dist/report/host/execute.cjs.map +1 -1
  304. package/dist/report/host/execute.d.cts +4 -0
  305. package/dist/report/host/execute.d.mts +4 -0
  306. package/dist/report/host/execute.d.ts +4 -0
  307. package/dist/report/host/from-record.cjs +22 -5
  308. package/dist/report/host/from-record.cjs.map +1 -1
  309. package/dist/report/host/machine.cjs +53 -11
  310. package/dist/report/host/machine.cjs.map +1 -1
  311. package/dist/report/host/site-runtime.cjs +9 -8
  312. package/dist/report/host/site-runtime.cjs.map +1 -1
  313. package/dist/report/host/site-runtime.d.cts +1 -1
  314. package/dist/report/host/site-runtime.d.mts +1 -1
  315. package/dist/report/host/site-runtime.d.ts +1 -1
  316. package/dist/report/host/static.cjs +56 -9
  317. package/dist/report/host/static.cjs.map +1 -1
  318. package/dist/report/index.cjs.map +1 -1
  319. package/dist/report/index.d.cts +1 -1
  320. package/dist/report/index.d.mts +1 -1
  321. package/dist/report/index.d.ts +1 -1
  322. package/dist/report/model/dataset.cjs +7 -2
  323. package/dist/report/model/dataset.cjs.map +1 -1
  324. package/dist/report/model/locale.cjs +2 -0
  325. package/dist/report/model/locale.cjs.map +1 -1
  326. package/dist/report/model/locale.d.cts +1 -0
  327. package/dist/report/model/locale.d.mts +1 -0
  328. package/dist/report/model/locale.d.ts +1 -0
  329. package/dist/report/model/metrics.cjs +3 -1
  330. package/dist/report/model/metrics.cjs.map +1 -1
  331. package/dist/report/model/pricing.cjs +16 -11
  332. package/dist/report/model/pricing.cjs.map +1 -1
  333. package/dist/report/react/styles.css +53 -29
  334. package/dist/runner/adoption.cjs +1 -0
  335. package/dist/runner/adoption.cjs.map +1 -1
  336. package/dist/runner/attempt.cjs +44 -5
  337. package/dist/runner/attempt.cjs.map +1 -1
  338. package/dist/runner/attempt.d.cts +13 -1
  339. package/dist/runner/attempt.d.mts +13 -1
  340. package/dist/runner/attempt.d.ts +13 -1
  341. package/dist/runner/config-identity.cjs +10 -2
  342. package/dist/runner/config-identity.cjs.map +1 -1
  343. package/dist/runner/config-identity.d.cts +4 -2
  344. package/dist/runner/config-identity.d.mts +4 -2
  345. package/dist/runner/config-identity.d.ts +4 -2
  346. package/dist/runner/feedback/human.cjs +139 -64
  347. package/dist/runner/feedback/human.cjs.map +1 -1
  348. package/dist/runner/feedback/json.cjs +14 -6
  349. package/dist/runner/feedback/json.cjs.map +1 -1
  350. package/dist/runner/feedback/json.d.cts +3 -2
  351. package/dist/runner/feedback/json.d.mts +3 -2
  352. package/dist/runner/feedback/json.d.ts +3 -2
  353. package/dist/runner/feedback/reducer.cjs +2 -0
  354. package/dist/runner/feedback/reducer.cjs.map +1 -1
  355. package/dist/runner/judge-config.cjs +1 -1
  356. package/dist/runner/judge-config.cjs.map +1 -1
  357. package/dist/runner/ledger.cjs +124 -12
  358. package/dist/runner/ledger.cjs.map +1 -1
  359. package/dist/runner/project-current.cjs +13 -5
  360. package/dist/runner/project-current.cjs.map +1 -1
  361. package/dist/runner/run.cjs +521 -256
  362. package/dist/runner/run.cjs.map +1 -1
  363. package/dist/runner/sandbox-pool.cjs +161 -30
  364. package/dist/runner/sandbox-pool.cjs.map +1 -1
  365. package/dist/runner/sandbox-pool.d.cts +34 -2
  366. package/dist/runner/sandbox-pool.d.mts +34 -2
  367. package/dist/runner/sandbox-pool.d.ts +34 -2
  368. package/dist/runner/shared-state-lease.cjs +803 -0
  369. package/dist/runner/shared-state-lease.cjs.map +1 -0
  370. package/dist/runner/shared-state-lease.d.cts +121 -0
  371. package/dist/runner/shared-state-lease.d.mts +121 -0
  372. package/dist/runner/shared-state-lease.d.ts +121 -0
  373. package/dist/runner/shared-state-lease.js +24 -0
  374. package/dist/runner/shared-state-lease.js.map +1 -0
  375. package/dist/runner/teardown-registry.cjs +97 -34
  376. package/dist/runner/teardown-registry.cjs.map +1 -1
  377. package/dist/runner/teardown-registry.d.cts +27 -1
  378. package/dist/runner/teardown-registry.d.mts +27 -1
  379. package/dist/runner/teardown-registry.d.ts +27 -1
  380. package/dist/runner/teardown-registry.js +20 -16
  381. package/dist/runner/types.cjs.map +1 -1
  382. package/dist/runner/types.d.cts +31 -5
  383. package/dist/runner/types.d.mts +31 -5
  384. package/dist/runner/types.d.ts +31 -5
  385. package/dist/sample/analysis.cjs +249 -459
  386. package/dist/sample/analysis.cjs.map +1 -1
  387. package/dist/sample/capability.cjs +46 -8
  388. package/dist/sample/capability.cjs.map +1 -1
  389. package/dist/sandbox/docker-profile/schema.cjs +283 -534
  390. package/dist/sandbox/docker-profile/schema.cjs.map +1 -1
  391. package/dist/sandbox/docker-profile/schema.d.cts +10 -27
  392. package/dist/sandbox/docker-profile/schema.d.mts +10 -27
  393. package/dist/sandbox/docker-profile/schema.d.ts +10 -27
  394. package/dist/sandbox/e2b-agent-template.cjs +5 -2
  395. package/dist/sandbox/e2b-agent-template.cjs.map +1 -1
  396. package/dist/sandbox/keep-registry.cjs +28 -66
  397. package/dist/sandbox/keep-registry.cjs.map +1 -1
  398. package/dist/sandbox/types.cjs.map +1 -1
  399. package/dist/sandbox/types.d.cts +1 -1
  400. package/dist/sandbox/types.d.mts +1 -1
  401. package/dist/sandbox/types.d.ts +1 -1
  402. package/dist/shared/aggregate.cjs +16 -6
  403. package/dist/shared/aggregate.cjs.map +1 -1
  404. package/dist/shared/aggregate.d.cts +12 -4
  405. package/dist/shared/aggregate.d.mts +12 -4
  406. package/dist/shared/aggregate.d.ts +12 -4
  407. package/dist/shared/turn-label.cjs +20 -2
  408. package/dist/shared/turn-label.cjs.map +1 -1
  409. package/dist/shared/turn-label.d.cts +2 -0
  410. package/dist/shared/turn-label.d.mts +2 -0
  411. package/dist/shared/turn-label.d.ts +2 -0
  412. package/dist/shared/turn-label.js +4 -2
  413. package/dist/vendor/remark/remark-gfm.cjs +7 -7
  414. package/dist/vendor/remark/remark-gfm.cjs.map +1 -1
  415. package/dist/vendor/remark/remark-parse.cjs +12 -12
  416. package/dist/vendor/remark/remark-parse.cjs.map +1 -1
  417. package/dist/vendor/remark/remark.cjs +13 -13
  418. package/dist/vendor/remark/remark.cjs.map +1 -1
  419. package/dist/view/server.cjs +6 -0
  420. package/dist/view/server.cjs.map +1 -1
  421. package/docs-site/zh/examples/ai-agent-application.mdx +1 -1
  422. package/docs-site/zh/examples/coding-agent-extensions.mdx +1 -1
  423. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +1 -0
  424. package/docs-site/zh/examples/integrations/langgraph.mdx +1 -1
  425. package/docs-site/zh/explanation/assert.mdx +6 -6
  426. package/docs-site/zh/explanation/drive.mdx +1 -1
  427. package/docs-site/zh/explanation/evals.mdx +9 -9
  428. package/docs-site/zh/explanation/experiment.mdx +2 -2
  429. package/docs-site/zh/explanation/hitl.mdx +3 -3
  430. package/docs-site/zh/explanation/judge.mdx +5 -5
  431. package/docs-site/zh/explanation/overview.mdx +3 -3
  432. package/docs-site/zh/explanation/runner.mdx +1 -1
  433. package/docs-site/zh/index.mdx +3 -3
  434. package/docs-site/zh/reference/builtin-agents.mdx +9 -9
  435. package/docs-site/zh/reference/cli.mdx +18 -12
  436. package/docs-site/zh/reference/define-agent.mdx +130 -5
  437. package/docs-site/zh/reference/define-config.mdx +2 -2
  438. package/docs-site/zh/reference/define-eval.mdx +4 -4
  439. package/docs-site/zh/reference/events.mdx +1 -1
  440. package/docs-site/zh/reference/official-adapters.mdx +5 -5
  441. package/docs-site/zh/troubleshooting/debugging.mdx +1 -1
  442. package/docs-site/zh/troubleshooting/recover-after-kill.mdx +37 -5
  443. package/docs-site/zh/tutorials/accept-results.mdx +72 -0
  444. package/docs-site/zh/tutorials/agent-onboarding.mdx +6 -6
  445. package/docs-site/zh/tutorials/authoring.mdx +9 -13
  446. package/docs-site/zh/tutorials/ci-integration.mdx +6 -6
  447. package/docs-site/zh/tutorials/compare-runs.mdx +61 -0
  448. package/docs-site/zh/tutorials/concurrency.mdx +13 -12
  449. package/docs-site/zh/tutorials/configuration.mdx +4 -6
  450. package/docs-site/zh/tutorials/connect-your-agent.mdx +1 -1
  451. package/docs-site/zh/tutorials/control-run-cost.mdx +58 -0
  452. package/docs-site/zh/tutorials/criteria-files.mdx +2 -2
  453. package/docs-site/zh/tutorials/custom-reports.mdx +2 -2
  454. package/docs-site/zh/tutorials/debug-lifecycle-plan.mdx +46 -0
  455. package/docs-site/zh/tutorials/docker-in-docker.mdx +50 -44
  456. package/docs-site/zh/tutorials/eval-groups.mdx +6 -6
  457. package/docs-site/zh/tutorials/evaluation-kinds.mdx +3 -3
  458. package/docs-site/zh/tutorials/experiments.mdx +2 -3
  459. package/docs-site/zh/tutorials/handle-execution-failures.mdx +18 -10
  460. package/docs-site/zh/tutorials/install-custom-sandbox-agent.mdx +16 -8
  461. package/docs-site/zh/tutorials/plugins.mdx +14 -0
  462. package/docs-site/zh/tutorials/rerun-and-cache.mdx +3 -17
  463. package/docs-site/zh/tutorials/sandbox-agent.mdx +17 -11
  464. package/docs-site/zh/tutorials/sandbox-providers.mdx +17 -13
  465. package/docs-site/zh/tutorials/sandbox-reuse.mdx +12 -19
  466. package/docs-site/zh/tutorials/select-and-preview.mdx +65 -0
  467. package/docs-site/zh/tutorials/verify-judge.mdx +78 -0
  468. package/docs-site/zh/tutorials/viewing-results.mdx +27 -8
  469. package/docs-site/zh/tutorials/write-experiment.mdx +7 -5
  470. package/docs-site/zh/tutorials/write-send.mdx +2 -2
  471. package/package.json +9 -5
  472. package/dist/report/built-in/overview.cjs +0 -45
  473. package/dist/report/built-in/overview.cjs.map +0 -1
  474. package/dist/report/built-in/overview.d.cts +0 -6
  475. package/dist/report/built-in/overview.d.mts +0 -6
  476. package/dist/report/built-in/overview.d.ts +0 -6
  477. package/dist/report/built-in/overview.js +0 -9
  478. package/dist/report/built-in/overview.js.map +0 -1
  479. package/dist/runner/gate-lease.cjs +0 -283
  480. package/dist/runner/gate-lease.cjs.map +0 -1
  481. package/dist/runner/gate-lease.d.cts +0 -62
  482. package/dist/runner/gate-lease.d.mts +0 -62
  483. package/dist/runner/gate-lease.d.ts +0 -62
  484. package/dist/runner/gate-lease.js +0 -22
  485. 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");
@@ -244,7 +244,7 @@ function runEvals(opts) {
244
244
  return failure === undefined ? [] : [failure];
245
245
  }));
246
246
  if (opts.onCurrentRecordReusePlan !== undefined) {
247
- yield* opts.onCurrentRecordReusePlan({ reused: reusedAttempts.length, reusedFailures });
247
+ yield* opts.onCurrentRecordReusePlan({ reused: reusedAttempts.length, reusedFailures, runIds });
248
248
  }
249
249
  else {
250
250
  // Direct library callers have no feedback coordinator. Preserve their
@@ -429,6 +429,11 @@ function runEvals(opts) {
429
429
  const judgePrecheckFailures = new Map();
430
430
  {
431
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
+ }
432
437
  const { targets, evalKeys } = judgeProbePlan(uniquePairs.map(([id, a]) => ({
433
438
  id,
434
439
  judge: a.judge,
@@ -444,13 +449,35 @@ function runEvals(opts) {
444
449
  const failedByKey = new Map();
445
450
  for (const target of targets) {
446
451
  const err = yield* (0, judge_ts_1.probeJudgeEffect)(target.judge, opts.signal);
447
- if (err)
452
+ if (err) {
448
453
  failedByKey.set(target.key, err);
454
+ }
449
455
  }
450
456
  for (const [pairKey, key] of evalKeys) {
451
457
  const err = failedByKey.get(key);
452
- if (err !== undefined)
453
- 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
+ });
454
481
  }
455
482
  (0, sink_ts_1.reportPrecheck)({
456
483
  status: failedByKey.size > 0 ? "failed" : "done",
@@ -648,6 +675,8 @@ function runEvals(opts) {
648
675
  const sandboxSem = yield* effect_1.Effect.makeSemaphore(opts.maxConcurrency);
649
676
  // 相同 provider physical identity 共享一个按需池;不从 AgentRun 重选 template。
650
677
  const reusePools = new Map();
678
+ /** A terminal Experiment freezes its own registry before physical stop. */
679
+ const frozenReusePoolRuns = new Set();
651
680
  // A physical Sandbox number is Run-wide, not pool-local: distinct Eval
652
681
  // Groups may materialize identical provider plans concurrently and must not
653
682
  // both report `reuseSandbox: 1` for different instances.
@@ -684,6 +713,9 @@ function runEvals(opts) {
684
713
  if (key === undefined || a.plan._tag !== "Sandbox") {
685
714
  return effect_1.Effect.succeed({ _tag: "Fresh" });
686
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
+ }
687
719
  let bySpec = reusePools.get(a.run);
688
720
  if (bySpec === undefined) {
689
721
  bySpec = new Map();
@@ -724,20 +756,31 @@ function runEvals(opts) {
724
756
  }
725
757
  return effect_1.Effect.succeed({ _tag: "Reuse", pool });
726
758
  };
727
- // 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。
728
- // 实验级闸让「有共享状态、必须串行」的实验(如跨 eval 累积记忆,maxConcurrency: 1)只在
729
- // 自己内部排队,同批其它实验照常并发。它的名额域跨 Invocation 共用(见 gate-lease.ts 与
730
- // docs/feature/experiments/architecture.md「并发 Invocation:用例锁」末条),所以名额不是
731
- // 进程内信号量而是磁盘上的逐槽租约——多开不叠加 N,`maxConcurrency: 1` 的临界区声明在
732
- // 多开下同样成立。全局位反过来是每条 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` 误当成跨进程临界区。
733
782
  const globalSem = yield* effect_1.Effect.makeSemaphore(opts.maxConcurrency);
734
- // 实验闸在进程内先垫一层同名额的信号量,再去取租约。两个原因:
735
- // ① 名额在本进程内部交接是即时的(Effect 信号量把 permit 直接递给排队的下一个 attempt),
736
- // 不必等租约轮询的下一个周期——单开是绝大多数场景,不该为跨进程协调付整整一个轮询周期
737
- // 的空转;② 同实验同时去拍磁盘的 attempt 至多 N 个,不让 runs 展开出的一大批兄弟一起
738
- // 轮询。它只会更严、不会更松:permit 数恒等于该实验 resolved 的 N,真正的名额权威仍是
739
- // 租约(跨 Invocation 共用、min-N 收紧)。裸 run(没有 experimentId、没有可共享的名额域)
740
- // 就只有这一层,不产生任何跨进程协调。
783
+ // 实验闸是 Invocation 内信号量:同实验的 attempt 即时交接 permit,同批其它实验不受影响。
741
784
  const gateLocalSems = new Map();
742
785
  for (const run of effectiveAgentRuns) {
743
786
  if (run.maxConcurrency !== undefined) {
@@ -788,7 +831,7 @@ function runEvals(opts) {
788
831
  };
789
832
  const expLifecycles = new Map();
790
833
  for (const a of attempts) {
791
- if (!a.run.setup && !a.run.teardown)
834
+ if (!a.run.setup && !a.run.teardown && !a.run.sharedState)
792
835
  continue;
793
836
  const cell = expLifecycles.get(a.run);
794
837
  if (cell) {
@@ -800,9 +843,18 @@ function runEvals(opts) {
800
843
  expLifecycles.set(a.run, {
801
844
  state: { _tag: "Dormant", pendingAttempts: new Set([a]) },
802
845
  mutex: yield* effect_1.Effect.makeSemaphore(1),
846
+ physicalCleanupFailures: [],
803
847
  });
804
848
  }
805
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
+ };
806
858
  // 实验域诊断累积器(docs/runner.md「实验域诊断持久化」):只接无法归属单 Attempt 的实验
807
859
  // 事实——ctx.diagnostic、teardown failed/late、budget-unenforceable。相同 dedupeKey 只在
808
860
  // 同一个 experimentId 桶内折叠 count;不同 Experiment 各自独立累计,不跨来源合并。裸 run
@@ -852,10 +904,74 @@ function runEvals(opts) {
852
904
  list.push(record);
853
905
  experimentDiagnostics.set(input.experimentId, list);
854
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
+ });
855
968
  // 强杀后的收尾兜底(docs/feature/experiments/architecture.md「强杀后的收尾兜底」)的磁盘登记
856
969
  // 挂在本地协调根下,与留存注册表 `.niceeval/sandboxes/` 同一个根(默认 cwd/.niceeval,
857
970
  // 与 attempt.ts 的 `coordinationRoot` 兜底同一口径)。
858
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;
859
975
  const makeExperimentHookContext = (run, phase) => {
860
976
  const experimentId = run.experimentId;
861
977
  return {
@@ -903,6 +1019,35 @@ function runEvals(opts) {
903
1019
  return setup;
904
1020
  throw new Error("Experiment setup completed without recording its terminal outcome.");
905
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
+ });
906
1051
  const runExperimentTeardown = (run, cell) => effect_1.Effect.uninterruptibleMask((restore) => cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
907
1052
  const current = cell.state;
908
1053
  // setup 时点没走到就没有收尾义务;已完成与在飞状态分别复用自己的确定 Effect。
@@ -921,52 +1066,106 @@ function runEvals(opts) {
921
1066
  setup: current.setup,
922
1067
  completion,
923
1068
  };
1069
+ let cleanupSucceeded = true;
924
1070
  const teardown = effect_1.Effect.gen(function* () {
925
- // 强清可能先取得收尾执行权;必须等 setup 的 Effect 结算后再清理。
926
- yield* awaitSetup(current.setup);
927
- if (!run.teardown)
928
- return;
929
- (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "teardown", status: "started" });
930
- const startedAt = Date.now();
931
- const ctx = makeExperimentHookContext(run, "experiment.teardown");
932
- yield* (0, cleanup_timeout_ts_1.cleanupCallback)(() => run.teardown(ctx)).pipe(effect_1.Effect.matchEffect({
933
- onSuccess: () => effect_1.Effect.sync(() => {
934
- (0, sink_ts_1.reportExperimentHook)({
935
- experimentId,
936
- hook: "teardown",
937
- status: "done",
938
- durationMs: Date.now() - startedAt,
939
- });
940
- }),
941
- onFailure: (error) => effect_1.Effect.sync(() => {
942
- (0, sink_ts_1.reportExperimentHook)({
943
- experimentId,
944
- hook: "teardown",
945
- status: "failed",
946
- durationMs: Date.now() - startedAt,
947
- });
948
- // teardown 失败只作运行级诊断,不改任何已产出的 verdict。
949
- const message = (0, index_ts_1.t)("runner.experimentTeardownFailed", {
950
- experimentId,
951
- message: error instanceof Error ? error.message : String(error),
952
- }).trimEnd();
953
- (0, sink_ts_1.reportDiagnostic)({
954
- key: `experiment-teardown-failed:${experimentId}`,
955
- code: "experiment-teardown-failed",
956
- severity: "warning",
957
- message,
958
- data: { experimentId },
959
- });
960
- recordExperimentDiagnostic({
961
- experimentId: run.experimentId,
962
- code: "experiment-teardown-failed",
963
- level: "warning",
964
- message,
965
- phase: "experiment.teardown",
966
- });
967
- }),
968
- }));
969
- // 这个 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.
970
1169
  experimentCompletedAt.set(experimentId, new Date().toISOString());
971
1170
  }).pipe(effect_1.Effect.ensuring(cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
972
1171
  const state = cell.state;
@@ -980,15 +1179,37 @@ function runEvals(opts) {
980
1179
  if (run.experimentId) {
981
1180
  yield* (0, teardown_registry_ts_1.removeTeardownRegistrationIfPresentEffect)(coordinationRoot, (0, teardown_registry_ts_1.teardownEntryId)(run.experimentId, process.pid)).pipe(effect_1.Effect.ignore);
982
1181
  }
983
- }))));
984
- const settle = effect_1.Effect.exit(restore(teardown)).pipe(effect_1.Effect.flatMap((exit) => effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
985
- 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));
986
1206
  return completion;
987
1207
  })).pipe(effect_1.Effect.flatMap((completion) => completion === undefined ? effect_1.Effect.void : restore(effect_1.Deferred.await(completion)))));
988
1208
  /**
989
1209
  * 启动自愈:本实验触发 setup 之前,先核对磁盘上是否有它自己的遗留登记——上一次运行同一
990
1210
  * experimentId 被强杀、来不及删除。同宿主且 pid 已死才是遗留义务;pid 活或异宿主可能是
991
- * 并发 run,不触碰。先原子删登记拿到执行权,再补执行一次它的 teardown(新进程语义:
1211
+ * 并发 run,不触碰。sharedState caller 必须已先取得同 key exact authority;再原子删登记拿到执行权,
1212
+ * 再补执行一次它的 teardown(新进程语义:
992
1213
  * ctx.selectedEvalIds 从登记恢复,不依赖已丢失的 setup 产物)。失败只记诊断,不阻断、
993
1214
  * 不重试本次 run 的调度——recovery 补偿的是上一次的泄漏,不是这一次的前提条件。
994
1215
  */
@@ -1073,123 +1294,174 @@ function runEvals(opts) {
1073
1294
  }
1074
1295
  });
1075
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 = [];
1076
1303
  const ensureExperimentSetup = (a) => {
1077
1304
  const cell = expLifecycles.get(a.run);
1078
- return effect_1.Effect.uninterruptibleMask((restore) => cell.mutex.withPermits(1)(effect_1.Effect.gen(function* () {
1079
- const current = cell.state;
1080
- if (current._tag === "Active" || current._tag === "TearingDown")
1081
- return current.setup;
1082
- if (current._tag === "TornDown")
1083
- return undefined;
1084
- if (current._tag === "UntriggeredComplete") {
1085
- return yield* effect_1.Effect.die(new Error("Experiment lifecycle cannot be triggered after all attempts settled."));
1086
- }
1087
- const run = a.run;
1088
- const experimentId = run.experimentId ?? run.agent.name;
1089
- const completion = yield* effect_1.Deferred.make();
1090
- // 先写 Active、再登记和启动 worker:随后任何 attempt 或强清都会复用同一个 Effect 完成点。
1091
- cell.state = {
1092
- _tag: "Active",
1093
- pendingAttempts: current.pendingAttempts,
1094
- setup: { _tag: "InProgress", completion },
1095
- };
1096
- if (run.teardown) {
1097
- yield* (0, experiment_cleanup_registry_ts_1.registerExperimentTeardown)(experimentId, () => runExperimentTeardown(run, cell));
1098
- }
1099
- let setupStartedAt;
1100
- const setupBody = effect_1.Effect.gen(function* () {
1101
- // 上一进程的遗留收尾先补做,再原子写入本次登记;两步都先于 setup。
1102
- if (run.teardown && run.experimentId) {
1103
- yield* recoverOrphanedTeardownRegistration(run, experimentId);
1104
- yield* (0, teardown_registry_ts_1.writeTeardownRegistrationEffect)(coordinationRoot, {
1105
- experimentId: run.experimentId,
1106
- selectedEvalIds: run.selectedEvalIds,
1107
- pid: process.pid,
1108
- host: currentHost,
1109
- startedAt: new Date().toISOString(),
1110
- }).pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.sync(() => {
1111
- const message = (0, index_ts_1.t)("runner.teardownRegistrationWriteFailed", {
1112
- experimentId,
1113
- message: error instanceof Error ? error.message : String(error),
1114
- }).trimEnd();
1115
- (0, sink_ts_1.reportDiagnostic)({
1116
- key: `teardown-registration-write-failed:${experimentId}`,
1117
- code: "teardown-registration-write-failed",
1118
- severity: "warning",
1119
- message,
1120
- data: { experimentId },
1121
- });
1122
- recordExperimentDiagnostic({
1123
- experimentId: run.experimentId,
1124
- code: "teardown-registration-write-failed",
1125
- level: "warning",
1126
- message,
1127
- phase: "experiment.setup",
1128
- });
1129
- })));
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."));
1130
1315
  }
1131
- if (!run.setup)
1132
- return;
1133
- setupStartedAt = Date.now();
1134
- (0, sink_ts_1.reportExperimentHook)({ experimentId, hook: "setup", status: "started" });
1135
- const ctx = makeExperimentHookContext(run, "experiment.setup");
1136
- const value = yield* effect_1.Effect.tryPromise({
1137
- try: () => Promise.resolve().then(() => run.setup(ctx)),
1138
- catch: (error) => error,
1139
- });
1140
- const returned = value;
1141
- if (typeof returned === "function") {
1142
- // tsx 旧式 setup cleanup 只在 author callback 边界适配;主错误说明迁移方向。
1143
- yield* (0, cleanup_timeout_ts_1.cleanupCallback)(returned).pipe(effect_1.Effect.catchAll(() => effect_1.Effect.void));
1144
- return yield* effect_1.Effect.fail(new Error((0, index_ts_1.t)("runner.setupReturnedCleanup", {
1145
- layer: `ExperimentDef.setup (${experimentId})`,
1146
- hint: "ExperimentDef.teardown",
1147
- }).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));
1148
1320
  }
1149
- (0, sink_ts_1.reportExperimentHook)({
1150
- experimentId,
1151
- hook: "setup",
1152
- status: "done",
1153
- durationMs: Date.now() - (setupStartedAt ?? Date.now()),
1154
- });
1155
- });
1156
- const setupWorker = setupBody.pipe(effect_1.Effect.matchEffect({
1157
- onSuccess: () => replaceSetup(cell, { _tag: "Succeeded" }),
1158
- 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
+ }
1159
1382
  (0, sink_ts_1.reportExperimentHook)({
1160
1383
  experimentId,
1161
1384
  hook: "setup",
1162
- status: "failed",
1385
+ status: "done",
1163
1386
  durationMs: Date.now() - (setupStartedAt ?? Date.now()),
1164
1387
  });
1165
- }).pipe(effect_1.Effect.zipRight(replaceSetup(cell, {
1166
- _tag: "Failed",
1167
- error: { ...(0, attempt_ts_1.errorFromThrown)(error, "experiment.setup"), code: "experiment-setup-failed" },
1168
- }))),
1169
- }), effect_1.Effect.onExit((exit) => {
1170
- const recordUnexpectedExit = effect_1.Exit.isFailure(exit)
1171
- ? 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, {
1172
1399
  _tag: "Failed",
1173
- error: {
1174
- ...(0, attempt_ts_1.errorFromThrown)(effect_1.Cause.squash(exit.cause), "experiment.setup"),
1175
- code: "experiment-setup-failed",
1176
- },
1177
- })
1178
- : effect_1.Effect.void;
1179
- return recordUnexpectedExit.pipe(effect_1.Effect.zipRight(effect_1.Deferred.done(completion, exit)), effect_1.Effect.asVoid);
1180
- }));
1181
- yield* restore(setupWorker).pipe(effect_1.Effect.forkIn(invocationScope));
1182
- return cell.state.setup;
1183
- })).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
+ });
1184
1418
  };
1185
- // 自愈是「选中实验」的启动期职责,不是首个派发 attempt 的副作用:全携带使 attempts 为空时,
1186
- // 仍必须补上上一进程强杀遗留的收尾。无 teardown 的新定义无法安全补执行,交由 CLI 提醒。
1187
- 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();
1188
1452
  for (const run of effectiveAgentRuns) {
1189
- if (!run.experimentId || !run.teardown || recoveredExperimentIds.has(run.experimentId))
1453
+ if (!run.experimentId || !run.teardown)
1190
1454
  continue;
1191
- recoveredExperimentIds.add(run.experimentId);
1192
- 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);
1193
1465
  }
1194
1466
  const settleExperimentAttempt = (a) => {
1195
1467
  const cell = expLifecycles.get(a.run);
@@ -1603,53 +1875,6 @@ function runEvals(opts) {
1603
1875
  if (claim !== undefined)
1604
1876
  caseClaimsAwaitingPublication.push(claim);
1605
1877
  })));
1606
- /**
1607
- * 实验闸的取位:声明了 `maxConcurrency` 的实验从**跨 Invocation 共用**的逐槽租约取名额
1608
- * (gate-lease.ts);未声明的实验不走租约、不产生任何跨进程协调。取位在全局位之前——等名额
1609
- * 的 attempt 不占别的实验的并发位。返回 undefined 表示等待期间被中断,这条 attempt 就此放弃。
1610
- */
1611
- /**
1612
- * 撞满实验闸名额时报一条 info:名额域跨 Invocation,所以「等」的对象是别的进程,用户无从
1613
- * 从本进程的面板推断。生效名额与本次声明分开报——两者不等就是 min-N 夹低了(别的运行声明了
1614
- * 更小的 maxConcurrency),这是「我明明写了 3 却只跑 1 条」的唯一解释,不说清就只能去翻锁目录。
1615
- * 按实验折叠:同一实验的一堆 attempt 会前后脚撞上同一批持有者,不该刷屏。
1616
- */
1617
- const reportGateLeaseWait = (experimentId, declaredN, holders) => {
1618
- const effectiveN = holders.reduce((n, h) => Math.min(n, h.declaredN), declaredN);
1619
- const who = [...new Set(holders.map((h) => `pid ${h.pid}@${h.host}`))].join(", ") || "another run";
1620
- (0, sink_ts_1.reportDiagnostic)({
1621
- key: `gate-lease-waiting:${experimentId}`,
1622
- code: "gate-lease-waiting",
1623
- severity: "info",
1624
- message: (0, index_ts_1.t)("runner.gateLeaseWaiting", { experimentId, effectiveN, declaredN, holders: who }).trimEnd(),
1625
- data: { experimentId, effectiveN, declaredN },
1626
- });
1627
- };
1628
- const acquireGateLease = (experimentId, maxConcurrency) => effect_1.Effect.uninterruptibleMask((restore) => restore((0, gate_lease_ts_1.acquireGateSlotEffect)(coordinationRoot, experimentId, maxConcurrency, lockIdentity, {
1629
- // 撞满名额只发生在「别的 Invocation 正占着这个实验的位子」——本进程自己的并发早被
1630
- // gateLocalSems 挡在这一层之前了。不报的话面板就只剩 `0 running · N queued` 干等,
1631
- // 看不出在等什么、等谁,也看不出生效名额被对方更小的声明夹低了(min-N)。
1632
- onWaitStart: (holders) => reportGateLeaseWait(experimentId, maxConcurrency, holders),
1633
- })).pipe(effect_1.Effect.map(({ claim, takenOver, takenOverFrom }) => {
1634
- if (takenOver) {
1635
- const message = (0, index_ts_1.t)("runner.coordinationRecovered", { experimentId }).trimEnd();
1636
- (0, sink_ts_1.reportDiagnostic)({
1637
- key: `${types_ts_1.COORDINATION_RECOVERED_CODE}:concurrency-slot:${experimentId}|${claim.slot}`,
1638
- code: types_ts_1.COORDINATION_RECOVERED_CODE,
1639
- severity: "info",
1640
- message,
1641
- data: {
1642
- experimentId,
1643
- resource: "concurrency-slot",
1644
- slot: claim.slot,
1645
- ...(takenOverFrom !== undefined
1646
- ? { previousPid: takenOverFrom.pid, previousHost: takenOverFrom.host }
1647
- : {}),
1648
- },
1649
- });
1650
- }
1651
- return claim;
1652
- })));
1653
1878
  const withGlobalSlot = (haltSignal, use) => effect_1.Effect.uninterruptibleMask((restore) => {
1654
1879
  const state = { held: false };
1655
1880
  const release = effect_1.Effect.suspend(() => state.held
@@ -1675,24 +1900,11 @@ function runEvals(opts) {
1675
1900
  ? restore(use({ release, reacquire }))
1676
1901
  : effect_1.Effect.succeed({ kind: "recheck" })), effect_1.Effect.ensuring(release));
1677
1902
  });
1678
- /**
1679
- * 实验闸(② 道许可)的持有作用域:名额与 attempt **同生命周期**——从这里持有到执行体收尾
1680
- * (teardown 链、沙箱销毁)之后才归还,turn 退避等内部等待一律不释放
1681
- * (docs/runner.md「调度:有界并发」)。撞用例锁挂起是唯一的例外:那时执行体以 `suspend`
1682
- * 正常返回,作用域退出、名额归还,挂起结束后重新取——挂起的用例不占名额,否则同实验的
1683
- * 持锁方(可能就在另一条 Invocation 里)会被自己的等待方饿死。
1684
- */
1903
+ /** Invocation 内实验闸的持有作用域。它只限制此 Invocation 的 attempt,不取得任何
1904
+ * 跨进程资源;共享外部状态必须由 `sharedState` 生命周期租约保护。 */
1685
1905
  const withExperimentGate = (a, use) => {
1686
- const { maxConcurrency, experimentId } = a.run;
1687
1906
  const localSem = gateLocalSems.get(a.run);
1688
- if (maxConcurrency === undefined || localSem === undefined)
1689
- return use;
1690
- if (experimentId === undefined)
1691
- return localSem.withPermits(1)(use);
1692
- 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"
1693
- ? effect_1.Effect.succeed({ kind: "recheck" })
1694
- : restore(use).pipe(effect_1.Effect.ensuring(lease.claim.release.pipe(effect_1.Effect.ignore))))));
1695
- return localSem.withPermits(1)(leased);
1907
+ return localSem === undefined ? use : localSem.withPermits(1)(use);
1696
1908
  };
1697
1909
  // earlyExit:为每个 key 各建一个 AbortController。某 attempt 通过或 errored 时 abort 它,
1698
1910
  // 让并发进行中的同 key attempt 通过 signal 尽早退出,而不只是等排队的才能被跳过。
@@ -1703,9 +1915,11 @@ function runEvals(opts) {
1703
1915
  evalAbortControllers.set(a.key, new AbortController());
1704
1916
  }
1705
1917
  }
1706
- // 有界派发:forEach 的 worker 数就是 maxConcurrency,不先给全部 Slot 建 fiber。
1707
- // worker 内仍按止损闸、实验闸、全局 permit、用例锁的顺序取得资源;退避可以占住
1708
- // 这个有界 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 的物理并发。
1709
1923
  // runAttemptEffect 只把「执行错误」收进 EvalResult.error(不 fail),
1710
1924
  // 但中断(Ctrl+C / kill)照常向上传播 —— 所以一条挂掉不会中断其它 attempt,而中断能停掉全部。
1711
1925
  //
@@ -1766,7 +1980,8 @@ function runEvals(opts) {
1766
1980
  }
1767
1981
  const budget = a.run.budget;
1768
1982
  if (budget !== undefined) {
1769
- // 只看已完成 attempt 的实测花费(见上方 BudgetState 注释),到顶就跳过新 attempt,
1983
+ // 只看已完成 attempt 的价目表估算成本(见上方 BudgetState 注释;observed
1984
+ // usage.costUSD 不参与),到顶就跳过新 attempt,
1770
1985
  // 没到顶就立即放行——不等待、不做预测性节流。
1771
1986
  const s = budgetState(budgetKey);
1772
1987
  if (s.spent >= budget) {
@@ -1972,6 +2187,13 @@ function runEvals(opts) {
1972
2187
  // 到达调度器,不走错误通道向上传播——attempt fiber 的 E 保持 never,`errored`
1973
2188
  // 仍是 eval runner 的合法结果而不是调度失败(architecture.md「Effect 边界」)。
1974
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
+ : {}),
1975
2197
  onSealedEvaluation: (sealed) => recordCoordinator.noteSealedOrMarkIncomplete(recordedAttempt, sealed),
1976
2198
  });
1977
2199
  if (lease) {
@@ -2105,9 +2327,11 @@ function runEvals(opts) {
2105
2327
  // 「报告回调串行化」不一致。
2106
2328
  yield* reportMutex.withPermits(1)((0, report_ts_1.emitReporterEvent)(reporters, { type: "eval:complete", result }));
2107
2329
  });
2108
- // ③ 全局并发位 → ④ 派发时刻试锁 → preflight → 实验级 setup → body。
2109
- // 独占串行 provider(如 local):同一 provider 名的所有 attempt 共享一把 permit=1 的锁,
2110
- // 包在全局位之外(见上面 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 的执行段。
2111
2335
  const exclusiveSem = exclusiveSemFor(a.plan);
2112
2336
  const arriveAtDispatchWave = dispatchWaveArrive.get(a);
2113
2337
  const dispatch = withGlobalSlot(haltAwait(a), (slot) => effect_1.Effect.gen(function* () {
@@ -2116,6 +2340,18 @@ function runEvals(opts) {
2116
2340
  // 不在上面的竞速覆盖范围里(竞速只包全局位),闸可能正是在那段时间落下的。
2117
2341
  if (checkDispatchHalt(a).halted)
2118
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
+ }
2119
2355
  // 派发时刻取锁:授位之后才试,非阻塞。撞上别人持有的新鲜锁就把这个位子连同实验闸
2120
2356
  // 名额一起还回去(返回 "suspend"),由外层转入 elsewhere 挂起;位子当场空出来,
2121
2357
  // 排队中的下一条没被锁的用例接手。
@@ -2123,15 +2359,17 @@ function runEvals(opts) {
2123
2359
  const outcome = yield* tryAcquireCase(caseState);
2124
2360
  if (outcome.kind === "busy")
2125
2361
  return { kind: "suspend", window: outcome.window };
2126
- if (yield* recordCoordinator.adoptLatePublishedAttempt(a)) {
2362
+ if (!a.run.sharedState && (yield* recordCoordinator.adoptLatePublishedAttempt(a))) {
2127
2363
  return { kind: "done" };
2128
2364
  }
2129
2365
  }
2130
- const proceed = yield* preflight;
2131
- if (!proceed)
2132
- return { kind: "done" };
2133
- authorizedReuseAttempts.add(a);
2134
- 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)) {
2135
2373
  // 实验级 setup:第一个通过派发许可的 attempt 真正执行,其余等同一个 memoized
2136
2374
  // Effect completion(作者 setup 失败收进 lc.setupFailed,由 body 合成 errored 结果)。
2137
2375
  // 等它的时候让出全局并发位(docs/runner.md「调度:有界并发」——内部等待一律让位,
@@ -2140,10 +2378,16 @@ function runEvals(opts) {
2140
2378
  yield* ensureExperimentSetup(a);
2141
2379
  yield* slot.reacquire;
2142
2380
  }
2143
- yield* body(slot);
2144
- 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)));
2145
2390
  }));
2146
- const guarded = exclusiveSem ? exclusiveSem.withPermits(1)(dispatch) : dispatch;
2147
2391
  // 许可链的循环外壳:撞锁挂起的用例解决后从 ① 重新走一遍(实验闸名额与全局位都要
2148
2392
  // 重新取,不能拿着别人在等的名额干等)。
2149
2393
  const pipeline = effect_1.Effect.gen(function* () {
@@ -2170,8 +2414,9 @@ function runEvals(opts) {
2170
2414
  accountDispatchHalted(a, halt.gate);
2171
2415
  return;
2172
2416
  }
2173
- // ② 实验闸 → ③ 全局位 → ④ 用例锁 → preflight → body
2174
- const outcome = yield* withExperimentGate(a, guarded);
2417
+ // ② Invocation 内实验闸 → ③ 全局位 → sharedState / 生命周期协调 → Eval lock
2418
+ // → exclusive provider permit(如声明)→ body。
2419
+ const outcome = yield* withExperimentGate(a, dispatch);
2175
2420
  if (outcome.kind === "done")
2176
2421
  return;
2177
2422
  // 许可获取被落下的闸打断:许可已随作用域归还,回到 ① 让上面的检查点记账。
@@ -2188,10 +2433,10 @@ function runEvals(opts) {
2188
2433
  const orderedPipeline = predecessor === undefined
2189
2434
  ? wavePipeline
2190
2435
  : predecessor.pipe(effect_1.Effect.flatMap(() => wavePipeline));
2191
- // 实验级 teardown 结算:每个 attempt 收尾(含被 preflight 跳过、被中断的、被用例锁
2436
+ // 实验级 teardown / sharedState lease 结算:每个 attempt 收尾(含被 preflight 跳过、被中断的、被用例锁
2192
2437
  // 被 preflight 跳过、被中断的)都从身份集合移除;最后一个触发 ExperimentDef.teardown。ensuring
2193
2438
  // 在中断路径同样执行,重复 finalizer 也不会让状态下溢。
2194
- const withExpLifecycle = !a.run.setup && !a.run.teardown
2439
+ const withExpLifecycle = !a.run.setup && !a.run.teardown && !a.run.sharedState
2195
2440
  ? orderedPipeline
2196
2441
  : orderedPipeline.pipe(effect_1.Effect.ensuring(settleExperimentAttempt(a).pipe(effect_1.Effect.orDie)));
2197
2442
  // 用例锁释放:这个 key 的全部 attempt(真实派发的与被 late-carry 跳过的)都 settle 后
@@ -2206,7 +2451,7 @@ function runEvals(opts) {
2206
2451
  return withWaveLifecycle;
2207
2452
  const releaseSuccessor = groupedReleases.get(a);
2208
2453
  return withWaveLifecycle.pipe(effect_1.Effect.ensuring(releaseSuccessor));
2209
- }, { concurrency: opts.maxConcurrency, discard: true }).pipe(
2454
+ }, { concurrency: "unbounded", discard: true }).pipe(
2210
2455
  // 中断(用户 Ctrl+C):finalizer 已在中断过程中跑完(容器已停),这里只是把它咽下,
2211
2456
  // 好让流程走到 summarize / onInvocationComplete,用已完成的 results 出一份部分汇总,而不是抛栈。
2212
2457
  // 判据是 `isInterruptedOnly` 而不是 `isInterrupted`:一条 fiber 的缺陷会连带中断兄弟 fiber,
@@ -2223,6 +2468,18 @@ function runEvals(opts) {
2223
2468
  const exit = yield* effect_1.Effect.exit(opts.signal === undefined
2224
2469
  ? dispatchEffect
2225
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)));
2226
2483
  // provenance 在这里齐全:没有任何 attempt 依赖的 key 也照样跑完并留下自己那条记录,
2227
2484
  // 中断路径下同批构建随 signal 收束成 cancelled,不把 run.json 的 sandboxBuilds 落空。
2228
2485
  if (runningBuilds !== undefined) {
@@ -2304,6 +2561,14 @@ function runEvals(opts) {
2304
2561
  return yield* effect_1.Effect.failCause(exit.cause).pipe(effect_1.Effect.ensuring(sweepExperimentTeardowns().pipe(effect_1.Effect.orDie)));
2305
2562
  }
2306
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
+ }
2307
2572
  if (interrupted)
2308
2573
  (0, sink_ts_1.reportInterrupted)();
2309
2574
  yield* sweepExperimentTeardowns();