@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.
Files changed (211) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/README.md +7 -0
  4. package/dist/adapters.js +23 -23
  5. package/dist/benchmarks/_harness.d.ts +1 -1
  6. package/dist/benchmarks/_harness.js +1 -1
  7. package/dist/benchmarks/aec-bench.js +2 -2
  8. package/dist/benchmarks/agentbench.js +2 -2
  9. package/dist/benchmarks/appworld.js +2 -2
  10. package/dist/benchmarks/bfcl.js +2 -2
  11. package/dist/benchmarks/commit0.js +2 -2
  12. package/dist/benchmarks/crag.js +2 -2
  13. package/dist/benchmarks/dabstep.js +2 -2
  14. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  15. package/dist/benchmarks/finresearchbench.js +2 -2
  16. package/dist/benchmarks/nomiracl.js +2 -2
  17. package/dist/benchmarks/open-rag-bench.js +2 -2
  18. package/dist/benchmarks/programbench.js +2 -2
  19. package/dist/benchmarks/ragbench.js +2 -2
  20. package/dist/benchmarks/swe-bench.js +2 -2
  21. package/dist/benchmarks/t2-ragbench.js +2 -2
  22. package/dist/benchmarks/tau-bench-shared.js +2 -2
  23. package/dist/benchmarks/tau2-bench.js +3 -3
  24. package/dist/benchmarks/tau3-banking.js +3 -3
  25. package/dist/benchmarks/terminal-bench.js +2 -2
  26. package/dist/benchmarks/toollm.js +2 -2
  27. package/dist/benchmarks/webarena-verified.js +2 -2
  28. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  29. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  30. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  31. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  32. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  33. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  34. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  35. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  36. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  37. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  38. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  39. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  40. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  41. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  42. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  43. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  44. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  45. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  46. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  47. package/dist/chunk-WSKWVEQB.js.map +1 -0
  48. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  49. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  50. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  51. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  52. package/dist/index.js +23 -23
  53. package/package.json +5 -5
  54. package/scripts/trata-hedge/README.md +6 -5
  55. package/scripts/verify-packed-consumer.mjs +1 -1
  56. package/src/benchmarks/_harness.ts +20 -2
  57. package/src/benchmarks/humaneval.test.mts +2 -2
  58. package/src/gate.ts +1 -1
  59. package/src/hev-eval.mts +5 -2
  60. package/src/hev-improve.mts +118 -73
  61. package/src/official-optimizer-config.mts +89 -0
  62. package/src/official-optimizer-config.test.mts +88 -0
  63. package/src/profiles.ts +2 -2
  64. package/src/quant-arena/README.md +144 -0
  65. package/src/quant-arena/backtest.test.mts +135 -0
  66. package/src/quant-arena/backtest.ts +218 -0
  67. package/src/quant-arena/data.test.mts +44 -0
  68. package/src/quant-arena/data.ts +141 -0
  69. package/src/quant-arena/driver.test.mts +253 -0
  70. package/src/quant-arena/driver.ts +219 -0
  71. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  72. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  75. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  76. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  77. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  78. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  83. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  86. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  87. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  88. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  89. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  94. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  95. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  96. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  97. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  98. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  99. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  100. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  101. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  102. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  103. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  104. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  105. package/src/quant-arena/holdout-certify.mts +206 -0
  106. package/src/quant-arena/holdout-certify.test.mts +82 -0
  107. package/src/quant-arena/leak-audit.test.mts +79 -0
  108. package/src/quant-arena/leak-audit.ts +95 -0
  109. package/src/quant-arena/make-fixtures.mts +161 -0
  110. package/src/quant-arena/multiplicity.test.mts +68 -0
  111. package/src/quant-arena/multiplicity.ts +87 -0
  112. package/src/quant-arena/nautilus-certify.ts +31 -0
  113. package/src/quant-arena/oms.ts +90 -0
  114. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  115. package/src/quant-arena/python/pyproject.toml +8 -0
  116. package/src/quant-arena/python/uv.lock +1297 -0
  117. package/src/quant-arena/python/vbt-worker.py +192 -0
  118. package/src/quant-arena/quant-loop.mts +813 -0
  119. package/src/quant-arena/quant-loop.test.mts +75 -0
  120. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  121. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  122. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  123. package/src/quant-arena/types.ts +133 -0
  124. package/src/quant-arena/vbt-client.ts +321 -0
  125. package/src/quant-arena/vbt-parity.test.mts +183 -0
  126. package/src/quant-arena/windows.test.mts +45 -0
  127. package/src/quant-arena/windows.ts +54 -0
  128. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  129. package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
  130. package/src/rollout-ledger/settle-capture.mts +442 -0
  131. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  132. package/src/smoke-structural-rollout.mts +15 -9
  133. package/src/swe-arena/activation.mts +225 -0
  134. package/src/swe-arena/activation.test.mts +300 -0
  135. package/src/swe-arena/arms.ts +30 -14
  136. package/src/swe-arena/briefing.mts +217 -0
  137. package/src/swe-arena/briefing.test.mts +178 -0
  138. package/src/swe-arena/calibrate.ts +102 -1
  139. package/src/swe-arena/cell-evidence.mts +41 -9
  140. package/src/swe-arena/factory-command-container.ts +284 -0
  141. package/src/swe-arena/factory-judge-child.mts +228 -0
  142. package/src/swe-arena/factory.test.mts +643 -0
  143. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  144. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  145. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  146. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  147. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  148. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  149. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  150. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  151. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  152. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  153. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  154. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  155. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  156. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  157. package/src/swe-arena/fixtures.ts +135 -2
  158. package/src/swe-arena/gepa-seat.mts +877 -0
  159. package/src/swe-arena/gepa-seat.test.mts +1059 -0
  160. package/src/swe-arena/implementation-ref.test.mts +64 -0
  161. package/src/swe-arena/implementation-ref.ts +62 -0
  162. package/src/swe-arena/ledger-orphans.mts +77 -0
  163. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  164. package/src/swe-arena/outer-loop.mts +710 -33
  165. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  166. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  167. package/src/swe-arena/proposer-fanout.mts +327 -31
  168. package/src/swe-arena/proposer-fanout.test.mts +246 -0
  169. package/src/swe-arena/proposer-provenance.mts +172 -0
  170. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  171. package/src/swe-arena/run-experiment.mts +367 -1
  172. package/src/swe-arena/run-report.mts +75 -0
  173. package/src/swe-arena/run-supervisor.test.mts +5 -3
  174. package/src/swe-arena/score-split.mts +140 -0
  175. package/src/swe-arena/score-split.test.mts +123 -0
  176. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  177. package/src/swe-arena/scratch-worktree.ts +34 -0
  178. package/src/swe-arena/types.ts +52 -0
  179. package/src/swe-code-improve.mts +24 -25
  180. package/src/swe-improve.mts +129 -96
  181. package/src/swe-local-proof.mts +6 -1
  182. package/src/swe-stream.mts +4 -2
  183. package/src/swe-structural.mts +245 -837
  184. package/src/tb-container-executor.test.mts +30 -6
  185. package/src/tb-supervisor-sidecar.mts +2 -1
  186. package/src/trata-gepa.mts +182 -245
  187. package/dist/chunk-LRRD7NAG.js.map +0 -1
  188. package/src/live-improve-campaign-mbpp.mts +0 -641
  189. package/src/live-improve-campaign.mts +0 -500
  190. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  191. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  192. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  193. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  194. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  195. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  196. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  197. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  198. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  199. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  200. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  201. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  202. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  203. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  204. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  205. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  206. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  207. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  208. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  209. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  210. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  211. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,64 @@
1
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import {
6
+ fileTreeImplementationRef,
7
+ pythonDistributionImplementationRef,
8
+ } from './implementation-ref.ts'
9
+
10
+ describe('implementation refs', () => {
11
+ const roots: string[] = []
12
+
13
+ afterEach(async () => {
14
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
15
+ })
16
+
17
+ it('is stable across creation order and changes with file content or symlink targets', async () => {
18
+ const first = await mkdtemp(join(tmpdir(), 'implementation-ref-a-'))
19
+ const second = await mkdtemp(join(tmpdir(), 'implementation-ref-b-'))
20
+ roots.push(first, second)
21
+ await mkdir(join(first, 'nested'))
22
+ await writeFile(join(first, 'nested', 'b.ts'), 'export const b = 2\n')
23
+ await writeFile(join(first, 'a.ts'), 'export const a = 1\n')
24
+ await symlink('a.ts', join(first, 'entry.ts'))
25
+ await writeFile(join(second, 'a.ts'), 'export const a = 1\n')
26
+ await symlink('a.ts', join(second, 'entry.ts'))
27
+ await mkdir(join(second, 'nested'))
28
+ await writeFile(join(second, 'nested', 'b.ts'), 'export const b = 2\n')
29
+
30
+ const initial = await fileTreeImplementationRef(first)
31
+ expect(await fileTreeImplementationRef(second)).toBe(initial)
32
+
33
+ await writeFile(join(second, 'nested', 'b.ts'), 'export const b = 3\n')
34
+ expect(await fileTreeImplementationRef(second)).not.toBe(initial)
35
+ await writeFile(join(second, 'nested', 'b.ts'), 'export const b = 2\n')
36
+ await rm(join(second, 'entry.ts'))
37
+ await symlink('nested/b.ts', join(second, 'entry.ts'))
38
+ expect(await fileTreeImplementationRef(second)).not.toBe(initial)
39
+ })
40
+
41
+ it('binds a Python distribution to its complete manifest', async () => {
42
+ const runPython = async (_script: string, args: string[]): Promise<string> =>
43
+ `import warning\n${JSON.stringify({
44
+ distribution: args[0],
45
+ version: '1.0.0',
46
+ python: '3.12.4',
47
+ files: [{ path: '__init__.py', sha256: 'a'.repeat(64) }],
48
+ })}`
49
+ const original = await pythonDistributionImplementationRef('swebench', runPython)
50
+ const changed = await pythonDistributionImplementationRef(
51
+ 'swebench',
52
+ async (_script, args) =>
53
+ JSON.stringify({
54
+ distribution: args[0],
55
+ version: '1.0.1',
56
+ python: '3.12.4',
57
+ files: [{ path: '__init__.py', sha256: 'b'.repeat(64) }],
58
+ }),
59
+ )
60
+
61
+ expect(original).toMatch(/^sha256:[a-f0-9]{64}$/)
62
+ expect(changed).not.toBe(original)
63
+ })
64
+ })
@@ -0,0 +1,62 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { lstat, readFile, readdir, readlink, realpath } from 'node:fs/promises'
3
+ import { join, relative } from 'node:path'
4
+ import { canonicalCandidateDigest } from '@tangle-network/agent-interface'
5
+
6
+ const sha256Content = (value: Uint8Array): string =>
7
+ `sha256:${createHash('sha256').update(value).digest('hex')}`
8
+
9
+ export async function fileTreeImplementationRef(root: string): Promise<string> {
10
+ const canonicalRoot = await realpath(root)
11
+ const files: Array<{ path: string; sha256?: string; symlink?: string }> = []
12
+
13
+ const walk = async (directory: string): Promise<void> => {
14
+ const entries = await readdir(directory, { withFileTypes: true })
15
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
16
+ for (const entry of entries) {
17
+ const path = join(directory, entry.name)
18
+ const relativePath = relative(canonicalRoot, path)
19
+ if (entry.isDirectory()) {
20
+ await walk(path)
21
+ } else if (entry.isSymbolicLink()) {
22
+ files.push({ path: relativePath, symlink: await readlink(path) })
23
+ } else if (entry.isFile() || (await lstat(path)).isFile()) {
24
+ files.push({ path: relativePath, sha256: sha256Content(await readFile(path)) })
25
+ }
26
+ }
27
+ }
28
+
29
+ await walk(canonicalRoot)
30
+ return canonicalCandidateDigest(files)
31
+ }
32
+
33
+ export async function pythonDistributionImplementationRef(
34
+ distribution: string,
35
+ runPython: (script: string, args: string[]) => Promise<string>,
36
+ ): Promise<string> {
37
+ const output = await runPython(
38
+ [
39
+ 'import hashlib, importlib, importlib.metadata, json, pathlib, sys',
40
+ 'name = sys.argv[1]',
41
+ 'module = importlib.import_module(name)',
42
+ 'root = pathlib.Path(module.__file__).resolve().parent',
43
+ 'files = []',
44
+ 'for path in sorted(p for p in root.rglob("*") if p.is_file()):',
45
+ ' if "__pycache__" in path.parts or path.suffix == ".pyc":',
46
+ ' continue',
47
+ ' files.append({"path": str(path.relative_to(root)), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})',
48
+ 'try:',
49
+ ' version = importlib.metadata.version(name)',
50
+ 'except importlib.metadata.PackageNotFoundError:',
51
+ ' version = None',
52
+ 'print(json.dumps({"distribution": name, "version": version, "python": sys.version, "files": files}, sort_keys=True))',
53
+ ].join('\n'),
54
+ [distribution],
55
+ )
56
+ const line = output.trim().split('\n').at(-1)
57
+ if (line === undefined) {
58
+ throw new Error(`python distribution ${distribution} returned no implementation manifest`)
59
+ }
60
+ const manifest = JSON.parse(line) as unknown
61
+ return canonicalCandidateDigest(manifest)
62
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Crash-orphan reconciliation for the improve-run CostLedger.
3
+ *
4
+ * A killed outer-loop process leaves its in-flight paid call as a durable
5
+ * 'pending' record with no receipt. On resume the ledger's fail-closed
6
+ * admission guard ("N unresolved call(s) must be reconciled before new paid
7
+ * work") then refuses EVERY new paid call, so a single crash permanently
8
+ * blocks the run. This module settles those orphans at startup through the
9
+ * ledger's dedicated crash-recovery verb, `CostLedger.reconcile()`.
10
+ *
11
+ * Safety contract — never settle a live run's call:
12
+ * - The caller must hold the outDir single-instance pid lock
13
+ * (`acquireInstanceLock` in outer-loop.mts) before invoking this, so no
14
+ * concurrent outer-loop shares the ledger file. Every pending record
15
+ * restored from disk is therefore from a dead process.
16
+ * - Belt and braces, only `state: 'interrupted'` pendings are touched:
17
+ * restored-from-persistence calls this process's ledger instance never
18
+ * started. An 'active'/'late' pending belongs to an in-flight
19
+ * `runPaidCall` and settles through its own path.
20
+ * - The ledger's persistence append is compare-and-swap on the file
21
+ * revision, so even an unexpected concurrent writer makes reconciliation
22
+ * fail loud (`CostCallConflictError`) instead of corrupting the log.
23
+ *
24
+ * The orphan settles as a FAILED receipt with zero usage and a fully-known
25
+ * $0 actual cost (`actualCostUsd: 0`), reason 'process-crash-orphan'. A
26
+ * `costUnknown`/`usageUnknown` receipt would be marginally more honest about
27
+ * the crashed call's true spend, but an unknown-cost receipt marks the
28
+ * ledger's accounting incomplete forever, which blocks all paid work in
29
+ * capped runs — the same permanent lockout this module exists to fix.
30
+ */
31
+
32
+ import { existsSync } from 'node:fs'
33
+ import { join } from 'node:path'
34
+ import type { CostReceipt, PendingCostCallView } from '@tangle-network/agent-eval'
35
+ import { createRunCostLedger, fsCampaignStorage } from '@tangle-network/agent-eval/campaign'
36
+
37
+ export const CRASH_ORPHAN_REASON = 'process-crash-orphan'
38
+
39
+ /** The two ledger verbs reconciliation needs, structural for testability. */
40
+ export interface OrphanReconcilableLedger {
41
+ listPending(): PendingCostCallView[]
42
+ reconcile(
43
+ callId: string,
44
+ observed: {
45
+ model: string
46
+ inputTokens: number
47
+ outputTokens: number
48
+ actualCostUsd?: number
49
+ },
50
+ options?: { error?: string },
51
+ ): CostReceipt
52
+ }
53
+
54
+ /** Settle every crash-orphaned ('interrupted') pending call as a $0 failure. */
55
+ export function reconcileCrashOrphanCalls(ledger: OrphanReconcilableLedger): CostReceipt[] {
56
+ return ledger
57
+ .listPending()
58
+ .filter((pending) => pending.state === 'interrupted')
59
+ .map((pending) =>
60
+ ledger.reconcile(
61
+ pending.callId,
62
+ { model: pending.model, inputTokens: 0, outputTokens: 0, actualCostUsd: 0 },
63
+ { error: CRASH_ORPHAN_REASON },
64
+ ),
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Open the durable ledger under `improveRunDir` and settle crash orphans.
70
+ * No-op (empty result) when no ledger file exists yet — a fresh run.
71
+ * PRECONDITION: the caller holds the outDir instance lock (see module doc).
72
+ */
73
+ export function reconcileCrashOrphansOnDisk(improveRunDir: string): CostReceipt[] {
74
+ if (!existsSync(join(improveRunDir, 'cost-ledger.jsonl'))) return []
75
+ const ledger = createRunCostLedger({ storage: fsCampaignStorage(), runDir: improveRunDir })
76
+ return reconcileCrashOrphanCalls(ledger)
77
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Crash-orphan ledger reconciliation: a synthetic durable ledger with one
3
+ * orphaned pending call (crashed process — call opened, no receipt) and one
4
+ * resolved call. Reconciliation must settle ONLY the orphan, as a $0
5
+ * failure receipt tagged 'process-crash-orphan', after which the ledger's
6
+ * fail-closed admission guard passes new paid work again — the exact resume
7
+ * path improve() rides.
8
+ */
9
+
10
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { join } from 'node:path'
13
+ import { describe, expect, it } from 'vitest'
14
+ import type { CostReceipt, PendingCostCallView } from '@tangle-network/agent-eval'
15
+ import { createRunCostLedger, fsCampaignStorage } from '@tangle-network/agent-eval/campaign'
16
+ import {
17
+ CRASH_ORPHAN_REASON,
18
+ reconcileCrashOrphanCalls,
19
+ reconcileCrashOrphansOnDisk,
20
+ type OrphanReconcilableLedger,
21
+ } from './ledger-orphans.mts'
22
+
23
+ const ATTRIBUTION = { channel: 'agent', phase: 'search.baseline', model: 'm' }
24
+
25
+ /** A durable event line in the exact shape cost-ledger.jsonl persists. */
26
+ const line = (record: Record<string, unknown>): string =>
27
+ `${JSON.stringify({ version: 1, record })}\n`
28
+
29
+ /** Write a synthetic ledger: one settled pair + one crash-orphaned pending. */
30
+ function writeCrashedLedger(dir: string): void {
31
+ const settledPending = {
32
+ status: 'pending',
33
+ callId: 'settled-1',
34
+ ...ATTRIBUTION,
35
+ actor: 'worker:astropy#r0',
36
+ timestamp: 1_000,
37
+ }
38
+ const settledReceipt = {
39
+ ...settledPending,
40
+ status: 'settled',
41
+ inputTokens: 10,
42
+ outputTokens: 5,
43
+ costUsd: 0.01,
44
+ costUnknown: false,
45
+ actualCostUsd: 0.01,
46
+ }
47
+ const orphanPending = {
48
+ status: 'pending',
49
+ callId: 'orphan-1',
50
+ ...ATTRIBUTION,
51
+ actor: 'worker:xarray#r0',
52
+ timestamp: 2_000,
53
+ tags: { cellId: 'pydata__xarray-4687:0' },
54
+ }
55
+ writeFileSync(
56
+ join(dir, 'cost-ledger.jsonl'),
57
+ line(settledPending) + line(settledReceipt) + line(orphanPending),
58
+ )
59
+ }
60
+
61
+ const openLedger = (dir: string) =>
62
+ createRunCostLedger({ storage: fsCampaignStorage(), runDir: dir })
63
+
64
+ describe('reconcileCrashOrphansOnDisk', () => {
65
+ it('settles only the orphan, and the admission guard passes afterwards', async () => {
66
+ const dir = mkdtempSync(join(tmpdir(), 'ledger-orphans-'))
67
+ writeCrashedLedger(dir)
68
+
69
+ // The bug being fixed: with the orphan unresolved, ALL new paid work is refused.
70
+ const blocked = await openLedger(dir).runPaidCall({
71
+ ...ATTRIBUTION,
72
+ actor: 'worker:next',
73
+ execute: async () => 'ok',
74
+ receipt: () => ({ model: 'm', inputTokens: 1, outputTokens: 1, actualCostUsd: 0 }),
75
+ })
76
+ expect(blocked.succeeded).toBe(false)
77
+ if (!blocked.succeeded) {
78
+ expect(blocked.error.message).toContain('unresolved call(s) must be reconciled')
79
+ }
80
+
81
+ const receipts = reconcileCrashOrphansOnDisk(dir)
82
+ expect(receipts.map((r) => r.callId)).toEqual(['orphan-1'])
83
+ expect(receipts[0]).toMatchObject({
84
+ status: 'settled',
85
+ error: CRASH_ORPHAN_REASON,
86
+ inputTokens: 0,
87
+ outputTokens: 0,
88
+ costUsd: 0,
89
+ costUnknown: false,
90
+ })
91
+
92
+ // Durable: a fresh ledger instance sees zero unresolved calls, the
93
+ // settled call untouched, and admits new paid work again.
94
+ const reopened = openLedger(dir)
95
+ const summary = reopened.summary()
96
+ expect(summary.unresolvedCalls).toBe(0)
97
+ expect(summary.pendingCalls).toBe(0)
98
+ expect(summary.accountingComplete).toBe(true)
99
+ const settled = reopened.list().find((r) => r.callId === 'settled-1')
100
+ expect(settled?.costUsd).toBe(0.01)
101
+ expect(settled?.error).toBeUndefined()
102
+ const admitted = await reopened.runPaidCall({
103
+ ...ATTRIBUTION,
104
+ actor: 'worker:next',
105
+ execute: async () => 'ok',
106
+ receipt: () => ({ model: 'm', inputTokens: 1, outputTokens: 1, actualCostUsd: 0 }),
107
+ })
108
+ expect(admitted.succeeded).toBe(true)
109
+
110
+ // The durable log carries the orphan's failure receipt verbatim.
111
+ const persisted = readFileSync(join(dir, 'cost-ledger.jsonl'), 'utf8')
112
+ expect(persisted).toContain(CRASH_ORPHAN_REASON)
113
+ })
114
+
115
+ it('is idempotent — a second startup pass finds nothing to settle', () => {
116
+ const dir = mkdtempSync(join(tmpdir(), 'ledger-orphans-'))
117
+ writeCrashedLedger(dir)
118
+ expect(reconcileCrashOrphansOnDisk(dir)).toHaveLength(1)
119
+ expect(reconcileCrashOrphansOnDisk(dir)).toHaveLength(0)
120
+ })
121
+
122
+ it('is a no-op on a fresh run dir with no ledger file', () => {
123
+ const dir = mkdtempSync(join(tmpdir(), 'ledger-orphans-'))
124
+ expect(reconcileCrashOrphansOnDisk(dir)).toEqual([])
125
+ })
126
+ })
127
+
128
+ describe('reconcileCrashOrphanCalls', () => {
129
+ it("never settles a live in-process call — only 'interrupted' pendings", () => {
130
+ const base = { ...ATTRIBUTION, status: 'pending' as const, timestamp: 1 }
131
+ const pendings: PendingCostCallView[] = [
132
+ { ...base, callId: 'live-1', actor: 'a', state: 'active' },
133
+ { ...base, callId: 'late-1', actor: 'b', state: 'late' },
134
+ { ...base, callId: 'orphan-1', actor: 'c', state: 'interrupted' },
135
+ ]
136
+ const settled: string[] = []
137
+ const ledger: OrphanReconcilableLedger = {
138
+ listPending: () => pendings,
139
+ reconcile: (callId) => {
140
+ settled.push(callId)
141
+ return { callId } as CostReceipt
142
+ },
143
+ }
144
+ reconcileCrashOrphanCalls(ledger)
145
+ expect(settled).toEqual(['orphan-1'])
146
+ })
147
+ })