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