@tangle-network/agent-bench 0.3.5 → 0.3.7

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 (262) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +13 -1
  3. package/dist/adapters.d.ts +15 -0
  4. package/dist/adapters.js +43 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/benchmarks/_harness.d.ts +125 -0
  7. package/dist/benchmarks/_harness.js +33 -0
  8. package/dist/benchmarks/_harness.js.map +1 -0
  9. package/dist/benchmarks/aec-bench.d.ts +27 -0
  10. package/dist/benchmarks/aec-bench.js +8 -0
  11. package/dist/benchmarks/aec-bench.js.map +1 -0
  12. package/dist/benchmarks/agentbench.d.ts +16 -0
  13. package/dist/benchmarks/agentbench.js +10 -0
  14. package/dist/benchmarks/agentbench.js.map +1 -0
  15. package/dist/benchmarks/appworld.d.ts +37 -0
  16. package/dist/benchmarks/appworld.js +14 -0
  17. package/dist/benchmarks/appworld.js.map +1 -0
  18. package/dist/benchmarks/bfcl.d.ts +18 -0
  19. package/dist/benchmarks/bfcl.js +10 -0
  20. package/dist/benchmarks/bfcl.js.map +1 -0
  21. package/dist/benchmarks/cad-design.d.ts +45 -0
  22. package/dist/benchmarks/cad-design.js +7 -0
  23. package/dist/benchmarks/cad-design.js.map +1 -0
  24. package/dist/benchmarks/cadbench.d.ts +19 -0
  25. package/dist/benchmarks/cadbench.js +8 -0
  26. package/dist/benchmarks/cadbench.js.map +1 -0
  27. package/dist/benchmarks/cadgenbench.d.ts +22 -0
  28. package/dist/benchmarks/cadgenbench.js +8 -0
  29. package/dist/benchmarks/cadgenbench.js.map +1 -0
  30. package/dist/benchmarks/commit0.d.ts +31 -0
  31. package/dist/benchmarks/commit0.js +10 -0
  32. package/dist/benchmarks/commit0.js.map +1 -0
  33. package/dist/benchmarks/crag.d.ts +14 -0
  34. package/dist/benchmarks/crag.js +9 -0
  35. package/dist/benchmarks/crag.js.map +1 -0
  36. package/dist/benchmarks/dabstep.d.ts +18 -0
  37. package/dist/benchmarks/dabstep.js +10 -0
  38. package/dist/benchmarks/dabstep.js.map +1 -0
  39. package/dist/benchmarks/enterpriseops-gym.d.ts +38 -0
  40. package/dist/benchmarks/enterpriseops-gym.js +10 -0
  41. package/dist/benchmarks/enterpriseops-gym.js.map +1 -0
  42. package/dist/benchmarks/finresearchbench.d.ts +15 -0
  43. package/dist/benchmarks/finresearchbench.js +8 -0
  44. package/dist/benchmarks/finresearchbench.js.map +1 -0
  45. package/dist/benchmarks/finsearchcomp.d.ts +49 -0
  46. package/dist/benchmarks/finsearchcomp.js +7 -0
  47. package/dist/benchmarks/finsearchcomp.js.map +1 -0
  48. package/dist/benchmarks/frames.d.ts +59 -0
  49. package/dist/benchmarks/frames.js +13 -0
  50. package/dist/benchmarks/frames.js.map +1 -0
  51. package/dist/benchmarks/hotpotqa.d.ts +48 -0
  52. package/dist/benchmarks/hotpotqa.js +15 -0
  53. package/dist/benchmarks/hotpotqa.js.map +1 -0
  54. package/dist/benchmarks/humaneval.d.ts +62 -0
  55. package/dist/benchmarks/humaneval.js +17 -0
  56. package/dist/benchmarks/humaneval.js.map +1 -0
  57. package/dist/benchmarks/mind2web.d.ts +41 -0
  58. package/dist/benchmarks/mind2web.js +9 -0
  59. package/dist/benchmarks/mind2web.js.map +1 -0
  60. package/dist/benchmarks/nomiracl.d.ts +15 -0
  61. package/dist/benchmarks/nomiracl.js +9 -0
  62. package/dist/benchmarks/nomiracl.js.map +1 -0
  63. package/dist/benchmarks/open-rag-bench.d.ts +14 -0
  64. package/dist/benchmarks/open-rag-bench.js +9 -0
  65. package/dist/benchmarks/open-rag-bench.js.map +1 -0
  66. package/dist/benchmarks/programbench.d.ts +38 -0
  67. package/dist/benchmarks/programbench.js +10 -0
  68. package/dist/benchmarks/programbench.js.map +1 -0
  69. package/dist/benchmarks/rag-shared.d.ts +42 -0
  70. package/dist/benchmarks/rag-shared.js +39 -0
  71. package/dist/benchmarks/rag-shared.js.map +1 -0
  72. package/dist/benchmarks/ragbench.d.ts +16 -0
  73. package/dist/benchmarks/ragbench.js +9 -0
  74. package/dist/benchmarks/ragbench.js.map +1 -0
  75. package/dist/benchmarks/simpleqa.d.ts +64 -0
  76. package/dist/benchmarks/simpleqa.js +11 -0
  77. package/dist/benchmarks/simpleqa.js.map +1 -0
  78. package/dist/benchmarks/swe-bench.d.ts +56 -0
  79. package/dist/benchmarks/swe-bench.js +14 -0
  80. package/dist/benchmarks/swe-bench.js.map +1 -0
  81. package/dist/benchmarks/t2-ragbench.d.ts +14 -0
  82. package/dist/benchmarks/t2-ragbench.js +9 -0
  83. package/dist/benchmarks/t2-ragbench.js.map +1 -0
  84. package/dist/benchmarks/tau-bench-shared.d.ts +26 -0
  85. package/dist/benchmarks/tau-bench-shared.js +10 -0
  86. package/dist/benchmarks/tau-bench-shared.js.map +1 -0
  87. package/dist/benchmarks/tau2-bench.d.ts +7 -0
  88. package/dist/benchmarks/tau2-bench.js +11 -0
  89. package/dist/benchmarks/tau2-bench.js.map +1 -0
  90. package/dist/benchmarks/tau3-banking.d.ts +15 -0
  91. package/dist/benchmarks/tau3-banking.js +9 -0
  92. package/dist/benchmarks/tau3-banking.js.map +1 -0
  93. package/dist/benchmarks/terminal-bench.d.ts +24 -0
  94. package/dist/benchmarks/terminal-bench.js +8 -0
  95. package/dist/benchmarks/terminal-bench.js.map +1 -0
  96. package/dist/benchmarks/toollm.d.ts +16 -0
  97. package/dist/benchmarks/toollm.js +10 -0
  98. package/dist/benchmarks/toollm.js.map +1 -0
  99. package/dist/benchmarks/trata-hedge.d.ts +32 -0
  100. package/dist/benchmarks/trata-hedge.js +7 -0
  101. package/dist/benchmarks/trata-hedge.js.map +1 -0
  102. package/dist/benchmarks/types.d.ts +107 -0
  103. package/dist/benchmarks/types.js +1 -0
  104. package/dist/benchmarks/types.js.map +1 -0
  105. package/dist/benchmarks/webarena-verified.d.ts +16 -0
  106. package/dist/benchmarks/webarena-verified.js +10 -0
  107. package/dist/benchmarks/webarena-verified.js.map +1 -0
  108. package/dist/chunk-2PVVP7GN.js +197 -0
  109. package/dist/chunk-2PVVP7GN.js.map +1 -0
  110. package/dist/chunk-2XU6OGEN.js +170 -0
  111. package/dist/chunk-2XU6OGEN.js.map +1 -0
  112. package/dist/chunk-53UPUNBZ.js +325 -0
  113. package/dist/chunk-53UPUNBZ.js.map +1 -0
  114. package/dist/chunk-5H5XV76F.js +240 -0
  115. package/dist/chunk-5H5XV76F.js.map +1 -0
  116. package/dist/chunk-7WSD27QQ.js +118 -0
  117. package/dist/chunk-7WSD27QQ.js.map +1 -0
  118. package/dist/chunk-C7T7WEK2.js +103 -0
  119. package/dist/chunk-C7T7WEK2.js.map +1 -0
  120. package/dist/chunk-CKUVRZ2T.js +251 -0
  121. package/dist/chunk-CKUVRZ2T.js.map +1 -0
  122. package/dist/chunk-HBSWHQNJ.js +30 -0
  123. package/dist/chunk-HBSWHQNJ.js.map +1 -0
  124. package/dist/chunk-HHXFIHXC.js +116 -0
  125. package/dist/chunk-HHXFIHXC.js.map +1 -0
  126. package/dist/chunk-IFAV6KEM.js +276 -0
  127. package/dist/chunk-IFAV6KEM.js.map +1 -0
  128. package/dist/chunk-INNOYXCP.js +387 -0
  129. package/dist/chunk-INNOYXCP.js.map +1 -0
  130. package/dist/chunk-J3KDJNX2.js +182 -0
  131. package/dist/chunk-J3KDJNX2.js.map +1 -0
  132. package/dist/chunk-JRWWGMK7.js +148 -0
  133. package/dist/chunk-JRWWGMK7.js.map +1 -0
  134. package/dist/chunk-JTHWEDEW.js +32 -0
  135. package/dist/chunk-JTHWEDEW.js.map +1 -0
  136. package/dist/chunk-KDIKRJGB.js +120 -0
  137. package/dist/chunk-KDIKRJGB.js.map +1 -0
  138. package/dist/chunk-LRRD7NAG.js +301 -0
  139. package/dist/chunk-LRRD7NAG.js.map +1 -0
  140. package/dist/chunk-ODT47UAY.js +221 -0
  141. package/dist/chunk-ODT47UAY.js.map +1 -0
  142. package/dist/chunk-PA2ZKHJC.js +230 -0
  143. package/dist/chunk-PA2ZKHJC.js.map +1 -0
  144. package/dist/chunk-PUIRNYI7.js +189 -0
  145. package/dist/chunk-PUIRNYI7.js.map +1 -0
  146. package/dist/chunk-PWQVGAJB.js +144 -0
  147. package/dist/chunk-PWQVGAJB.js.map +1 -0
  148. package/dist/chunk-R36V2VP7.js +169 -0
  149. package/dist/chunk-R36V2VP7.js.map +1 -0
  150. package/dist/chunk-R67DFVLO.js +142 -0
  151. package/dist/chunk-R67DFVLO.js.map +1 -0
  152. package/dist/chunk-SEVJPLZC.js +260 -0
  153. package/dist/chunk-SEVJPLZC.js.map +1 -0
  154. package/dist/chunk-SYDW647C.js +318 -0
  155. package/dist/chunk-SYDW647C.js.map +1 -0
  156. package/dist/chunk-TBKU5XQI.js +228 -0
  157. package/dist/chunk-TBKU5XQI.js.map +1 -0
  158. package/dist/chunk-TSWPNOYM.js +147 -0
  159. package/dist/chunk-TSWPNOYM.js.map +1 -0
  160. package/dist/chunk-UAIOHCUK.js +27 -0
  161. package/dist/chunk-UAIOHCUK.js.map +1 -0
  162. package/dist/chunk-UPAMRDX4.js +233 -0
  163. package/dist/chunk-UPAMRDX4.js.map +1 -0
  164. package/dist/chunk-VQRS7VUC.js +342 -0
  165. package/dist/chunk-VQRS7VUC.js.map +1 -0
  166. package/dist/chunk-X3BTXCJ4.js +262 -0
  167. package/dist/chunk-X3BTXCJ4.js.map +1 -0
  168. package/dist/chunk-X5YKXC6V.js +211 -0
  169. package/dist/chunk-X5YKXC6V.js.map +1 -0
  170. package/dist/chunk-Y6O2OCUO.js +130 -0
  171. package/dist/chunk-Y6O2OCUO.js.map +1 -0
  172. package/dist/chunk-YCGY7UIZ.js +208 -0
  173. package/dist/chunk-YCGY7UIZ.js.map +1 -0
  174. package/dist/chunk-Z7ML6L77.js +162 -0
  175. package/dist/chunk-Z7ML6L77.js.map +1 -0
  176. package/dist/chunk-ZEWMTR5M.js +136 -0
  177. package/dist/chunk-ZEWMTR5M.js.map +1 -0
  178. package/dist/index.d.ts +355 -0
  179. package/dist/index.js +1908 -0
  180. package/dist/index.js.map +1 -0
  181. package/package.json +26 -9
  182. package/scripts/run-package-tests.mjs +30 -8
  183. package/scripts/verify-packed-consumer.mjs +12 -1
  184. package/scripts/verify-pier-agent.mts +1 -0
  185. package/src/benchmarks/humaneval.test.mts +122 -0
  186. package/src/benchmarks/humaneval.ts +100 -27
  187. package/src/david-attribution.mts +78 -0
  188. package/src/david-goliath.mts +149 -0
  189. package/src/hev-improve.mts +25 -6
  190. package/src/humaneval-object-ablation.mts +201 -0
  191. package/src/live-improve-campaign-mbpp.mts +641 -0
  192. package/src/live-improve-campaign.mts +500 -0
  193. package/src/mbpp-structural.mts +12 -7
  194. package/src/stream-observe.py +45 -0
  195. package/src/stream-observe.tpl.html +247 -0
  196. package/src/supervisor-arena.mts +816 -0
  197. package/src/swe-arena/analyze.ts +211 -0
  198. package/src/swe-arena/arms.ts +788 -0
  199. package/src/swe-arena/bootstrap-meta.mts +188 -0
  200. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  201. package/src/swe-arena/calibrate.ts +116 -0
  202. package/src/swe-arena/capabilities.mts +76 -0
  203. package/src/swe-arena/capabilities.test.mts +57 -0
  204. package/src/swe-arena/capacity.ts +194 -0
  205. package/src/swe-arena/cell-evidence.mts +405 -0
  206. package/src/swe-arena/cell-evidence.test.mts +248 -0
  207. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  208. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  209. package/src/swe-arena/execution.test.mts +1170 -0
  210. package/src/swe-arena/fixtures/analyze.py +80 -0
  211. package/src/swe-arena/fixtures/excludes.txt +8 -0
  212. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  213. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  214. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  215. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  216. package/src/swe-arena/fixtures/holdout.json +44 -0
  217. package/src/swe-arena/fixtures/instances.json +146 -0
  218. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  219. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  220. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  221. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  222. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  223. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  224. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  225. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  226. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  227. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  228. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  229. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  230. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  231. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  232. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  233. package/src/swe-arena/fixtures.ts +104 -0
  234. package/src/swe-arena/holdout-certify.mts +408 -0
  235. package/src/swe-arena/holdout-certify.test.mts +160 -0
  236. package/src/swe-arena/judge-child.mts +37 -0
  237. package/src/swe-arena/manifest.mts +293 -0
  238. package/src/swe-arena/manifest.test.mts +169 -0
  239. package/src/swe-arena/materialize.ts +142 -0
  240. package/src/swe-arena/outer-loop.mts +2145 -0
  241. package/src/swe-arena/outer-loop.test.mts +696 -0
  242. package/src/swe-arena/parity.test.mts +87 -0
  243. package/src/swe-arena/proc.test.mts +174 -0
  244. package/src/swe-arena/proc.ts +260 -0
  245. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  246. package/src/swe-arena/proposer-fanout.mts +489 -0
  247. package/src/swe-arena/proposer-fanout.test.mts +372 -0
  248. package/src/swe-arena/reconcile.ts +0 -0
  249. package/src/swe-arena/replay.mts +183 -0
  250. package/src/swe-arena/replay.test.mts +300 -0
  251. package/src/swe-arena/run-experiment.mts +361 -0
  252. package/src/swe-arena/run-supervisor.mjs +297 -0
  253. package/src/swe-arena/run-supervisor.test.mts +498 -0
  254. package/src/swe-arena/serialized-judge.ts +414 -0
  255. package/src/swe-arena/types.ts +166 -0
  256. package/src/swe-code-improve.mts +328 -0
  257. package/src/swe-emit-patch.mts +104 -0
  258. package/src/swe-improve.mts +232 -0
  259. package/src/swe-jail.ts +2 -2
  260. package/src/swe-local-proof.mts +169 -0
  261. package/src/swe-repro-calibrate.mts +446 -0
  262. package/src/swe-stream.mts +1497 -0
@@ -0,0 +1,1497 @@
1
+ /**
2
+ * DAY 1 of the STREAM LOOP — the running two-arm stream over SWE-bench Verified
3
+ * (contract: supervisor-lab/docs/design/stream-loop.md; base: swe-structural.mts @ 393ee50b).
4
+ *
5
+ * Two arms, interleaved per instance (same slot, F then L):
6
+ * F (frozen-v0) — the swe-structural system config exactly: canary at open, Stage-0 repro reuse
7
+ * (REPRO_MANIFEST) re-verified on this substrate — else authored fresh with the
8
+ * calibrator's protocol — k=4 candidates, repro-argmax selection, ≤2 guarded
9
+ * repairs. No memory.
10
+ * L (learning) — byte-identical config PLUS (a) memory recall at open: searchKnowledge over a
11
+ * stream-local store (starts EMPTY) with repo+issue tokens, top-3 provenance-
12
+ * labeled notes injected via the promptAppendix seam; (b) after settle: a
13
+ * templated outcome-anchored note (fields from the Row only — NEVER judge text,
14
+ * NEVER FAIL_TO_PASS names) written via applyKnowledgeWriteBlocks; (c) a
15
+ * failure-class tally persisted in the ledger.
16
+ *
17
+ * Phase separation per instance: both arms' decisions LOCK, then the official judge
18
+ * (adapter.judge, serialized) grades that instance for both arms; judge outputs never reach any
19
+ * model context (the L note carries the resolved BOOLEAN only — a tier-2 anchor).
20
+ *
21
+ * HARD DRIVER REQUIREMENTS (each traces to a measured failure):
22
+ * 1. Per-instance wall-clock deadline (DEADLINE_MS, default 30min — Stage-1 hung 29h on one
23
+ * instance): a promise race around the per-instance arm pipeline; breach → error rows with
24
+ * partial receipts → next instance. The deadline reaches into the zai retry ladder
25
+ * (zaiChatRaw deadlineAt) so a written-off instance stops spending within one call.
26
+ * The judge runs OUTSIDE this race (first-judge env-image builds legitimately exceed 30min)
27
+ * under its own JUDGE_TIMEOUT_MS race, so no instance holds the stream either way.
28
+ * 2. Per-candidate turn cap (TURN_CAP, default 12 — Stage-1 spent 160 calls / 8.7M input tokens
29
+ * on ONE instance): enforced at the transport chokepoint; on breach the loop is ended with a
30
+ * synthetic no-tool-call completion and the candidate is scored AS-IS from whatever diff
31
+ * exists (capBreached in receipts).
32
+ * 3. zai discipline: conc ≤2 TOTAL (instance workers are the only callers; attempts are serial
33
+ * within an instance), 429 ladder 60/120/240s, client timeout 480s (swe-jail's zaiChatRaw).
34
+ * 4. Image pull→run→delete rotation: the fail-closed resolveImageForMetadata never pulls — this
35
+ * driver pulls explicitly, hard-asserts presence, and deletes after both arms + judge finish,
36
+ * EXCEPT images already cached at stream start (the keep-set). SWEBENCH_CACHE_LEVEL=instance
37
+ * stops the judge from deleting images behind the rotation's back.
38
+ *
39
+ * Ledger: STREAM_DIR/ledger.jsonl — one row per (instance × arm): streamIndex, arm,
40
+ * profileVersion, all swe-structural receipts, recall/write receipts, tally snapshot, cumulative
41
+ * resolved counts + $ per arm. events.jsonl records stream events incl. the day-1 STUB batch
42
+ * trigger: streamIndex % 25 === 0 → 'batch-look (stub)'. The divergence curve is a pure fold over
43
+ * the ledger, printed at the end.
44
+ *
45
+ * cd ~/company/devops/secrets && dotenvx run -f agent-state.env -- bash -c \
46
+ * 'cd ~/code/agent-runtime-swe && REPRO_MANIFEST=/path/manifest.json \
47
+ * STREAM_DIR=~/.swe-stream/day1 bench/node_modules/.bin/tsx bench/src/swe-stream.mts'
48
+ *
49
+ * Env: ZAI_API_KEY (required), ZAI_BASE, WORKER_MODEL=glm-4.5-air (both arms, all solve+repair
50
+ * calls), SUPERVISOR_MODEL=glm-5.2 (arm L repair advice ONLY — the stronger model),
51
+ * REPRO_MODEL=glm-5.2 (shared repro instrument — strong author, default = SUPERVISOR_MODEL),
52
+ * REASONING_EFFORT=enabled (SYMMETRIC thinking budget on the worker; 'off' disables),
53
+ * SUPERVISOR_MAX_TOKENS=12000, MAX_TOKENS=12000, K=4, REPAIRS=2,
54
+ * TEMP=0.8, INNER_TURNS=40, TURN_CAP=12, DEADLINE_MS=1800000, JUDGE_TIMEOUT_MS=2400000,
55
+ * CONC=2 (hard max 2 — zai discipline), REPRO_TIMEOUT=120 (s), LLM_TIMEOUT_MS=480000,
56
+ * SEED=0x5eed, IDS=comma-list (default: the 23-instance Stage-0 fingerprint set),
57
+ * STREAM_N=max instances this run, STREAM_DIR=state dir (ledger/events/kb),
58
+ * REPRO_MANIFEST=Stage-0 valid+sound repro scripts (optional — absent entries author fresh),
59
+ * PRICE_IN/PRICE_OUT (USD per Mtok, assumed zai list rate). Resume: by instance id from the
60
+ * ledger (an instance re-runs unless BOTH arm rows settled).
61
+ */
62
+ import { execFile } from 'node:child_process'
63
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
64
+ import { homedir } from 'node:os'
65
+ import { join } from 'node:path'
66
+ import { promisify } from 'node:util'
67
+ import { applyKnowledgeWriteBlocks, buildKnowledgeIndex, initKnowledgeBase, searchKnowledge } from '@tangle-network/agent-knowledge'
68
+ import type { AgenticSurface, AgenticTask, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/loops'
69
+ import { refine, runAgentic } from '@tangle-network/agent-runtime/loops'
70
+ import type { BenchTask } from './benchmarks/types'
71
+ import { createSweBenchEnvironment, resolveImageForMetadata, SWE_SEED_PROMPT } from './swe-bench-env'
72
+ import {
73
+ APPLY_SENTINEL,
74
+ assertNoHiddenLeak,
75
+ cachedInstanceIds,
76
+ extractReadRequests,
77
+ extractReproScript,
78
+ IMPORT_NAME,
79
+ importCanaryScript,
80
+ reproAuthorSystem,
81
+ runPyInJail,
82
+ tail,
83
+ zaiChatRaw,
84
+ } from './swe-jail'
85
+
86
+ const exec = promisify(execFile)
87
+
88
+ // The judge must not delete instance images behind the rotation's back (its default 'env' cache
89
+ // level removes them after every run — measured: it pruned the 23-image fingerprint cache).
90
+ process.env.SWEBENCH_CACHE_LEVEL ??= 'instance'
91
+
92
+ // ---------- config ----------
93
+
94
+ const ZAI_BASE = process.env.ZAI_BASE ?? 'https://api.z.ai/api/coding/paas/v4'
95
+ const ZAI_KEY = process.env.ZAI_API_KEY ?? ''
96
+ if (!ZAI_KEY) throw new Error('ZAI_API_KEY required (run under dotenvx: agent-state.env)')
97
+ // WORKER runs every solve+repair call in BOTH arms — glm-4.5-air: cheap, with SWE headroom.
98
+ // SUPERVISOR (Step 2) runs arm L's repair-advice call ONLY — a STRONGER model (glm-5.2). The
99
+ // strength GAP over the worker is what recreates the arm-C win; a same-model pair is the arm-B null.
100
+ // MODEL kept as a back-compat alias defaulting to the worker (older invocations set MODEL=…).
101
+ const WORKER_MODEL = process.env.WORKER_MODEL ?? process.env.MODEL ?? 'glm-4.5-air'
102
+ // Both glm-4.5-air and glm-5.2 are reasoning models: hidden reasoning consumes max_tokens, so a
103
+ // small budget starves `content`. 12000 leaves ample headroom (probe: worker+thinking = 1905 out).
104
+ const MAX_TOKENS = Number(process.env.MAX_TOKENS ?? 12_000)
105
+ // Reasoning budget — SYMMETRIC across F and L (injected at the shared worker chokepoint, so both
106
+ // arms send byte-identical bodies). The zai coding endpoint HONORS `thinking:{type:'enabled'}` on
107
+ // TOOLLESS completions (probe 2026-07-12: glm-4.5-air reasoning 831→1820 tok, +119%) and SILENTLY
108
+ // IGNORES `reasoning_effort` (200, zero lift), so this env toggles the thinking shape, not a level.
109
+ // CAVEAT (probe): with a `tools` array present the endpoint ignores `thinking` too (289→275 compl_
110
+ // tok), so this lever's real effect lands on the TOOLLESS calls — repro authoring + the supervisor
111
+ // — not the worker's tool-loop solve/repair turns. Kept on (harmless, 200) and symmetric regardless.
112
+ const REASONING_EFFORT = (process.env.REASONING_EFFORT ?? 'enabled').toLowerCase()
113
+ const REASONING_ON = ['enabled', 'on', 'thinking', 'true', '1', 'high', 'medium'].includes(REASONING_EFFORT)
114
+ const WORKER_REASONING: Record<string, unknown> = REASONING_ON ? { thinking: { type: 'enabled' } } : {}
115
+ // The SUPERVISOR (arm L repair advice only): a STRONGER model than the worker. glm-5.2 reasons hard
116
+ // at baseline (probe: 733 reasoning tok, and the thinking knob did NOT lift it), so no knob is
117
+ // injected — the supervisor runs on its OWN token budget, wholly separate from the worker's, and
118
+ // changing SUPERVISOR_MAX_TOKENS never touches the worker's MAX_TOKENS / thinking budget.
119
+ const SUPERVISOR_MODEL = process.env.SUPERVISOR_MODEL ?? 'glm-5.2'
120
+ const SUPERVISOR_MAX_TOKENS = Number(process.env.SUPERVISOR_MAX_TOKENS ?? 12_000)
121
+ // The repro is a SHARED MEASUREMENT INSTRUMENT: it grades both arms' candidates AND defines the
122
+ // supervisor-fire predicate (severity===1). Authoring it with the weaker worker leaks worker
123
+ // weakness into the instrument (observed: a degraded-unsound matplotlib repro). So fresh authoring
124
+ // runs on the STRONG model — default = SUPERVISOR_MODEL (glm-5.2), the same author as the Stage-0
125
+ // manifest, so all 23 instances (reused + fresh) share one strong instrument. No thinking knob:
126
+ // Stage-0 authored without one, and glm-5.2 reasons at baseline. Worker solving stays glm-4.5-air.
127
+ const REPRO_MODEL = process.env.REPRO_MODEL ?? SUPERVISOR_MODEL
128
+ const K = Number(process.env.K ?? 4)
129
+ const REPAIRS = Number(process.env.REPAIRS ?? 2)
130
+ // NOT `TEMP`: Node's os.tmpdir() honors the TEMP env var as the temp DIRECTORY, so setting
131
+ // TEMP=0.8 made mkdtemp build a relative path "0.8/swe-repro-…" and every docker -v mount was
132
+ // rejected as an invalid volume name. Sampling temperature reads SAMPLE_TEMP (TEMP still accepted
133
+ // only if it parses as a number < 2, so a stray TEMP=/some/dir never leaks in as a temperature).
134
+ const TEMP = (() => {
135
+ const s = process.env.SAMPLE_TEMP ?? (process.env.TEMP && Number(process.env.TEMP) < 2 ? process.env.TEMP : undefined)
136
+ return Number(s ?? 0.8)
137
+ })()
138
+ const INNER_TURNS = Number(process.env.INNER_TURNS ?? 40)
139
+ const TURN_CAP = Number(process.env.TURN_CAP ?? 12)
140
+ const DEADLINE_MS = Number(process.env.DEADLINE_MS ?? 1_800_000)
141
+ const JUDGE_TIMEOUT_MS = Number(process.env.JUDGE_TIMEOUT_MS ?? 2_400_000)
142
+ const CONC = Math.max(1, Math.min(2, Number(process.env.CONC ?? 2)))
143
+ const REPRO_TIMEOUT_S = Number(process.env.REPRO_TIMEOUT ?? 120)
144
+ const LLM_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS ?? 480_000)
145
+ const SEED = Number(process.env.SEED ?? 0x5eed)
146
+ const STREAM_N = Number(process.env.STREAM_N ?? Number.POSITIVE_INFINITY)
147
+ const STREAM_DIR = process.env.STREAM_DIR ?? join(homedir(), '.swe-stream', 'day1')
148
+ const LEDGER = join(STREAM_DIR, 'ledger.jsonl')
149
+ const EVENTS = join(STREAM_DIR, 'events.jsonl')
150
+ const STORE = join(STREAM_DIR, 'kb')
151
+ const PROFILE_VERSION = 'v0'
152
+ // Cost-table rates, USD per Mtok. ASSUMED (zai coding-plan tokens have no per-call list price).
153
+ const PRICE_IN = Number(process.env.PRICE_IN ?? 0.6)
154
+ const PRICE_OUT = Number(process.env.PRICE_OUT ?? 2.2)
155
+
156
+ /**
157
+ * The Stage-0 fingerprint set: the 23 instances whose swebench eval images were cached locally at
158
+ * the PREREG-swe-frontier commit (enumerated then via `docker images`, recorded in the Stage-0
159
+ * canary sweep). The cache has since been pruned by the judge's old 'env' cache level, so the list
160
+ * is pinned HERE as the durable source — day-1 streams these first, re-pulling as needed.
161
+ */
162
+ const FINGERPRINT_23 = [
163
+ 'astropy__astropy-12907', 'astropy__astropy-13033', 'django__django-12419', 'django__django-13406',
164
+ 'django__django-14089', 'django__django-14534', 'django__django-16082', 'django__django-16429',
165
+ 'matplotlib__matplotlib-23314', 'pallets__flask-5014', 'psf__requests-1142', 'psf__requests-1921',
166
+ 'psf__requests-2931', 'pylint-dev__pylint-7080', 'pytest-dev__pytest-6202',
167
+ 'scikit-learn__scikit-learn-14053', 'scikit-learn__scikit-learn-14141', 'sphinx-doc__sphinx-8595',
168
+ 'sphinx-doc__sphinx-8721', 'sympy__sympy-13757', 'sympy__sympy-22914', 'sympy__sympy-23534',
169
+ 'sympy__sympy-23950',
170
+ ] as const
171
+
172
+ interface ReproManifestEntry {
173
+ script: string
174
+ source: string
175
+ stage0Class: string
176
+ }
177
+
178
+ const MANIFEST: Record<string, ReproManifestEntry> = (() => {
179
+ const p = process.env.REPRO_MANIFEST
180
+ if (!p) return {}
181
+ return JSON.parse(readFileSync(p, 'utf8')) as Record<string, ReproManifestEntry>
182
+ })()
183
+
184
+ // ---------- seeded shuffle (mulberry32 — deterministic stream order) ----------
185
+
186
+ function seededShuffle<T>(arr: readonly T[], seed: number): T[] {
187
+ const a = [...arr]
188
+ let s = seed >>> 0
189
+ const rnd = (): number => {
190
+ s = (s + 0x6d2b79f5) >>> 0
191
+ let t = Math.imul(s ^ (s >>> 15), 1 | s)
192
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
193
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
194
+ }
195
+ for (let i = a.length - 1; i > 0; i -= 1) {
196
+ const j = Math.floor(rnd() * (i + 1))
197
+ ;[a[i], a[j]] = [a[j] as T, a[i] as T]
198
+ }
199
+ return a
200
+ }
201
+
202
+ // ---------- transport: leak guard + turn cap + deadline at the single chokepoint ----------
203
+
204
+ interface Counter {
205
+ calls: number
206
+ httpAttempts: number
207
+ tokensIn: number
208
+ tokensOut: number
209
+ guardedMsgs: number
210
+ }
211
+
212
+ const newCounter = (): Counter => ({ calls: 0, httpAttempts: 0, tokensIn: 0, tokensOut: 0, guardedMsgs: 0 })
213
+
214
+ const addInto = (into: Counter, from: Counter): void => {
215
+ into.calls += from.calls
216
+ into.httpAttempts += from.httpAttempts
217
+ into.tokensIn += from.tokensIn
218
+ into.tokensOut += from.tokensOut
219
+ into.guardedMsgs += from.guardedMsgs
220
+ }
221
+
222
+ /** Distinctive content marks for the leak guard: the first substantive ADDED line of the gold patch
223
+ * and of the hidden test patch. Never shown to any model; used only to refuse outbound messages. */
224
+ function leakMarks(md: Record<string, string>): string[] {
225
+ const marks: string[] = []
226
+ for (const src of [md.patch, md.test_patch]) {
227
+ const m = String(src ?? '')
228
+ .split('\n')
229
+ .find((l) => l.startsWith('+') && !l.startsWith('+++') && l.trim().length > 12)
230
+ ?.slice(0, 80)
231
+ if (m) marks.push(m)
232
+ }
233
+ return marks
234
+ }
235
+
236
+ /** FAIL_TO_PASS test node ids for an instance — the hidden-grading vocabulary the L arm's memory
237
+ * must never carry forward. Full pytest node ids (file::class::test) are distinctive enough to
238
+ * serve as leak-guard marks without false positives. */
239
+ function failToPassNames(md: Record<string, unknown>): string[] {
240
+ const raw = md.FAIL_TO_PASS
241
+ try {
242
+ const list = Array.isArray(raw) ? raw : (JSON.parse(String(raw ?? '[]')) as unknown[])
243
+ return list.map((x) => String(x)).filter((s) => s.length >= 8)
244
+ } catch {
245
+ return []
246
+ }
247
+ }
248
+
249
+ /** Per-attempt enforcement state threaded into the transport. */
250
+ interface AttemptGuard {
251
+ deadlineAt: number
252
+ turnCap: number
253
+ capBreached: boolean
254
+ }
255
+
256
+ /** Synthetic no-tool-call completion: ends the tool loop cleanly on a turn-cap breach so the
257
+ * candidate is scored AS-IS from whatever diff exists (requirement 2). Zero usage — no real call. */
258
+ const CAP_COMPLETION = {
259
+ choices: [{ message: { role: 'assistant', content: 'TURN CAP REACHED — finalize with the current state.' }, finish_reason: 'stop' }],
260
+ usage: { prompt_tokens: 0, completion_tokens: 0 },
261
+ }
262
+
263
+ /** Every model call flows through here (runAgentic's `complete` seam): judge separation asserted,
264
+ * turn cap + per-instance deadline enforced, usage counted — once, at the single chokepoint. */
265
+ const makeTransport =
266
+ (marks: readonly string[], counter: Counter, guard: AttemptGuard) =>
267
+ async (body: Record<string, unknown>): Promise<unknown> => {
268
+ if (Date.now() >= guard.deadlineAt) throw new Error('DEADLINE: per-instance wall clock exhausted')
269
+ if (counter.calls >= guard.turnCap) {
270
+ guard.capBreached = true
271
+ return CAP_COMPLETION
272
+ }
273
+ const msgs = (body.messages ?? []) as Array<{ role?: string; content?: unknown }>
274
+ counter.guardedMsgs += assertNoHiddenLeak(marks, msgs)
275
+ // Inject the honored reasoning-budget knob (thinking) here at the single shared worker
276
+ // chokepoint: makeTransport is byte-identical across arms F and L, so the budget is symmetric.
277
+ const { json, attempts } = await zaiChatRaw(
278
+ { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt: guard.deadlineAt },
279
+ { ...body, ...WORKER_REASONING },
280
+ )
281
+ counter.calls += 1
282
+ counter.httpAttempts += attempts
283
+ const u = (json as { usage?: { prompt_tokens?: number; completion_tokens?: number } }).usage
284
+ counter.tokensIn += u?.prompt_tokens ?? 0
285
+ counter.tokensOut += u?.completion_tokens ?? 0
286
+ return json
287
+ }
288
+
289
+ // ---------- one emit-patch attempt (swe-structural's protocol + cap/deadline receipts) ----------
290
+
291
+ interface AttemptOut {
292
+ diff: string
293
+ completions: number
294
+ tokensIn: number
295
+ tokensOut: number
296
+ calls: number
297
+ wallMs: number
298
+ capBreached: boolean
299
+ error?: string
300
+ }
301
+
302
+ async function emitAttempt(
303
+ environment: AgenticSurface,
304
+ bt: BenchTask,
305
+ cfg: {
306
+ temperature: number
307
+ marks: readonly string[]
308
+ instanceCounter: Counter
309
+ deadlineAt: number
310
+ preApply?: string
311
+ promptAppendix?: string
312
+ },
313
+ ): Promise<AttemptOut> {
314
+ const t0 = Date.now()
315
+ const counter = newCounter()
316
+ const guard: AttemptGuard = { deadlineAt: cfg.deadlineAt, turnCap: TURN_CAP, capBreached: false }
317
+ // Capture the patch from inside score() (called during the refine loop, BEFORE the surface closes
318
+ // and rms the checkout). Keep the LATEST non-empty diff — the emit-patch pattern.
319
+ const capture = { patch: '' }
320
+ const proxy: AgenticSurface = {
321
+ ...environment,
322
+ async open(t: AgenticTask): Promise<ArtifactHandle> {
323
+ const h = await environment.open(t)
324
+ const pre = cfg.preApply
325
+ if (pre?.trim()) {
326
+ const f = join(h.id, '.swe-preapply.diff')
327
+ writeFileSync(f, pre.endsWith('\n') ? pre : `${pre}\n`)
328
+ try {
329
+ await exec('git', ['-C', h.id, 'apply', '--whitespace=nowarn', f], { timeout: 60_000 })
330
+ } finally {
331
+ rmSync(f, { force: true })
332
+ }
333
+ }
334
+ return h
335
+ },
336
+ async score(_t: AgenticTask, handle: ArtifactHandle): Promise<SurfaceScore> {
337
+ try {
338
+ const d = await exec('git', ['-C', handle.id, 'diff'], { maxBuffer: 40_000_000, timeout: 60_000 })
339
+ if (d.stdout.trim()) capture.patch = d.stdout
340
+ } catch {
341
+ /* workspace gone or git error → keep whatever we already captured */
342
+ }
343
+ return { passes: capture.patch.trim() ? 1 : 0, total: 1, errored: 0 }
344
+ },
345
+ }
346
+ const task: AgenticTask = {
347
+ id: bt.id,
348
+ systemPrompt: SWE_SEED_PROMPT,
349
+ userPrompt: cfg.promptAppendix ? `${bt.prompt}\n\n${cfg.promptAppendix}` : bt.prompt,
350
+ meta: { instanceId: bt.id },
351
+ }
352
+ let error: string | undefined
353
+ try {
354
+ const r = await runAgentic({
355
+ surface: proxy,
356
+ task,
357
+ strategy: refine,
358
+ routerBaseUrl: 'zai-direct', // unused: the `complete` transport short-circuits the router
359
+ routerKey: 'zai-direct',
360
+ model: WORKER_MODEL,
361
+ maxTokens: MAX_TOKENS,
362
+ temperature: cfg.temperature,
363
+ innerTurns: INNER_TURNS,
364
+ budget: 1,
365
+ complete: makeTransport(cfg.marks, counter, guard),
366
+ })
367
+ addInto(cfg.instanceCounter, counter)
368
+ return {
369
+ diff: capture.patch, completions: r.completions, tokensIn: counter.tokensIn,
370
+ tokensOut: counter.tokensOut, calls: counter.calls, wallMs: Date.now() - t0,
371
+ capBreached: guard.capBreached,
372
+ }
373
+ } catch (e) {
374
+ error = e instanceof Error ? e.message.slice(0, 300) : String(e).slice(0, 300)
375
+ addInto(cfg.instanceCounter, counter)
376
+ return {
377
+ diff: capture.patch, completions: 0, tokensIn: counter.tokensIn, tokensOut: counter.tokensOut,
378
+ calls: counter.calls, wallMs: Date.now() - t0, capBreached: guard.capBreached, error,
379
+ }
380
+ }
381
+ }
382
+
383
+ // ---------- in-image candidate scoring (swe-structural verbatim) ----------
384
+
385
+ interface CandScore {
386
+ applyOk: boolean | null
387
+ exit: number | null
388
+ timedOut: boolean
389
+ severity: number
390
+ out: string
391
+ }
392
+
393
+ const EMPTY_SCORE: CandScore = { applyOk: null, exit: null, timedOut: false, severity: 4, out: '' }
394
+ const UNSCORED: CandScore = { applyOk: null, exit: null, timedOut: false, severity: 1, out: '(no repro signal)' }
395
+
396
+ async function scoreCandidate(imageTag: string, repro: string | null, diff: string): Promise<CandScore> {
397
+ if (!diff.trim()) return EMPTY_SCORE
398
+ if (!repro) return UNSCORED
399
+ const r = await runPyInJail(imageTag, null, repro, diff, { timeoutS: REPRO_TIMEOUT_S })
400
+ if (r.infraError) throw new Error(r.infraError)
401
+ const applyOk = r.out.includes(APPLY_SENTINEL)
402
+ if (!applyOk) return { applyOk, exit: r.code, timedOut: r.timedOut, severity: 3, out: tail(r.out, 800) }
403
+ if (r.timedOut) return { applyOk, exit: r.code, timedOut: true, severity: 2, out: tail(r.out, 800) }
404
+ return { applyOk, exit: r.code, timedOut: false, severity: r.code === 0 ? 0 : 1, out: tail(r.out, 1_500) }
405
+ }
406
+
407
+ // ---------- rows ----------
408
+
409
+ interface CandidateRow {
410
+ idx: number
411
+ diff: string
412
+ diffBytes: number
413
+ completions: number
414
+ calls: number
415
+ tokensIn: number
416
+ tokensOut: number
417
+ wallMs: number
418
+ capBreached: boolean
419
+ attemptError: string | null
420
+ applyOk: boolean | null
421
+ reproExit: number | null
422
+ reproTimedOut: boolean
423
+ severity: number
424
+ reproOutTail: string
425
+ }
426
+
427
+ interface RepairRow {
428
+ round: number
429
+ baseFrom: string
430
+ baseSeverity: number
431
+ diff: string
432
+ diffBytes: number
433
+ completions: number
434
+ calls: number
435
+ tokensIn: number
436
+ tokensOut: number
437
+ wallMs: number
438
+ capBreached: boolean
439
+ attemptError: string | null
440
+ applyOk: boolean | null
441
+ reproExit: number | null
442
+ severity: number
443
+ accepted: boolean
444
+ }
445
+
446
+ interface RecallReceipt {
447
+ query: string
448
+ storePages: number
449
+ hits: Array<{ path: string; rrfScore: number; streamIndex: number | null }>
450
+ injectedChars: number
451
+ }
452
+
453
+ interface WriteReceipt {
454
+ path: string
455
+ bytes: number
456
+ lintMarksChecked: number
457
+ written: boolean
458
+ error: string | null
459
+ }
460
+
461
+ /** Supervisor-steered-repair receipt (arm L only; null on F). `fired` is the VERIFIED-failure gate:
462
+ * true iff the selected candidate's diff APPLIED and the gold-verified repro still reports the bug
463
+ * (severity===1). `reason` records the gate decision either way. The evidence is built from
464
+ * execution-verified / model-visible inputs ONLY (issue, the candidate diff, the repro-output tail)
465
+ * — never FAIL_TO_PASS, never gold, never any worker self-report. */
466
+ interface SupervisorPlanReceipt {
467
+ fired: boolean
468
+ reason: string
469
+ model: string
470
+ groundedOnReproTail: boolean
471
+ evidenceChars: number
472
+ planRaw: string
473
+ plan: string
474
+ leaked: boolean
475
+ planCalls: number
476
+ planAttempts: number
477
+ planTokensIn: number
478
+ planTokensOut: number
479
+ }
480
+
481
+ interface Row {
482
+ streamIndex: number
483
+ arm: 'F' | 'L'
484
+ profileVersion: string
485
+ instanceId: string
486
+ repo: string
487
+ model: string
488
+ image: string | null
489
+ imagePulled: boolean
490
+ imagePullMs: number
491
+ execMode: 'image'
492
+ temperature: number
493
+ innerTurns: number
494
+ turnCap: number
495
+ deadlineMs: number
496
+ maxTokens: number
497
+ reasoningEffort: string
498
+ k: number
499
+ issueTitle: string
500
+ // canary + repro provenance (shared per instance, recorded on both arms)
501
+ canaryExit: number | null
502
+ canaryPass: boolean | null
503
+ reproSource: string
504
+ reproStatus: string
505
+ reproScript: string | null
506
+ reproPreExit: number | null
507
+ reproGoldExit: number | null
508
+ reproAuthorCalls: number
509
+ reproAuthorTokensIn: number
510
+ reproAuthorTokensOut: number
511
+ // candidates + selection + repair receipts
512
+ candidates: CandidateRow[]
513
+ selection: { mode: string; selectedIdx: number; movedOffFirst: boolean } | null
514
+ repairs: RepairRow[]
515
+ repairStop: string | null
516
+ finalFrom: string
517
+ finalDiff: string
518
+ capBreaches: number
519
+ deadlineHit: boolean
520
+ // memory receipts (L arm only; null on F)
521
+ recall: RecallReceipt | null
522
+ noteWrite: WriteReceipt | null
523
+ // supervisor-steered repair (L arm only; null on F)
524
+ supervisorPlan: SupervisorPlanReceipt | null
525
+ failureClass: string | null
526
+ tallySnapshot: Record<string, number> | null
527
+ marksDroppedDatasetText: number
528
+ // hidden judge (per-instance phase B; locked-after-decisions)
529
+ hiddenResolved: boolean | null
530
+ judgeDetail: string | null
531
+ judgeMs: number | null
532
+ judgeSkipped: string | null
533
+ // cost + guard receipts
534
+ llmCalls: number
535
+ httpAttempts: number
536
+ tokensIn: number
537
+ tokensOut: number
538
+ guardedMsgs: number
539
+ usd: number
540
+ wallMs: number
541
+ // cumulative (this arm, at append time — completion order; the curve refolds in stream order)
542
+ cumN: number
543
+ cumResolved: number
544
+ cumUsd: number
545
+ error?: string
546
+ }
547
+
548
+ function newRow(streamIndex: number, arm: 'F' | 'L', bt: BenchTask): Row {
549
+ const md = bt.metadata as Record<string, string>
550
+ const issueTitle = String(md.problem_statement ?? '').split('\n').map((l) => l.trim()).find(Boolean) ?? ''
551
+ return {
552
+ streamIndex, arm, profileVersion: PROFILE_VERSION, instanceId: bt.id, repo: md.repo, model: WORKER_MODEL,
553
+ image: null, imagePulled: false, imagePullMs: 0, execMode: 'image', temperature: TEMP,
554
+ innerTurns: INNER_TURNS, turnCap: TURN_CAP, deadlineMs: DEADLINE_MS,
555
+ maxTokens: MAX_TOKENS, reasoningEffort: REASONING_EFFORT, k: K,
556
+ issueTitle: issueTitle.slice(0, 200),
557
+ canaryExit: null, canaryPass: null, reproSource: 'none', reproStatus: 'none', reproScript: null,
558
+ reproPreExit: null, reproGoldExit: null, reproAuthorCalls: 0, reproAuthorTokensIn: 0,
559
+ reproAuthorTokensOut: 0, candidates: [], selection: null, repairs: [], repairStop: null,
560
+ finalFrom: 'none', finalDiff: '', capBreaches: 0, deadlineHit: false, recall: null,
561
+ noteWrite: null, supervisorPlan: null, failureClass: null, tallySnapshot: null, marksDroppedDatasetText: 0,
562
+ hiddenResolved: null, judgeDetail: null, judgeMs: null, judgeSkipped: null,
563
+ llmCalls: 0, httpAttempts: 0, tokensIn: 0, tokensOut: 0, guardedMsgs: 0, usd: 0, wallMs: 0,
564
+ cumN: 0, cumResolved: 0, cumUsd: 0,
565
+ }
566
+ }
567
+
568
+ const usdOf = (tokensIn: number, tokensOut: number): number =>
569
+ (tokensIn / 1e6) * PRICE_IN + (tokensOut / 1e6) * PRICE_OUT
570
+
571
+ // ---------- repro acquisition (manifest reuse re-verified — swe-structural verbatim — else fresh
572
+ // authoring with the calibrator's protocol; ONCE per instance, shared by both arms so the pairing
573
+ // stays tight; authoring usd is split 50/50 into each arm's cost track) ----------
574
+
575
+ interface ReproOut {
576
+ script: string | null
577
+ source: string
578
+ status: string
579
+ preExit: number | null
580
+ goldExit: number | null
581
+ authorCalls: number
582
+ authorTokensIn: number
583
+ authorTokensOut: number
584
+ }
585
+
586
+ async function acquireRepro(
587
+ env: Env,
588
+ bt: BenchTask,
589
+ imageTag: string,
590
+ marks: readonly string[],
591
+ deadlineAt: number,
592
+ ): Promise<ReproOut> {
593
+ const md = bt.metadata as Record<string, string>
594
+ const gold = String(md.patch ?? '')
595
+ const out: ReproOut = {
596
+ script: null, source: 'none', status: 'none', preExit: null, goldExit: null,
597
+ authorCalls: 0, authorTokensIn: 0, authorTokensOut: 0,
598
+ }
599
+
600
+ const verify = async (script: string): Promise<'ok' | 'degraded-timeout' | 'degraded-invalid' | 'degraded-unsound'> => {
601
+ const pre = await runPyInJail(imageTag, null, script, undefined, { timeoutS: REPRO_TIMEOUT_S })
602
+ if (pre.infraError) throw new Error(pre.infraError)
603
+ out.preExit = pre.code
604
+ if (pre.timedOut) return 'degraded-timeout'
605
+ if (pre.code === 0) return 'degraded-invalid'
606
+ const post = await runPyInJail(imageTag, null, script, gold, { timeoutS: REPRO_TIMEOUT_S })
607
+ if (post.infraError) throw new Error(post.infraError)
608
+ out.goldExit = post.code
609
+ return post.code === 0 && post.out.includes(APPLY_SENTINEL) ? 'ok' : 'degraded-unsound'
610
+ }
611
+
612
+ const manifest = MANIFEST[bt.id]
613
+ if (manifest) {
614
+ out.source = manifest.source
615
+ out.status = await verify(manifest.script)
616
+ if (out.status === 'ok') out.script = manifest.script
617
+ return out
618
+ }
619
+
620
+ // Fresh authoring — the calibrator's protocol (issue + listing, one optional read round, one
621
+ // validity retry), guarded by the same marks and the instance deadline. Model-visible inputs
622
+ // only; gold is used strictly script-side in verify().
623
+ out.source = 'stream-authored'
624
+ const handle = await env.environment.open({ id: bt.id, systemPrompt: '', userPrompt: '', meta: {} } as AgenticTask)
625
+ try {
626
+ const listing = String(await env.environment.call(handle, 'list_files', { dir: '' })).slice(0, 5_000)
627
+ const issue = String(md.problem_statement ?? '').slice(0, 20_000)
628
+ const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [
629
+ { role: 'system', content: reproAuthorSystem(REPRO_TIMEOUT_S) },
630
+ {
631
+ role: 'user',
632
+ content:
633
+ `Repository: ${md.repo} (checked out at the commit where the bug is PRESENT).\n\n` +
634
+ `Repository file listing (top levels):\n${listing}\n\n--- Issue ---\n${issue}\n\n--- Instructions ---\n` +
635
+ 'If you need to see specific source files before writing the script, reply with ONLY read requests, ' +
636
+ 'one per line, at most 3, in the form:\nREAD: path/relative/to/repo/root\n' +
637
+ 'Otherwise reply now with the final script in a single ```python fenced block.',
638
+ },
639
+ ]
640
+ const guardedComplete = async (): Promise<string> => {
641
+ if (Date.now() >= deadlineAt) throw new Error('DEADLINE: repro authoring abandoned')
642
+ assertNoHiddenLeak(marks, messages)
643
+ const { json } = await zaiChatRaw(
644
+ { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt },
645
+ { model: REPRO_MODEL, max_tokens: MAX_TOKENS, temperature: 0.2, messages },
646
+ )
647
+ const d = json as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } }
648
+ out.authorCalls += 1
649
+ out.authorTokensIn += d.usage?.prompt_tokens ?? 0
650
+ out.authorTokensOut += d.usage?.completion_tokens ?? 0
651
+ return d.choices?.[0]?.message?.content ?? ''
652
+ }
653
+
654
+ let resp = await guardedComplete()
655
+ let script = extractReproScript(resp)
656
+ if (!script) {
657
+ const reads = extractReadRequests(resp)
658
+ messages.push({ role: 'assistant', content: resp })
659
+ if (reads.length) {
660
+ const bodies: string[] = []
661
+ for (const p of reads) {
662
+ const c = String(await env.environment.call(handle, 'read_file', { path: p }))
663
+ bodies.push(`----- ${p} -----\n${c.slice(0, 12_000)}${c.length > 12_000 ? '\n…[truncated]' : ''}`)
664
+ }
665
+ messages.push({ role: 'user', content: `${bodies.join('\n\n')}\n\nNow reply with the final script in a single \`\`\`python fenced block.` })
666
+ } else {
667
+ messages.push({ role: 'user', content: 'Reply with ONLY the final Python script in a single ```python fenced block.' })
668
+ }
669
+ resp = await guardedComplete()
670
+ script = extractReproScript(resp)
671
+ }
672
+ if (!script) {
673
+ out.status = 'authoring-failed'
674
+ return out
675
+ }
676
+ out.status = await verify(script)
677
+ if (out.status === 'degraded-invalid' || out.status === 'degraded-timeout') {
678
+ // One retry with feedback, per the Stage-0 protocol.
679
+ const pre = await runPyInJail(imageTag, null, script, undefined, { timeoutS: REPRO_TIMEOUT_S })
680
+ const feedback = out.status === 'degraded-timeout'
681
+ ? `your script timed out after ${REPRO_TIMEOUT_S}s on the known-buggy code. Write a faster, simpler script that still detects the bug.`
682
+ : 'your script did not detect the bug on the known-buggy code: it exited 0 on the UNPATCHED repository. ' +
683
+ `Its output was:\n${tail(pre.out, 1_500)}\nWrite a corrected script that FAILS (nonzero exit) on the buggy code.`
684
+ messages.push({ role: 'assistant', content: `\`\`\`python\n${script}\n\`\`\`` }, { role: 'user', content: feedback })
685
+ const retry = await guardedComplete()
686
+ const script2 = extractReproScript(retry)
687
+ if (script2) {
688
+ script = script2
689
+ out.status = await verify(script2)
690
+ }
691
+ }
692
+ if (out.status === 'ok') out.script = script
693
+ return out
694
+ } finally {
695
+ await env.environment.close(handle).catch(() => {})
696
+ }
697
+ }
698
+
699
+ // ---------- supervisor-steered repair (arm L): a STRONGER model diagnoses the verified failure and
700
+ // hands the worker a no-code plan. Primitives replicated from supervisor-arena.mts (that file runs
701
+ // main() on import, so it cannot be imported) — evidence from execution-verified inputs only. ----------
702
+
703
+ /** Evidence for the supervisor: the issue, the worker's own candidate diff, and the tail of the
704
+ * gold-verified reproduction's output on that diff. Execution-verified / model-visible ONLY — never
705
+ * FAIL_TO_PASS, never the gold patch, never any worker self-report. Bounded to maxChars. */
706
+ function renderRepairEvidence(
707
+ issue: string,
708
+ candidateDiff: string,
709
+ reproTail: string,
710
+ reproExit: number | null,
711
+ maxChars: number,
712
+ failureKind: 'wrong-fix' | 'apply-failed' = 'wrong-fix',
713
+ ): string {
714
+ const header =
715
+ failureKind === 'apply-failed'
716
+ ? 'A bug was reported in an open-source Python repository. A programmer wrote a candidate patch, but the ' +
717
+ 'patch FAILED TO APPLY to the repository — its diff context/line numbers do not match the current source. ' +
718
+ 'The diff must be re-anchored to the real file contents. Only execution-verified evidence is shown.'
719
+ : 'A bug was reported in an open-source Python repository. A programmer wrote a candidate patch, but a ' +
720
+ 'REPRODUCTION SCRIPT (authored from the issue; verified to fail on the buggy code and pass on a correct ' +
721
+ 'fix) STILL FAILS after the patch is applied. Only execution-verified evidence is shown.'
722
+ const diffLabel =
723
+ failureKind === 'apply-failed'
724
+ ? "--- THE PROGRAMMER'S CANDIDATE PATCH (git diff — it did NOT apply; the context lines are stale/mismatched) ---"
725
+ : "--- THE PROGRAMMER'S CANDIDATE PATCH (git diff — it applied cleanly but did NOT fix the bug) ---"
726
+ const outLabel =
727
+ failureKind === 'apply-failed'
728
+ ? `--- git apply ERROR (exit ${reproExit ?? 'n/a'}) — which hunks failed and why ---`
729
+ : `--- REPRODUCTION OUTPUT after applying the patch (exit ${reproExit ?? 'n/a'}; nonzero = bug still present) ---`
730
+ const parts: string[] = [
731
+ header,
732
+ '',
733
+ '--- ISSUE ---',
734
+ issue.trim().slice(0, 12_000),
735
+ '',
736
+ diffLabel,
737
+ tail(candidateDiff.trim(), 8_000) || '(empty diff — the attempt produced no change)',
738
+ '',
739
+ outLabel,
740
+ tail(reproTail.trim(), 4_000) || '(no output captured)',
741
+ ]
742
+ let text = parts.join('\n')
743
+ if (text.length > maxChars) text = `${text.slice(0, maxChars)}\n…[evidence truncated at ${maxChars} chars]`
744
+ return text
745
+ }
746
+
747
+ /** The no-code contract (replicated from arena's planContract, adapted to a diff-shaped fix). */
748
+ const planContract =
749
+ 'Diagnose why the candidate patch failed to make the reproduction pass, then write a concise plan for the ' +
750
+ 'programmer: the specific mistake in the current patch, the correct root-cause approach, and edge cases to ' +
751
+ 'handle. You may name at most 3 short symbols or expressions. Do NOT write the fix: no fenced code blocks, no ' +
752
+ "lines starting with 'def ', and no raw diff/patch hunks. Keep the plan under 400 tokens."
753
+
754
+ const supervisorPrompt = (evidence: string): string =>
755
+ ["You are a senior engineer reviewing a junior programmer's failed bug-fix attempt.", '', evidence, '', planContract].join('\n')
756
+
757
+ /** Enforce the no-code contract: strip fenced blocks, `def ` lines (arena), and unambiguous patch
758
+ * headers (SWE hardening — `diff --git`/`@@`/`+++ `/`--- a|b/` never occur in legitimate plan prose,
759
+ * so this cannot eat bullets or narrative dashes). `leaked` is a measured signal, not a drop: the
760
+ * stripped plan is still used (matching arena — leak RATE is a pre-registered measurement). */
761
+ function stripPlanCode(raw: string): { plan: string; leaked: boolean } {
762
+ let leaked = false
763
+ let s = raw
764
+ if (s.includes('```')) {
765
+ const stripped = s.replace(/```[a-zA-Z]*[^\n]*\n?[\s\S]*?(?:```|$)/g, '')
766
+ if (stripped !== s) leaked = true
767
+ s = stripped
768
+ }
769
+ const lines = s.split('\n')
770
+ const kept = lines.filter((l) => !/^\s*def\s/.test(l) && !/^\s*(?:diff --git |@@ |\+\+\+ |--- [ab]\/)/.test(l))
771
+ if (kept.length !== lines.length) leaked = true
772
+ return { plan: kept.join('\n').trim(), leaked }
773
+ }
774
+
775
+ /** A held (non-firing) supervisor receipt — the verified-failure gate was NOT met. */
776
+ const heldSupervisorReceipt = (reason: string): SupervisorPlanReceipt => ({
777
+ fired: false, reason, model: SUPERVISOR_MODEL, groundedOnReproTail: false, evidenceChars: 0,
778
+ planRaw: '', plan: '', leaked: false, planCalls: 0, planAttempts: 0, planTokensIn: 0, planTokensOut: 0,
779
+ })
780
+
781
+ /** Fire ONCE per instance on the verified-failure gate: fetch the stronger model's no-code plan on
782
+ * its OWN token budget. `fired` stays true (the gate fired) even if the plan comes back empty / the
783
+ * guard trips / the call fails — those are recorded in `reason` and leave `plan` empty so the L
784
+ * repair simply proceeds raw (like F). The leak guard HOLDS rather than crashes. */
785
+ async function superviseRepair(
786
+ bt: BenchTask,
787
+ candidateDiff: string,
788
+ reproTail: string,
789
+ reproExit: number | null,
790
+ marks: readonly string[],
791
+ deadlineAt: number,
792
+ failureKind: 'wrong-fix' | 'apply-failed' = 'wrong-fix',
793
+ ): Promise<SupervisorPlanReceipt> {
794
+ const md = bt.metadata as Record<string, string>
795
+ const evidence = renderRepairEvidence(String(md.problem_statement ?? ''), candidateDiff, reproTail, reproExit, 14_000, failureKind)
796
+ const messages = [{ role: 'user' as const, content: supervisorPrompt(evidence) }]
797
+ const base: SupervisorPlanReceipt = {
798
+ fired: true, reason: 'ok', model: SUPERVISOR_MODEL, groundedOnReproTail: true, evidenceChars: evidence.length,
799
+ planRaw: '', plan: '', leaked: false, planCalls: 0, planAttempts: 0, planTokensIn: 0, planTokensOut: 0,
800
+ }
801
+ try {
802
+ assertNoHiddenLeak(marks, messages)
803
+ } catch {
804
+ return { ...base, reason: 'leak-guard-tripped' }
805
+ }
806
+ if (Date.now() >= deadlineAt) return { ...base, reason: 'deadline' }
807
+ try {
808
+ const { json, attempts } = await zaiChatRaw(
809
+ { base: ZAI_BASE, key: ZAI_KEY, timeoutMs: LLM_TIMEOUT_MS, deadlineAt },
810
+ { model: SUPERVISOR_MODEL, max_tokens: SUPERVISOR_MAX_TOKENS, temperature: 0.2, messages },
811
+ )
812
+ const d = json as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } }
813
+ const planRaw = d.choices?.[0]?.message?.content ?? ''
814
+ const { plan, leaked } = stripPlanCode(planRaw)
815
+ return {
816
+ ...base, reason: plan.trim() ? 'ok' : 'empty-plan', planRaw, plan, leaked, planCalls: 1, planAttempts: attempts,
817
+ planTokensIn: d.usage?.prompt_tokens ?? 0, planTokensOut: d.usage?.completion_tokens ?? 0,
818
+ }
819
+ } catch (e) {
820
+ return { ...base, reason: `supervisor-call-failed: ${(e instanceof Error ? e.message : String(e)).slice(0, 120)}` }
821
+ }
822
+ }
823
+
824
+ // ---------- one arm: k candidates → argmax → guarded repair (swe-structural steps 3-5) ----------
825
+
826
+ type Env = Awaited<ReturnType<typeof createSweBenchEnvironment>>
827
+
828
+ async function runArm(
829
+ env: Env,
830
+ bt: BenchTask,
831
+ row: Row,
832
+ repro: string | null,
833
+ marks: readonly string[],
834
+ deadlineAt: number,
835
+ promptAppendix: string | undefined,
836
+ counter: Counter,
837
+ supervise: boolean,
838
+ ): Promise<void> {
839
+ const imageTag = row.image as string
840
+ const diffs: string[] = []
841
+ for (let i = 0; i < K; i += 1) {
842
+ const a = await emitAttempt(env.environment, bt, {
843
+ temperature: TEMP, marks, instanceCounter: counter, deadlineAt,
844
+ ...(promptAppendix ? { promptAppendix } : {}),
845
+ })
846
+ diffs.push(a.diff)
847
+ if (a.capBreached) row.capBreaches += 1
848
+ row.candidates.push({
849
+ idx: i, diff: a.diff, diffBytes: a.diff.length, completions: a.completions, calls: a.calls,
850
+ tokensIn: a.tokensIn, tokensOut: a.tokensOut, wallMs: a.wallMs, capBreached: a.capBreached,
851
+ attemptError: a.error ?? null, applyOk: null, reproExit: null, reproTimedOut: false,
852
+ severity: -1, reproOutTail: '',
853
+ })
854
+ }
855
+
856
+ const scores: CandScore[] = []
857
+ for (let i = 0; i < K; i += 1) {
858
+ const s = await scoreCandidate(imageTag, repro, diffs[i] as string)
859
+ scores.push(s)
860
+ const cand = row.candidates[i] as CandidateRow
861
+ cand.applyOk = s.applyOk
862
+ cand.reproExit = s.exit
863
+ cand.reproTimedOut = s.timedOut
864
+ cand.severity = s.severity
865
+ cand.reproOutTail = s.out
866
+ }
867
+ let selectedIdx: number
868
+ let mode: string
869
+ if (repro) {
870
+ mode = 'repro-argmax'
871
+ selectedIdx = 0
872
+ for (let i = 1; i < K; i += 1) {
873
+ if ((scores[i] as CandScore).severity < (scores[selectedIdx] as CandScore).severity) selectedIdx = i
874
+ }
875
+ } else {
876
+ mode = 'blind-first'
877
+ const firstNonEmpty = diffs.findIndex((d) => d.trim().length > 0)
878
+ selectedIdx = firstNonEmpty === -1 ? 0 : firstNonEmpty
879
+ }
880
+ row.selection = { mode, selectedIdx, movedOffFirst: mode === 'repro-argmax' && selectedIdx !== 0 }
881
+
882
+ let best = { diff: diffs[selectedIdx] as string, score: scores[selectedIdx] as CandScore, from: `candidate:${selectedIdx}` }
883
+ if (!repro) {
884
+ row.repairStop = 'no-signal'
885
+ if (supervise) row.supervisorPlan = heldSupervisorReceipt('no-repro')
886
+ } else if (best.score.severity === 0) {
887
+ row.repairStop = 'already-passing'
888
+ if (supervise) row.supervisorPlan = heldSupervisorReceipt('already-passing')
889
+ } else {
890
+ // ── Supervisor gate (arm L only): fire ONCE on a VERIFIED failure — the selected diff APPLIED
891
+ // and the gold-verified repro STILL reports the bug (severity===1) — then reuse the stronger
892
+ // model's no-code plan across every repair round. Arm F is never supervised (raw failure =
893
+ // control); severity 2 (repro timeout) / 3 (apply failed) are HELD, not steered. ──
894
+ let supPreamble = ''
895
+ if (supervise) {
896
+ // Fire on any EXECUTION-VERIFIED failure the supervisor can act on: severity 1 = patch applied
897
+ // but the gold-verified repro still reports the bug (wrong fix); severity 3 = patch FAILED TO
898
+ // APPLY (git apply objectively rejected it — the weak worker's DOMINANT failure, and stale-diff
899
+ // re-anchoring is exactly where a stronger reviewer helps). Both are objective, non-credulous
900
+ // signals. Held: severity 0 (already passing), 2 (repro timeout — ambiguous), 4 (empty diff —
901
+ // nothing to advise on).
902
+ const failureKind =
903
+ best.score.severity === 3 ? ('apply-failed' as const) : best.score.severity === 4 ? ('empty-diff' as const) : ('wrong-fix' as const)
904
+ // Fire on every OBJECTIVE, execution-verified failure a plan can act on: 1=applied-but-wrong,
905
+ // 3=apply-failed (stale diff), 4=empty-diff (worker produced nothing — the case a plan helps
906
+ // MOST). Hold only 0 (already passing) and 2 (repro-timeout — ambiguous, not a clean signal).
907
+ const gateOk = best.score.severity === 1 || best.score.severity === 3 || best.score.severity === 4
908
+ if (!gateOk) {
909
+ const reason = best.score.severity === 2 ? 'repro-timeout' : `severity-${best.score.severity}`
910
+ row.supervisorPlan = heldSupervisorReceipt(reason)
911
+ logEvent('supervisor-held', { streamIndex: row.streamIndex, instanceId: row.instanceId, arm: row.arm, reason })
912
+ } else {
913
+ const sp = await superviseRepair(bt, best.diff, best.score.out, best.score.exit, marks, deadlineAt, failureKind)
914
+ row.supervisorPlan = sp
915
+ logEvent('supervisor-fired', {
916
+ streamIndex: row.streamIndex, instanceId: row.instanceId, arm: row.arm,
917
+ reason: sp.reason, leaked: sp.leaked, planTokensIn: sp.planTokensIn, planTokensOut: sp.planTokensOut,
918
+ })
919
+ if (sp.plan.trim()) {
920
+ supPreamble =
921
+ "--- SUPERVISOR DIAGNOSIS (a stronger reviewer's no-code plan for why the fix failed; weigh it " +
922
+ 'against the code — it may be wrong) ---\n' +
923
+ `${sp.plan}\n--- END SUPERVISOR DIAGNOSIS ---\n\n`
924
+ }
925
+ }
926
+ }
927
+ for (let round = 1; round <= REPAIRS && best.score.severity > 0; round += 1) {
928
+ if (Date.now() >= deadlineAt) throw new Error('DEADLINE: per-instance wall clock exhausted (repair)')
929
+ const appendix =
930
+ supPreamble +
931
+ (best.diff.trim()
932
+ ? `--- PREVIOUS FIX (already applied to this checkout) ---\n${tail(best.diff, 8_000)}\n\n`
933
+ : '--- NO FIX APPLIED YET (every prior attempt produced no change) ---\n\n') +
934
+ `--- REPRODUCTION SCRIPT (written from the issue; exit 0 = fixed) ---\n${tail(repro, 6_000)}\n\n` +
935
+ `--- REPRODUCTION OUTPUT on the current state (exit ${best.score.exit ?? 'n/a'}) ---\n${best.score.out}\n\n` +
936
+ '--- REPAIR INSTRUCTIONS ---\n' +
937
+ 'The reproduction above STILL FAILS. Diagnose why the current state does not resolve the issue, ' +
938
+ 'then correct the SOURCE with minimal edit_file changes (you may revise or revert parts of the ' +
939
+ 'previous fix — it is already in the files). Do not modify tests.'
940
+ const a = await emitAttempt(env.environment, bt, {
941
+ temperature: TEMP, marks, instanceCounter: counter, deadlineAt, preApply: best.diff, promptAppendix: appendix,
942
+ })
943
+ if (a.capBreached) row.capBreaches += 1
944
+ const ns = await scoreCandidate(imageTag, repro, a.diff)
945
+ const accepted = ns.severity < best.score.severity
946
+ row.repairs.push({
947
+ round, baseFrom: best.from, baseSeverity: best.score.severity, diff: a.diff, diffBytes: a.diff.length,
948
+ completions: a.completions, calls: a.calls, tokensIn: a.tokensIn, tokensOut: a.tokensOut,
949
+ wallMs: a.wallMs, capBreached: a.capBreached, attemptError: a.error ?? null, applyOk: ns.applyOk,
950
+ reproExit: ns.exit, severity: ns.severity, accepted,
951
+ })
952
+ if (accepted) best = { diff: a.diff, score: ns, from: `repair:${round}` }
953
+ }
954
+ row.repairStop = best.score.severity === 0 ? 'repaired-pass' : 'rounds-exhausted'
955
+ }
956
+ row.finalDiff = best.diff
957
+ row.finalFrom = best.from
958
+ }
959
+
960
+ // ---------- memory: recall at open, templated note after settle (L arm only) ----------
961
+
962
+ async function recallForInstance(bt: BenchTask): Promise<{ appendix?: string; receipt: RecallReceipt }> {
963
+ const md = bt.metadata as Record<string, string>
964
+ const issue = String(md.problem_statement ?? '')
965
+ const title = issue.split('\n').map((l) => l.trim()).find(Boolean) ?? ''
966
+ const query = `${md.repo} ${md.repo.split('/').join(' ')} ${title} ${issue.slice(0, 400)}`
967
+ const index = await buildKnowledgeIndex(STORE)
968
+ const receipt: RecallReceipt = { query: query.slice(0, 300), storePages: index.pages.length, hits: [], injectedChars: 0 }
969
+ if (index.pages.length === 0) return { receipt }
970
+ const hits = searchKnowledge(index, query, 3)
971
+ if (!hits.length) return { receipt }
972
+ const parts: string[] = [
973
+ '--- PRIOR EXPERIENCE (notes recalled from this stream\'s own earlier tasks; provenance-labeled; ' +
974
+ 'they may or may not apply — weigh them against the issue) ---',
975
+ ]
976
+ for (const h of hits) {
977
+ const si = Number((h.page.frontmatter as Record<string, unknown>).streamIndex ?? Number.NaN)
978
+ receipt.hits.push({ path: h.page.path, rrfScore: h.rrfScore, streamIndex: Number.isNaN(si) ? null : si })
979
+ parts.push(`\n[note ${h.rank}] source=${h.page.path} rrf=${h.rrfScore.toFixed(4)}\n${h.page.text.trim().slice(0, 1_200)}`)
980
+ }
981
+ parts.push('--- END PRIOR EXPERIENCE ---')
982
+ const appendix = parts.join('\n')
983
+ receipt.injectedChars = appendix.length
984
+ return { appendix, receipt }
985
+ }
986
+
987
+ /** Fixed per-class lesson lines — templated (class → string), so the note stays "fields from the
988
+ * Row only": no judge text, no free-form model text, no hidden-test vocabulary. */
989
+ const CLASS_LESSON: Record<string, string> = {
990
+ 'repro-pass-resolved': 'repro-verified fix confirmed by the official grade — the repro was a faithful check here.',
991
+ 'repro-pass-but-unresolved': 'the repro passed but the official grade failed — the reproduction under-covered the hidden requirement; prefer root-cause edits and re-check adjacent behaviors, not just the literal symptom.',
992
+ 'resolved-despite-repro-fail': 'the official grade passed although the repro still failed — the repro was measuring something stricter or adjacent; treat repro failures as advisory, not fatal.',
993
+ 'repro-fail-unresolved': 'no candidate made the repro pass and the official grade failed — candidates likely missed the root cause; explore more files before editing.',
994
+ 'no-repro-resolved': 'resolved with no repro signal (blind-first selection) — the first non-empty candidate was good.',
995
+ 'no-repro-unresolved': 'no repro signal and the official grade failed — without a visible check, selection was blind; invest in a better reproduction next time.',
996
+ error: 'the instance errored before settling — receipts are partial.',
997
+ }
998
+
999
+ function classifyOutcome(row: Row): string {
1000
+ if (row.error) return 'error'
1001
+ const reproArmed = row.reproStatus === 'ok'
1002
+ const finalPass = row.repairStop === 'already-passing' || row.repairStop === 'repaired-pass'
1003
+ if (row.hiddenResolved === true) {
1004
+ if (!reproArmed) return 'no-repro-resolved'
1005
+ return finalPass ? 'repro-pass-resolved' : 'resolved-despite-repro-fail'
1006
+ }
1007
+ if (!reproArmed) return 'no-repro-unresolved'
1008
+ return finalPass ? 'repro-pass-but-unresolved' : 'repro-fail-unresolved'
1009
+ }
1010
+
1011
+ /** Files touched by the final diff — the model's OWN output (model-visible by construction). */
1012
+ function filesTouched(diff: string): string[] {
1013
+ return [...new Set([...diff.matchAll(/^\+\+\+ b\/(\S+)/gm)].map((m) => m[1] as string))].slice(0, 8)
1014
+ }
1015
+
1016
+ async function writeNote(row: Row, allFtpMarks: readonly string[], instanceMarks: readonly string[]): Promise<WriteReceipt> {
1017
+ const cls = row.failureClass ?? 'error'
1018
+ const notePath = `knowledge/notes/${String(row.streamIndex).padStart(4, '0')}-${row.instanceId}.md`
1019
+ const repoSlug = row.repo.replace('/', '-')
1020
+ const sev = row.candidates.map((c) => c.severity).join(',')
1021
+ const sel = row.selection ? `${row.selection.mode}@${row.selection.selectedIdx}${row.selection.movedOffFirst ? ' (moved off first)' : ''}` : 'none'
1022
+ const body = [
1023
+ '---',
1024
+ `title: 'stream note: ${row.instanceId}'`,
1025
+ `tags: [swe-stream, ${repoSlug}, ${cls}]`,
1026
+ `streamIndex: ${row.streamIndex}`,
1027
+ `profileVersion: ${row.profileVersion}`,
1028
+ '---',
1029
+ `# ${row.instanceId} — ${row.repo}`,
1030
+ '',
1031
+ `- provenance: streamIndex ${row.streamIndex}, arm L, ${row.profileVersion} (this stream's own run receipts)`,
1032
+ `- issue: ${row.issueTitle}`,
1033
+ `- repro: ${row.reproStatus} (source ${row.reproSource}); candidate severities [${sev}]; selection ${sel}`,
1034
+ `- repairs: ${row.repairs.length} round(s), accepted=${row.repairs.some((r) => r.accepted)}, stop=${row.repairStop ?? 'n/a'}`,
1035
+ `- final: from ${row.finalFrom}, ${row.finalDiff.length} diff bytes, files: ${filesTouched(row.finalDiff).join(', ') || '(none)'}`,
1036
+ `- outcome: resolved=${row.hiddenResolved === true} (official grade, boolean anchor only)`,
1037
+ `- failureClass: ${cls}`,
1038
+ `- lesson: ${CLASS_LESSON[cls] ?? CLASS_LESSON.error}`,
1039
+ ].join('\n')
1040
+
1041
+ // LEAK LINT (fails loud): the note must carry NO hidden-grading vocabulary — not this instance's
1042
+ // FAIL_TO_PASS names, not any accumulated FTP name from the stream, not gold/test-patch marks,
1043
+ // and no judge-report vocabulary. A violation throws: a leaking note is never written.
1044
+ const lintMarks = [...new Set([...allFtpMarks, ...instanceMarks])]
1045
+ for (const mark of lintMarks) {
1046
+ if (mark && body.includes(mark)) {
1047
+ throw new Error(`NOTE LINT REFUSED: hidden-grading mark would leak into the memory store (${mark.slice(0, 40)}…)`)
1048
+ }
1049
+ }
1050
+ if (row.judgeDetail && row.judgeDetail.length > 20 && body.includes(row.judgeDetail.slice(0, 60))) {
1051
+ throw new Error('NOTE LINT REFUSED: judge text would leak into the memory store')
1052
+ }
1053
+ const proposal = `--- FILE: ${notePath} ---\n${body}\n--- END FILE ---\n`
1054
+ const applied = await applyKnowledgeWriteBlocks(STORE, proposal)
1055
+ if (!applied.written.includes(notePath)) {
1056
+ throw new Error(`note write failed: ${applied.warnings.join('; ') || 'no block written'}`)
1057
+ }
1058
+ return { path: notePath, bytes: body.length, lintMarksChecked: lintMarks.length, written: true, error: null }
1059
+ }
1060
+
1061
+ // ---------- image rotation ----------
1062
+
1063
+ async function ensureImage(bt: BenchTask): Promise<{ tag: string; pulled: boolean; pullMs: number }> {
1064
+ let r = await resolveImageForMetadata(bt.metadata ?? {})
1065
+ if (r.ok) return { tag: r.tag, pulled: false, pullMs: 0 }
1066
+ const remote = `swebench/sweb.eval.x86_64.${bt.id.replace('__', '_1776_')}:latest`
1067
+ const t0 = Date.now()
1068
+ await exec('docker', ['pull', remote], { timeout: 1_500_000, maxBuffer: 8_000_000 })
1069
+ r = await resolveImageForMetadata(bt.metadata ?? {})
1070
+ // Hard assert (requirement 4): a missing image after an explicit pull is an error row, never
1071
+ // silent signal loss.
1072
+ if (!r.ok) throw new Error(`image missing after explicit pull of ${remote}: ${r.reason}`)
1073
+ return { tag: r.tag, pulled: true, pullMs: Date.now() - t0 }
1074
+ }
1075
+
1076
+ // ---------- ledger + events ----------
1077
+
1078
+ function appendRow(row: Row): void {
1079
+ appendFileSync(LEDGER, `${JSON.stringify(row)}\n`)
1080
+ }
1081
+
1082
+ function logEvent(type: string, data: Record<string, unknown>): void {
1083
+ appendFileSync(EVENTS, `${JSON.stringify({ at: new Date().toISOString(), type, ...data })}\n`)
1084
+ }
1085
+
1086
+ function loadLedger(): Row[] {
1087
+ if (!existsSync(LEDGER)) return []
1088
+ const rows: Row[] = []
1089
+ for (const line of readFileSync(LEDGER, 'utf8').split('\n')) {
1090
+ if (!line.trim()) continue
1091
+ rows.push(JSON.parse(line) as Row)
1092
+ }
1093
+ return rows
1094
+ }
1095
+
1096
+ // ---------- per-instance pipeline ----------
1097
+
1098
+ interface StreamState {
1099
+ keepImages: Set<string>
1100
+ ftpMarks: Set<string>
1101
+ tally: Record<string, number>
1102
+ cum: { F: { n: number; resolved: number; usd: number }; L: { n: number; resolved: number; usd: number } }
1103
+ judgeChain: Promise<void>
1104
+ }
1105
+
1106
+ /** Serialized official judge with its own timeout race — the chain never blocks past the timeout
1107
+ * (a zombie evaluator can linger; max_workers=1 keeps it single). Judge output goes to the ROW
1108
+ * only, never near a model message. */
1109
+ function serializedJudge(
1110
+ state: StreamState,
1111
+ env: Env,
1112
+ bt: BenchTask,
1113
+ diff: string,
1114
+ ): Promise<{ resolved: boolean; detail: string | null; skipped: string | null; ms: number }> {
1115
+ const run = async (): Promise<{ resolved: boolean; detail: string | null; skipped: string | null; ms: number }> => {
1116
+ const t0 = Date.now()
1117
+ if (!diff.trim()) return { resolved: false, detail: null, skipped: 'empty-patch', ms: 0 }
1118
+ let timer: NodeJS.Timeout | undefined
1119
+ try {
1120
+ const timeout = new Promise<never>((_, rej) => {
1121
+ timer = setTimeout(() => rej(new Error('judge-timeout')), JUDGE_TIMEOUT_MS)
1122
+ })
1123
+ const s = await Promise.race([env.adapter.judge(bt, diff), timeout])
1124
+ return { resolved: s.resolved ?? false, detail: String(s.detail ?? '').slice(0, 1_000), skipped: null, ms: Date.now() - t0 }
1125
+ } catch (e) {
1126
+ const msg = e instanceof Error ? e.message.slice(0, 200) : String(e).slice(0, 200)
1127
+ return { resolved: false, detail: null, skipped: `judge-error: ${msg}`, ms: Date.now() - t0 }
1128
+ } finally {
1129
+ clearTimeout(timer)
1130
+ }
1131
+ }
1132
+ const p = state.judgeChain.then(run)
1133
+ state.judgeChain = p.then(
1134
+ () => undefined,
1135
+ () => undefined,
1136
+ )
1137
+ return p
1138
+ }
1139
+
1140
+ async function processInstance(state: StreamState, env: Env, bt: BenchTask, streamIndex: number): Promise<void> {
1141
+ const t0 = Date.now()
1142
+ const md = bt.metadata as Record<string, string>
1143
+ const rowF = newRow(streamIndex, 'F', bt)
1144
+ const rowL = newRow(streamIndex, 'L', bt)
1145
+ const deadlineAt = t0 + DEADLINE_MS
1146
+ const counterF = newCounter()
1147
+ const counterL = newCounter()
1148
+
1149
+ // Leak-guard marks: this instance's gold/test marks + EVERY FAIL_TO_PASS name accumulated over
1150
+ // the stream so far (the memory channel is the only path that could carry one forward — a trip
1151
+ // means the note lint failed and MUST fail loud). Marks that already occur in the dataset's own
1152
+ // issue text are dropped (dataset-authored, not memory-authored) and receipted.
1153
+ const ftpThis = failToPassNames(bt.metadata ?? {})
1154
+ for (const m of ftpThis) state.ftpMarks.add(m)
1155
+ const issueText = String(md.problem_statement ?? '')
1156
+ const promptText = bt.prompt
1157
+ const allMarks = [...leakMarks(md), ...state.ftpMarks]
1158
+ const marks = allMarks.filter((m) => !issueText.includes(m) && !promptText.includes(m))
1159
+ rowF.marksDroppedDatasetText = rowL.marksDroppedDatasetText = allMarks.length - marks.length
1160
+
1161
+ let repro: ReproOut | null = null
1162
+ const checkDeadline = (where: string): void => {
1163
+ if (Date.now() >= deadlineAt) throw new Error(`DEADLINE: per-instance wall clock exhausted (${where})`)
1164
+ }
1165
+
1166
+ const pipeline = async (): Promise<void> => {
1167
+ // Image: hard-asserted per instance; pulled explicitly when the cache lacks it (requirement 4).
1168
+ const img = await ensureImage(bt)
1169
+ rowF.image = rowL.image = img.tag
1170
+ rowF.imagePulled = rowL.imagePulled = img.pulled
1171
+ rowF.imagePullMs = rowL.imagePullMs = img.pullMs
1172
+ if (img.pulled) logEvent('image-pulled', { streamIndex, instanceId: bt.id, tag: img.tag, pullMs: img.pullMs })
1173
+
1174
+ // Execution canary (zero model calls) — this substrate must be able to grade this instance.
1175
+ const pkg = IMPORT_NAME[md.repo]
1176
+ if (!pkg) throw new Error(`no IMPORT_NAME for ${md.repo} — canary not expressible`)
1177
+ const gold = String(md.patch ?? '')
1178
+ if (!gold.trim()) throw new Error('gold patch missing from metadata')
1179
+ const c = await runPyInJail(img.tag, null, importCanaryScript(pkg), gold, { timeoutS: REPRO_TIMEOUT_S })
1180
+ if (c.infraError) throw new Error(c.infraError)
1181
+ rowF.canaryExit = rowL.canaryExit = c.code
1182
+ const canaryPass = c.code === 0 && c.out.includes(APPLY_SENTINEL)
1183
+ rowF.canaryPass = rowL.canaryPass = canaryPass
1184
+ if (!canaryPass) throw new Error(`canary failed (exit ${c.code}): this substrate cannot grade this instance`)
1185
+
1186
+ // Repro: manifest reuse re-verified, else authored fresh — ONCE, shared by both arms.
1187
+ repro = await acquireRepro(env, bt, img.tag, marks, deadlineAt)
1188
+ for (const row of [rowF, rowL]) {
1189
+ row.reproSource = repro.source
1190
+ row.reproStatus = repro.status
1191
+ row.reproScript = repro.script
1192
+ row.reproPreExit = repro.preExit
1193
+ row.reproGoldExit = repro.goldExit
1194
+ row.reproAuthorCalls = repro.authorCalls
1195
+ row.reproAuthorTokensIn = repro.authorTokensIn
1196
+ row.reproAuthorTokensOut = repro.authorTokensOut
1197
+ }
1198
+
1199
+ // Arm F (frozen): no memory, no supervisor.
1200
+ checkDeadline('before arm F')
1201
+ await runArm(env, bt, rowF, repro.script, marks, deadlineAt, undefined, counterF, false)
1202
+
1203
+ // Arm L (learning): recall at open via the promptAppendix seam + supervisor-steered repair;
1204
+ // worker budget byte-identical to F otherwise.
1205
+ checkDeadline('before arm L')
1206
+ const recall = await recallForInstance(bt)
1207
+ rowL.recall = recall.receipt
1208
+ await runArm(env, bt, rowL, repro.script, marks, deadlineAt, recall.appendix, counterL, true)
1209
+ }
1210
+
1211
+ let deadlineErr: string | null = null
1212
+ try {
1213
+ let timer: NodeJS.Timeout | undefined
1214
+ const breach = new Promise<never>((_, rej) => {
1215
+ timer = setTimeout(
1216
+ () => rej(new Error(`DEADLINE: instance exceeded ${DEADLINE_MS}ms — error row, stream continues`)),
1217
+ DEADLINE_MS,
1218
+ )
1219
+ })
1220
+ try {
1221
+ await Promise.race([pipeline(), breach])
1222
+ } finally {
1223
+ clearTimeout(timer)
1224
+ }
1225
+ } catch (e) {
1226
+ const msg = e instanceof Error ? e.message.slice(0, 400) : String(e).slice(0, 400)
1227
+ deadlineErr = msg
1228
+ const hit = msg.startsWith('DEADLINE')
1229
+ for (const row of [rowF, rowL]) {
1230
+ if (!row.error) row.error = msg
1231
+ row.deadlineHit = row.deadlineHit || hit
1232
+ }
1233
+ if (hit) logEvent('deadline-breach', { streamIndex, instanceId: bt.id, afterMs: Date.now() - t0 })
1234
+ }
1235
+
1236
+ // ── Per-instance phase B: decisions are locked; the official judge grades both arms,
1237
+ // serialized. Judge outputs live in rows only — never in any model context. ──
1238
+ for (const row of [rowF, rowL]) {
1239
+ if (row.error && !row.finalDiff.trim()) {
1240
+ row.hiddenResolved = false
1241
+ row.judgeSkipped = deadlineErr ? 'error-no-patch' : 'empty-patch'
1242
+ continue
1243
+ }
1244
+ const j = await serializedJudge(state, env, bt, row.finalDiff)
1245
+ row.hiddenResolved = j.resolved
1246
+ row.judgeDetail = j.detail
1247
+ row.judgeMs = j.ms
1248
+ row.judgeSkipped = j.skipped
1249
+ }
1250
+
1251
+ // ── L settle: failure-class tally + templated outcome-anchored note (Row fields only). ──
1252
+ rowL.failureClass = classifyOutcome(rowL)
1253
+ rowF.failureClass = classifyOutcome(rowF)
1254
+ state.tally[rowL.failureClass] = (state.tally[rowL.failureClass] ?? 0) + 1
1255
+ rowL.tallySnapshot = { ...state.tally }
1256
+ try {
1257
+ rowL.noteWrite = await writeNote(rowL, [...state.ftpMarks], leakMarks(md))
1258
+ logEvent('note-written', { streamIndex, instanceId: bt.id, path: rowL.noteWrite.path })
1259
+ } catch (e) {
1260
+ const msg = e instanceof Error ? e.message.slice(0, 300) : String(e).slice(0, 300)
1261
+ rowL.noteWrite = { path: '', bytes: 0, lintMarksChecked: 0, written: false, error: msg }
1262
+ logEvent('note-lint-violation', { streamIndex, instanceId: bt.id, error: msg })
1263
+ }
1264
+
1265
+ // ── Finalize receipts + cumulative tracks; append both rows. Authoring usd splits 50/50 so the
1266
+ // arm-vs-arm $ comparison stays unbiased (the repro is a shared instrument). ──
1267
+ const authorUsd = usdOf(rowF.reproAuthorTokensIn, rowF.reproAuthorTokensOut)
1268
+ for (const [row, counter] of [
1269
+ [rowF, counterF],
1270
+ [rowL, counterL],
1271
+ ] as Array<[Row, Counter]>) {
1272
+ row.llmCalls = counter.calls
1273
+ row.httpAttempts = counter.httpAttempts
1274
+ // WORKER tokens only — kept clean so the "worker budget identical across F/L" audit reads the
1275
+ // ledger directly. The supervisor's spend lives in row.supervisorPlan and is added to $ below.
1276
+ row.tokensIn = counter.tokensIn
1277
+ row.tokensOut = counter.tokensOut
1278
+ row.guardedMsgs = counter.guardedMsgs
1279
+ const supUsd = row.supervisorPlan ? usdOf(row.supervisorPlan.planTokensIn, row.supervisorPlan.planTokensOut) : 0
1280
+ row.usd = usdOf(counter.tokensIn, counter.tokensOut) + authorUsd / 2 + supUsd
1281
+ row.wallMs = Date.now() - t0
1282
+ const cum = state.cum[row.arm]
1283
+ cum.n += 1
1284
+ if (row.hiddenResolved === true) cum.resolved += 1
1285
+ cum.usd += row.usd
1286
+ row.cumN = cum.n
1287
+ row.cumResolved = cum.resolved
1288
+ row.cumUsd = cum.usd
1289
+ appendRow(row)
1290
+ }
1291
+
1292
+ // ── Rotation: delete the instance image AFTER both arms + judge, unless it was cached at
1293
+ // stream start (the keep-set). ──
1294
+ const tag = rowF.image
1295
+ if (tag && !state.keepImages.has(tag)) {
1296
+ try {
1297
+ await exec('docker', ['rmi', tag], { timeout: 120_000 })
1298
+ logEvent('image-deleted', { streamIndex, instanceId: bt.id, tag })
1299
+ } catch (e) {
1300
+ logEvent('image-delete-failed', { streamIndex, instanceId: bt.id, tag, error: String(e).slice(0, 200) })
1301
+ }
1302
+ }
1303
+
1304
+ const sup = (r: Row): string =>
1305
+ r.supervisorPlan ? ` sup=${r.supervisorPlan.fired ? `fired(${r.supervisorPlan.reason},${r.supervisorPlan.planTokensOut}out)` : `held(${r.supervisorPlan.reason})`}` : ''
1306
+ const fmt = (r: Row): string =>
1307
+ `${r.arm}: repro=${r.reproStatus} sel=${r.selection ? `${r.selection.mode}@${r.selection.selectedIdx}` : '-'} ` +
1308
+ `sev=[${r.candidates.map((x) => x.severity).join(',')}] repairs=${r.repairs.length} caps=${r.capBreaches} ` +
1309
+ `resolved=${r.hiddenResolved === null ? '?' : r.hiddenResolved ? 1 : 0} calls=${r.llmCalls} $${r.usd.toFixed(3)}${sup(r)}` +
1310
+ `${r.error ? ` ERR=${r.error.slice(0, 80)}` : ''}`
1311
+ console.log(`[#${streamIndex}] ${bt.id} settle (${Math.round((Date.now() - t0) / 1000)}s)\n ${fmt(rowF)}\n ${fmt(rowL)}`)
1312
+ }
1313
+
1314
+ // ---------- curve (a pure fold over the ledger, in stream order) ----------
1315
+
1316
+ function printCurve(rows: Row[]): void {
1317
+ const byKey = new Map<string, Row>()
1318
+ for (const r of rows) byKey.set(`${r.streamIndex}:${r.arm}`, r) // last write wins (resume reruns)
1319
+ const indices = [...new Set([...byKey.values()].map((r) => r.streamIndex))].sort((a, b) => a - b)
1320
+ let fN = 0
1321
+ let fRes = 0
1322
+ let fUsd = 0
1323
+ let lRes = 0
1324
+ let lUsd = 0
1325
+ console.log('\n══ cumulative divergence curve (fold over the ledger, stream order) ══')
1326
+ console.log('idx | instance | F | L | F-cum% | L-cum% | delta(pp) | F-cum$ | L-cum$')
1327
+ for (const i of indices) {
1328
+ const f = byKey.get(`${i}:F`)
1329
+ const l = byKey.get(`${i}:L`)
1330
+ if (!f || !l) continue
1331
+ fN += 1
1332
+ if (f.hiddenResolved === true) fRes += 1
1333
+ if (l.hiddenResolved === true) lRes += 1
1334
+ fUsd += f.usd
1335
+ lUsd += l.usd
1336
+ const fPct = (100 * fRes) / fN
1337
+ const lPct = (100 * lRes) / fN
1338
+ console.log(
1339
+ `${String(i).padStart(3)} | ${f.instanceId.padEnd(38)} | ${f.hiddenResolved ? 1 : 0} | ${l.hiddenResolved ? 1 : 0} | ` +
1340
+ `${fPct.toFixed(1).padStart(5)} | ${lPct.toFixed(1).padStart(5)} | ${(lPct - fPct).toFixed(1).padStart(6)} | ` +
1341
+ `${fUsd.toFixed(2).padStart(6)} | ${lUsd.toFixed(2).padStart(6)}`,
1342
+ )
1343
+ }
1344
+ console.log(`n=${fN} paired | F resolved ${fRes}/${fN} | L resolved ${lRes}/${fN} | final delta ${((100 * (lRes - fRes)) / Math.max(1, fN)).toFixed(1)}pp`)
1345
+ }
1346
+
1347
+ // ---------- driver ----------
1348
+
1349
+ async function main(): Promise<void> {
1350
+ mkdirSync(STREAM_DIR, { recursive: true })
1351
+ await initKnowledgeBase(STORE)
1352
+
1353
+ const pool = process.env.IDS
1354
+ ? process.env.IDS.split(',').map((s) => s.trim()).filter(Boolean)
1355
+ : [...FINGERPRINT_23]
1356
+ // Stream order is an APPEND-ONLY pre-registered plan persisted in STREAM_DIR/plan.json: the
1357
+ // saved order is immutable (streamIndex must stay stable across continuations); ids new to the
1358
+ // pool are seeded-shuffled, cached-images-first, and APPENDED. Day-1 starts the plan; later days
1359
+ // extend it by passing a larger IDS pool to the same STREAM_DIR.
1360
+ const planPath = join(STREAM_DIR, 'plan.json')
1361
+ const prefix: string[] = existsSync(planPath) ? (JSON.parse(readFileSync(planPath, 'utf8')) as string[]) : []
1362
+ const known = new Set(prefix)
1363
+ const shuffled = seededShuffle(pool.filter((id) => !known.has(id)), SEED)
1364
+ const cached = new Set(await cachedInstanceIds())
1365
+ const fullPlan = [...prefix, ...shuffled.filter((id) => cached.has(id)), ...shuffled.filter((id) => !cached.has(id))]
1366
+ writeFileSync(planPath, `${JSON.stringify(fullPlan, null, 1)}\n`)
1367
+ const plan = fullPlan.slice(0, Math.min(fullPlan.length, STREAM_N))
1368
+
1369
+ console.log('═══ SWE-bench STREAM — day 1 (two-arm, F=frozen-v0 vs L=learning) ═══')
1370
+ console.log(
1371
+ `worker=${WORKER_MODEL} supervisor=${SUPERVISOR_MODEL} (arm L repair only) repro-author=${REPRO_MODEL} ` +
1372
+ `reasoning=${REASONING_ON ? 'thinking' : 'off'} base=${ZAI_BASE} ` +
1373
+ `maxTokens=${MAX_TOKENS} supMaxTokens=${SUPERVISOR_MAX_TOKENS} k=${K} temp=${TEMP} repairs<=${REPAIRS} ` +
1374
+ `innerTurns=${INNER_TURNS} TURN_CAP=${TURN_CAP} DEADLINE_MS=${DEADLINE_MS} conc=${CONC} seed=0x${SEED.toString(16)} ` +
1375
+ `judge=serialized(cache_level=${process.env.SWEBENCH_CACHE_LEVEL}) profile=${PROFILE_VERSION}`,
1376
+ )
1377
+ console.log(`stream dir=${STREAM_DIR}\nplan (${plan.length}): ${plan.join(', ')}`)
1378
+ const manifestN = plan.filter((id) => MANIFEST[id]).length
1379
+ console.log(`repro manifest: ${manifestN}/${plan.length} reused (re-verified); the rest author fresh`)
1380
+
1381
+ const env = await createSweBenchEnvironment(plan.length, { ids: plan, cloneCache: true })
1382
+ await env.adapter.preflight?.()
1383
+ const taskById = new Map((await env.adapter.loadTasks({ ids: plan, split: 'test' })).map((t) => [t.id, t]))
1384
+ const missing = plan.filter((id) => !taskById.has(id))
1385
+ if (missing.length) throw new Error(`instances not found in SWE-bench_Verified: ${missing.join(', ')}`)
1386
+
1387
+ // Resume: an instance is DONE iff both arm rows settled. Cumulative tracks, the failure tally,
1388
+ // and the FTP mark set refold from the ledger.
1389
+ const prior = loadLedger()
1390
+ const doneIds = new Set<string>()
1391
+ const seen = new Map<string, Set<string>>()
1392
+ for (const r of prior) {
1393
+ const arms = seen.get(r.instanceId) ?? new Set<string>()
1394
+ arms.add(r.arm)
1395
+ seen.set(r.instanceId, arms)
1396
+ if (arms.has('F') && arms.has('L')) doneIds.add(r.instanceId)
1397
+ }
1398
+ const state: StreamState = {
1399
+ keepImages: new Set<string>(),
1400
+ ftpMarks: new Set<string>(),
1401
+ tally: {},
1402
+ cum: { F: { n: 0, resolved: 0, usd: 0 }, L: { n: 0, resolved: 0, usd: 0 } },
1403
+ judgeChain: Promise.resolve(),
1404
+ }
1405
+ for (const r of prior) {
1406
+ const cum = state.cum[r.arm]
1407
+ cum.n = Math.max(cum.n, r.cumN)
1408
+ cum.resolved = Math.max(cum.resolved, r.cumResolved)
1409
+ cum.usd = Math.max(cum.usd, r.cumUsd)
1410
+ if (r.arm === 'L' && r.tallySnapshot) state.tally = { ...r.tallySnapshot }
1411
+ }
1412
+ for (const id of doneIds) {
1413
+ const t = taskById.get(id)
1414
+ if (t) for (const m of failToPassNames(t.metadata ?? {})) state.ftpMarks.add(m)
1415
+ }
1416
+ // The keep-set: every instance image cached at stream start survives rotation.
1417
+ const { stdout } = await exec('docker', ['images', '--format', '{{.Repository}}:{{.Tag}}'], { timeout: 30_000 })
1418
+ for (const line of stdout.split('\n')) {
1419
+ if (/sweb\.eval\.x86_64\./.test(line.trim())) state.keepImages.add(line.trim())
1420
+ }
1421
+ // Fresh stream ⇒ the memory store must start EMPTY (the doc's divergence-only-from-learning
1422
+ // invariant). A resumed stream carries its own notes forward.
1423
+ if (prior.length === 0) {
1424
+ const idx = await buildKnowledgeIndex(STORE)
1425
+ if (idx.pages.length > 0) {
1426
+ throw new Error(`stream is fresh (empty ledger) but the memory store has ${idx.pages.length} page(s) — stale STORE at ${STORE}`)
1427
+ }
1428
+ }
1429
+ if (prior.length === 0) {
1430
+ logEvent('stream-start', {
1431
+ seed: SEED, plan, deadlineMs: DEADLINE_MS, turnCap: TURN_CAP, k: K, repairs: REPAIRS,
1432
+ temp: TEMP, workerModel: WORKER_MODEL, supervisorModel: SUPERVISOR_MODEL, reproModel: REPRO_MODEL,
1433
+ reasoningEffort: REASONING_EFFORT, profileVersion: PROFILE_VERSION, keepImages: [...state.keepImages],
1434
+ })
1435
+ } else {
1436
+ logEvent('stream-resume', { done: doneIds.size, planned: plan.length })
1437
+ }
1438
+ if (doneIds.size) console.log(`resume: ${doneIds.size} instance(s) already settled; ${plan.filter((id) => !doneIds.has(id)).length} to run`)
1439
+
1440
+ let next = 0
1441
+ const worker = async (): Promise<void> => {
1442
+ while (next < plan.length) {
1443
+ const i = next++
1444
+ const id = plan[i] as string
1445
+ if (doneIds.has(id)) continue
1446
+ // Day-1 STUB of the every-N batch trigger (N=25): the gate look is wired on later days.
1447
+ if (i > 0 && i % 25 === 0) logEvent('batch-look (stub)', { streamIndex: i })
1448
+ console.log(`[#${i}] ${id} …`)
1449
+ try {
1450
+ await processInstance(state, env, taskById.get(id) as BenchTask, i)
1451
+ } catch (e) {
1452
+ // processInstance settles its own rows; reaching here means settling itself failed.
1453
+ console.error(`[#${i}] ${id} SETTLE FAILURE: ${e instanceof Error ? e.message : String(e)}`)
1454
+ logEvent('settle-failure', { streamIndex: i, instanceId: id, error: String(e).slice(0, 300) })
1455
+ }
1456
+ }
1457
+ }
1458
+ await Promise.all(Array.from({ length: CONC }, () => worker()))
1459
+
1460
+ const rows = loadLedger()
1461
+ printCurve(rows)
1462
+
1463
+ // Sound-repro rate — the instrument's health (per instance, shared across arms; read off F rows).
1464
+ // A low rate means the supervisor-fire predicate (repro=ok + severity===1) rarely gets a chance.
1465
+ const fRows = rows.filter((r) => r.arm === 'F')
1466
+ const soundRepro = fRows.filter((r) => r.reproStatus === 'ok').length
1467
+ const reproDist = fRows.reduce<Record<string, number>>((m, r) => ({ ...m, [r.reproStatus]: (m[r.reproStatus] ?? 0) + 1 }), {})
1468
+ console.log(
1469
+ `sound-repro rate: ${soundRepro}/${fRows.length} ok (author=${REPRO_MODEL}) — status dist ${JSON.stringify(reproDist)}`,
1470
+ )
1471
+
1472
+ const byArm = (arm: 'F' | 'L'): Row[] => rows.filter((r) => r.arm === arm)
1473
+ for (const arm of ['F', 'L'] as const) {
1474
+ const a = byArm(arm)
1475
+ const caps = a.reduce((s, r) => s + r.capBreaches, 0)
1476
+ const dl = a.filter((r) => r.deadlineHit).length
1477
+ const usd = a.reduce((s, r) => s + r.usd, 0)
1478
+ const tokIn = a.reduce((s, r) => s + r.tokensIn, 0)
1479
+ const tokOut = a.reduce((s, r) => s + r.tokensOut, 0)
1480
+ const guarded = a.reduce((s, r) => s + r.guardedMsgs, 0)
1481
+ const supFired = a.filter((r) => r.supervisorPlan?.fired).length
1482
+ const supLeaked = a.filter((r) => r.supervisorPlan?.leaked).length
1483
+ const supTrips = a.filter((r) => r.supervisorPlan?.reason === 'leak-guard-tripped').length
1484
+ console.log(
1485
+ `arm ${arm}: rows=${a.length} resolved=${a.filter((r) => r.hiddenResolved === true).length} ` +
1486
+ `errorRows=${a.filter((r) => r.error).length} deadlineHits=${dl} capBreaches=${caps} ` +
1487
+ `supervisorFired=${supFired} planLeaked=${supLeaked} leakTrips=${supTrips} ` +
1488
+ `tokens=${tokIn}/${tokOut} guardedMsgs=${guarded} $${usd.toFixed(2)} (assumed $${PRICE_IN}/M in, $${PRICE_OUT}/M out)`,
1489
+ )
1490
+ }
1491
+ console.log(`ledger=${LEDGER}\nevents=${EVENTS}\nstore=${STORE}`)
1492
+ }
1493
+
1494
+ main().catch((e) => {
1495
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
1496
+ process.exit(1)
1497
+ })