@tangle-network/agent-bench 0.3.6 → 0.3.8

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 (265) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +24 -24
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/humaneval.d.ts +10 -1
  16. package/dist/benchmarks/humaneval.js +5 -3
  17. package/dist/benchmarks/nomiracl.js +2 -2
  18. package/dist/benchmarks/open-rag-bench.js +2 -2
  19. package/dist/benchmarks/programbench.js +2 -2
  20. package/dist/benchmarks/ragbench.js +2 -2
  21. package/dist/benchmarks/swe-bench.js +2 -2
  22. package/dist/benchmarks/t2-ragbench.js +2 -2
  23. package/dist/benchmarks/tau-bench-shared.js +2 -2
  24. package/dist/benchmarks/tau2-bench.js +3 -3
  25. package/dist/benchmarks/tau3-banking.js +3 -3
  26. package/dist/benchmarks/terminal-bench.js +2 -2
  27. package/dist/benchmarks/toollm.js +2 -2
  28. package/dist/benchmarks/webarena-verified.js +2 -2
  29. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  30. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  31. package/dist/chunk-5H5XV76F.js.map +1 -0
  32. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  33. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  34. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  35. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  36. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  37. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  38. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  39. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  40. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  41. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  42. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  43. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  44. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  45. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  46. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  47. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  48. package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
  49. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  50. package/dist/chunk-WSKWVEQB.js.map +1 -0
  51. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  52. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  53. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  54. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  55. package/dist/index.js +24 -24
  56. package/package.json +6 -5
  57. package/scripts/run-package-tests.mjs +30 -8
  58. package/scripts/verify-packed-consumer.mjs +1 -1
  59. package/scripts/verify-pier-agent.mts +1 -0
  60. package/src/benchmarks/_harness.ts +20 -2
  61. package/src/benchmarks/humaneval.test.mts +122 -0
  62. package/src/benchmarks/humaneval.ts +100 -27
  63. package/src/david-attribution.mts +78 -0
  64. package/src/david-goliath.mts +149 -0
  65. package/src/hev-improve.mts +25 -6
  66. package/src/humaneval-object-ablation.mts +201 -0
  67. package/src/live-improve-campaign-mbpp.mts +641 -0
  68. package/src/live-improve-campaign.mts +500 -0
  69. package/src/mbpp-structural.mts +12 -7
  70. package/src/quant-arena/README.md +144 -0
  71. package/src/quant-arena/backtest.test.mts +135 -0
  72. package/src/quant-arena/backtest.ts +218 -0
  73. package/src/quant-arena/data.test.mts +44 -0
  74. package/src/quant-arena/data.ts +141 -0
  75. package/src/quant-arena/driver.test.mts +253 -0
  76. package/src/quant-arena/driver.ts +219 -0
  77. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  78. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  83. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  84. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  85. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  86. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  87. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  88. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  89. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  94. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  95. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  96. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  97. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  98. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  99. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  100. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  101. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  102. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  103. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  104. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  105. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  106. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  107. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  108. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  109. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  110. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  111. package/src/quant-arena/holdout-certify.mts +206 -0
  112. package/src/quant-arena/holdout-certify.test.mts +82 -0
  113. package/src/quant-arena/leak-audit.test.mts +79 -0
  114. package/src/quant-arena/leak-audit.ts +95 -0
  115. package/src/quant-arena/make-fixtures.mts +161 -0
  116. package/src/quant-arena/multiplicity.test.mts +68 -0
  117. package/src/quant-arena/multiplicity.ts +87 -0
  118. package/src/quant-arena/nautilus-certify.ts +31 -0
  119. package/src/quant-arena/oms.ts +90 -0
  120. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  121. package/src/quant-arena/python/pyproject.toml +8 -0
  122. package/src/quant-arena/python/uv.lock +1297 -0
  123. package/src/quant-arena/python/vbt-worker.py +192 -0
  124. package/src/quant-arena/quant-loop.mts +813 -0
  125. package/src/quant-arena/quant-loop.test.mts +75 -0
  126. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  127. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  128. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  129. package/src/quant-arena/types.ts +133 -0
  130. package/src/quant-arena/vbt-client.ts +321 -0
  131. package/src/quant-arena/vbt-parity.test.mts +183 -0
  132. package/src/quant-arena/windows.test.mts +45 -0
  133. package/src/quant-arena/windows.ts +54 -0
  134. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  135. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  136. package/src/rollout-ledger/settle-capture.mts +442 -0
  137. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  138. package/src/stream-observe.py +45 -0
  139. package/src/stream-observe.tpl.html +247 -0
  140. package/src/supervisor-arena.mts +816 -0
  141. package/src/swe-arena/activation.mts +228 -0
  142. package/src/swe-arena/activation.test.mts +303 -0
  143. package/src/swe-arena/analyze.ts +211 -0
  144. package/src/swe-arena/arms.ts +804 -0
  145. package/src/swe-arena/bootstrap-meta.mts +188 -0
  146. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  147. package/src/swe-arena/briefing.mts +217 -0
  148. package/src/swe-arena/briefing.test.mts +178 -0
  149. package/src/swe-arena/calibrate.ts +217 -0
  150. package/src/swe-arena/capabilities.mts +76 -0
  151. package/src/swe-arena/capabilities.test.mts +57 -0
  152. package/src/swe-arena/capacity.ts +194 -0
  153. package/src/swe-arena/cell-evidence.mts +437 -0
  154. package/src/swe-arena/cell-evidence.test.mts +248 -0
  155. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  156. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  157. package/src/swe-arena/execution.test.mts +1170 -0
  158. package/src/swe-arena/factory-command-container.ts +284 -0
  159. package/src/swe-arena/factory-judge-child.mts +228 -0
  160. package/src/swe-arena/factory.test.mts +643 -0
  161. package/src/swe-arena/fixtures/analyze.py +80 -0
  162. package/src/swe-arena/fixtures/excludes.txt +8 -0
  163. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  164. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  165. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  166. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  167. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  168. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  169. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  170. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  171. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  172. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  173. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  174. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  175. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  176. package/src/swe-arena/fixtures/holdout.json +44 -0
  177. package/src/swe-arena/fixtures/instances.json +146 -0
  178. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  179. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  180. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  181. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  182. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  183. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  184. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  185. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  186. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  187. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  188. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  189. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  190. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  191. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  192. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  193. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  194. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  195. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  196. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  197. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  198. package/src/swe-arena/fixtures.ts +237 -0
  199. package/src/swe-arena/gepa-seat.mts +583 -0
  200. package/src/swe-arena/gepa-seat.test.mts +635 -0
  201. package/src/swe-arena/holdout-certify.mts +408 -0
  202. package/src/swe-arena/holdout-certify.test.mts +160 -0
  203. package/src/swe-arena/judge-child.mts +37 -0
  204. package/src/swe-arena/ledger-orphans.mts +77 -0
  205. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  206. package/src/swe-arena/lineage-record.mts +164 -0
  207. package/src/swe-arena/lineage-record.test.mts +115 -0
  208. package/src/swe-arena/manifest.mts +293 -0
  209. package/src/swe-arena/manifest.test.mts +169 -0
  210. package/src/swe-arena/materialize.ts +142 -0
  211. package/src/swe-arena/outer-loop.mts +2795 -0
  212. package/src/swe-arena/outer-loop.test.mts +696 -0
  213. package/src/swe-arena/parity.test.mts +87 -0
  214. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  215. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  216. package/src/swe-arena/proc.test.mts +174 -0
  217. package/src/swe-arena/proc.ts +260 -0
  218. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  219. package/src/swe-arena/proposer-fanout.mts +770 -0
  220. package/src/swe-arena/proposer-fanout.test.mts +619 -0
  221. package/src/swe-arena/proposer-provenance.mts +177 -0
  222. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  223. package/src/swe-arena/reconcile.ts +0 -0
  224. package/src/swe-arena/replay.mts +183 -0
  225. package/src/swe-arena/replay.test.mts +300 -0
  226. package/src/swe-arena/run-experiment.mts +727 -0
  227. package/src/swe-arena/run-report.mts +75 -0
  228. package/src/swe-arena/run-supervisor.mjs +297 -0
  229. package/src/swe-arena/run-supervisor.test.mts +500 -0
  230. package/src/swe-arena/score-split.mts +140 -0
  231. package/src/swe-arena/score-split.test.mts +123 -0
  232. package/src/swe-arena/serialized-judge.ts +414 -0
  233. package/src/swe-arena/types.ts +218 -0
  234. package/src/swe-code-improve.mts +328 -0
  235. package/src/swe-emit-patch.mts +104 -0
  236. package/src/swe-improve.mts +232 -0
  237. package/src/swe-jail.ts +2 -2
  238. package/src/swe-local-proof.mts +169 -0
  239. package/src/swe-repro-calibrate.mts +446 -0
  240. package/src/swe-stream.mts +1497 -0
  241. package/src/swe-structural.mts +245 -837
  242. package/dist/chunk-LRRD7NAG.js.map +0 -1
  243. package/dist/chunk-PPYSEKFM.js.map +0 -1
  244. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  245. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  246. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  247. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  248. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  249. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  250. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  251. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  252. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  253. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  254. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  255. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  256. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  257. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  258. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  259. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  260. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  261. /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
  262. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  263. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  264. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  265. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,500 @@
1
+ import { access, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterAll, describe, expect, it } from 'vitest'
5
+ import { runSupervisorShim, supervisorDriverKillGraceMs } from './arms.ts'
6
+ import { run, TIMEOUT_RC } from './proc.ts'
7
+
8
+ const dirs: string[] = []
9
+ const workerPids: number[] = []
10
+
11
+ afterAll(async () => {
12
+ for (const pid of workerPids) {
13
+ try {
14
+ process.kill(-pid, 'SIGKILL')
15
+ } catch {
16
+ // The expected path already reaped the process group.
17
+ }
18
+ }
19
+ await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })))
20
+ })
21
+
22
+ describe('supervisor driver cancellation', () => {
23
+ it('waits for worker cleanup before returning its deadline result', async () => {
24
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-cancel-'))
25
+ dirs.push(dir)
26
+ const workspace = join(dir, 'workspace')
27
+ const params = join(dir, 'params.json')
28
+ const extension = join(dir, 'fake-extension.mjs')
29
+ await mkdir(workspace)
30
+ await writeFile(params, '{}')
31
+ const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
32
+ await writeFile(extension, `
33
+ import { spawn } from 'node:child_process'
34
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
35
+ import { join } from 'node:path'
36
+
37
+ const id = 'sup-1-fake12'
38
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
39
+ let child
40
+
41
+ export default function fakeExtension(pi) {
42
+ pi.registerTool({
43
+ name: 'spawn_supervisor',
44
+ async execute(_call, _params, _signal, _update, ctx) {
45
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
46
+ mkdirSync(runDir, { recursive: true })
47
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running', progress: 'driving' }))
48
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
49
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
50
+ const readyPath = join(ctx.cwd, 'worker-ready.txt')
51
+ child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'worker-cleaned.txt'), readyPath], {
52
+ detached: true,
53
+ stdio: 'ignore',
54
+ })
55
+ writeFileSync(join(ctx.cwd, 'worker.pid'), String(child.pid))
56
+ while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
57
+ return text('spawned supervisor ' + id)
58
+ },
59
+ })
60
+ pi.registerTool({
61
+ name: 'supervisor_cancel',
62
+ async execute(_call, _params, _signal, _update, ctx) {
63
+ appendFileSync(join(ctx.cwd, 'cancel-count.txt'), '1\\n')
64
+ process.kill(-child.pid, 'SIGTERM')
65
+ child.once('close', () => {
66
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
67
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1', reason: 'test cancellation' }) + '\\n')
68
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled', progress: 'cancelled' }))
69
+ writeFileSync(join(ctx.cwd, 'cancel-settled.txt'), 'settled')
70
+ })
71
+ return text('cancellation signalled before worker cleanup')
72
+ },
73
+ })
74
+ }
75
+ `)
76
+
77
+ const result = await run(process.execPath, [
78
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
79
+ ], {
80
+ timeoutMs: 5_000,
81
+ killGraceMs: 500,
82
+ env: {
83
+ ...process.env,
84
+ DRIVER_DEADLINE_MS: '40',
85
+ DRIVER_POLL_MS: '10',
86
+ DRIVER_CANCEL_TIMEOUT_MS: '2000',
87
+ },
88
+ })
89
+ const workerPid = Number(await readFile(join(workspace, 'worker.pid'), 'utf8'))
90
+ workerPids.push(workerPid)
91
+
92
+ expect(result.code, result.stdout + result.stderr).toBe(3)
93
+ expect(result.timedOut).toBe(false)
94
+ expect(await readFile(join(workspace, 'cancel-count.txt'), 'utf8')).toBe('1\n')
95
+ expect(await readFile(join(workspace, 'worker-cleaned.txt'), 'utf8')).toBe('clean')
96
+ expect(await readFile(join(workspace, 'cancel-settled.txt'), 'utf8')).toBe('settled')
97
+ expect(JSON.parse(await readFile(join(workspace, '.loops', 'supervisor', 'sup-1-fake12', 'state.json'), 'utf8')))
98
+ .toMatchObject({ status: 'cancelled' })
99
+ await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
100
+ })
101
+
102
+ it('lets near-deadline cancellation settle after outer SIGTERM', async () => {
103
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-outer-term-'))
104
+ dirs.push(dir)
105
+ const workspace = join(dir, 'workspace')
106
+ const params = join(dir, 'params.json')
107
+ const extension = join(dir, 'fake-extension.mjs')
108
+ await mkdir(workspace)
109
+ await writeFile(params, '{}')
110
+ await writeFile(extension, `
111
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
112
+ import { join } from 'node:path'
113
+
114
+ const id = 'sup-2-fake34'
115
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
116
+ export default function fakeExtension(pi) {
117
+ pi.registerTool({
118
+ name: 'spawn_supervisor',
119
+ async execute(_call, _params, _signal, _update, ctx) {
120
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
121
+ mkdirSync(runDir, { recursive: true })
122
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
123
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
124
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
125
+ return text('spawned supervisor ' + id)
126
+ },
127
+ })
128
+ pi.registerTool({
129
+ name: 'supervisor_cancel',
130
+ async execute(_call, _params, _signal, _update, ctx) {
131
+ await new Promise((resolve) => setTimeout(resolve, 1200))
132
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
133
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
134
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
135
+ writeFileSync(join(ctx.cwd, 'outer-cancel-settled.txt'), 'settled')
136
+ return text('cancellation settled')
137
+ },
138
+ })
139
+ }
140
+ `)
141
+
142
+ const cancelTimeoutMs = 1_500
143
+ const result = await run(process.execPath, [
144
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
145
+ ], {
146
+ timeoutMs: 1_000,
147
+ killGraceMs: supervisorDriverKillGraceMs({
148
+ DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
149
+ }),
150
+ env: {
151
+ ...process.env,
152
+ DRIVER_DEADLINE_MS: '60000',
153
+ DRIVER_POLL_MS: '10',
154
+ DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
155
+ },
156
+ })
157
+
158
+ expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
159
+ expect(result.timedOut).toBe(true)
160
+ expect(await readFile(join(workspace, 'outer-cancel-settled.txt'), 'utf8')).toBe('settled')
161
+ expect(JSON.parse(await readFile(join(workspace, '.loops', 'supervisor', 'sup-2-fake34', 'state.json'), 'utf8')))
162
+ .toMatchObject({ status: 'cancelled' })
163
+ }, 15_000)
164
+
165
+ it('latches SIGTERM while spawn_supervisor is still returning and cancels its detached worker', async () => {
166
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-signal-during-spawn-'))
167
+ dirs.push(dir)
168
+ const workspace = join(dir, 'workspace')
169
+ const params = join(dir, 'params.json')
170
+ const extension = join(dir, 'fake-extension.mjs')
171
+ await mkdir(workspace)
172
+ await writeFile(params, '{}')
173
+ const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
174
+ await writeFile(extension, `
175
+ import { spawn } from 'node:child_process'
176
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
177
+ import { join } from 'node:path'
178
+
179
+ const id = 'sup-3-signal56'
180
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
181
+ let child
182
+ export default function fakeExtension(pi) {
183
+ pi.registerTool({
184
+ name: 'spawn_supervisor',
185
+ async execute(_call, _params, _signal, _update, ctx) {
186
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
187
+ mkdirSync(runDir, { recursive: true })
188
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
189
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
190
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
191
+ child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'signal-worker-cleaned.txt')], {
192
+ detached: true,
193
+ stdio: 'ignore',
194
+ })
195
+ writeFileSync(join(ctx.cwd, 'signal-worker.pid'), String(child.pid))
196
+ writeFileSync(join(ctx.cwd, 'spawn-entered.txt'), 'entered')
197
+ await new Promise((resolve) => setTimeout(resolve, 30_000))
198
+ return text('spawned supervisor ' + id)
199
+ },
200
+ })
201
+ pi.registerTool({
202
+ name: 'supervisor_cancel',
203
+ async execute(_call, _params, _signal, _update, ctx) {
204
+ process.kill(-child.pid, 'SIGTERM')
205
+ await new Promise((resolve) => child.once('close', resolve))
206
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
207
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
208
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
209
+ writeFileSync(join(ctx.cwd, 'signal-cancel-settled.txt'), 'settled')
210
+ return text('cancellation settled')
211
+ },
212
+ })
213
+ }
214
+ `)
215
+
216
+ const cancelTimeoutMs = 2_000
217
+ const result = await run(process.execPath, [
218
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
219
+ ], {
220
+ timeoutMs: 1_000,
221
+ killGraceMs: supervisorDriverKillGraceMs({
222
+ DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
223
+ }),
224
+ env: {
225
+ ...process.env,
226
+ DRIVER_DEADLINE_MS: '60000',
227
+ DRIVER_POLL_MS: '10',
228
+ DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
229
+ },
230
+ })
231
+ const workerPid = Number(await readFile(join(workspace, 'signal-worker.pid'), 'utf8'))
232
+ workerPids.push(workerPid)
233
+
234
+ expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
235
+ expect(result.timedOut).toBe(true)
236
+ expect(await readFile(join(workspace, 'spawn-entered.txt'), 'utf8')).toBe('entered')
237
+ expect(await readFile(join(workspace, 'signal-worker-cleaned.txt'), 'utf8')).toBe('clean')
238
+ expect(await readFile(join(workspace, 'signal-cancel-settled.txt'), 'utf8')).toBe('settled')
239
+ await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
240
+ }, 15_000)
241
+
242
+ it('recovers an on-disk supervisor id from an unparseable spawn response and cancels it', async () => {
243
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-id-recovery-'))
244
+ dirs.push(dir)
245
+ const workspace = join(dir, 'workspace')
246
+ const params = join(dir, 'params.json')
247
+ const extension = join(dir, 'fake-extension.mjs')
248
+ await mkdir(workspace)
249
+ await writeFile(params, '{}')
250
+ const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
251
+ await writeFile(extension, `
252
+ import { spawn } from 'node:child_process'
253
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
254
+ import { join } from 'node:path'
255
+
256
+ const id = 'sup-4-recover78'
257
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
258
+ let child
259
+ export default function fakeExtension(pi) {
260
+ pi.registerTool({
261
+ name: 'spawn_supervisor',
262
+ async execute(_call, _params, _signal, _update, ctx) {
263
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
264
+ mkdirSync(runDir, { recursive: true })
265
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
266
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
267
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
268
+ child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'recovered-worker-cleaned.txt')], {
269
+ detached: true,
270
+ stdio: 'ignore',
271
+ })
272
+ writeFileSync(join(ctx.cwd, 'recovered-worker.pid'), String(child.pid))
273
+ return text('the supervisor started, but this response omitted its identifier')
274
+ },
275
+ })
276
+ pi.registerTool({
277
+ name: 'supervisor_cancel',
278
+ async execute(_call, params, _signal, _update, ctx) {
279
+ writeFileSync(join(ctx.cwd, 'recovered-id.txt'), params.id)
280
+ process.kill(-child.pid, 'SIGTERM')
281
+ await new Promise((resolve) => child.once('close', resolve))
282
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
283
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
284
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
285
+ return text('cancellation settled')
286
+ },
287
+ })
288
+ }
289
+ `)
290
+
291
+ const result = await run(process.execPath, [
292
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
293
+ ], {
294
+ timeoutMs: 5_000,
295
+ killGraceMs: 500,
296
+ env: {
297
+ ...process.env,
298
+ DRIVER_DEADLINE_MS: '60000',
299
+ DRIVER_POLL_MS: '10',
300
+ DRIVER_CANCEL_TIMEOUT_MS: '2000',
301
+ },
302
+ })
303
+ const workerPid = Number(await readFile(join(workspace, 'recovered-worker.pid'), 'utf8'))
304
+ workerPids.push(workerPid)
305
+
306
+ expect(result.code, result.stdout + result.stderr).toBe(1)
307
+ expect(result.timedOut).toBe(false)
308
+ expect(result.stderr).toMatch(/cancelling recovered run\(s\): sup-4-recover78/)
309
+ expect(await readFile(join(workspace, 'recovered-id.txt'), 'utf8')).toBe('sup-4-recover78')
310
+ await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
311
+ }, 15_000)
312
+
313
+ it('waits for a late worker terminal record before accepting completed state', async () => {
314
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-late-settlement-'))
315
+ dirs.push(dir)
316
+ const workspace = join(dir, 'workspace')
317
+ const params = join(dir, 'params.json')
318
+ const extension = join(dir, 'fake-extension.mjs')
319
+ await mkdir(workspace)
320
+ await writeFile(params, '{}')
321
+ await writeFile(extension, `
322
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
323
+ import { join } from 'node:path'
324
+
325
+ const id = 'sup-5-late90'
326
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
327
+ export default function fakeExtension(pi) {
328
+ pi.registerTool({
329
+ name: 'spawn_supervisor',
330
+ async execute(_call, _params, _signal, _update, ctx) {
331
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
332
+ mkdirSync(runDir, { recursive: true })
333
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'completed', verdict: 'delivered' }))
334
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
335
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
336
+ setTimeout(() => {
337
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'settled', id: id + ':s1', status: 'done' }) + '\\n')
338
+ }, 40)
339
+ return text('spawned supervisor ' + id)
340
+ },
341
+ })
342
+ pi.registerTool({
343
+ name: 'supervisor_cancel',
344
+ async execute() { return text('already completed') },
345
+ })
346
+ }
347
+ `)
348
+
349
+ const result = await run(process.execPath, [
350
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
351
+ ], {
352
+ timeoutMs: 5_000,
353
+ killGraceMs: 500,
354
+ env: {
355
+ ...process.env,
356
+ DRIVER_DEADLINE_MS: '60000',
357
+ DRIVER_POLL_MS: '10',
358
+ DRIVER_CANCEL_TIMEOUT_MS: '2000',
359
+ DRIVER_SETTLEMENT_TIMEOUT_MS: '500',
360
+ },
361
+ })
362
+
363
+ expect(result.code, result.stdout + result.stderr).toBe(0)
364
+ expect(result.timedOut).toBe(false)
365
+ expect(result.stdout).toMatch(/worker settlement: workers=1, terminal=1/)
366
+ })
367
+
368
+ it('refuses completed state while a journal worker remains live', async () => {
369
+ const dir = await mkdtemp(join(tmpdir(), 'swe-driver-live-completed-worker-'))
370
+ dirs.push(dir)
371
+ const workspace = join(dir, 'workspace')
372
+ const params = join(dir, 'params.json')
373
+ const extension = join(dir, 'fake-extension.mjs')
374
+ await mkdir(workspace)
375
+ await writeFile(params, '{}')
376
+ await writeFile(extension, `
377
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
378
+ import { join } from 'node:path'
379
+
380
+ const id = 'sup-5-live90'
381
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
382
+ export default function fakeExtension(pi) {
383
+ pi.registerTool({
384
+ name: 'spawn_supervisor',
385
+ async execute(_call, _params, _signal, _update, ctx) {
386
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
387
+ mkdirSync(runDir, { recursive: true })
388
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'completed', verdict: 'delivered' }))
389
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
390
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
391
+ return text('spawned supervisor ' + id)
392
+ },
393
+ })
394
+ pi.registerTool({
395
+ name: 'supervisor_cancel',
396
+ async execute() { return text('already completed') },
397
+ })
398
+ }
399
+ `)
400
+
401
+ const result = await run(process.execPath, [
402
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
403
+ ], {
404
+ timeoutMs: 5_000,
405
+ killGraceMs: 500,
406
+ env: {
407
+ ...process.env,
408
+ DRIVER_DEADLINE_MS: '60000',
409
+ DRIVER_POLL_MS: '10',
410
+ DRIVER_CANCEL_TIMEOUT_MS: '2000',
411
+ DRIVER_SETTLEMENT_TIMEOUT_MS: '80',
412
+ },
413
+ })
414
+
415
+ expect(result.code, result.stdout + result.stderr).toBe(4)
416
+ expect(result.timedOut).toBe(false)
417
+ expect(result.stderr).toMatch(/refusing completed supervisor/)
418
+ expect(result.stderr).toMatch(/workers=1, terminal=0, unsettled=1/)
419
+ expect(result.stdout).not.toContain('[done] id=')
420
+ })
421
+
422
+ it.each(['failed', 'cancelled'] as const)(
423
+ 'settles a detached worker before returning terminal status $status',
424
+ async (status) => {
425
+ const dir = await mkdtemp(join(tmpdir(), `swe-driver-${status}-worker-`))
426
+ dirs.push(dir)
427
+ const workspace = join(dir, 'workspace')
428
+ const params = join(dir, 'params.json')
429
+ const extension = join(dir, 'fake-extension.mjs')
430
+ await mkdir(workspace)
431
+ await writeFile(params, '{}')
432
+ const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
433
+ await writeFile(extension, `
434
+ import { spawn } from 'node:child_process'
435
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
436
+ import { join } from 'node:path'
437
+
438
+ const id = 'sup-6-${status}12'
439
+ const text = (value) => ({ content: [{ type: 'text', text: value }] })
440
+ let child
441
+ export default function fakeExtension(pi) {
442
+ pi.registerTool({
443
+ name: 'spawn_supervisor',
444
+ async execute(_call, _params, _signal, _update, ctx) {
445
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
446
+ mkdirSync(runDir, { recursive: true })
447
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
448
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
449
+ const readyPath = join(ctx.cwd, 'terminal-worker-ready.txt')
450
+ child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'terminal-worker-cleaned.txt'), readyPath], {
451
+ detached: true,
452
+ stdio: 'ignore',
453
+ })
454
+ writeFileSync(join(ctx.cwd, 'terminal-worker.pid'), String(child.pid))
455
+ while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
456
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: '${status}', error: 'fixture terminal state' }))
457
+ return text('spawned supervisor ' + id)
458
+ },
459
+ })
460
+ pi.registerTool({
461
+ name: 'supervisor_cancel',
462
+ async execute(_call, _params, _signal, _update, ctx) {
463
+ appendFileSync(join(ctx.cwd, 'terminal-cancel-count.txt'), '1\\n')
464
+ process.kill(-child.pid, 'SIGTERM')
465
+ await new Promise((resolve) => child.once('close', resolve))
466
+ const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
467
+ appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
468
+ writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
469
+ return text('terminal worker settled')
470
+ },
471
+ })
472
+ }
473
+ `)
474
+
475
+ const result = await run(process.execPath, [
476
+ '--import', 'tsx', runSupervisorShim, extension, workspace, params,
477
+ ], {
478
+ timeoutMs: 5_000,
479
+ killGraceMs: 500,
480
+ env: {
481
+ ...process.env,
482
+ DRIVER_DEADLINE_MS: '60000',
483
+ DRIVER_POLL_MS: '10',
484
+ DRIVER_CANCEL_TIMEOUT_MS: '2000',
485
+ },
486
+ })
487
+ const workerPid = Number(await readFile(join(workspace, 'terminal-worker.pid'), 'utf8'))
488
+ workerPids.push(workerPid)
489
+
490
+ expect(result.code, result.stdout + result.stderr).toBe(1)
491
+ expect(result.timedOut).toBe(false)
492
+ expect(result.stdout).toContain(`[done] id=sup-6-${status}12 status=${status}`)
493
+ expect(result.stdout).toContain('worker settlement: workers=1, terminal=1')
494
+ expect(await readFile(join(workspace, 'terminal-cancel-count.txt'), 'utf8')).toBe('1\n')
495
+ expect(await readFile(join(workspace, 'terminal-worker-cleaned.txt'), 'utf8')).toBe('clean')
496
+ await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
497
+ },
498
+ 15_000,
499
+ )
500
+ })
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Gen-5 public/private score split (SOTA adoption #1, AIDE2-style).
3
+ *
4
+ * Per generation the improvement instances are DETERMINISTICALLY split
5
+ * (seeded by runId) into a PUBLIC set — whose scores/evidence proposers and
6
+ * the pre-filter may see — and a PRIVATE set that is scored identically but
7
+ * NEVER surfaced to proposers. Winner selection stays on the COMBINED
8
+ * public+private score (the lib judge already averages every scenario), so
9
+ * the split changes only what feedback the search sees, exactly the
10
+ * anti-overfitting mechanism: a candidate that games the visible instances
11
+ * still has to survive the instances it never saw named.
12
+ *
13
+ * SMALL-N CAVEAT: with only 6 improvement instances the split is 4 public /
14
+ * 2 private. Two private instances give a coarse overfit signal (0, 1/2 or
15
+ * 2/2) and a single flake flips it — treat the private sub-score as a
16
+ * direction check, not a certification; the pre-registered 6-instance holdout
17
+ * remains the only promotion evidence.
18
+ *
19
+ * RESUME STABILITY: runId embeds a launch timestamp, so a resumed run gets a
20
+ * fresh runId. The split is therefore PERSISTED to <outDir>/score-split.json
21
+ * on first computation and reloaded verbatim afterwards — a resume can never
22
+ * rotate previously-private instances into view.
23
+ */
24
+
25
+ import { createHash } from 'node:crypto'
26
+ import { existsSync } from 'node:fs'
27
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
28
+ import { dirname, join } from 'node:path'
29
+
30
+ export const SCORE_SPLIT_SCHEMA = 'swe-arena.score-split.v1'
31
+ export const SCORE_SPLIT_FILENAME = 'score-split.json'
32
+
33
+ export interface ScoreSplitConfig {
34
+ /** Number of PUBLIC instances (the rest are private). Must satisfy
35
+ * 0 < publicCount < instances.length. */
36
+ publicCount: number
37
+ }
38
+
39
+ export interface ScoreSplit {
40
+ schema: typeof SCORE_SPLIT_SCHEMA
41
+ /** The runId that seeded the split (provenance; a resumed run keeps it). */
42
+ seededBy: string
43
+ publicInstances: string[]
44
+ privateInstances: string[]
45
+ }
46
+
47
+ /** Deterministic split: instances are ranked by sha256(runId + ':' + iid) and
48
+ * the first `publicCount` become public. Pure — same (runId, instances,
49
+ * publicCount) always yields the same split; instance-list ORDER does not
50
+ * matter (ranking is content-derived). */
51
+ export function splitInstances(runId: string, instances: readonly string[], publicCount: number): ScoreSplit {
52
+ if (!Number.isInteger(publicCount) || publicCount <= 0 || publicCount >= instances.length) {
53
+ throw new Error(
54
+ `score-split: publicCount must be an integer in (0, ${instances.length}), got ${JSON.stringify(publicCount)}`,
55
+ )
56
+ }
57
+ if (new Set(instances).size !== instances.length) {
58
+ throw new Error('score-split: duplicate instance ids')
59
+ }
60
+ const ranked = [...instances]
61
+ .map((iid) => ({ iid, rank: createHash('sha256').update(`${runId}:${iid}`).digest('hex') }))
62
+ .sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0))
63
+ const pub = ranked.slice(0, publicCount).map((r) => r.iid)
64
+ const priv = ranked.slice(publicCount).map((r) => r.iid)
65
+ return {
66
+ schema: SCORE_SPLIT_SCHEMA,
67
+ seededBy: runId,
68
+ publicInstances: [...pub].sort(),
69
+ privateInstances: [...priv].sort(),
70
+ }
71
+ }
72
+
73
+ /** Load the persisted split for this outDir, or compute + persist it. The
74
+ * persisted file always wins (resume stability); it is validated against the
75
+ * current instance set so a config edit mid-outDir fails loud. */
76
+ export async function loadOrCreateScoreSplit(args: {
77
+ outDir: string
78
+ runId: string
79
+ instances: readonly string[]
80
+ publicCount: number
81
+ }): Promise<ScoreSplit> {
82
+ const path = join(args.outDir, SCORE_SPLIT_FILENAME)
83
+ if (existsSync(path)) {
84
+ const split = JSON.parse(await readFile(path, 'utf8')) as ScoreSplit
85
+ if (split.schema !== SCORE_SPLIT_SCHEMA) {
86
+ throw new Error(`score-split: ${path} has unknown schema ${JSON.stringify(split.schema)}`)
87
+ }
88
+ const persisted = [...split.publicInstances, ...split.privateInstances].sort()
89
+ const expected = [...args.instances].sort()
90
+ if (JSON.stringify(persisted) !== JSON.stringify(expected)) {
91
+ throw new Error(
92
+ `score-split: persisted split at ${path} covers [${persisted.join(', ')}] but the config ` +
93
+ `names [${expected.join(', ')}] — refusing to silently re-split; move the outDir or fix the config`,
94
+ )
95
+ }
96
+ if (split.publicInstances.length !== args.publicCount) {
97
+ throw new Error(
98
+ `score-split: persisted split has ${split.publicInstances.length} public instances, config wants ` +
99
+ `${args.publicCount} — refusing to silently re-split`,
100
+ )
101
+ }
102
+ return split
103
+ }
104
+ const split = splitInstances(args.runId, args.instances, args.publicCount)
105
+ await mkdir(dirname(path), { recursive: true })
106
+ await writeFile(path, JSON.stringify(split, null, 2))
107
+ return split
108
+ }
109
+
110
+ /** True when `text` mentions any private instance id — the NEVER-SURFACED
111
+ * invariant's checkable form. Used by tests and by the evidence-index writer
112
+ * as a final guard on proposer-visible text. */
113
+ export function leaksPrivateInstance(text: string, split: Pick<ScoreSplit, 'privateInstances'>): string[] {
114
+ return split.privateInstances.filter((iid) => text.includes(iid))
115
+ }
116
+
117
+ /** Filter per-instance evidence records to the PUBLIC set. Identity function
118
+ * when no split is configured (split === null). */
119
+ export function publicOnly<T>(
120
+ records: readonly T[],
121
+ iidOf: (record: T) => string,
122
+ split: Pick<ScoreSplit, 'privateInstances'> | null,
123
+ ): T[] {
124
+ if (split === null) return [...records]
125
+ const priv = new Set(split.privateInstances)
126
+ return records.filter((r) => !priv.has(iidOf(r)))
127
+ }
128
+
129
+ /** Public/private sub-scores for one candidate, from per-instance AND-verdicts
130
+ * (fail-closed: an instance with no conclusive full-reps verdict counts 0). */
131
+ export function subScores(
132
+ verdicts: Record<string, boolean>,
133
+ split: Pick<ScoreSplit, 'publicInstances' | 'privateInstances'>,
134
+ ): { publicResolvedCount: number; privateResolvedCount: number } {
135
+ const count = (iids: readonly string[]): number => iids.filter((iid) => verdicts[iid] === true).length
136
+ return {
137
+ publicResolvedCount: count(split.publicInstances),
138
+ privateResolvedCount: count(split.privateInstances),
139
+ }
140
+ }