@tangle-network/agent-bench 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +23 -23
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/nomiracl.js +2 -2
  16. package/dist/benchmarks/open-rag-bench.js +2 -2
  17. package/dist/benchmarks/programbench.js +2 -2
  18. package/dist/benchmarks/ragbench.js +2 -2
  19. package/dist/benchmarks/swe-bench.js +2 -2
  20. package/dist/benchmarks/t2-ragbench.js +2 -2
  21. package/dist/benchmarks/tau-bench-shared.js +2 -2
  22. package/dist/benchmarks/tau2-bench.js +3 -3
  23. package/dist/benchmarks/tau3-banking.js +3 -3
  24. package/dist/benchmarks/terminal-bench.js +2 -2
  25. package/dist/benchmarks/toollm.js +2 -2
  26. package/dist/benchmarks/webarena-verified.js +2 -2
  27. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  28. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  29. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  30. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  31. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  32. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  33. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  34. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  35. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  36. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  37. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  38. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  39. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  40. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  41. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  42. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  43. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  44. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  45. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  46. package/dist/chunk-WSKWVEQB.js.map +1 -0
  47. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  48. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  49. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  50. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  51. package/dist/index.js +23 -23
  52. package/package.json +4 -4
  53. package/scripts/verify-packed-consumer.mjs +1 -1
  54. package/src/benchmarks/_harness.ts +20 -2
  55. package/src/benchmarks/humaneval.test.mts +2 -2
  56. package/src/quant-arena/README.md +144 -0
  57. package/src/quant-arena/backtest.test.mts +135 -0
  58. package/src/quant-arena/backtest.ts +218 -0
  59. package/src/quant-arena/data.test.mts +44 -0
  60. package/src/quant-arena/data.ts +141 -0
  61. package/src/quant-arena/driver.test.mts +253 -0
  62. package/src/quant-arena/driver.ts +219 -0
  63. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  64. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  65. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  66. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  67. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  68. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  69. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  70. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  71. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  72. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  75. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  76. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  77. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  78. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  79. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  80. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  81. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  82. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  83. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  86. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  87. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  88. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  89. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  90. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  91. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  92. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  93. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  94. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  95. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  96. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  97. package/src/quant-arena/holdout-certify.mts +206 -0
  98. package/src/quant-arena/holdout-certify.test.mts +82 -0
  99. package/src/quant-arena/leak-audit.test.mts +79 -0
  100. package/src/quant-arena/leak-audit.ts +95 -0
  101. package/src/quant-arena/make-fixtures.mts +161 -0
  102. package/src/quant-arena/multiplicity.test.mts +68 -0
  103. package/src/quant-arena/multiplicity.ts +87 -0
  104. package/src/quant-arena/nautilus-certify.ts +31 -0
  105. package/src/quant-arena/oms.ts +90 -0
  106. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  107. package/src/quant-arena/python/pyproject.toml +8 -0
  108. package/src/quant-arena/python/uv.lock +1297 -0
  109. package/src/quant-arena/python/vbt-worker.py +192 -0
  110. package/src/quant-arena/quant-loop.mts +813 -0
  111. package/src/quant-arena/quant-loop.test.mts +75 -0
  112. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  113. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  114. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  115. package/src/quant-arena/types.ts +133 -0
  116. package/src/quant-arena/vbt-client.ts +321 -0
  117. package/src/quant-arena/vbt-parity.test.mts +183 -0
  118. package/src/quant-arena/windows.test.mts +45 -0
  119. package/src/quant-arena/windows.ts +54 -0
  120. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  121. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  122. package/src/rollout-ledger/settle-capture.mts +442 -0
  123. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  124. package/src/swe-arena/activation.mts +228 -0
  125. package/src/swe-arena/activation.test.mts +303 -0
  126. package/src/swe-arena/arms.ts +30 -14
  127. package/src/swe-arena/briefing.mts +217 -0
  128. package/src/swe-arena/briefing.test.mts +178 -0
  129. package/src/swe-arena/calibrate.ts +102 -1
  130. package/src/swe-arena/cell-evidence.mts +41 -9
  131. package/src/swe-arena/factory-command-container.ts +284 -0
  132. package/src/swe-arena/factory-judge-child.mts +228 -0
  133. package/src/swe-arena/factory.test.mts +643 -0
  134. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  135. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  136. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  137. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  138. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  139. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  140. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  141. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  142. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  143. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  144. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  145. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  146. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  147. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  148. package/src/swe-arena/fixtures.ts +135 -2
  149. package/src/swe-arena/gepa-seat.mts +583 -0
  150. package/src/swe-arena/gepa-seat.test.mts +635 -0
  151. package/src/swe-arena/ledger-orphans.mts +77 -0
  152. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  153. package/src/swe-arena/lineage-record.mts +164 -0
  154. package/src/swe-arena/lineage-record.test.mts +115 -0
  155. package/src/swe-arena/outer-loop.mts +677 -27
  156. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  157. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  158. package/src/swe-arena/proposer-fanout.mts +295 -14
  159. package/src/swe-arena/proposer-fanout.test.mts +247 -0
  160. package/src/swe-arena/proposer-provenance.mts +177 -0
  161. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  162. package/src/swe-arena/run-experiment.mts +367 -1
  163. package/src/swe-arena/run-report.mts +75 -0
  164. package/src/swe-arena/run-supervisor.test.mts +5 -3
  165. package/src/swe-arena/score-split.mts +140 -0
  166. package/src/swe-arena/score-split.test.mts +123 -0
  167. package/src/swe-arena/types.ts +52 -0
  168. package/src/swe-structural.mts +245 -837
  169. package/dist/chunk-LRRD7NAG.js.map +0 -1
  170. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  171. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  172. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  173. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  174. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  175. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  176. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  177. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  178. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  179. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  180. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  181. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  182. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  183. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  184. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  185. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  186. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  187. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  188. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  189. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  190. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  191. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Settle-time rollout capture with LABEL v2 — `tangle.rollout.v1` lines
3
+ * emitted LIVE, right after each campaign cell is judged, while the harness
4
+ * stores still hold the transcripts (the whole reason settle-time capture
5
+ * beats backfill: the opencode sqlite db and claude project transcripts are
6
+ * mutable/garbage-collected).
7
+ *
8
+ * LABEL v2 semantics (reward_source tags distinguish them from the v1
9
+ * backfill's inherited labels — both coexist in one ledger):
10
+ *
11
+ * - WORKERS get CONTRIBUTION-AWARE reward: reward=1 only when the cell
12
+ * resolved AND that worker's patch was the delivered one (delivered-patch
13
+ * identity from the supervisor run dir's per-worker evidence,
14
+ * workers/<label>.patch); sibling bystanders in a resolved cell get
15
+ * reward=0 with metrics.bystander=true. In an unresolved cell every worker
16
+ * gets reward=0. A resolved cell whose delivered patch matches NO worker
17
+ * patch is a labeled identity gap: reward=null,
18
+ * metrics.delivered_match='unknown' — never a fabricated credit.
19
+ * - The SUPERVISOR line keeps the official-judge verdict as its reward
20
+ * (unchanged semantics; brain transcripts remain honest gap lines until
21
+ * the loops brain persists message content — journal + stats are joined
22
+ * into metrics).
23
+ * - PROPOSERS get BASELINE-RELATIVE reward: candidate_score −
24
+ * baseline_score (resolved fractions), so an improvement is positive and
25
+ * a regression negative — emitted once per candidate when the round's
26
+ * scores are final.
27
+ */
28
+
29
+ import { randomUUID } from 'node:crypto'
30
+ import { existsSync } from 'node:fs'
31
+ import { readFile, readdir } from 'node:fs/promises'
32
+ import { join } from 'node:path'
33
+ import type { DatabaseSync } from 'node:sqlite'
34
+ import {
35
+ appendRolloutLines,
36
+ DEFAULT_OPENCODE_DB,
37
+ findOpencodeSessionsByDirectory,
38
+ openOpencodeDb,
39
+ readOpencodeSessionMessages,
40
+ ROLLOUT_SCHEMA,
41
+ type ChatMessage,
42
+ type OpencodeSessionRow,
43
+ type RolloutLine,
44
+ } from '@tangle-network/agent-eval/rollout'
45
+
46
+ export const OFFICIAL_JUDGE = 'swe-arena-official-judge'
47
+ export const WORKER_REWARD_SOURCE_V2 = `${OFFICIAL_JUDGE}/contribution-v2`
48
+ export const PROPOSER_REWARD_SOURCE_V2 = `${OFFICIAL_JUDGE}/baseline-relative-v2`
49
+
50
+ /** Stable candidate identity from the improvement-loop coordinates. */
51
+ export function candidateId(generation: number, candidateIndex: number): string {
52
+ return candidateIndex === -1 ? 'baseline' : `gen${generation}-cand${candidateIndex}`
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Worker evidence — the supervisor run dir's per-worker record.
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export interface WorkerEvidenceRecord {
60
+ label: string
61
+ /** Worker clone cwd from the ndjson `started` event (opencode join key). */
62
+ cwd: string | null
63
+ /** Patch persisted at settle time (workers/<label>.patch); null = none. */
64
+ patch: string | null
65
+ }
66
+
67
+ /** Read workers/<label>.{ndjson,patch} under one supervisor run dir. */
68
+ export async function readWorkerEvidence(supRunDir: string): Promise<WorkerEvidenceRecord[]> {
69
+ const workersDir = join(supRunDir, 'workers')
70
+ const names = await readdir(workersDir).catch(() => [])
71
+ const labels = new Set<string>()
72
+ for (const name of names) {
73
+ if (name.endsWith('.ndjson') && !name.endsWith('.inbox.ndjson')) labels.add(name.slice(0, -'.ndjson'.length))
74
+ else if (name.endsWith('.patch')) labels.add(name.slice(0, -'.patch'.length))
75
+ }
76
+ const records: WorkerEvidenceRecord[] = []
77
+ for (const label of [...labels].sort()) {
78
+ let cwd: string | null = null
79
+ const nd = await readFile(join(workersDir, `${label}.ndjson`), 'utf8').catch(() => '')
80
+ for (const line of nd.split('\n')) {
81
+ if (!line.trim()) continue
82
+ try {
83
+ const o = JSON.parse(line) as Record<string, unknown>
84
+ if (o.kind === 'started' && typeof o.cwd === 'string') cwd = o.cwd
85
+ } catch {
86
+ // A corrupt event line cannot invalidate the rest of the evidence.
87
+ }
88
+ }
89
+ const patch = await readFile(join(workersDir, `${label}.patch`), 'utf8').catch(() => null)
90
+ records.push({ label, cwd, patch: patch !== null && patch.trim().length > 0 ? patch : null })
91
+ }
92
+ return records
93
+ }
94
+
95
+ /** Labels whose persisted patch IS the delivered patch (trimmed equality —
96
+ * the supervisor delivers a settled worker's patch verbatim). */
97
+ export function deliveredWorkerLabels(deliveredPatch: string, workers: readonly WorkerEvidenceRecord[]): string[] {
98
+ const delivered = deliveredPatch.trim()
99
+ if (delivered.length === 0) return []
100
+ return workers.filter((w) => w.patch !== null && w.patch.trim() === delivered).map((w) => w.label)
101
+ }
102
+
103
+ export interface WorkerRewardV2 {
104
+ reward: number | null
105
+ bystander: boolean
106
+ /** 'delivered' | 'bystander' | 'unresolved' | 'unknown' (identity gap). */
107
+ deliveredMatch: 'delivered' | 'bystander' | 'unresolved' | 'unknown'
108
+ }
109
+
110
+ /** The v2 contribution rule for one worker in one cell. */
111
+ export function workerRewardV2(input: {
112
+ /** Official judge verdict for the cell; null = inconclusive. */
113
+ resolved: boolean | null
114
+ /** This worker's label is among the delivered-patch matches. */
115
+ isDelivered: boolean
116
+ /** At least one worker matched the delivered patch (identity known). */
117
+ identityKnown: boolean
118
+ }): WorkerRewardV2 {
119
+ if (input.resolved !== true) {
120
+ return { reward: input.resolved === false ? 0 : null, bystander: false, deliveredMatch: 'unresolved' }
121
+ }
122
+ if (!input.identityKnown) {
123
+ return { reward: null, bystander: false, deliveredMatch: 'unknown' }
124
+ }
125
+ if (input.isDelivered) return { reward: 1, bystander: false, deliveredMatch: 'delivered' }
126
+ return { reward: 0, bystander: true, deliveredMatch: 'bystander' }
127
+ }
128
+
129
+ /** Baseline-relative proposer reward (resolved fractions; improvement > 0). */
130
+ export function proposerRewardV2(candResolved: number, baselineResolved: number, instanceCount: number): number {
131
+ if (!(instanceCount > 0)) throw new Error('proposerRewardV2: instanceCount must be > 0')
132
+ return (candResolved - baselineResolved) / instanceCount
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // The live capture handle.
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export interface SettleCaptureOptions {
140
+ /** Ledger JSONL destination (appended live, one flush per cell). */
141
+ ledgerPath: string
142
+ runId: string
143
+ instanceCount: number
144
+ opencodeDb?: string
145
+ /** Injected clock for deterministic tests. */
146
+ now?: () => Date
147
+ log?: (msg: string) => void
148
+ }
149
+
150
+ export interface CellCaptureArgs {
151
+ generation: number
152
+ candidateIndex: number
153
+ iid: string
154
+ rep: number
155
+ seed: number | null
156
+ /** Our public/private sub-split of the train instances (metrics label). */
157
+ splitVisibility: 'public' | 'private' | null
158
+ commit: string | null
159
+ /** Official judge verdict (null = inconclusive). */
160
+ resolved: boolean | null
161
+ /** Raw judge verdict record, verbatim. */
162
+ judgeVerdict: unknown
163
+ runDir: string | null
164
+ patchPath: string | null
165
+ /** Supervisor run dir (<ws>/.loops/supervisor/<id>); null = unlocatable. */
166
+ supRunDir: string | null
167
+ /** The delivered patch text (empty = no delivery). */
168
+ deliveredPatch: string
169
+ workerModel: string | null
170
+ metrics: Record<string, unknown>
171
+ cost: { usd: number | null; wallS: number | null; spentTokens: number | null }
172
+ }
173
+
174
+ export interface ProposerCaptureArgs {
175
+ generation: number
176
+ candidateIndex: number
177
+ proposer: string
178
+ harness: string | null
179
+ commit: string | null
180
+ candResolved: number
181
+ baselineResolved: number
182
+ shotReceiptPaths: string[]
183
+ diffPath: string | null
184
+ }
185
+
186
+ export interface SettleCapture {
187
+ readonly path: string
188
+ captureCell(args: CellCaptureArgs): Promise<{ lines: number }>
189
+ captureProposer(args: ProposerCaptureArgs): Promise<{ lines: number }>
190
+ }
191
+
192
+ export function createSettleCapture(opts: SettleCaptureOptions): SettleCapture {
193
+ const now = opts.now ?? (() => new Date())
194
+ const log = opts.log ?? (() => {})
195
+ const dbPath = opts.opencodeDb ?? DEFAULT_OPENCODE_DB
196
+
197
+ const base = (capturedAt: string): Pick<RolloutLine, 'schema' | 'run_id'> & { provenance: RolloutLine['provenance'] } => ({
198
+ schema: ROLLOUT_SCHEMA,
199
+ run_id: opts.runId,
200
+ provenance: { captured_at: capturedAt, capture: 'settle-time' },
201
+ })
202
+
203
+ return {
204
+ path: opts.ledgerPath,
205
+
206
+ async captureCell(args: CellCaptureArgs): Promise<{ lines: number }> {
207
+ const capturedAt = now().toISOString()
208
+ const supervisorId = randomUUID()
209
+ const reward = args.resolved === null ? null : args.resolved ? 1 : 0
210
+ const lines: RolloutLine[] = []
211
+
212
+ lines.push({
213
+ ...base(capturedAt),
214
+ rollout_id: supervisorId,
215
+ parent_rollout_id: null,
216
+ candidate_id: candidateId(args.generation, args.candidateIndex),
217
+ generation: args.generation,
218
+ candidate_index: args.candidateIndex,
219
+ role: 'supervisor',
220
+ task: { suite: 'swe-bench-verified', instance_id: args.iid, split: 'search', seed: args.seed, rep: args.rep },
221
+ policy: {
222
+ harness: 'pi-loops',
223
+ harness_version: null,
224
+ model: args.workerModel,
225
+ provider: null,
226
+ profile_commit: args.commit,
227
+ sampling: null,
228
+ },
229
+ messages: [],
230
+ tool_defs: [],
231
+ outcome: {
232
+ reward,
233
+ reward_source: reward === null ? null : OFFICIAL_JUDGE,
234
+ verdict: args.judgeVerdict,
235
+ metrics: {
236
+ ...args.metrics,
237
+ ...(args.splitVisibility !== null ? { split_visibility: args.splitVisibility } : {}),
238
+ },
239
+ is_completed: args.resolved !== null,
240
+ is_truncated: false,
241
+ error: null,
242
+ },
243
+ cost: {
244
+ usd: args.cost.usd,
245
+ tokens_in: null,
246
+ tokens_out: null,
247
+ tokens_reasoning: null,
248
+ cache_read: null,
249
+ cache_write: null,
250
+ wall_s: args.cost.wallS,
251
+ },
252
+ artifacts: {
253
+ patch_path: args.patchPath,
254
+ run_dir: args.runDir,
255
+ transcript_ref: args.runDir !== null ? join(args.runDir, 'brain.jsonl') : null,
256
+ },
257
+ provenance: {
258
+ captured_at: capturedAt,
259
+ capture: 'settle-time',
260
+ gap: 'supervisor brain transcript not persisted (brain.jsonl carries per-request stats only)',
261
+ },
262
+ })
263
+
264
+ // Worker lines with v2 contribution labels.
265
+ const workers = args.supRunDir !== null ? await readWorkerEvidence(args.supRunDir) : []
266
+ if (workers.length > 0) {
267
+ const delivered = new Set(deliveredWorkerLabels(args.deliveredPatch, workers))
268
+ const identityKnown = delivered.size > 0
269
+ let db: DatabaseSync | null = null
270
+ try {
271
+ db = await openOpencodeDb(dbPath)
272
+ for (const worker of workers) {
273
+ const v2 = workerRewardV2({
274
+ resolved: args.resolved,
275
+ isDelivered: delivered.has(worker.label),
276
+ identityKnown,
277
+ })
278
+ let session: OpencodeSessionRow | null = null
279
+ let messages: ChatMessage[] = []
280
+ if (db !== null && worker.cwd !== null) {
281
+ const sessions = findOpencodeSessionsByDirectory(db, worker.cwd)
282
+ session = sessions[0] ?? null
283
+ if (session !== null) messages = readOpencodeSessionMessages(db, session.id)
284
+ }
285
+ lines.push({
286
+ ...base(capturedAt),
287
+ rollout_id: randomUUID(),
288
+ parent_rollout_id: supervisorId,
289
+ candidate_id: candidateId(args.generation, args.candidateIndex),
290
+ generation: args.generation,
291
+ candidate_index: args.candidateIndex,
292
+ role: 'worker',
293
+ task: {
294
+ suite: 'swe-bench-verified',
295
+ instance_id: args.iid,
296
+ split: 'search',
297
+ seed: args.seed,
298
+ rep: args.rep,
299
+ },
300
+ policy: {
301
+ harness: 'opencode',
302
+ harness_version: null,
303
+ model: session?.model?.id ?? null,
304
+ provider: session?.model?.providerID ?? null,
305
+ profile_commit: args.commit,
306
+ sampling: null,
307
+ },
308
+ messages,
309
+ tool_defs: [],
310
+ outcome: {
311
+ reward: v2.reward,
312
+ reward_source: v2.reward === null && v2.deliveredMatch !== 'unknown' ? null : WORKER_REWARD_SOURCE_V2,
313
+ verdict: null,
314
+ metrics: {
315
+ worker_label: worker.label,
316
+ worker_cwd: worker.cwd,
317
+ bystander: v2.bystander,
318
+ delivered_match: v2.deliveredMatch,
319
+ has_patch: worker.patch !== null,
320
+ has_session: session !== null,
321
+ ...(args.splitVisibility !== null ? { split_visibility: args.splitVisibility } : {}),
322
+ },
323
+ // Matches the backfill rule: a session with no readable parts is
324
+ // a gap line, not a completed invocation.
325
+ is_completed: session !== null && messages.length > 0,
326
+ is_truncated: false,
327
+ error: null,
328
+ },
329
+ cost: {
330
+ usd: session?.costUsd ?? null,
331
+ tokens_in: session?.tokensInput ?? null,
332
+ tokens_out: session?.tokensOutput ?? null,
333
+ tokens_reasoning: session?.tokensReasoning ?? null,
334
+ cache_read: session?.tokensCacheRead ?? null,
335
+ cache_write: session?.tokensCacheWrite ?? null,
336
+ wall_s: session !== null ? Math.round((session.timeUpdated - session.timeCreated) / 1000) : null,
337
+ },
338
+ artifacts: {
339
+ patch_path: null,
340
+ run_dir: args.runDir,
341
+ transcript_ref: session !== null ? `opencode:${session.id}` : null,
342
+ },
343
+ provenance: {
344
+ captured_at: capturedAt,
345
+ capture: 'settle-time',
346
+ ...(messages.length === 0
347
+ ? {
348
+ gap:
349
+ worker.cwd === null
350
+ ? `worker ${worker.label} recorded no clone cwd in its ndjson`
351
+ : session === null
352
+ ? db === null
353
+ ? `opencode store unavailable; worker cwd ${worker.cwd} unrecoverable`
354
+ : `no opencode session found for worker cwd ${worker.cwd}`
355
+ : `opencode session ${session.id} has no readable message parts`,
356
+ }
357
+ : {}),
358
+ },
359
+ })
360
+ }
361
+ } finally {
362
+ db?.close()
363
+ }
364
+ }
365
+
366
+ await appendRolloutLines(opts.ledgerPath, lines)
367
+ log(`rollout-ledger: +${lines.length} line(s) (cell ${args.iid} r${args.rep}) → ${opts.ledgerPath}`)
368
+ return { lines: lines.length }
369
+ },
370
+
371
+ async captureProposer(args: ProposerCaptureArgs): Promise<{ lines: number }> {
372
+ const capturedAt = now().toISOString()
373
+ const reward = proposerRewardV2(args.candResolved, args.baselineResolved, opts.instanceCount)
374
+ const line: RolloutLine = {
375
+ ...base(capturedAt),
376
+ rollout_id: randomUUID(),
377
+ parent_rollout_id: null,
378
+ candidate_id: candidateId(args.generation, args.candidateIndex),
379
+ generation: args.generation,
380
+ candidate_index: args.candidateIndex,
381
+ role: 'proposer',
382
+ task: {
383
+ suite: 'swe-arena-proposer',
384
+ instance_id: `${candidateId(args.generation, args.candidateIndex)}-${args.proposer}`,
385
+ split: 'search',
386
+ seed: null,
387
+ rep: 0,
388
+ },
389
+ policy: {
390
+ harness: args.harness,
391
+ harness_version: null,
392
+ model: null,
393
+ provider: null,
394
+ profile_commit: args.commit,
395
+ sampling: null,
396
+ },
397
+ messages: [],
398
+ tool_defs: [],
399
+ outcome: {
400
+ reward,
401
+ reward_source: PROPOSER_REWARD_SOURCE_V2,
402
+ verdict: null,
403
+ metrics: {
404
+ resolved_count: args.candResolved,
405
+ baseline_resolved_count: args.baselineResolved,
406
+ instance_count: opts.instanceCount,
407
+ proposer: args.proposer,
408
+ shot_receipts: args.shotReceiptPaths,
409
+ },
410
+ is_completed: true,
411
+ is_truncated: false,
412
+ error: null,
413
+ },
414
+ cost: { usd: null, tokens_in: null, tokens_out: null, tokens_reasoning: null, cache_read: null, cache_write: null, wall_s: null },
415
+ artifacts: {
416
+ patch_path: args.diffPath !== null && existsSync(args.diffPath) ? args.diffPath : null,
417
+ run_dir: null,
418
+ transcript_ref: args.shotReceiptPaths[0] ?? null,
419
+ },
420
+ provenance: {
421
+ captured_at: capturedAt,
422
+ capture: 'settle-time',
423
+ gap: 'proposer transcript joined at backfill time (settle-time line carries the v2 reward label)',
424
+ },
425
+ }
426
+ await appendRolloutLines(opts.ledgerPath, [line])
427
+ log(`rollout-ledger: +1 proposer line (${args.proposer}, reward ${reward.toFixed(3)}) → ${opts.ledgerPath}`)
428
+ return { lines: 1 }
429
+ },
430
+ }
431
+ }
432
+
433
+ /** Parse (generation, candidateIndex) off a campaign cell artifact path — the
434
+ * campaign DIRECTORY is the blessed attribution source (never dispatch
435
+ * order): `<improveRunDir>/gen-<g>/candidate-<k>/<cell>/…` for candidates,
436
+ * `<improveRunDir>/baseline/<cell>/…` (→ −1/−1) for the baseline. */
437
+ export function campaignCoordsFromCellPath(path: string): { generation: number; candidateIndex: number } | null {
438
+ const cand = /\/gen-(\d+)\/candidate-(\d+)\//.exec(path)
439
+ if (cand !== null) return { generation: Number(cand[1]), candidateIndex: Number(cand[2]) }
440
+ if (/\/baseline\//.test(path)) return { generation: -1, candidateIndex: -1 }
441
+ return null
442
+ }