@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,500 @@
|
|
|
1
|
+
import { access, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterAll, describe, expect, it } from 'vitest'
|
|
5
|
+
import { runSupervisorShim, supervisorDriverKillGraceMs } from './arms.ts'
|
|
6
|
+
import { run, TIMEOUT_RC } from './proc.ts'
|
|
7
|
+
|
|
8
|
+
const dirs: string[] = []
|
|
9
|
+
const workerPids: number[] = []
|
|
10
|
+
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
for (const pid of workerPids) {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(-pid, 'SIGKILL')
|
|
15
|
+
} catch {
|
|
16
|
+
// The expected path already reaped the process group.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('supervisor driver cancellation', () => {
|
|
23
|
+
it('waits for worker cleanup before returning its deadline result', async () => {
|
|
24
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-cancel-'))
|
|
25
|
+
dirs.push(dir)
|
|
26
|
+
const workspace = join(dir, 'workspace')
|
|
27
|
+
const params = join(dir, 'params.json')
|
|
28
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
29
|
+
await mkdir(workspace)
|
|
30
|
+
await writeFile(params, '{}')
|
|
31
|
+
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
|
|
32
|
+
await writeFile(extension, `
|
|
33
|
+
import { spawn } from 'node:child_process'
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
35
|
+
import { join } from 'node:path'
|
|
36
|
+
|
|
37
|
+
const id = 'sup-1-fake12'
|
|
38
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
39
|
+
let child
|
|
40
|
+
|
|
41
|
+
export default function fakeExtension(pi) {
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
name: 'spawn_supervisor',
|
|
44
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
45
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
46
|
+
mkdirSync(runDir, { recursive: true })
|
|
47
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running', progress: 'driving' }))
|
|
48
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
49
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
50
|
+
const readyPath = join(ctx.cwd, 'worker-ready.txt')
|
|
51
|
+
child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'worker-cleaned.txt'), readyPath], {
|
|
52
|
+
detached: true,
|
|
53
|
+
stdio: 'ignore',
|
|
54
|
+
})
|
|
55
|
+
writeFileSync(join(ctx.cwd, 'worker.pid'), String(child.pid))
|
|
56
|
+
while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
|
|
57
|
+
return text('spawned supervisor ' + id)
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
pi.registerTool({
|
|
61
|
+
name: 'supervisor_cancel',
|
|
62
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
63
|
+
appendFileSync(join(ctx.cwd, 'cancel-count.txt'), '1\\n')
|
|
64
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
65
|
+
child.once('close', () => {
|
|
66
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
67
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1', reason: 'test cancellation' }) + '\\n')
|
|
68
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled', progress: 'cancelled' }))
|
|
69
|
+
writeFileSync(join(ctx.cwd, 'cancel-settled.txt'), 'settled')
|
|
70
|
+
})
|
|
71
|
+
return text('cancellation signalled before worker cleanup')
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
`)
|
|
76
|
+
|
|
77
|
+
const result = await run(process.execPath, [
|
|
78
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
79
|
+
], {
|
|
80
|
+
timeoutMs: 5_000,
|
|
81
|
+
killGraceMs: 500,
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
DRIVER_DEADLINE_MS: '40',
|
|
85
|
+
DRIVER_POLL_MS: '10',
|
|
86
|
+
DRIVER_CANCEL_TIMEOUT_MS: '2000',
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
const workerPid = Number(await readFile(join(workspace, 'worker.pid'), 'utf8'))
|
|
90
|
+
workerPids.push(workerPid)
|
|
91
|
+
|
|
92
|
+
expect(result.code, result.stdout + result.stderr).toBe(3)
|
|
93
|
+
expect(result.timedOut).toBe(false)
|
|
94
|
+
expect(await readFile(join(workspace, 'cancel-count.txt'), 'utf8')).toBe('1\n')
|
|
95
|
+
expect(await readFile(join(workspace, 'worker-cleaned.txt'), 'utf8')).toBe('clean')
|
|
96
|
+
expect(await readFile(join(workspace, 'cancel-settled.txt'), 'utf8')).toBe('settled')
|
|
97
|
+
expect(JSON.parse(await readFile(join(workspace, '.loops', 'supervisor', 'sup-1-fake12', 'state.json'), 'utf8')))
|
|
98
|
+
.toMatchObject({ status: 'cancelled' })
|
|
99
|
+
await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('lets near-deadline cancellation settle after outer SIGTERM', async () => {
|
|
103
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-outer-term-'))
|
|
104
|
+
dirs.push(dir)
|
|
105
|
+
const workspace = join(dir, 'workspace')
|
|
106
|
+
const params = join(dir, 'params.json')
|
|
107
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
108
|
+
await mkdir(workspace)
|
|
109
|
+
await writeFile(params, '{}')
|
|
110
|
+
await writeFile(extension, `
|
|
111
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
112
|
+
import { join } from 'node:path'
|
|
113
|
+
|
|
114
|
+
const id = 'sup-2-fake34'
|
|
115
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
116
|
+
export default function fakeExtension(pi) {
|
|
117
|
+
pi.registerTool({
|
|
118
|
+
name: 'spawn_supervisor',
|
|
119
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
120
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
121
|
+
mkdirSync(runDir, { recursive: true })
|
|
122
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
|
|
123
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
124
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
125
|
+
return text('spawned supervisor ' + id)
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
pi.registerTool({
|
|
129
|
+
name: 'supervisor_cancel',
|
|
130
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 1200))
|
|
132
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
133
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
|
|
134
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
|
|
135
|
+
writeFileSync(join(ctx.cwd, 'outer-cancel-settled.txt'), 'settled')
|
|
136
|
+
return text('cancellation settled')
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
`)
|
|
141
|
+
|
|
142
|
+
const cancelTimeoutMs = 1_500
|
|
143
|
+
const result = await run(process.execPath, [
|
|
144
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
145
|
+
], {
|
|
146
|
+
timeoutMs: 1_000,
|
|
147
|
+
killGraceMs: supervisorDriverKillGraceMs({
|
|
148
|
+
DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
|
|
149
|
+
}),
|
|
150
|
+
env: {
|
|
151
|
+
...process.env,
|
|
152
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
153
|
+
DRIVER_POLL_MS: '10',
|
|
154
|
+
DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
|
|
159
|
+
expect(result.timedOut).toBe(true)
|
|
160
|
+
expect(await readFile(join(workspace, 'outer-cancel-settled.txt'), 'utf8')).toBe('settled')
|
|
161
|
+
expect(JSON.parse(await readFile(join(workspace, '.loops', 'supervisor', 'sup-2-fake34', 'state.json'), 'utf8')))
|
|
162
|
+
.toMatchObject({ status: 'cancelled' })
|
|
163
|
+
}, 15_000)
|
|
164
|
+
|
|
165
|
+
it('latches SIGTERM while spawn_supervisor is still returning and cancels its detached worker', async () => {
|
|
166
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-signal-during-spawn-'))
|
|
167
|
+
dirs.push(dir)
|
|
168
|
+
const workspace = join(dir, 'workspace')
|
|
169
|
+
const params = join(dir, 'params.json')
|
|
170
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
171
|
+
await mkdir(workspace)
|
|
172
|
+
await writeFile(params, '{}')
|
|
173
|
+
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
|
|
174
|
+
await writeFile(extension, `
|
|
175
|
+
import { spawn } from 'node:child_process'
|
|
176
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
177
|
+
import { join } from 'node:path'
|
|
178
|
+
|
|
179
|
+
const id = 'sup-3-signal56'
|
|
180
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
181
|
+
let child
|
|
182
|
+
export default function fakeExtension(pi) {
|
|
183
|
+
pi.registerTool({
|
|
184
|
+
name: 'spawn_supervisor',
|
|
185
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
186
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
187
|
+
mkdirSync(runDir, { recursive: true })
|
|
188
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
|
|
189
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
190
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
191
|
+
child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'signal-worker-cleaned.txt')], {
|
|
192
|
+
detached: true,
|
|
193
|
+
stdio: 'ignore',
|
|
194
|
+
})
|
|
195
|
+
writeFileSync(join(ctx.cwd, 'signal-worker.pid'), String(child.pid))
|
|
196
|
+
writeFileSync(join(ctx.cwd, 'spawn-entered.txt'), 'entered')
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 30_000))
|
|
198
|
+
return text('spawned supervisor ' + id)
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name: 'supervisor_cancel',
|
|
203
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
204
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
205
|
+
await new Promise((resolve) => child.once('close', resolve))
|
|
206
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
207
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
|
|
208
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
|
|
209
|
+
writeFileSync(join(ctx.cwd, 'signal-cancel-settled.txt'), 'settled')
|
|
210
|
+
return text('cancellation settled')
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
`)
|
|
215
|
+
|
|
216
|
+
const cancelTimeoutMs = 2_000
|
|
217
|
+
const result = await run(process.execPath, [
|
|
218
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
219
|
+
], {
|
|
220
|
+
timeoutMs: 1_000,
|
|
221
|
+
killGraceMs: supervisorDriverKillGraceMs({
|
|
222
|
+
DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
|
|
223
|
+
}),
|
|
224
|
+
env: {
|
|
225
|
+
...process.env,
|
|
226
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
227
|
+
DRIVER_POLL_MS: '10',
|
|
228
|
+
DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
const workerPid = Number(await readFile(join(workspace, 'signal-worker.pid'), 'utf8'))
|
|
232
|
+
workerPids.push(workerPid)
|
|
233
|
+
|
|
234
|
+
expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
|
|
235
|
+
expect(result.timedOut).toBe(true)
|
|
236
|
+
expect(await readFile(join(workspace, 'spawn-entered.txt'), 'utf8')).toBe('entered')
|
|
237
|
+
expect(await readFile(join(workspace, 'signal-worker-cleaned.txt'), 'utf8')).toBe('clean')
|
|
238
|
+
expect(await readFile(join(workspace, 'signal-cancel-settled.txt'), 'utf8')).toBe('settled')
|
|
239
|
+
await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
240
|
+
}, 15_000)
|
|
241
|
+
|
|
242
|
+
it('recovers an on-disk supervisor id from an unparseable spawn response and cancels it', async () => {
|
|
243
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-id-recovery-'))
|
|
244
|
+
dirs.push(dir)
|
|
245
|
+
const workspace = join(dir, 'workspace')
|
|
246
|
+
const params = join(dir, 'params.json')
|
|
247
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
248
|
+
await mkdir(workspace)
|
|
249
|
+
await writeFile(params, '{}')
|
|
250
|
+
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
|
|
251
|
+
await writeFile(extension, `
|
|
252
|
+
import { spawn } from 'node:child_process'
|
|
253
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
254
|
+
import { join } from 'node:path'
|
|
255
|
+
|
|
256
|
+
const id = 'sup-4-recover78'
|
|
257
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
258
|
+
let child
|
|
259
|
+
export default function fakeExtension(pi) {
|
|
260
|
+
pi.registerTool({
|
|
261
|
+
name: 'spawn_supervisor',
|
|
262
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
263
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
264
|
+
mkdirSync(runDir, { recursive: true })
|
|
265
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
|
|
266
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
267
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
268
|
+
child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'recovered-worker-cleaned.txt')], {
|
|
269
|
+
detached: true,
|
|
270
|
+
stdio: 'ignore',
|
|
271
|
+
})
|
|
272
|
+
writeFileSync(join(ctx.cwd, 'recovered-worker.pid'), String(child.pid))
|
|
273
|
+
return text('the supervisor started, but this response omitted its identifier')
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
pi.registerTool({
|
|
277
|
+
name: 'supervisor_cancel',
|
|
278
|
+
async execute(_call, params, _signal, _update, ctx) {
|
|
279
|
+
writeFileSync(join(ctx.cwd, 'recovered-id.txt'), params.id)
|
|
280
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
281
|
+
await new Promise((resolve) => child.once('close', resolve))
|
|
282
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
283
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
|
|
284
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
|
|
285
|
+
return text('cancellation settled')
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
`)
|
|
290
|
+
|
|
291
|
+
const result = await run(process.execPath, [
|
|
292
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
293
|
+
], {
|
|
294
|
+
timeoutMs: 5_000,
|
|
295
|
+
killGraceMs: 500,
|
|
296
|
+
env: {
|
|
297
|
+
...process.env,
|
|
298
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
299
|
+
DRIVER_POLL_MS: '10',
|
|
300
|
+
DRIVER_CANCEL_TIMEOUT_MS: '2000',
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
const workerPid = Number(await readFile(join(workspace, 'recovered-worker.pid'), 'utf8'))
|
|
304
|
+
workerPids.push(workerPid)
|
|
305
|
+
|
|
306
|
+
expect(result.code, result.stdout + result.stderr).toBe(1)
|
|
307
|
+
expect(result.timedOut).toBe(false)
|
|
308
|
+
expect(result.stderr).toMatch(/cancelling recovered run\(s\): sup-4-recover78/)
|
|
309
|
+
expect(await readFile(join(workspace, 'recovered-id.txt'), 'utf8')).toBe('sup-4-recover78')
|
|
310
|
+
await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
311
|
+
}, 15_000)
|
|
312
|
+
|
|
313
|
+
it('waits for a late worker terminal record before accepting completed state', async () => {
|
|
314
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-late-settlement-'))
|
|
315
|
+
dirs.push(dir)
|
|
316
|
+
const workspace = join(dir, 'workspace')
|
|
317
|
+
const params = join(dir, 'params.json')
|
|
318
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
319
|
+
await mkdir(workspace)
|
|
320
|
+
await writeFile(params, '{}')
|
|
321
|
+
await writeFile(extension, `
|
|
322
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
323
|
+
import { join } from 'node:path'
|
|
324
|
+
|
|
325
|
+
const id = 'sup-5-late90'
|
|
326
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
327
|
+
export default function fakeExtension(pi) {
|
|
328
|
+
pi.registerTool({
|
|
329
|
+
name: 'spawn_supervisor',
|
|
330
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
331
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
332
|
+
mkdirSync(runDir, { recursive: true })
|
|
333
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'completed', verdict: 'delivered' }))
|
|
334
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
335
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'settled', id: id + ':s1', status: 'done' }) + '\\n')
|
|
338
|
+
}, 40)
|
|
339
|
+
return text('spawned supervisor ' + id)
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
pi.registerTool({
|
|
343
|
+
name: 'supervisor_cancel',
|
|
344
|
+
async execute() { return text('already completed') },
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
`)
|
|
348
|
+
|
|
349
|
+
const result = await run(process.execPath, [
|
|
350
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
351
|
+
], {
|
|
352
|
+
timeoutMs: 5_000,
|
|
353
|
+
killGraceMs: 500,
|
|
354
|
+
env: {
|
|
355
|
+
...process.env,
|
|
356
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
357
|
+
DRIVER_POLL_MS: '10',
|
|
358
|
+
DRIVER_CANCEL_TIMEOUT_MS: '2000',
|
|
359
|
+
DRIVER_SETTLEMENT_TIMEOUT_MS: '500',
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
expect(result.code, result.stdout + result.stderr).toBe(0)
|
|
364
|
+
expect(result.timedOut).toBe(false)
|
|
365
|
+
expect(result.stdout).toMatch(/worker settlement: workers=1, terminal=1/)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('refuses completed state while a journal worker remains live', async () => {
|
|
369
|
+
const dir = await mkdtemp(join(tmpdir(), 'swe-driver-live-completed-worker-'))
|
|
370
|
+
dirs.push(dir)
|
|
371
|
+
const workspace = join(dir, 'workspace')
|
|
372
|
+
const params = join(dir, 'params.json')
|
|
373
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
374
|
+
await mkdir(workspace)
|
|
375
|
+
await writeFile(params, '{}')
|
|
376
|
+
await writeFile(extension, `
|
|
377
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
378
|
+
import { join } from 'node:path'
|
|
379
|
+
|
|
380
|
+
const id = 'sup-5-live90'
|
|
381
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
382
|
+
export default function fakeExtension(pi) {
|
|
383
|
+
pi.registerTool({
|
|
384
|
+
name: 'spawn_supervisor',
|
|
385
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
386
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
387
|
+
mkdirSync(runDir, { recursive: true })
|
|
388
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'completed', verdict: 'delivered' }))
|
|
389
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
390
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
391
|
+
return text('spawned supervisor ' + id)
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
pi.registerTool({
|
|
395
|
+
name: 'supervisor_cancel',
|
|
396
|
+
async execute() { return text('already completed') },
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
`)
|
|
400
|
+
|
|
401
|
+
const result = await run(process.execPath, [
|
|
402
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
403
|
+
], {
|
|
404
|
+
timeoutMs: 5_000,
|
|
405
|
+
killGraceMs: 500,
|
|
406
|
+
env: {
|
|
407
|
+
...process.env,
|
|
408
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
409
|
+
DRIVER_POLL_MS: '10',
|
|
410
|
+
DRIVER_CANCEL_TIMEOUT_MS: '2000',
|
|
411
|
+
DRIVER_SETTLEMENT_TIMEOUT_MS: '80',
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
expect(result.code, result.stdout + result.stderr).toBe(4)
|
|
416
|
+
expect(result.timedOut).toBe(false)
|
|
417
|
+
expect(result.stderr).toMatch(/refusing completed supervisor/)
|
|
418
|
+
expect(result.stderr).toMatch(/workers=1, terminal=0, unsettled=1/)
|
|
419
|
+
expect(result.stdout).not.toContain('[done] id=')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it.each(['failed', 'cancelled'] as const)(
|
|
423
|
+
'settles a detached worker before returning terminal status $status',
|
|
424
|
+
async (status) => {
|
|
425
|
+
const dir = await mkdtemp(join(tmpdir(), `swe-driver-${status}-worker-`))
|
|
426
|
+
dirs.push(dir)
|
|
427
|
+
const workspace = join(dir, 'workspace')
|
|
428
|
+
const params = join(dir, 'params.json')
|
|
429
|
+
const extension = join(dir, 'fake-extension.mjs')
|
|
430
|
+
await mkdir(workspace)
|
|
431
|
+
await writeFile(params, '{}')
|
|
432
|
+
const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
|
|
433
|
+
await writeFile(extension, `
|
|
434
|
+
import { spawn } from 'node:child_process'
|
|
435
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
436
|
+
import { join } from 'node:path'
|
|
437
|
+
|
|
438
|
+
const id = 'sup-6-${status}12'
|
|
439
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] })
|
|
440
|
+
let child
|
|
441
|
+
export default function fakeExtension(pi) {
|
|
442
|
+
pi.registerTool({
|
|
443
|
+
name: 'spawn_supervisor',
|
|
444
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
445
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
446
|
+
mkdirSync(runDir, { recursive: true })
|
|
447
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
|
|
448
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
|
|
449
|
+
const readyPath = join(ctx.cwd, 'terminal-worker-ready.txt')
|
|
450
|
+
child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'terminal-worker-cleaned.txt'), readyPath], {
|
|
451
|
+
detached: true,
|
|
452
|
+
stdio: 'ignore',
|
|
453
|
+
})
|
|
454
|
+
writeFileSync(join(ctx.cwd, 'terminal-worker.pid'), String(child.pid))
|
|
455
|
+
while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
|
|
456
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: '${status}', error: 'fixture terminal state' }))
|
|
457
|
+
return text('spawned supervisor ' + id)
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
pi.registerTool({
|
|
461
|
+
name: 'supervisor_cancel',
|
|
462
|
+
async execute(_call, _params, _signal, _update, ctx) {
|
|
463
|
+
appendFileSync(join(ctx.cwd, 'terminal-cancel-count.txt'), '1\\n')
|
|
464
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
465
|
+
await new Promise((resolve) => child.once('close', resolve))
|
|
466
|
+
const runDir = join(ctx.cwd, '.loops', 'supervisor', id)
|
|
467
|
+
appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'cancelled', id: id + ':s1' }) + '\\n')
|
|
468
|
+
writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'cancelled' }))
|
|
469
|
+
return text('terminal worker settled')
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
`)
|
|
474
|
+
|
|
475
|
+
const result = await run(process.execPath, [
|
|
476
|
+
'--import', 'tsx', runSupervisorShim, extension, workspace, params,
|
|
477
|
+
], {
|
|
478
|
+
timeoutMs: 5_000,
|
|
479
|
+
killGraceMs: 500,
|
|
480
|
+
env: {
|
|
481
|
+
...process.env,
|
|
482
|
+
DRIVER_DEADLINE_MS: '60000',
|
|
483
|
+
DRIVER_POLL_MS: '10',
|
|
484
|
+
DRIVER_CANCEL_TIMEOUT_MS: '2000',
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
const workerPid = Number(await readFile(join(workspace, 'terminal-worker.pid'), 'utf8'))
|
|
488
|
+
workerPids.push(workerPid)
|
|
489
|
+
|
|
490
|
+
expect(result.code, result.stdout + result.stderr).toBe(1)
|
|
491
|
+
expect(result.timedOut).toBe(false)
|
|
492
|
+
expect(result.stdout).toContain(`[done] id=sup-6-${status}12 status=${status}`)
|
|
493
|
+
expect(result.stdout).toContain('worker settlement: workers=1, terminal=1')
|
|
494
|
+
expect(await readFile(join(workspace, 'terminal-cancel-count.txt'), 'utf8')).toBe('1\n')
|
|
495
|
+
expect(await readFile(join(workspace, 'terminal-worker-cleaned.txt'), 'utf8')).toBe('clean')
|
|
496
|
+
await expect(access(`/proc/${workerPid}`)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
497
|
+
},
|
|
498
|
+
15_000,
|
|
499
|
+
)
|
|
500
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gen-5 public/private score split (SOTA adoption #1, AIDE2-style).
|
|
3
|
+
*
|
|
4
|
+
* Per generation the improvement instances are DETERMINISTICALLY split
|
|
5
|
+
* (seeded by runId) into a PUBLIC set — whose scores/evidence proposers and
|
|
6
|
+
* the pre-filter may see — and a PRIVATE set that is scored identically but
|
|
7
|
+
* NEVER surfaced to proposers. Winner selection stays on the COMBINED
|
|
8
|
+
* public+private score (the lib judge already averages every scenario), so
|
|
9
|
+
* the split changes only what feedback the search sees, exactly the
|
|
10
|
+
* anti-overfitting mechanism: a candidate that games the visible instances
|
|
11
|
+
* still has to survive the instances it never saw named.
|
|
12
|
+
*
|
|
13
|
+
* SMALL-N CAVEAT: with only 6 improvement instances the split is 4 public /
|
|
14
|
+
* 2 private. Two private instances give a coarse overfit signal (0, 1/2 or
|
|
15
|
+
* 2/2) and a single flake flips it — treat the private sub-score as a
|
|
16
|
+
* direction check, not a certification; the pre-registered 6-instance holdout
|
|
17
|
+
* remains the only promotion evidence.
|
|
18
|
+
*
|
|
19
|
+
* RESUME STABILITY: runId embeds a launch timestamp, so a resumed run gets a
|
|
20
|
+
* fresh runId. The split is therefore PERSISTED to <outDir>/score-split.json
|
|
21
|
+
* on first computation and reloaded verbatim afterwards — a resume can never
|
|
22
|
+
* rotate previously-private instances into view.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import { existsSync } from 'node:fs'
|
|
27
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
28
|
+
import { dirname, join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
export const SCORE_SPLIT_SCHEMA = 'swe-arena.score-split.v1'
|
|
31
|
+
export const SCORE_SPLIT_FILENAME = 'score-split.json'
|
|
32
|
+
|
|
33
|
+
export interface ScoreSplitConfig {
|
|
34
|
+
/** Number of PUBLIC instances (the rest are private). Must satisfy
|
|
35
|
+
* 0 < publicCount < instances.length. */
|
|
36
|
+
publicCount: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScoreSplit {
|
|
40
|
+
schema: typeof SCORE_SPLIT_SCHEMA
|
|
41
|
+
/** The runId that seeded the split (provenance; a resumed run keeps it). */
|
|
42
|
+
seededBy: string
|
|
43
|
+
publicInstances: string[]
|
|
44
|
+
privateInstances: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Deterministic split: instances are ranked by sha256(runId + ':' + iid) and
|
|
48
|
+
* the first `publicCount` become public. Pure — same (runId, instances,
|
|
49
|
+
* publicCount) always yields the same split; instance-list ORDER does not
|
|
50
|
+
* matter (ranking is content-derived). */
|
|
51
|
+
export function splitInstances(runId: string, instances: readonly string[], publicCount: number): ScoreSplit {
|
|
52
|
+
if (!Number.isInteger(publicCount) || publicCount <= 0 || publicCount >= instances.length) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`score-split: publicCount must be an integer in (0, ${instances.length}), got ${JSON.stringify(publicCount)}`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (new Set(instances).size !== instances.length) {
|
|
58
|
+
throw new Error('score-split: duplicate instance ids')
|
|
59
|
+
}
|
|
60
|
+
const ranked = [...instances]
|
|
61
|
+
.map((iid) => ({ iid, rank: createHash('sha256').update(`${runId}:${iid}`).digest('hex') }))
|
|
62
|
+
.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0))
|
|
63
|
+
const pub = ranked.slice(0, publicCount).map((r) => r.iid)
|
|
64
|
+
const priv = ranked.slice(publicCount).map((r) => r.iid)
|
|
65
|
+
return {
|
|
66
|
+
schema: SCORE_SPLIT_SCHEMA,
|
|
67
|
+
seededBy: runId,
|
|
68
|
+
publicInstances: [...pub].sort(),
|
|
69
|
+
privateInstances: [...priv].sort(),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Load the persisted split for this outDir, or compute + persist it. The
|
|
74
|
+
* persisted file always wins (resume stability); it is validated against the
|
|
75
|
+
* current instance set so a config edit mid-outDir fails loud. */
|
|
76
|
+
export async function loadOrCreateScoreSplit(args: {
|
|
77
|
+
outDir: string
|
|
78
|
+
runId: string
|
|
79
|
+
instances: readonly string[]
|
|
80
|
+
publicCount: number
|
|
81
|
+
}): Promise<ScoreSplit> {
|
|
82
|
+
const path = join(args.outDir, SCORE_SPLIT_FILENAME)
|
|
83
|
+
if (existsSync(path)) {
|
|
84
|
+
const split = JSON.parse(await readFile(path, 'utf8')) as ScoreSplit
|
|
85
|
+
if (split.schema !== SCORE_SPLIT_SCHEMA) {
|
|
86
|
+
throw new Error(`score-split: ${path} has unknown schema ${JSON.stringify(split.schema)}`)
|
|
87
|
+
}
|
|
88
|
+
const persisted = [...split.publicInstances, ...split.privateInstances].sort()
|
|
89
|
+
const expected = [...args.instances].sort()
|
|
90
|
+
if (JSON.stringify(persisted) !== JSON.stringify(expected)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`score-split: persisted split at ${path} covers [${persisted.join(', ')}] but the config ` +
|
|
93
|
+
`names [${expected.join(', ')}] — refusing to silently re-split; move the outDir or fix the config`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (split.publicInstances.length !== args.publicCount) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`score-split: persisted split has ${split.publicInstances.length} public instances, config wants ` +
|
|
99
|
+
`${args.publicCount} — refusing to silently re-split`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return split
|
|
103
|
+
}
|
|
104
|
+
const split = splitInstances(args.runId, args.instances, args.publicCount)
|
|
105
|
+
await mkdir(dirname(path), { recursive: true })
|
|
106
|
+
await writeFile(path, JSON.stringify(split, null, 2))
|
|
107
|
+
return split
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True when `text` mentions any private instance id — the NEVER-SURFACED
|
|
111
|
+
* invariant's checkable form. Used by tests and by the evidence-index writer
|
|
112
|
+
* as a final guard on proposer-visible text. */
|
|
113
|
+
export function leaksPrivateInstance(text: string, split: Pick<ScoreSplit, 'privateInstances'>): string[] {
|
|
114
|
+
return split.privateInstances.filter((iid) => text.includes(iid))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Filter per-instance evidence records to the PUBLIC set. Identity function
|
|
118
|
+
* when no split is configured (split === null). */
|
|
119
|
+
export function publicOnly<T>(
|
|
120
|
+
records: readonly T[],
|
|
121
|
+
iidOf: (record: T) => string,
|
|
122
|
+
split: Pick<ScoreSplit, 'privateInstances'> | null,
|
|
123
|
+
): T[] {
|
|
124
|
+
if (split === null) return [...records]
|
|
125
|
+
const priv = new Set(split.privateInstances)
|
|
126
|
+
return records.filter((r) => !priv.has(iidOf(r)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Public/private sub-scores for one candidate, from per-instance AND-verdicts
|
|
130
|
+
* (fail-closed: an instance with no conclusive full-reps verdict counts 0). */
|
|
131
|
+
export function subScores(
|
|
132
|
+
verdicts: Record<string, boolean>,
|
|
133
|
+
split: Pick<ScoreSplit, 'publicInstances' | 'privateInstances'>,
|
|
134
|
+
): { publicResolvedCount: number; privateResolvedCount: number } {
|
|
135
|
+
const count = (iids: readonly string[]): number => iids.filter((iid) => verdicts[iid] === true).length
|
|
136
|
+
return {
|
|
137
|
+
publicResolvedCount: count(split.publicInstances),
|
|
138
|
+
privateResolvedCount: count(split.privateInstances),
|
|
139
|
+
}
|
|
140
|
+
}
|