@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.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +24 -24
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +24 -24
- package/package.json +6 -5
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/scripts/verify-pier-agent.mts +1 -0
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +122 -0
- package/src/benchmarks/humaneval.ts +100 -27
- package/src/david-attribution.mts +78 -0
- package/src/david-goliath.mts +149 -0
- package/src/hev-improve.mts +25 -6
- package/src/humaneval-object-ablation.mts +201 -0
- package/src/live-improve-campaign-mbpp.mts +641 -0
- package/src/live-improve-campaign.mts +500 -0
- package/src/mbpp-structural.mts +12 -7
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +804 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +217 -0
- package/src/swe-arena/capabilities.mts +76 -0
- package/src/swe-arena/capabilities.test.mts +57 -0
- package/src/swe-arena/capacity.ts +194 -0
- package/src/swe-arena/cell-evidence.mts +437 -0
- package/src/swe-arena/cell-evidence.test.mts +248 -0
- package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
- package/src/swe-arena/diagnosis-ensemble.ts +520 -0
- package/src/swe-arena/execution.test.mts +1170 -0
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
- package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
- package/src/swe-arena/fixtures/holdout.json +44 -0
- package/src/swe-arena/fixtures/instances.json +146 -0
- package/src/swe-arena/fixtures/ledger.jsonl +12 -0
- package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
- package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
- package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
- package/src/swe-arena/fixtures/rematch.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
- package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
- package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
- package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
- package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
- package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
- package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
- package/src/swe-arena/fixtures/worker-tokens.json +42 -0
- package/src/swe-arena/fixtures.ts +237 -0
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -0
- package/src/swe-arena/holdout-certify.mts +408 -0
- package/src/swe-arena/holdout-certify.test.mts +160 -0
- package/src/swe-arena/judge-child.mts +37 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/lineage-record.mts +164 -0
- package/src/swe-arena/lineage-record.test.mts +115 -0
- package/src/swe-arena/manifest.mts +293 -0
- package/src/swe-arena/manifest.test.mts +169 -0
- package/src/swe-arena/materialize.ts +142 -0
- package/src/swe-arena/outer-loop.mts +2795 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -0
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proc.test.mts +174 -0
- package/src/swe-arena/proc.ts +260 -0
- package/src/swe-arena/profiles/default-author.profile.json +4 -0
- package/src/swe-arena/proposer-fanout.mts +770 -0
- package/src/swe-arena/proposer-fanout.test.mts +619 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/reconcile.ts +0 -0
- package/src/swe-arena/replay.mts +183 -0
- package/src/swe-arena/replay.test.mts +300 -0
- package/src/swe-arena/run-experiment.mts +727 -0
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +500 -0
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +218 -0
- package/src/swe-code-improve.mts +328 -0
- package/src/swe-emit-patch.mts +104 -0
- package/src/swe-improve.mts +232 -0
- package/src/swe-jail.ts +2 -2
- package/src/swe-local-proof.mts +169 -0
- package/src/swe-repro-calibrate.mts +446 -0
- package/src/swe-stream.mts +1497 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gen-5 public/private score split: determinism (seeded by runId), persisted
|
|
3
|
+
* resume stability, and the never-surfaced invariant's checkable pieces.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
10
|
+
import {
|
|
11
|
+
leaksPrivateInstance,
|
|
12
|
+
loadOrCreateScoreSplit,
|
|
13
|
+
publicOnly,
|
|
14
|
+
SCORE_SPLIT_FILENAME,
|
|
15
|
+
splitInstances,
|
|
16
|
+
subScores,
|
|
17
|
+
} from './score-split.mts'
|
|
18
|
+
|
|
19
|
+
const SIX = [
|
|
20
|
+
'astropy__astropy-13033',
|
|
21
|
+
'django__django-11532',
|
|
22
|
+
'matplotlib__matplotlib-20826',
|
|
23
|
+
'pydata__xarray-4687',
|
|
24
|
+
'pytest-dev__pytest-6197',
|
|
25
|
+
'sphinx-doc__sphinx-9658',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
describe('splitInstances', () => {
|
|
29
|
+
it('is deterministic: same (runId, instances, publicCount) → identical split', () => {
|
|
30
|
+
const a = splitInstances('r4-abc123', SIX, 4)
|
|
31
|
+
const b = splitInstances('r4-abc123', SIX, 4)
|
|
32
|
+
expect(a).toEqual(b)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('is instance-ORDER-insensitive (ranking is content-derived)', () => {
|
|
36
|
+
const a = splitInstances('r4-abc123', SIX, 4)
|
|
37
|
+
const b = splitInstances('r4-abc123', [...SIX].reverse(), 4)
|
|
38
|
+
expect(a).toEqual(b)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('respects publicCount and partitions exactly (4 public / 2 private of 6)', () => {
|
|
42
|
+
const split = splitInstances('r4-abc123', SIX, 4)
|
|
43
|
+
expect(split.publicInstances).toHaveLength(4)
|
|
44
|
+
expect(split.privateInstances).toHaveLength(2)
|
|
45
|
+
expect([...split.publicInstances, ...split.privateInstances].sort()).toEqual([...SIX].sort())
|
|
46
|
+
expect(split.publicInstances.filter((i) => split.privateInstances.includes(i))).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('is SEEDED by runId: different runIds produce different splits', () => {
|
|
50
|
+
const splits = new Set(
|
|
51
|
+
Array.from({ length: 10 }, (_, i) => splitInstances(`r4-run${i}`, SIX, 4).privateInstances.join('|')),
|
|
52
|
+
)
|
|
53
|
+
expect(splits.size).toBeGreaterThan(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('rejects publicCount outside (0, n) and duplicate instance ids', () => {
|
|
57
|
+
expect(() => splitInstances('r', SIX, 0)).toThrow(/publicCount/)
|
|
58
|
+
expect(() => splitInstances('r', SIX, 6)).toThrow(/publicCount/)
|
|
59
|
+
expect(() => splitInstances('r', SIX, 2.5)).toThrow(/publicCount/)
|
|
60
|
+
expect(() => splitInstances('r', ['a', 'a', 'b'], 1)).toThrow(/duplicate/)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('loadOrCreateScoreSplit (resume stability)', () => {
|
|
65
|
+
let outDir: string
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
outDir = await mkdtemp(join(tmpdir(), 'split-'))
|
|
68
|
+
})
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await rm(outDir, { recursive: true, force: true })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('persists on first computation and RELOADS verbatim under a NEW runId (a resume cannot rotate private into view)', async () => {
|
|
74
|
+
const first = await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
|
|
75
|
+
expect(JSON.parse(await readFile(join(outDir, SCORE_SPLIT_FILENAME), 'utf8'))).toEqual(first)
|
|
76
|
+
const resumed = await loadOrCreateScoreSplit({ outDir, runId: 'r4-DIFFERENT', instances: SIX, publicCount: 4 })
|
|
77
|
+
expect(resumed).toEqual(first)
|
|
78
|
+
expect(resumed.seededBy).toBe('r4-first')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('fails loud when the persisted split covers different instances or a different publicCount', async () => {
|
|
82
|
+
await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
|
|
83
|
+
await expect(
|
|
84
|
+
loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: [...SIX.slice(0, 5), 'other__iid-1'], publicCount: 4 }),
|
|
85
|
+
).rejects.toThrow(/refusing to silently re-split/)
|
|
86
|
+
await expect(
|
|
87
|
+
loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: SIX, publicCount: 3 }),
|
|
88
|
+
).rejects.toThrow(/refusing to silently re-split/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('fails loud on an unknown schema in the persisted file', async () => {
|
|
92
|
+
await writeFile(join(outDir, SCORE_SPLIT_FILENAME), JSON.stringify({ schema: 'nope' }))
|
|
93
|
+
await expect(
|
|
94
|
+
loadOrCreateScoreSplit({ outDir, runId: 'r', instances: SIX, publicCount: 4 }),
|
|
95
|
+
).rejects.toThrow(/unknown schema/)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('never-surfaced invariant helpers', () => {
|
|
100
|
+
const split = splitInstances('r4-abc123', SIX, 4)
|
|
101
|
+
|
|
102
|
+
it('leaksPrivateInstance names every private id present in a text', () => {
|
|
103
|
+
expect(leaksPrivateInstance('nothing to see', split)).toEqual([])
|
|
104
|
+
const leakText = `evidence for ${split.privateInstances[0]} here`
|
|
105
|
+
expect(leaksPrivateInstance(leakText, split)).toEqual([split.privateInstances[0]])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('publicOnly drops private-instance records and is identity without a split', () => {
|
|
109
|
+
const records = SIX.map((iid) => ({ iid, x: 1 }))
|
|
110
|
+
const filtered = publicOnly(records, (r) => r.iid, split)
|
|
111
|
+
expect(filtered.map((r) => r.iid).sort()).toEqual(split.publicInstances)
|
|
112
|
+
expect(publicOnly(records, (r) => r.iid, null)).toEqual(records)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('subScores counts fail-closed AND-verdicts per half (missing verdict = 0)', () => {
|
|
116
|
+
const verdicts: Record<string, boolean> = {}
|
|
117
|
+
verdicts[split.publicInstances[0]!] = true
|
|
118
|
+
verdicts[split.publicInstances[1]!] = false
|
|
119
|
+
verdicts[split.privateInstances[0]!] = true
|
|
120
|
+
// privateInstances[1] has NO verdict — counts 0.
|
|
121
|
+
expect(subScores(verdicts, split)).toEqual({ publicResolvedCount: 1, privateResolvedCount: 1 })
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialized official judge — the typed port of the experiment's `judge.sh`,
|
|
3
|
+
* wrapping `adapter.judge` (via the tracked judge-child.mts) with the three
|
|
4
|
+
* protections the bash experiment had to learn the hard way:
|
|
5
|
+
*
|
|
6
|
+
* 1. ONE JUDGE AT A TIME. The stale-container pre-clean does `docker rm -f`
|
|
7
|
+
* by instance short-name; two overlapping judges of the same instance nuke
|
|
8
|
+
* each other's container mid-run and each records a spurious
|
|
9
|
+
* `resolved:false` (proven false-negative on psf__requests-1766). The lock
|
|
10
|
+
* here is two-layer: an in-process promise chain plus a kernel-backed
|
|
11
|
+
* `flock`, so the pre-clean can only ever remove a genuinely stale
|
|
12
|
+
* container — never a live grade.
|
|
13
|
+
*
|
|
14
|
+
* 2. CEILING ≥ 1800s. The original 700s ceiling caused 3 spurious timeouts on
|
|
15
|
+
* psf__requests-2317 (a successful grade took 1181s). The floor is
|
|
16
|
+
* enforced, not advisory: a shorter ceiling for the REAL judge is a
|
|
17
|
+
* protocol violation and throws.
|
|
18
|
+
*
|
|
19
|
+
* 3. ONE RETRY on empty/unparseable judge output (single-run judge flake was
|
|
20
|
+
* observed live: byte-identical patches split-verdicted). A second failure
|
|
21
|
+
* returns `resolved: null` — inconclusive, never a fabricated verdict —
|
|
22
|
+
* matching the RejudgeRow semantics M1 pinned.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawn } from 'node:child_process'
|
|
26
|
+
import { open, readFile, type FileHandle } from 'node:fs/promises'
|
|
27
|
+
import { tmpdir } from 'node:os'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { run } from './proc'
|
|
31
|
+
|
|
32
|
+
/** Verdict shape — field names match the experiment's JUDGE_RESULT/RejudgeRow rows. */
|
|
33
|
+
export interface JudgeVerdict {
|
|
34
|
+
iid: string
|
|
35
|
+
/** `null` = inconclusive (judge failed twice); never a guessed boolean. */
|
|
36
|
+
resolved: boolean | null
|
|
37
|
+
score?: number
|
|
38
|
+
secs?: number
|
|
39
|
+
patch_bytes?: number
|
|
40
|
+
note?: string
|
|
41
|
+
error?: string
|
|
42
|
+
/** First 200 chars of the failing output, for post-mortem (mirrors rejudge rows). */
|
|
43
|
+
raw?: string
|
|
44
|
+
/** How many judge child runs it took (1 = clean, 2 = retried). */
|
|
45
|
+
attempts?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface JudgeCommand {
|
|
49
|
+
bin: string
|
|
50
|
+
argv: string[]
|
|
51
|
+
cwd: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SerializedJudgeOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Hard ceiling per judge child. Default 1_800_000 ms; values below the floor
|
|
57
|
+
* throw unless `unsafeAllowShortTimeout` (test hook) is set.
|
|
58
|
+
*/
|
|
59
|
+
timeoutMs?: number
|
|
60
|
+
/** Maximum time spent waiting to acquire both judge locks. */
|
|
61
|
+
lockWaitTimeoutMs?: number
|
|
62
|
+
/** Hard ceiling for each stale-container pre-clean command. */
|
|
63
|
+
preCleanTimeoutMs?: number
|
|
64
|
+
/** Test-only escape hatch for the 1800s floor. NEVER set on a real judge. */
|
|
65
|
+
unsafeAllowShortTimeout?: boolean
|
|
66
|
+
/** Cross-process kernel lock file. One per docker daemon. */
|
|
67
|
+
lockFile?: string
|
|
68
|
+
/** SWEBENCH_CACHE_LEVEL for the child. `instance` = we manage image rotation. */
|
|
69
|
+
cacheLevel?: string
|
|
70
|
+
/** Override the judge child invocation (tests inject fakes here). */
|
|
71
|
+
command?: (iid: string, patchPath: string) => JudgeCommand
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const JUDGE_TIMEOUT_FLOOR_MS = 1_800_000
|
|
75
|
+
export const JUDGE_PRE_CLEAN_TIMEOUT_MS = 60_000
|
|
76
|
+
/** Process-group settlement and scheduler margin beyond all command ceilings. */
|
|
77
|
+
export const JUDGE_LOCK_SETTLEMENT_MARGIN_MS = 30_000
|
|
78
|
+
/** One live holder may use two attempts, each with docker ps + docker rm. */
|
|
79
|
+
export const JUDGE_LOCK_WAIT_TIMEOUT_MS =
|
|
80
|
+
2 * (JUDGE_TIMEOUT_FLOOR_MS + 2 * JUDGE_PRE_CLEAN_TIMEOUT_MS) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
|
|
81
|
+
|
|
82
|
+
const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
|
|
83
|
+
const judgeChild = fileURLToPath(new URL('./judge-child.mts', import.meta.url))
|
|
84
|
+
|
|
85
|
+
const defaultCommand = (iid: string, patchPath: string): JudgeCommand => ({
|
|
86
|
+
bin: 'node',
|
|
87
|
+
argv: ['--import', 'tsx', judgeChild, iid, patchPath],
|
|
88
|
+
cwd: benchRoot,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** judge.sh's `sed 's/.*__//'` — the docker container filter for pre-clean. */
|
|
92
|
+
export function instanceShortName(iid: string): string {
|
|
93
|
+
return iid.replace(/.*__/, '')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Locking: in-process promise chain + cross-process kernel flock.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
let inProcessChain: Promise<unknown> = Promise.resolve()
|
|
101
|
+
|
|
102
|
+
export interface JudgeLockOptions {
|
|
103
|
+
/** Maximum total wait across the in-process queue and kernel lock. */
|
|
104
|
+
timeoutMs?: number
|
|
105
|
+
signal?: AbortSignal
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function abortReason(signal: AbortSignal): Error {
|
|
109
|
+
if (signal.reason instanceof Error) return signal.reason
|
|
110
|
+
const error = new Error('serialized-judge: aborted while waiting for the judge lock')
|
|
111
|
+
error.name = 'AbortError'
|
|
112
|
+
return error
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
116
|
+
if (signal?.aborted) throw abortReason(signal)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function lockTimeoutError(timeoutMs: number): Error {
|
|
120
|
+
return new Error(`serialized-judge: lock wait exceeded ${timeoutMs}ms`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function waitFor<T>(promise: Promise<T>, deadline: number, timeoutMs: number, signal?: AbortSignal): Promise<T> {
|
|
124
|
+
throwIfAborted(signal)
|
|
125
|
+
const remainingMs = deadline - Date.now()
|
|
126
|
+
if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
|
|
127
|
+
|
|
128
|
+
return new Promise<T>((resolve, reject) => {
|
|
129
|
+
let settled = false
|
|
130
|
+
const finish = (callback: () => void) => {
|
|
131
|
+
if (settled) return
|
|
132
|
+
settled = true
|
|
133
|
+
clearTimeout(timer)
|
|
134
|
+
signal?.removeEventListener('abort', onAbort)
|
|
135
|
+
callback()
|
|
136
|
+
}
|
|
137
|
+
const onAbort = () => finish(() => reject(abortReason(signal!)))
|
|
138
|
+
const timer = setTimeout(() => finish(() => reject(lockTimeoutError(timeoutMs))), remainingMs)
|
|
139
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
140
|
+
if (signal?.aborted) onAbort()
|
|
141
|
+
promise.then(
|
|
142
|
+
(value) => finish(() => resolve(value)),
|
|
143
|
+
(error: unknown) => finish(() => reject(error)),
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Ask util-linux `flock` to lock an fd inherited from this process. Linux
|
|
150
|
+
* associates a flock with the open file description, so the lock remains held
|
|
151
|
+
* by `handle` after the short helper exits and is released atomically on close.
|
|
152
|
+
* There is no owner record to initialize, inspect, or unlink: process death and
|
|
153
|
+
* fd close are the only stale-owner recovery path, both enforced by the kernel.
|
|
154
|
+
*/
|
|
155
|
+
function tryAcquireFlock(
|
|
156
|
+
handle: FileHandle,
|
|
157
|
+
deadline: number,
|
|
158
|
+
timeoutMs: number,
|
|
159
|
+
signal?: AbortSignal,
|
|
160
|
+
): Promise<boolean> {
|
|
161
|
+
throwIfAborted(signal)
|
|
162
|
+
const remainingMs = deadline - Date.now()
|
|
163
|
+
if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
|
|
164
|
+
|
|
165
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
166
|
+
const waitSeconds = Math.max(0.001, remainingMs / 1_000).toFixed(3)
|
|
167
|
+
const child = spawn('flock', ['--exclusive', '--wait', waitSeconds, '3'], {
|
|
168
|
+
stdio: ['ignore', 'ignore', 'pipe', handle.fd],
|
|
169
|
+
})
|
|
170
|
+
let stderr = ''
|
|
171
|
+
let pendingError: Error | undefined
|
|
172
|
+
let settled = false
|
|
173
|
+
const finish = (callback: () => void) => {
|
|
174
|
+
if (settled) return
|
|
175
|
+
settled = true
|
|
176
|
+
clearTimeout(timer)
|
|
177
|
+
signal?.removeEventListener('abort', onAbort)
|
|
178
|
+
callback()
|
|
179
|
+
}
|
|
180
|
+
const stopWith = (error: Error) => {
|
|
181
|
+
if (pendingError || settled) return
|
|
182
|
+
pendingError = error
|
|
183
|
+
child.kill('SIGKILL')
|
|
184
|
+
}
|
|
185
|
+
const onAbort = () => stopWith(abortReason(signal!))
|
|
186
|
+
const timer = setTimeout(() => stopWith(lockTimeoutError(timeoutMs)), remainingMs)
|
|
187
|
+
|
|
188
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
189
|
+
if (signal?.aborted) onAbort()
|
|
190
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
191
|
+
if (stderr.length < 2_000) stderr += chunk.toString('utf8')
|
|
192
|
+
})
|
|
193
|
+
child.on('error', (error) => finish(() => reject(pendingError ?? error)))
|
|
194
|
+
child.on('close', (code, closeSignal) => {
|
|
195
|
+
if (pendingError) {
|
|
196
|
+
finish(() => reject(pendingError!))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
if (code === 0) {
|
|
200
|
+
finish(() => resolve(true))
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
// util-linux flock uses rc=1 when --wait expires without ownership.
|
|
204
|
+
if (code === 1) {
|
|
205
|
+
finish(() => resolve(false))
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const detail = stderr.trim()
|
|
209
|
+
finish(() => reject(new Error(
|
|
210
|
+
`serialized-judge: flock failed with ${closeSignal ?? `exit ${code ?? 'unknown'}`}${detail ? `: ${detail}` : ''}`,
|
|
211
|
+
)))
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function acquireFlock(
|
|
217
|
+
lockFile: string,
|
|
218
|
+
deadline: number,
|
|
219
|
+
timeoutMs: number,
|
|
220
|
+
signal?: AbortSignal,
|
|
221
|
+
): Promise<() => Promise<void>> {
|
|
222
|
+
for (;;) {
|
|
223
|
+
throwIfAborted(signal)
|
|
224
|
+
if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
|
|
225
|
+
const handle = await open(lockFile, 'a')
|
|
226
|
+
try {
|
|
227
|
+
if (await tryAcquireFlock(handle, deadline, timeoutMs, signal)) {
|
|
228
|
+
let released = false
|
|
229
|
+
return async () => {
|
|
230
|
+
if (released) return
|
|
231
|
+
released = true
|
|
232
|
+
await handle.close()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
await handle.close().catch(() => {})
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
await handle.close()
|
|
240
|
+
if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run `fn` holding BOTH locks. Exported so calibrate/parity paths can pin any
|
|
246
|
+
* docker-touching critical section to the same mutex the judge uses.
|
|
247
|
+
*/
|
|
248
|
+
export function withJudgeLock<T>(
|
|
249
|
+
lockFile: string,
|
|
250
|
+
fn: () => Promise<T>,
|
|
251
|
+
opts: JudgeLockOptions = {},
|
|
252
|
+
): Promise<T> {
|
|
253
|
+
const timeoutMs = opts.timeoutMs ?? JUDGE_LOCK_WAIT_TIMEOUT_MS
|
|
254
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
255
|
+
return Promise.reject(new Error('serialized-judge: lock timeoutMs must be a positive finite number'))
|
|
256
|
+
}
|
|
257
|
+
const deadline = Date.now() + timeoutMs
|
|
258
|
+
const predecessor = inProcessChain.catch(() => {})
|
|
259
|
+
const task = (async () => {
|
|
260
|
+
// Cancellation stops this waiter, not the predecessor. A later waiter
|
|
261
|
+
// still has to acquire the kernel lock, so the live holder is never bypassed.
|
|
262
|
+
await waitFor(predecessor, deadline, timeoutMs, opts.signal)
|
|
263
|
+
const release = await acquireFlock(lockFile, deadline, timeoutMs, opts.signal)
|
|
264
|
+
try {
|
|
265
|
+
throwIfAborted(opts.signal)
|
|
266
|
+
return await fn()
|
|
267
|
+
} finally {
|
|
268
|
+
await release()
|
|
269
|
+
}
|
|
270
|
+
})()
|
|
271
|
+
inProcessChain = task.catch(() => {})
|
|
272
|
+
return task
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// The judge itself.
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
export interface SerializedJudge {
|
|
280
|
+
judge(iid: string, patchPath: string, tag?: string, signal?: AbortSignal): Promise<JudgeVerdict>
|
|
281
|
+
readonly lockFile: string
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseJudgeResult(stdout: string): Omit<JudgeVerdict, 'attempts'> | undefined {
|
|
285
|
+
const line = stdout.split('\n').find((l) => l.includes('JUDGE_RESULT'))
|
|
286
|
+
if (!line) return undefined
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(line.slice(line.indexOf('JUDGE_RESULT') + 'JUDGE_RESULT'.length).trim())
|
|
289
|
+
if (typeof parsed !== 'object' || parsed === null || typeof parsed.resolved !== 'boolean') return undefined
|
|
290
|
+
return parsed as Omit<JudgeVerdict, 'attempts'>
|
|
291
|
+
} catch {
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function createSerializedJudge(opts: SerializedJudgeOptions = {}): SerializedJudge {
|
|
297
|
+
const timeoutMs = opts.timeoutMs ?? JUDGE_TIMEOUT_FLOOR_MS
|
|
298
|
+
const usingRealJudge = opts.command === undefined
|
|
299
|
+
if (timeoutMs < JUDGE_TIMEOUT_FLOOR_MS && (usingRealJudge || !opts.unsafeAllowShortTimeout)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`serialized-judge: timeoutMs=${timeoutMs} is below the ${JUDGE_TIMEOUT_FLOOR_MS}ms floor ` +
|
|
302
|
+
`(700s provably caused 3 spurious timeouts on psf__requests-2317)`,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
const lockFile = opts.lockFile ?? join(tmpdir(), 'swe-arena-judge.lock')
|
|
306
|
+
const preCleanTimeoutMs = opts.preCleanTimeoutMs ?? JUDGE_PRE_CLEAN_TIMEOUT_MS
|
|
307
|
+
const lockWaitTimeoutMs =
|
|
308
|
+
opts.lockWaitTimeoutMs ??
|
|
309
|
+
2 * (timeoutMs + 2 * preCleanTimeoutMs) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
|
|
310
|
+
if (!Number.isFinite(lockWaitTimeoutMs) || lockWaitTimeoutMs <= 0) {
|
|
311
|
+
throw new Error('serialized-judge: lockWaitTimeoutMs must be a positive finite number')
|
|
312
|
+
}
|
|
313
|
+
if (!Number.isFinite(preCleanTimeoutMs) || preCleanTimeoutMs <= 0) {
|
|
314
|
+
throw new Error('serialized-judge: preCleanTimeoutMs must be a positive finite number')
|
|
315
|
+
}
|
|
316
|
+
const cacheLevel = opts.cacheLevel ?? 'instance'
|
|
317
|
+
const command = opts.command ?? defaultCommand
|
|
318
|
+
// A command override is a hermetic test hook and does not touch Docker.
|
|
319
|
+
// Tests that exercise cleanup opt in by setting preCleanTimeoutMs.
|
|
320
|
+
const shouldPreClean = usingRealJudge || opts.preCleanTimeoutMs !== undefined
|
|
321
|
+
|
|
322
|
+
async function runDockerCleanup(argv: string[], signal?: AbortSignal): Promise<Awaited<ReturnType<typeof run>>> {
|
|
323
|
+
let result: Awaited<ReturnType<typeof run>>
|
|
324
|
+
try {
|
|
325
|
+
result = await run('docker', argv, { timeoutMs: preCleanTimeoutMs, signal })
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new Error(`serialized-judge: docker pre-clean could not start: ${(error as Error).message}`, {
|
|
328
|
+
cause: error,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
throwIfAborted(signal)
|
|
332
|
+
if (result.timedOut) {
|
|
333
|
+
throw new Error(`serialized-judge: docker ${argv[0]} pre-clean timed out after ${preCleanTimeoutMs}ms`)
|
|
334
|
+
}
|
|
335
|
+
if (result.code !== 0) {
|
|
336
|
+
const detail = (result.stderr || result.stdout).trim().slice(0, 1_500)
|
|
337
|
+
throw new Error(
|
|
338
|
+
`serialized-judge: docker ${argv[0]} pre-clean exited ${result.code}${detail ? `: ${detail}` : ''}`,
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
return result
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function preClean(iid: string, signal?: AbortSignal): Promise<void> {
|
|
345
|
+
throwIfAborted(signal)
|
|
346
|
+
const short = instanceShortName(iid)
|
|
347
|
+
const ps = await runDockerCleanup(['ps', '-aq', '--filter', `name=${short}`], signal)
|
|
348
|
+
throwIfAborted(signal)
|
|
349
|
+
const ids = ps.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
350
|
+
if (ids.length > 0) {
|
|
351
|
+
await runDockerCleanup(['rm', '-f', ...ids], signal)
|
|
352
|
+
throwIfAborted(signal)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function attempt(
|
|
357
|
+
iid: string,
|
|
358
|
+
patchPath: string,
|
|
359
|
+
signal?: AbortSignal,
|
|
360
|
+
): Promise<{ verdict?: Omit<JudgeVerdict, 'attempts'>; raw: string }> {
|
|
361
|
+
// Pre-clean INSIDE the mutex: with judging serialized this can only ever
|
|
362
|
+
// remove a stale container, never a live grade.
|
|
363
|
+
if (shouldPreClean) await preClean(iid, signal)
|
|
364
|
+
throwIfAborted(signal)
|
|
365
|
+
const { bin, argv, cwd } = command(iid, patchPath)
|
|
366
|
+
const res = await run(bin, argv, {
|
|
367
|
+
cwd,
|
|
368
|
+
timeoutMs,
|
|
369
|
+
signal,
|
|
370
|
+
env: { ...process.env, SWEBENCH_CACHE_LEVEL: cacheLevel },
|
|
371
|
+
})
|
|
372
|
+
throwIfAborted(signal)
|
|
373
|
+
const raw = res.timedOut
|
|
374
|
+
? `timeout after ${timeoutMs}ms`
|
|
375
|
+
: res.code === 0
|
|
376
|
+
? res.stdout || res.stderr
|
|
377
|
+
: `exit ${res.code}: ${res.stdout || res.stderr}`
|
|
378
|
+
return {
|
|
379
|
+
verdict: !res.timedOut && res.code === 0 ? parseJudgeResult(res.stdout) : undefined,
|
|
380
|
+
raw,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
lockFile,
|
|
386
|
+
async judge(iid, patchPath, _tag = 'x', signal) {
|
|
387
|
+
throwIfAborted(signal)
|
|
388
|
+
// Empty patch never reaches docker — same short-circuit as judge.sh.
|
|
389
|
+
const patch = await readFile(patchPath, 'utf8')
|
|
390
|
+
throwIfAborted(signal)
|
|
391
|
+
if (patch.trim().length === 0) {
|
|
392
|
+
return { iid, resolved: false, score: 0, note: 'empty-patch', attempts: 0 }
|
|
393
|
+
}
|
|
394
|
+
return withJudgeLock(
|
|
395
|
+
lockFile,
|
|
396
|
+
async () => {
|
|
397
|
+
const first = await attempt(iid, patchPath, signal)
|
|
398
|
+
if (first.verdict) return { ...first.verdict, attempts: 1 }
|
|
399
|
+
throwIfAborted(signal)
|
|
400
|
+
const second = await attempt(iid, patchPath, signal)
|
|
401
|
+
if (second.verdict) return { ...second.verdict, attempts: 2 }
|
|
402
|
+
return {
|
|
403
|
+
iid,
|
|
404
|
+
resolved: null,
|
|
405
|
+
error: 'parse-or-timeout',
|
|
406
|
+
raw: second.raw.slice(0, 200),
|
|
407
|
+
attempts: 2,
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{ timeoutMs: lockWaitTimeoutMs, signal },
|
|
411
|
+
)
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
}
|