@@ -53,14 +53,22 @@ def harness_dirty(root):
5353 return bool (proc .stdout .strip ()) if proc .returncode == 0 else None
5454
5555
56- def newest_reports (eval_dir ):
57- """Latest evaluator report per condition, plus the ones it supersedes."""
56+ def newest_reports (eval_dir , run_label = None ):
57+ """Latest evaluator report per condition, plus the ones it supersedes.
58+
59+ A report is named only for the evaluator run that produced it, so reports
60+ from two benchmark runs of the same condition are indistinguishable and a
61+ page built across them silently mixes subsets. run_label restricts the scan
62+ to one batch, which is what makes a per-run page possible.
63+ """
5864 latest , superseded = {}, []
5965 for path in sorted (glob .glob (os .path .join (eval_dir , '*.json' ))):
6066 match = REPORT_NAME_RE .match (os .path .basename (path ))
6167 if not match :
6268 continue
6369 cond , run_id = match .group ('cond' ), match .group ('run_id' )
70+ if run_label is not None and not run_id .startswith (run_label + '-' ):
71+ continue
6472 previous = latest .get (cond )
6573 if previous is None or run_id > previous [0 ]:
6674 if previous is not None :
@@ -184,6 +192,8 @@ def cell_telemetry(cell_dir):
184192 'claude_output_tokens' : 0 ,
185193 'claude_wall_seconds' : 0 ,
186194 'codex_phases' : 0 ,
195+ 'codex_wall_seconds' : 0 ,
196+ 'codex_tokens' : 0 ,
187197 'single_shot_attempts' : None ,
188198 'sandbox' : None ,
189199 }
@@ -220,9 +230,55 @@ def cell_telemetry(cell_dir):
220230 # Each Codex phase writes both a transcript and a stderr log.
221231 # Counting every .log double-counted every phase.
222232 out ['codex_phases' ] += 1
233+ elif name .startswith ('codex-' ) and name .endswith ('.stderr.log' ):
234+ seconds , tokens = codex_effort (path )
235+ out ['codex_wall_seconds' ] += seconds
236+ out ['codex_tokens' ] += tokens
223237 return out
224238
225239
240+ CODEX_TS_RE = re .compile (r'^(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(?:\.\d+)?Z' )
241+ CODEX_TOKENS_RE = re .compile (r'^([\d,]+)$' )
242+
243+
244+ def codex_effort (stderr_path ):
245+ """Wall seconds and token count for one Codex phase, from its own log.
246+
247+ The Codex CLI reports no cost, so tokens and elapsed time are the only
248+ honest units of effort available for it. Both are read out of the log the
249+ phase already wrote: the span between its first and last timestamp, and the
250+ figure it prints under "tokens used".
251+ """
252+ first = last = None
253+ tokens = 0
254+ want_tokens = False
255+ try :
256+ with open (stderr_path , encoding = 'utf-8' , errors = 'replace' ) as handle :
257+ for line in handle :
258+ match = CODEX_TS_RE .match (line )
259+ if match :
260+ if first is None :
261+ first = match .group (1 )
262+ last = match .group (1 )
263+ stripped = line .strip ()
264+ if want_tokens :
265+ count = CODEX_TOKENS_RE .match (stripped )
266+ if count :
267+ tokens += int (count .group (1 ).replace (',' , '' ))
268+ want_tokens = False
269+ elif stripped == 'tokens used' :
270+ want_tokens = True
271+ except OSError :
272+ return 0 , 0
273+ seconds = 0
274+ if first and last and last >= first :
275+ import datetime
276+ fmt = '%Y-%m-%dT%H:%M:%S'
277+ seconds = int ((datetime .datetime .strptime (last , fmt )
278+ - datetime .datetime .strptime (first , fmt )).total_seconds ())
279+ return seconds , tokens
280+
281+
226282def main ():
227283 ap = argparse .ArgumentParser ()
228284 ap .add_argument ('--repo-root' , required = True )
@@ -231,6 +287,8 @@ def main():
231287 ap .add_argument ('--output' , required = True )
232288 ap .add_argument ('--generated-at' , required = True ,
233289 help = 'UTC timestamp supplied by the caller' )
290+ ap .add_argument ('--run-label' , default = None ,
291+ help = 'only read evaluator reports from this labelled batch' )
234292 args = ap .parse_args ()
235293
236294 root = os .path .abspath (args .repo_root )
@@ -250,7 +308,7 @@ def main():
250308 repos = {row ['instance_id' ]: row ['repo' ] for row in subset ['instances' ]}
251309 lock = read_json (os .path .join (code_dir , 'external-sources.lock.json' ))
252310
253- latest , superseded = newest_reports (eval_dir )
311+ latest , superseded = newest_reports (eval_dir , args . run_label )
254312 cells = index_cells (runs_root )
255313 attempts_index = index_attempts (runs_root )
256314
@@ -276,6 +334,8 @@ def main():
276334 'claude_output_tokens' : 0 ,
277335 'claude_wall_seconds' : 0 ,
278336 'codex_phases' : 0 ,
337+ 'codex_wall_seconds' : 0 ,
338+ 'codex_tokens' : 0 ,
279339 'cells_linked' : 0 ,
280340 'sandbox_modes' : [],
281341 'single_shot_attempts' : [],
@@ -334,6 +394,8 @@ def main():
334394 row ['telemetry' ]['claude_output_tokens' ] += telemetry ['claude_output_tokens' ]
335395 row ['telemetry' ]['claude_wall_seconds' ] += telemetry ['claude_wall_seconds' ]
336396 row ['telemetry' ]['codex_phases' ] += telemetry ['codex_phases' ]
397+ row ['telemetry' ]['codex_wall_seconds' ] += telemetry ['codex_wall_seconds' ]
398+ row ['telemetry' ]['codex_tokens' ] += telemetry ['codex_tokens' ]
337399 if telemetry ['sandbox' ]:
338400 sandboxes .add (telemetry ['sandbox' ])
339401 if telemetry ['single_shot_attempts' ] is not None :
@@ -354,6 +416,35 @@ def main():
354416 if attempt else None ),
355417 })
356418 row ['telemetry' ]['claude_cost_usd' ] = round (row ['telemetry' ]['claude_cost_usd' ], 4 )
419+
420+ # A task the arm actually ran, whether or not it yielded a patch. The
421+ # distinction matters: a task that ran and produced nothing is a zero,
422+ # but a task that was never reached is not a result at all. Scoring an
423+ # unreached task as zero understates an interrupted run as badly as
424+ # dropping a failed one would flatter a finished one.
425+ ran = [task for task in row ['per_task' ]
426+ if task ['status' ] in ('resolved' , 'unresolved' , 'no-patch' )]
427+ row ['attempted_count' ] = len (ran )
428+ row ['complete' ] = len (ran ) == len (instances )
429+
430+ # Effort per arm. Codex, GLM and Kimi report no cost, so an arm that
431+ # uses them has a dollar figure covering only its Claude phases; saying
432+ # so is the difference between a partial figure and a wrong one.
433+ telemetry = row ['telemetry' ]
434+ telemetry ['total_wall_seconds' ] = (telemetry ['claude_wall_seconds' ]
435+ + telemetry ['codex_wall_seconds' ])
436+ telemetry ['cost_is_complete' ] = (
437+ telemetry ['codex_phases' ] == 0
438+ and not telemetry ['single_shot_attempts' ])
439+ resolved = row ['resolved' ] or 0
440+ # No reported cost is not zero cost: Codex, GLM and Kimi bill elsewhere.
441+ # Emitting 0.0 here would render as "$0.00 per resolved task", which
442+ # reads as free rather than as unmeasured.
443+ telemetry ['cost_per_resolved' ] = (
444+ round (telemetry ['claude_cost_usd' ] / resolved , 4 )
445+ if resolved and telemetry ['claude_cost_usd' ] else None )
446+ telemetry ['seconds_per_resolved' ] = (
447+ round (telemetry ['total_wall_seconds' ] / resolved ) if resolved else None )
357448 rows .append (row )
358449
359450 payload = {
0 commit comments