@tangle-network/agent-bench 0.3.6 → 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.
- package/CHANGELOG.md +7 -0
- package/dist/adapters.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-5SBJCB6W.js → chunk-PWQVGAJB.js} +2 -2
- package/dist/index.js +2 -2
- package/package.json +5 -4
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-pier-agent.mts +1 -0
- 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/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +788 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/calibrate.ts +116 -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 +405 -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/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -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/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 +104 -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/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 +2145 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -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 +489 -0
- package/src/swe-arena/proposer-fanout.test.mts +372 -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 +361 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +498 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +166 -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/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-PWQVGAJB.js.map} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* swe-arena — typed replay of the committed SOLO-vs-SUPERVISOR head-to-head
|
|
3
|
+
* artifacts (SWE-bench Verified, glm-5.2 both arms).
|
|
4
|
+
*
|
|
5
|
+
* MILESTONE 1: these types mirror the fixture files byte-for-byte semantics.
|
|
6
|
+
* They are the proof-of-faithfulness layer: `reconcile.ts` + `analyze.ts`
|
|
7
|
+
* must reproduce the reference `fixtures/analyze.py` output exactly (pinned in
|
|
8
|
+
* `replay.test.mts`) before any typed execution path is built on top.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The fields of one SWE-bench Verified instance we actually consume from
|
|
13
|
+
* `task-meta.json` (generated by the experiment's `load_meta.py` from
|
|
14
|
+
* princeton-nlp/SWE-bench_Verified). `FAIL_TO_PASS` / `PASS_TO_PASS` are
|
|
15
|
+
* JSON-encoded string arrays as shipped by the HF dataset — kept as raw
|
|
16
|
+
* strings here; decode at the point of use.
|
|
17
|
+
*/
|
|
18
|
+
export interface SweInstance {
|
|
19
|
+
instance_id: string
|
|
20
|
+
repo: string
|
|
21
|
+
base_commit: string
|
|
22
|
+
problem_statement: string
|
|
23
|
+
patch: string
|
|
24
|
+
test_patch: string
|
|
25
|
+
/** JSON-encoded string[] (raw HF dataset encoding). */
|
|
26
|
+
FAIL_TO_PASS: string
|
|
27
|
+
/** JSON-encoded string[] (raw HF dataset encoding). */
|
|
28
|
+
PASS_TO_PASS: string
|
|
29
|
+
version: string | null
|
|
30
|
+
environment_setup_commit: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-run opencode usage breakdown captured on the SOLO arm. */
|
|
34
|
+
export interface SoloUsage {
|
|
35
|
+
steps: number
|
|
36
|
+
in: number
|
|
37
|
+
out: number
|
|
38
|
+
reasoning: number
|
|
39
|
+
cache_w: number
|
|
40
|
+
cache_r: number
|
|
41
|
+
max_ctx: number
|
|
42
|
+
oc_cost: number
|
|
43
|
+
total_io: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One paired row of `ledger.jsonl` — exactly the schema the experiment wrote.
|
|
48
|
+
* `null` values are real telemetry gaps (e.g. `sup_spentTokens: null` when the
|
|
49
|
+
* supervisor driver exited rc=3 mid-run), not absent data to be defaulted.
|
|
50
|
+
*/
|
|
51
|
+
export interface LedgerRow {
|
|
52
|
+
iid: string
|
|
53
|
+
solo_resolved: boolean
|
|
54
|
+
sup_resolved: boolean
|
|
55
|
+
solo_verify_pass: boolean
|
|
56
|
+
sup_verify_pass: boolean
|
|
57
|
+
solo_patch_lines: number
|
|
58
|
+
sup_patch_lines: number
|
|
59
|
+
solo_wall_s: number
|
|
60
|
+
sup_wall_s: number
|
|
61
|
+
solo_tokens: number
|
|
62
|
+
solo_usage: SoloUsage
|
|
63
|
+
sup_spentTokens: number | null
|
|
64
|
+
sup_spentUsd: number | null
|
|
65
|
+
sup_spawned: number
|
|
66
|
+
/** Missing on rows written before the field was added (pallets__flask-5014). */
|
|
67
|
+
sup_workers?: number
|
|
68
|
+
sup_settled: number
|
|
69
|
+
sup_subtasks: string[]
|
|
70
|
+
sup_delivered: boolean | null
|
|
71
|
+
/**
|
|
72
|
+
* The fixture ledger observed only completed/running; the driver can also
|
|
73
|
+
* settle failed/cancelled (M2 widened the union — the typed execution path
|
|
74
|
+
* records those honestly instead of coercing them).
|
|
75
|
+
*/
|
|
76
|
+
sup_status: 'completed' | 'running' | 'failed' | 'cancelled' | null
|
|
77
|
+
sup_verdict: 'delivered' | 'no-winner' | 'best-effort' | null
|
|
78
|
+
solo_oc_rc: number
|
|
79
|
+
sup_driver_rc: number
|
|
80
|
+
solo_patch: string
|
|
81
|
+
sup_patch: string
|
|
82
|
+
/** Free-form correction note (e.g. psf__requests-1766 sup re-judge). */
|
|
83
|
+
_note?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Tags carried by `rejudge.jsonl` rows. Gold-family tags grade the OFFICIAL
|
|
88
|
+
* gold patch (calibrating the judge itself); final/my-reverify tags are the
|
|
89
|
+
* personal authoritative re-judgements of arm patches that override the
|
|
90
|
+
* automated ledger verdicts.
|
|
91
|
+
*/
|
|
92
|
+
export type RejudgeTag =
|
|
93
|
+
| 'gold-control'
|
|
94
|
+
| 'gold'
|
|
95
|
+
| 'gold2'
|
|
96
|
+
| 'solo-final'
|
|
97
|
+
| 'sup-final'
|
|
98
|
+
| 'solo-final2'
|
|
99
|
+
| 'sup-final2'
|
|
100
|
+
| 'my-reverify'
|
|
101
|
+
|
|
102
|
+
/** One row of `rejudge.jsonl`. `resolved: null` = the judge run failed to parse (inconclusive). */
|
|
103
|
+
export interface RejudgeRow {
|
|
104
|
+
iid: string
|
|
105
|
+
tag: RejudgeTag
|
|
106
|
+
patch: string
|
|
107
|
+
resolved: boolean | null
|
|
108
|
+
score?: number
|
|
109
|
+
secs?: number
|
|
110
|
+
patch_bytes?: number
|
|
111
|
+
error?: string
|
|
112
|
+
raw?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Evolution-round labels: SUP2/3/4 = rematch.jsonl / rematch2.jsonl / rematch3.jsonl. */
|
|
116
|
+
export type RematchArm = 'SUP2' | 'SUP3' | 'SUP4'
|
|
117
|
+
|
|
118
|
+
/** One row of a rematch*.jsonl evolution round. */
|
|
119
|
+
export interface RematchRow {
|
|
120
|
+
iid: string
|
|
121
|
+
arm: RematchArm
|
|
122
|
+
resolved: boolean
|
|
123
|
+
verify_pass: boolean
|
|
124
|
+
patch_lines: number
|
|
125
|
+
wall_s: number
|
|
126
|
+
spawned: number
|
|
127
|
+
workers: number
|
|
128
|
+
sup_status: 'completed' | 'running'
|
|
129
|
+
sup_verdict: 'delivered' | 'no-winner' | 'best-effort' | null
|
|
130
|
+
delivered: boolean | null
|
|
131
|
+
spentTokens: number | null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Declarative arm identity for typed execution paths built on this module. */
|
|
135
|
+
export interface ArmSpec {
|
|
136
|
+
name: string
|
|
137
|
+
kind: 'solo' | 'supervisor'
|
|
138
|
+
env: Record<string, string>
|
|
139
|
+
provenance: { repo: string; commit: string }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One pre-registered holdout instance from `holdout.json`. */
|
|
143
|
+
export interface HoldoutEntry {
|
|
144
|
+
iid: string
|
|
145
|
+
repo: string
|
|
146
|
+
gold_official_resolved: boolean
|
|
147
|
+
verify_calibrated: boolean
|
|
148
|
+
selected_at_commit: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The holdout registry: instances selected BEFORE any arm ran (see
|
|
153
|
+
* `holdout-preregister.log`), each with its gold patch verified against the
|
|
154
|
+
* calibrated judge. Only entries passing both checks are usable.
|
|
155
|
+
*/
|
|
156
|
+
export interface HoldoutRegistry {
|
|
157
|
+
entries: HoldoutEntry[]
|
|
158
|
+
/** Commit of the supervisor runtime (`loops`) at selection time. */
|
|
159
|
+
selectedAtCommit: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Per-instance supervisor worker-session spend from `worker-tokens.json`. */
|
|
163
|
+
export interface WorkerTokens {
|
|
164
|
+
worker_sessions: number
|
|
165
|
+
worker_tok: number
|
|
166
|
+
}
|