@tangle-network/agent-bench 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/README.md +7 -0
- package/dist/adapters.js +23 -23
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +23 -23
- package/package.json +5 -5
- package/scripts/trata-hedge/README.md +6 -5
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/gate.ts +1 -1
- package/src/hev-eval.mts +5 -2
- package/src/hev-improve.mts +118 -73
- package/src/official-optimizer-config.mts +89 -0
- package/src/official-optimizer-config.test.mts +88 -0
- package/src/profiles.ts +2 -2
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/smoke-structural-rollout.mts +15 -9
- package/src/swe-arena/activation.mts +225 -0
- package/src/swe-arena/activation.test.mts +300 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +877 -0
- package/src/swe-arena/gepa-seat.test.mts +1059 -0
- package/src/swe-arena/implementation-ref.test.mts +64 -0
- package/src/swe-arena/implementation-ref.ts +62 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/outer-loop.mts +710 -33
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proposer-fanout.mts +327 -31
- package/src/swe-arena/proposer-fanout.test.mts +246 -0
- package/src/swe-arena/proposer-provenance.mts +172 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/scratch-worktree.test.mts +55 -0
- package/src/swe-arena/scratch-worktree.ts +34 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-code-improve.mts +24 -25
- package/src/swe-improve.mts +129 -96
- package/src/swe-local-proof.mts +6 -1
- package/src/swe-stream.mts +4 -2
- package/src/swe-structural.mts +245 -837
- package/src/tb-container-executor.test.mts +30 -6
- package/src/tb-supervisor-sidecar.mts +2 -1
- package/src/trata-gepa.mts +182 -245
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/src/live-improve-campaign-mbpp.mts +0 -641
- package/src/live-improve-campaign.mts +0 -500
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* swe-arena → rollout backfill — DOMAIN GLUE ONLY. Bench owns the JOIN
|
|
3
|
+
* (manifest + cached cells + judge.json + result.json workerCwds + proposer
|
|
4
|
+
* shot receipts → harness stores); the `tangle.rollout.v1` schema, line
|
|
5
|
+
* validation, serialization, readers, exporters, and release pipeline are
|
|
6
|
+
* owned by `@tangle-network/agent-eval/rollout` — bench never writes a
|
|
7
|
+
* rollout row through anything but that API.
|
|
8
|
+
*
|
|
9
|
+
* Pure READ over one round's outDir
|
|
10
|
+
* (the same artifact tree `swe-arena/manifest.mts` joins) plus the harness
|
|
11
|
+
* stores, emitting `tangle.rollout.v1` lines with capture:"backfill":
|
|
12
|
+
*
|
|
13
|
+
* - one SUPERVISOR line per campaign cell (instance × rep × candidate):
|
|
14
|
+
* reward = the official-judge verdict off cached-result.json, verdict =
|
|
15
|
+
* raw judge.json, cost = the durable episode receipt. The loops brain
|
|
16
|
+
* persists per-request STATS only (brain.jsonl has no message content),
|
|
17
|
+
* so supervisor transcripts are honest gap lines until settle-time
|
|
18
|
+
* capture lands.
|
|
19
|
+
* - one WORKER line per opencode worker session, joined via result.json
|
|
20
|
+
* recoveredSpend.workerCwds → opencode sqlite session.directory, with the
|
|
21
|
+
* FULL transcript inlined and reward inherited from the episode.
|
|
22
|
+
* - one PROPOSER line per proposer shot receipt, joined to the Claude
|
|
23
|
+
* project transcript of the shot's worktree cwd by time window.
|
|
24
|
+
*
|
|
25
|
+
* A failed join is a labeled gap line (messages: [], provenance.gap) — never
|
|
26
|
+
* a silent drop: the ledger must account for every invocation it knows of.
|
|
27
|
+
*
|
|
28
|
+
* tsx src/rollout-ledger/backfill-swe-arena.mts <outDir> <out.jsonl>
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { randomUUID } from 'node:crypto'
|
|
32
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
33
|
+
import { join } from 'node:path'
|
|
34
|
+
import { pathToFileURL } from 'node:url'
|
|
35
|
+
import type { DatabaseSync } from 'node:sqlite'
|
|
36
|
+
import { loadCampaignCellRecords } from '../swe-arena/cell-evidence.mts'
|
|
37
|
+
import { buildRolloutManifest, type RolloutEntry } from '../swe-arena/manifest.mts'
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_CLAUDE_PROJECTS_DIR,
|
|
40
|
+
DEFAULT_OPENCODE_DB,
|
|
41
|
+
findClaudeTranscripts,
|
|
42
|
+
findOpencodeSessionsByDirectory,
|
|
43
|
+
openOpencodeDb,
|
|
44
|
+
readClaudeTranscript,
|
|
45
|
+
readOpencodeSessionMessages,
|
|
46
|
+
ROLLOUT_SCHEMA,
|
|
47
|
+
type OpencodeSessionRow,
|
|
48
|
+
type RolloutLine,
|
|
49
|
+
writeRolloutLedger,
|
|
50
|
+
} from '@tangle-network/agent-eval/rollout'
|
|
51
|
+
import { candidateId } from './settle-capture.mts'
|
|
52
|
+
|
|
53
|
+
const OFFICIAL_JUDGE = 'swe-arena-official-judge'
|
|
54
|
+
|
|
55
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
56
|
+
typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
57
|
+
|
|
58
|
+
/** Full cached-result.json record (superset of the EvidenceCell slice). */
|
|
59
|
+
interface CellRecord {
|
|
60
|
+
scenarioId: string
|
|
61
|
+
rep: number
|
|
62
|
+
artifact: Record<string, unknown> | null
|
|
63
|
+
error?: string
|
|
64
|
+
judgeScores?: Record<string, { composite?: number }>
|
|
65
|
+
costUsd?: number
|
|
66
|
+
tokenUsage?: { input?: number; output?: number }
|
|
67
|
+
resolvedModel?: string
|
|
68
|
+
seed?: number
|
|
69
|
+
cached?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The same cell caches `loadCampaignCells` reads, kept verbatim: the backfill
|
|
73
|
+
* needs fields (judgeScores, resolvedModel, seed) the EvidenceCell slice drops.
|
|
74
|
+
* Traversal and the fail-loud identity check are owned by cell-evidence — one
|
|
75
|
+
* rule for what counts as a campaign cell.
|
|
76
|
+
*
|
|
77
|
+
* Only scenarioId and rep are proven by that check; `artifact` is normalized to
|
|
78
|
+
* a record-or-null here so the `artifact !== null` guards downstream cannot be
|
|
79
|
+
* handed an `undefined` that passes them. Every remaining field is optional and
|
|
80
|
+
* read behind its own typeof guard. */
|
|
81
|
+
async function readCellRecords(campaignDir: string): Promise<CellRecord[]> {
|
|
82
|
+
const records = (await loadCampaignCellRecords(campaignDir)).map((record) => ({
|
|
83
|
+
...record,
|
|
84
|
+
artifact: isRecord(record.artifact) ? record.artifact : null,
|
|
85
|
+
})) as unknown as CellRecord[]
|
|
86
|
+
return records.sort((a, b) => a.scenarioId.localeCompare(b.scenarioId) || a.rep - b.rep)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readJson(path: string): Promise<unknown | null> {
|
|
90
|
+
const raw = await readFile(path, 'utf8').catch(() => null)
|
|
91
|
+
if (raw === null) return null
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw) as unknown
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function splitModelRef(ref: string | undefined): { provider: string | null; model: string | null } {
|
|
100
|
+
if (typeof ref !== 'string' || ref.length === 0) return { provider: null, model: null }
|
|
101
|
+
const slash = ref.indexOf('/')
|
|
102
|
+
if (slash === -1) return { provider: null, model: ref }
|
|
103
|
+
return { provider: ref.slice(0, slash), model: ref.slice(slash + 1) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BackfillStats {
|
|
107
|
+
lines: number
|
|
108
|
+
byRole: Record<string, number>
|
|
109
|
+
fullTranscripts: number
|
|
110
|
+
gapLines: number
|
|
111
|
+
rewardLabeled: number
|
|
112
|
+
workerSessionsJoined: number
|
|
113
|
+
/** Worker cwds with no opencode session row — a join failure. */
|
|
114
|
+
workerCwdsMissed: number
|
|
115
|
+
/** Sessions found but carrying no readable message parts — a store failure. */
|
|
116
|
+
workerSessionsEmpty: number
|
|
117
|
+
proposerShotsJoined: number
|
|
118
|
+
proposerShotsMissed: number
|
|
119
|
+
opencodeDbAvailable: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface BackfillResult {
|
|
123
|
+
lines: RolloutLine[]
|
|
124
|
+
stats: BackfillStats
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface BackfillOptions {
|
|
128
|
+
opencodeDb?: string
|
|
129
|
+
claudeProjectsDir?: string
|
|
130
|
+
/** Injected clock for deterministic tests. */
|
|
131
|
+
now?: () => Date
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface Ctx {
|
|
135
|
+
outDir: string
|
|
136
|
+
runId: string
|
|
137
|
+
instanceCount: number
|
|
138
|
+
db: DatabaseSync | null
|
|
139
|
+
claudeProjectsDir: string
|
|
140
|
+
capturedAt: string
|
|
141
|
+
stats: BackfillStats
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function baseLine(ctx: Ctx): Pick<RolloutLine, 'schema' | 'run_id' | 'provenance'> {
|
|
145
|
+
return {
|
|
146
|
+
schema: ROLLOUT_SCHEMA,
|
|
147
|
+
run_id: ctx.runId,
|
|
148
|
+
provenance: { captured_at: ctx.capturedAt, capture: 'backfill' },
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function push(ctx: Ctx, lines: RolloutLine[], line: RolloutLine): void {
|
|
153
|
+
lines.push(line)
|
|
154
|
+
ctx.stats.lines += 1
|
|
155
|
+
ctx.stats.byRole[line.role] = (ctx.stats.byRole[line.role] ?? 0) + 1
|
|
156
|
+
if (line.messages.length > 0) ctx.stats.fullTranscripts += 1
|
|
157
|
+
else ctx.stats.gapLines += 1
|
|
158
|
+
if (line.outcome.reward !== null) ctx.stats.rewardLabeled += 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Supervisor episode + worker sessions for one campaign cell.
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
async function emitCellLines(
|
|
166
|
+
ctx: Ctx,
|
|
167
|
+
lines: RolloutLine[],
|
|
168
|
+
entry: RolloutEntry,
|
|
169
|
+
cell: CellRecord,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const artifact = cell.artifact
|
|
172
|
+
const runDir = artifact !== null && typeof artifact.runDir === 'string' ? artifact.runDir : null
|
|
173
|
+
const judgeVerdict = runDir !== null ? await readJson(join(runDir, 'judge.json')) : null
|
|
174
|
+
const resultJson = runDir !== null ? await readJson(join(runDir, 'result.json')) : null
|
|
175
|
+
const result = isRecord(resultJson) ? resultJson : null
|
|
176
|
+
|
|
177
|
+
const composite = cell.judgeScores?.[OFFICIAL_JUDGE]?.composite
|
|
178
|
+
const reward =
|
|
179
|
+
typeof composite === 'number'
|
|
180
|
+
? composite
|
|
181
|
+
: artifact !== null && typeof artifact.resolved === 'boolean'
|
|
182
|
+
? (artifact.resolved ? 1 : 0)
|
|
183
|
+
: null
|
|
184
|
+
|
|
185
|
+
const { provider, model } = splitModelRef(cell.resolvedModel)
|
|
186
|
+
const supStatus = result !== null && typeof result.sup_status === 'string' ? result.sup_status : null
|
|
187
|
+
const supervisorId = randomUUID()
|
|
188
|
+
|
|
189
|
+
push(ctx, lines, {
|
|
190
|
+
...baseLine(ctx),
|
|
191
|
+
rollout_id: supervisorId,
|
|
192
|
+
parent_rollout_id: null,
|
|
193
|
+
candidate_id: candidateId(entry.state.generation, entry.state.candidateIndex),
|
|
194
|
+
generation: entry.state.generation,
|
|
195
|
+
candidate_index: entry.state.candidateIndex,
|
|
196
|
+
role: 'supervisor',
|
|
197
|
+
task: {
|
|
198
|
+
suite: 'swe-bench-verified',
|
|
199
|
+
instance_id: cell.scenarioId,
|
|
200
|
+
split: 'search',
|
|
201
|
+
seed: typeof cell.seed === 'number' ? cell.seed : null,
|
|
202
|
+
rep: cell.rep,
|
|
203
|
+
},
|
|
204
|
+
policy: {
|
|
205
|
+
harness: 'pi-loops',
|
|
206
|
+
harness_version: null,
|
|
207
|
+
model,
|
|
208
|
+
provider,
|
|
209
|
+
profile_commit:
|
|
210
|
+
artifact !== null && typeof artifact.commit === 'string' ? artifact.commit : entry.state.commit,
|
|
211
|
+
sampling: null,
|
|
212
|
+
},
|
|
213
|
+
messages: [],
|
|
214
|
+
tool_defs: [],
|
|
215
|
+
outcome: {
|
|
216
|
+
reward,
|
|
217
|
+
reward_source: reward === null ? null : OFFICIAL_JUDGE,
|
|
218
|
+
verdict: judgeVerdict,
|
|
219
|
+
metrics: {
|
|
220
|
+
resolved: artifact?.resolved ?? null,
|
|
221
|
+
verify_pass: artifact?.verifyPass ?? null,
|
|
222
|
+
patch_lines: artifact?.patchLines ?? null,
|
|
223
|
+
judge_attempts: artifact?.judgeAttempts ?? null,
|
|
224
|
+
judge_wall_s: artifact?.judgeWallS ?? null,
|
|
225
|
+
// Supervisor-tree spend (state.json spend tree); the durable episode
|
|
226
|
+
// receipt usd is on cost.usd. Worker-session tokens live on the
|
|
227
|
+
// worker lines — recovered_tokens here is the EPISODE total, so do
|
|
228
|
+
// not sum tokens across roles without dedup.
|
|
229
|
+
spent_tokens: artifact?.spentTokens ?? null,
|
|
230
|
+
spent_usd: artifact?.spentUsd ?? null,
|
|
231
|
+
recovered_tokens: artifact?.recoveredTokens ?? null,
|
|
232
|
+
sup_status: supStatus,
|
|
233
|
+
sup_verdict: result !== null && typeof result.sup_verdict === 'string' ? result.sup_verdict : null,
|
|
234
|
+
spawned: result !== null && typeof result.spawned === 'number' ? result.spawned : null,
|
|
235
|
+
workers: result !== null && typeof result.workers === 'number' ? result.workers : null,
|
|
236
|
+
settled: result !== null && typeof result.settled === 'number' ? result.settled : null,
|
|
237
|
+
subtasks: result !== null && Array.isArray(result.subtasks) ? result.subtasks : null,
|
|
238
|
+
cached: cell.cached ?? null,
|
|
239
|
+
},
|
|
240
|
+
is_completed: artifact !== null && cell.error === undefined,
|
|
241
|
+
is_truncated: supStatus === 'running',
|
|
242
|
+
error: cell.error ?? null,
|
|
243
|
+
},
|
|
244
|
+
cost: {
|
|
245
|
+
// The campaign CostLedger receipt for the whole episode.
|
|
246
|
+
usd: typeof cell.costUsd === 'number' ? cell.costUsd : null,
|
|
247
|
+
// Brain-side token split is not recorded (brain.jsonl is stats-only);
|
|
248
|
+
// totals live in metrics.spent_tokens / recovered_tokens.
|
|
249
|
+
tokens_in: null,
|
|
250
|
+
tokens_out: null,
|
|
251
|
+
tokens_reasoning: null,
|
|
252
|
+
cache_read: null,
|
|
253
|
+
cache_write: null,
|
|
254
|
+
wall_s: artifact !== null && typeof artifact.wallS === 'number' ? artifact.wallS : null,
|
|
255
|
+
},
|
|
256
|
+
artifacts: {
|
|
257
|
+
patch_path: artifact !== null && typeof artifact.patchPath === 'string' ? artifact.patchPath : null,
|
|
258
|
+
run_dir: runDir,
|
|
259
|
+
transcript_ref: runDir !== null ? join(runDir, 'brain.jsonl') : null,
|
|
260
|
+
},
|
|
261
|
+
provenance: {
|
|
262
|
+
captured_at: ctx.capturedAt,
|
|
263
|
+
capture: 'backfill',
|
|
264
|
+
gap: 'supervisor brain transcript not persisted (brain.jsonl carries per-request stats only)',
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Worker sessions: result.json recoveredSpend.workerCwds → opencode store.
|
|
269
|
+
const recovered = result !== null && isRecord(result.recoveredSpend) ? result.recoveredSpend : null
|
|
270
|
+
// A respawned worker reuses its clone cwd, so the recovered list can name the
|
|
271
|
+
// same directory twice. Joining it twice would mint two rollout ids over one
|
|
272
|
+
// opencode session — duplicate training signal, inflated worker counts.
|
|
273
|
+
const workerCwds =
|
|
274
|
+
recovered !== null && Array.isArray(recovered.workerCwds)
|
|
275
|
+
? [...new Set(recovered.workerCwds.filter((c): c is string => typeof c === 'string'))]
|
|
276
|
+
: []
|
|
277
|
+
for (const cwd of workerCwds) {
|
|
278
|
+
const sessions = ctx.db === null ? [] : findOpencodeSessionsByDirectory(ctx.db, cwd)
|
|
279
|
+
if (sessions.length === 0) {
|
|
280
|
+
ctx.stats.workerCwdsMissed += 1
|
|
281
|
+
push(ctx, lines, workerLine(ctx, entry, cell, supervisorId, reward, cwd, null, []))
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
for (const session of sessions) {
|
|
285
|
+
const messages = ctx.db === null ? [] : readOpencodeSessionMessages(ctx.db, session.id)
|
|
286
|
+
// Two distinct failures: no session row for the cwd (join failure) versus
|
|
287
|
+
// a session row whose parts are unreadable (store integrity). One counter
|
|
288
|
+
// for both would hide store corruption behind a cwd-join metric.
|
|
289
|
+
if (messages.length > 0) ctx.stats.workerSessionsJoined += 1
|
|
290
|
+
else ctx.stats.workerSessionsEmpty += 1
|
|
291
|
+
push(ctx, lines, workerLine(ctx, entry, cell, supervisorId, reward, cwd, session, messages))
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function workerLine(
|
|
297
|
+
ctx: Ctx,
|
|
298
|
+
entry: RolloutEntry,
|
|
299
|
+
cell: CellRecord,
|
|
300
|
+
supervisorId: string,
|
|
301
|
+
reward: number | null,
|
|
302
|
+
cwd: string,
|
|
303
|
+
session: OpencodeSessionRow | null,
|
|
304
|
+
messages: RolloutLine['messages'],
|
|
305
|
+
): RolloutLine {
|
|
306
|
+
const artifact = cell.artifact
|
|
307
|
+
return {
|
|
308
|
+
...baseLine(ctx),
|
|
309
|
+
rollout_id: randomUUID(),
|
|
310
|
+
parent_rollout_id: supervisorId,
|
|
311
|
+
candidate_id: candidateId(entry.state.generation, entry.state.candidateIndex),
|
|
312
|
+
generation: entry.state.generation,
|
|
313
|
+
candidate_index: entry.state.candidateIndex,
|
|
314
|
+
role: 'worker',
|
|
315
|
+
task: {
|
|
316
|
+
suite: 'swe-bench-verified',
|
|
317
|
+
instance_id: cell.scenarioId,
|
|
318
|
+
split: 'search',
|
|
319
|
+
seed: typeof cell.seed === 'number' ? cell.seed : null,
|
|
320
|
+
rep: cell.rep,
|
|
321
|
+
},
|
|
322
|
+
policy: {
|
|
323
|
+
harness: 'opencode',
|
|
324
|
+
harness_version: null,
|
|
325
|
+
model: session?.model?.id ?? null,
|
|
326
|
+
provider: session?.model?.providerID ?? null,
|
|
327
|
+
profile_commit:
|
|
328
|
+
artifact !== null && typeof artifact.commit === 'string' ? artifact.commit : entry.state.commit,
|
|
329
|
+
sampling: null,
|
|
330
|
+
},
|
|
331
|
+
messages,
|
|
332
|
+
tool_defs: [],
|
|
333
|
+
outcome: {
|
|
334
|
+
// Episode-level credit: the worker has no verdict of its own.
|
|
335
|
+
reward,
|
|
336
|
+
reward_source: reward === null ? null : `${OFFICIAL_JUDGE}/inherited`,
|
|
337
|
+
verdict: null,
|
|
338
|
+
metrics: {
|
|
339
|
+
worker_cwd: cwd,
|
|
340
|
+
session_agent: session?.agent ?? null,
|
|
341
|
+
session_parent_id: session?.parentId ?? null,
|
|
342
|
+
has_session: session !== null,
|
|
343
|
+
},
|
|
344
|
+
// A session row with no readable parts is a gap line, not a completed
|
|
345
|
+
// invocation — training filters that key on is_completed alone would
|
|
346
|
+
// otherwise pull empty transcripts.
|
|
347
|
+
is_completed: session !== null && messages.length > 0,
|
|
348
|
+
is_truncated: false,
|
|
349
|
+
error: null,
|
|
350
|
+
},
|
|
351
|
+
cost: {
|
|
352
|
+
usd: session?.costUsd ?? null,
|
|
353
|
+
tokens_in: session?.tokensInput ?? null,
|
|
354
|
+
tokens_out: session?.tokensOutput ?? null,
|
|
355
|
+
tokens_reasoning: session?.tokensReasoning ?? null,
|
|
356
|
+
cache_read: session?.tokensCacheRead ?? null,
|
|
357
|
+
cache_write: session?.tokensCacheWrite ?? null,
|
|
358
|
+
wall_s: session !== null ? Math.round((session.timeUpdated - session.timeCreated) / 1000) : null,
|
|
359
|
+
},
|
|
360
|
+
artifacts: {
|
|
361
|
+
patch_path: null,
|
|
362
|
+
run_dir: artifact !== null && typeof artifact.runDir === 'string' ? artifact.runDir : null,
|
|
363
|
+
transcript_ref: session !== null ? `opencode:${session.id}` : null,
|
|
364
|
+
},
|
|
365
|
+
provenance: {
|
|
366
|
+
captured_at: ctx.capturedAt,
|
|
367
|
+
capture: 'backfill',
|
|
368
|
+
...(messages.length === 0
|
|
369
|
+
? {
|
|
370
|
+
gap:
|
|
371
|
+
session === null
|
|
372
|
+
? ctx.db === null
|
|
373
|
+
? `opencode store unavailable; worker cwd ${cwd} unrecoverable`
|
|
374
|
+
: `no opencode session found for worker cwd ${cwd}`
|
|
375
|
+
: `opencode session ${session.id} has no readable message parts`,
|
|
376
|
+
}
|
|
377
|
+
: {}),
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Proposer shots — receipts under proposer-shots/ (flat or per-author dirs),
|
|
384
|
+
// joined to Claude transcripts of the shot worktree by time window.
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
interface ShotReceiptFile {
|
|
388
|
+
path: string
|
|
389
|
+
author: string | null
|
|
390
|
+
generation: number
|
|
391
|
+
candidateIndex: number
|
|
392
|
+
shot: number
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function listShotReceipts(outDir: string): Promise<ShotReceiptFile[]> {
|
|
396
|
+
const shotDir = join(outDir, 'proposer-shots')
|
|
397
|
+
const found: ShotReceiptFile[] = []
|
|
398
|
+
const parse = (name: string, author: string | null, path: string): void => {
|
|
399
|
+
const match = /^gen(\d+)-cand(\d+)-shot(\d+)\.json$/.exec(name)
|
|
400
|
+
if (match === null) return
|
|
401
|
+
found.push({
|
|
402
|
+
path,
|
|
403
|
+
author,
|
|
404
|
+
generation: Number(match[1]),
|
|
405
|
+
candidateIndex: Number(match[2]),
|
|
406
|
+
shot: Number(match[3]),
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
for (const entry of await readdir(shotDir, { withFileTypes: true }).catch(() => [])) {
|
|
410
|
+
if (entry.isDirectory()) {
|
|
411
|
+
const authorDir = join(shotDir, entry.name)
|
|
412
|
+
for (const name of await readdir(authorDir).catch(() => [])) {
|
|
413
|
+
parse(name, entry.name, join(authorDir, name))
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
parse(entry.name, null, join(shotDir, entry.name))
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return found.sort((a, b) => a.path.localeCompare(b.path))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function emitProposerLines(ctx: Ctx, lines: RolloutLine[], entries: RolloutEntry[]): Promise<void> {
|
|
423
|
+
const receipts = await listShotReceipts(ctx.outDir)
|
|
424
|
+
const entryFor = new Map<string, RolloutEntry>()
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
entryFor.set(`${entry.state.generation}:${entry.state.candidateIndex}`, entry)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const shot of receipts) {
|
|
430
|
+
const parsed = await readJson(shot.path)
|
|
431
|
+
const receipt = isRecord(parsed) && isRecord(parsed.receipt) ? parsed.receipt : null
|
|
432
|
+
const entry = entryFor.get(`${shot.generation}:${shot.candidateIndex}`) ?? null
|
|
433
|
+
|
|
434
|
+
// The proposer worktree the arena dispatched this shot into.
|
|
435
|
+
const worktreeCwd =
|
|
436
|
+
shot.author !== null
|
|
437
|
+
? join(ctx.outDir, 'proposer-wt', `gen${shot.generation}-cand${shot.candidateIndex}-${shot.author}`)
|
|
438
|
+
: join(ctx.outDir, 'proposer-wt', `gen${shot.generation}-cand${shot.candidateIndex}`)
|
|
439
|
+
|
|
440
|
+
const startedAt = receipt !== null && typeof receipt.startedAt === 'string' ? Date.parse(receipt.startedAt) : NaN
|
|
441
|
+
const completedAt =
|
|
442
|
+
receipt !== null && typeof receipt.completedAt === 'string' ? Date.parse(receipt.completedAt) : NaN
|
|
443
|
+
|
|
444
|
+
let transcript: Awaited<ReturnType<typeof readClaudeTranscript>> | null = null
|
|
445
|
+
let transcriptRef: string | null = null
|
|
446
|
+
const candidates = await findClaudeTranscripts(worktreeCwd, ctx.claudeProjectsDir)
|
|
447
|
+
const windowed: Array<{ path: string; startMs: number }> = []
|
|
448
|
+
for (const ref of candidates) {
|
|
449
|
+
const t = await readClaudeTranscript(ref.path)
|
|
450
|
+
if (t.startedAt === null) continue
|
|
451
|
+
const startMs = Date.parse(t.startedAt)
|
|
452
|
+
const inWindow =
|
|
453
|
+
Number.isNaN(startedAt) || Number.isNaN(completedAt)
|
|
454
|
+
? candidates.length === 1
|
|
455
|
+
: startMs >= startedAt - 120_000 && startMs <= completedAt + 120_000
|
|
456
|
+
if (inWindow) windowed.push({ path: ref.path, startMs })
|
|
457
|
+
}
|
|
458
|
+
if (windowed.length > 0) {
|
|
459
|
+
windowed.sort(
|
|
460
|
+
(a, b) => Math.abs(a.startMs - (startedAt || a.startMs)) - Math.abs(b.startMs - (startedAt || b.startMs)),
|
|
461
|
+
)
|
|
462
|
+
transcriptRef = windowed[0].path
|
|
463
|
+
transcript = await readClaudeTranscript(transcriptRef)
|
|
464
|
+
ctx.stats.proposerShotsJoined += 1
|
|
465
|
+
} else {
|
|
466
|
+
ctx.stats.proposerShotsMissed += 1
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// The proposer's reward is its candidate's official resolved fraction —
|
|
470
|
+
// fail-closed null when the candidate has no scored entry.
|
|
471
|
+
const reward =
|
|
472
|
+
entry !== null && ctx.instanceCount > 0 ? entry.outcome.resolvedCount / ctx.instanceCount : null
|
|
473
|
+
|
|
474
|
+
push(ctx, lines, {
|
|
475
|
+
...baseLine(ctx),
|
|
476
|
+
rollout_id: randomUUID(),
|
|
477
|
+
parent_rollout_id: null,
|
|
478
|
+
candidate_id: candidateId(shot.generation, shot.candidateIndex),
|
|
479
|
+
generation: shot.generation,
|
|
480
|
+
candidate_index: shot.candidateIndex,
|
|
481
|
+
role: 'proposer',
|
|
482
|
+
task: {
|
|
483
|
+
suite: 'swe-arena-proposer',
|
|
484
|
+
instance_id:
|
|
485
|
+
shot.author !== null
|
|
486
|
+
? `${candidateId(shot.generation, shot.candidateIndex)}-${shot.author}`
|
|
487
|
+
: candidateId(shot.generation, shot.candidateIndex),
|
|
488
|
+
split: 'search',
|
|
489
|
+
seed: null,
|
|
490
|
+
rep: shot.shot,
|
|
491
|
+
},
|
|
492
|
+
policy: {
|
|
493
|
+
harness: receipt !== null && typeof receipt.harness === 'string' ? receipt.harness : null,
|
|
494
|
+
harness_version: null,
|
|
495
|
+
model:
|
|
496
|
+
transcript?.model ?? (receipt !== null && typeof receipt.model === 'string' ? receipt.model : null),
|
|
497
|
+
provider: null,
|
|
498
|
+
profile_commit: entry?.state.commit ?? null,
|
|
499
|
+
sampling: null,
|
|
500
|
+
},
|
|
501
|
+
messages: transcript?.messages ?? [],
|
|
502
|
+
tool_defs: [],
|
|
503
|
+
outcome: {
|
|
504
|
+
reward,
|
|
505
|
+
reward_source: reward === null ? null : `${OFFICIAL_JUDGE}/candidate-resolved-fraction`,
|
|
506
|
+
verdict: null,
|
|
507
|
+
metrics: {
|
|
508
|
+
resolved_count: entry?.outcome.resolvedCount ?? null,
|
|
509
|
+
instance_count: ctx.instanceCount,
|
|
510
|
+
exit_code: receipt !== null && typeof receipt.exitCode === 'number' ? receipt.exitCode : null,
|
|
511
|
+
timed_out: receipt !== null && typeof receipt.timedOut === 'boolean' ? receipt.timedOut : null,
|
|
512
|
+
author: shot.author,
|
|
513
|
+
max_shots: receipt !== null && typeof receipt.maxShots === 'number' ? receipt.maxShots : null,
|
|
514
|
+
prompt_sha256: receipt !== null && typeof receipt.promptSha256 === 'string' ? receipt.promptSha256 : null,
|
|
515
|
+
},
|
|
516
|
+
is_completed: receipt !== null && receipt.exitCode === 0,
|
|
517
|
+
is_truncated: receipt !== null && receipt.timedOut === true,
|
|
518
|
+
error: receipt !== null && typeof receipt.error === 'string' ? receipt.error : null,
|
|
519
|
+
},
|
|
520
|
+
cost: {
|
|
521
|
+
usd: receipt !== null && typeof receipt.costUsd === 'number' ? receipt.costUsd : null,
|
|
522
|
+
tokens_in: transcript !== null ? transcript.usage.tokensIn : null,
|
|
523
|
+
tokens_out: transcript !== null ? transcript.usage.tokensOut : null,
|
|
524
|
+
tokens_reasoning: null,
|
|
525
|
+
cache_read: transcript !== null ? transcript.usage.cacheRead : null,
|
|
526
|
+
cache_write: transcript !== null ? transcript.usage.cacheWrite : null,
|
|
527
|
+
wall_s:
|
|
528
|
+
receipt !== null && typeof receipt.durationMs === 'number' ? Math.round(receipt.durationMs / 1000) : null,
|
|
529
|
+
},
|
|
530
|
+
artifacts: {
|
|
531
|
+
patch_path: entry?.action.diffPath ?? null,
|
|
532
|
+
run_dir: worktreeCwd,
|
|
533
|
+
transcript_ref: transcriptRef ?? shot.path,
|
|
534
|
+
},
|
|
535
|
+
provenance: {
|
|
536
|
+
captured_at: ctx.capturedAt,
|
|
537
|
+
capture: 'backfill',
|
|
538
|
+
...(transcript === null
|
|
539
|
+
? { gap: `no claude transcript matched worktree ${worktreeCwd} in the shot's time window` }
|
|
540
|
+
: {}),
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// Entry point.
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
export async function backfillSweArena(outDir: string, opts: BackfillOptions = {}): Promise<BackfillResult> {
|
|
551
|
+
const manifest = await buildRolloutManifest(outDir)
|
|
552
|
+
const provenance = isRecord(manifest.provenance) ? manifest.provenance : null
|
|
553
|
+
const runId = provenance !== null && typeof provenance.runId === 'string' ? provenance.runId : outDir
|
|
554
|
+
|
|
555
|
+
const db = await openOpencodeDb(opts.opencodeDb ?? DEFAULT_OPENCODE_DB)
|
|
556
|
+
const ctx: Ctx = {
|
|
557
|
+
outDir,
|
|
558
|
+
runId,
|
|
559
|
+
instanceCount: manifest.instances.length,
|
|
560
|
+
db,
|
|
561
|
+
claudeProjectsDir: opts.claudeProjectsDir ?? DEFAULT_CLAUDE_PROJECTS_DIR,
|
|
562
|
+
capturedAt: (opts.now?.() ?? new Date()).toISOString(),
|
|
563
|
+
stats: {
|
|
564
|
+
lines: 0,
|
|
565
|
+
byRole: {},
|
|
566
|
+
fullTranscripts: 0,
|
|
567
|
+
gapLines: 0,
|
|
568
|
+
rewardLabeled: 0,
|
|
569
|
+
workerSessionsJoined: 0,
|
|
570
|
+
workerCwdsMissed: 0,
|
|
571
|
+
workerSessionsEmpty: 0,
|
|
572
|
+
proposerShotsJoined: 0,
|
|
573
|
+
proposerShotsMissed: 0,
|
|
574
|
+
opencodeDbAvailable: db !== null,
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const lines: RolloutLine[] = []
|
|
579
|
+
const entries: RolloutEntry[] = manifest.generations.flatMap((g) => g.rollouts)
|
|
580
|
+
try {
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
for (const cell of await readCellRecords(entry.state.campaignDir)) {
|
|
583
|
+
await emitCellLines(ctx, lines, entry, cell)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
await emitProposerLines(ctx, lines, entries)
|
|
587
|
+
} finally {
|
|
588
|
+
db?.close()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { lines, stats: ctx.stats }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
595
|
+
|
|
596
|
+
if (isMain) {
|
|
597
|
+
const [outDir, ledgerPath] = process.argv.slice(2)
|
|
598
|
+
if (!outDir || !ledgerPath) {
|
|
599
|
+
console.error('usage: tsx src/rollout-ledger/backfill-swe-arena.mts <outDir> <out.jsonl>')
|
|
600
|
+
process.exit(2)
|
|
601
|
+
}
|
|
602
|
+
const { lines, stats } = await backfillSweArena(outDir)
|
|
603
|
+
await writeRolloutLedger(ledgerPath, lines)
|
|
604
|
+
console.log(JSON.stringify(stats, null, 2))
|
|
605
|
+
console.log(`rollout ledger → ${ledgerPath} (${lines.length} lines)`)
|
|
606
|
+
}
|