cdx-manager 0.2.1

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.
@@ -0,0 +1,572 @@
1
+ import json
2
+ import os
3
+ import re
4
+ from datetime import datetime, timezone
5
+
6
+
7
+ _ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
8
+ _ANSI_TERMINAL_CONTROL = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
9
+ _OSC_SEQUENCE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
10
+ _CONTROL_CHAR = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
11
+
12
+ MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
13
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
14
+ MAX_STATUS_READ_BYTES = 512 * 1024
15
+ MAX_STATUS_CANDIDATE_FILES = 64
16
+
17
+
18
+ def _strip_ansi(text):
19
+ return _ANSI_ESCAPE.sub("", str(text or ""))
20
+
21
+
22
+ def _normalize_terminal_transcript(text):
23
+ text = str(text or "")
24
+ text = _OSC_SEQUENCE.sub(" ", text)
25
+ text = _ANSI_TERMINAL_CONTROL.sub(" ", text)
26
+ text = _ANSI_ESCAPE.sub(" ", text)
27
+ text = _CONTROL_CHAR.sub(" ", text)
28
+ text = text.replace("\r", "\n")
29
+ return text
30
+
31
+
32
+ def _safe_read_text(file_path, max_bytes=MAX_STATUS_READ_BYTES):
33
+ try:
34
+ size = os.path.getsize(file_path)
35
+ with open(file_path, "rb") as f:
36
+ if size > max_bytes:
37
+ f.seek(-max_bytes, os.SEEK_END)
38
+ return f.read().decode("utf-8", errors="replace")
39
+ except (FileNotFoundError, OSError):
40
+ return None
41
+
42
+
43
+ def _safe_stat(file_path):
44
+ try:
45
+ return os.stat(file_path)
46
+ except OSError:
47
+ return None
48
+
49
+
50
+ def _collect_text_values(value, output=None):
51
+ if output is None:
52
+ output = []
53
+ if isinstance(value, str):
54
+ output.append(value)
55
+ elif isinstance(value, list):
56
+ for item in value:
57
+ _collect_text_values(item, output)
58
+ elif isinstance(value, dict):
59
+ for item in value.values():
60
+ _collect_text_values(item, output)
61
+ return output
62
+
63
+
64
+ def _extract_status_blocks_from_text(text, provider=None, source_ref=None, timestamp=None):
65
+ normalized = _normalize_terminal_transcript(text)
66
+ lines = normalized.split("\n")
67
+ items = []
68
+
69
+ def collect_blocks(
70
+ start_pattern,
71
+ end_patterns,
72
+ max_span=80,
73
+ pre_context=0,
74
+ context_pattern=None,
75
+ context_stop_patterns=None,
76
+ ):
77
+ blocks = []
78
+ index = 0
79
+ while index < len(lines):
80
+ if not start_pattern.search(lines[index]):
81
+ index += 1
82
+ continue
83
+ end_index = len(lines)
84
+ for cursor in range(index + 1, min(len(lines), index + max_span)):
85
+ if any(pattern.search(lines[cursor]) for pattern in end_patterns):
86
+ end_index = cursor
87
+ break
88
+ start_index = max(0, index - pre_context)
89
+ if context_pattern is not None:
90
+ cursor = index - 1
91
+ while cursor >= 0:
92
+ line = lines[cursor]
93
+ if context_stop_patterns and any(pattern.search(line) for pattern in context_stop_patterns):
94
+ break
95
+ if not context_pattern.search(line):
96
+ break
97
+ start_index = cursor
98
+ cursor -= 1
99
+ block = "\n".join(lines[start_index:end_index]).strip()
100
+ if block:
101
+ blocks.append(block)
102
+ index = max(index + 1, end_index)
103
+ return blocks
104
+
105
+ if provider != "codex":
106
+ for block in collect_blocks(
107
+ re.compile(r"^\s*(?:[│|]\s*)?Current session\b", re.I),
108
+ [re.compile(p, re.I) for p in [
109
+ r"^Extra usage\b", r"^Esc to cancel\b",
110
+ r"^To continue this session\b", r"^╰",
111
+ ]],
112
+ ):
113
+ items.append({"source_ref": source_ref, "timestamp": timestamp, "text": block})
114
+
115
+ if provider != "claude":
116
+ for block in collect_blocks(
117
+ re.compile(r"^\s*(?:[│|]\s*)?5h\s+limit\b", re.I),
118
+ [re.compile(p, re.I) for p in [
119
+ r"^To continue this session\b", r"^╰",
120
+ ]],
121
+ context_pattern=re.compile(
122
+ r"^\s*$|^\s*(?:[│|]\s*)?(?:╭|Visit\b|information\b|Model:|Directory:|Permissions:|Agents\.md:|Account:|Collaboration mode:|Session:)",
123
+ re.I,
124
+ ),
125
+ context_stop_patterns=[
126
+ re.compile(r"^\s*(?:[│|]\s*)?5h\s+limit\b", re.I),
127
+ re.compile(r"^\s*(?:[│|]\s*)?Weekly\s+limit\b", re.I),
128
+ re.compile(r"^To continue this session\b", re.I),
129
+ ],
130
+ ):
131
+ items.append({"source_ref": source_ref, "timestamp": timestamp, "text": block})
132
+
133
+ if items:
134
+ return items
135
+
136
+ if provider:
137
+ return []
138
+
139
+ keyword_re = re.compile(r"/status|usage|current|remaining|\d{1,3}%", re.I)
140
+ fallback_lines = str(text or "").splitlines()
141
+ for i in range(len(fallback_lines) - 1, -1, -1):
142
+ if not keyword_re.search(fallback_lines[i]):
143
+ continue
144
+ start = max(0, i - 4)
145
+ end = min(len(fallback_lines), i + 5)
146
+ snippet = "\n".join(fallback_lines[start:end]).strip()
147
+ if snippet:
148
+ return [{"source_ref": source_ref, "timestamp": timestamp, "text": snippet}]
149
+ return []
150
+
151
+
152
+ def _extract_jsonl_texts(file_path, provider=None):
153
+ text = _safe_read_text(file_path)
154
+ if not text:
155
+ return []
156
+ items = []
157
+ for line_index, line in enumerate(text.splitlines()):
158
+ line = line.strip()
159
+ if not line:
160
+ continue
161
+ try:
162
+ record = json.loads(line)
163
+ payload_texts = _collect_text_values(record.get("payload") or {})
164
+ for candidate in payload_texts:
165
+ if isinstance(candidate, str) and candidate.strip():
166
+ items.extend(_extract_status_blocks_from_text(
167
+ candidate,
168
+ provider=provider,
169
+ source_ref=f"{file_path}:{line_index + 1}",
170
+ timestamp=record.get("timestamp"),
171
+ ))
172
+ except (json.JSONDecodeError, AttributeError):
173
+ continue
174
+ return items
175
+
176
+
177
+ def _extract_log_block(file_path, provider=None):
178
+ text = _safe_read_text(file_path)
179
+ if not text:
180
+ return []
181
+ return _extract_status_blocks_from_text(text, provider=provider, source_ref=file_path, timestamp=None)
182
+
183
+
184
+ def _parse_month_index(name):
185
+ lower = name[:3].lower()
186
+ for i, m in enumerate(MONTH_ABBR):
187
+ if m.lower() == lower:
188
+ return i
189
+ return -1
190
+
191
+
192
+ def _infer_reset_year(month, day):
193
+ now = datetime.now().astimezone()
194
+ year = now.year
195
+ try:
196
+ candidate = datetime(
197
+ year,
198
+ month + 1,
199
+ day,
200
+ tzinfo=now.tzinfo,
201
+ )
202
+ except ValueError:
203
+ return year
204
+ two_days_ago = datetime.fromtimestamp(now.timestamp() - 2 * 24 * 3600, tz=now.tzinfo)
205
+ return year + 1 if candidate < two_days_ago else year
206
+
207
+
208
+ def _normalize_reset_date(raw):
209
+ if not raw:
210
+ return None
211
+
212
+ raw = str(raw).strip()
213
+
214
+ def pad(n):
215
+ return str(n).zfill(2)
216
+
217
+ def format_time(hours, minutes):
218
+ return f"{pad(hours)}:{pad(minutes)}"
219
+
220
+ def parse_ampm(hours, minutes, meridiem):
221
+ normalized = meridiem.lower().replace(".", "")
222
+ if normalized == "pm" and hours != 12:
223
+ hours += 12
224
+ if normalized == "am" and hours == 12:
225
+ hours = 0
226
+ return hours, minutes
227
+
228
+ # Codex: "10:10 on 17 Apr"
229
+ m = re.match(r"(\d{1,2}):(\d{2})\s+on\s+(\d{1,2})\s+([A-Za-z]+)", raw, re.I)
230
+ if m:
231
+ hours, minutes, day, month_str = int(m[1]), int(m[2]), int(m[3]), m[4]
232
+ month = _parse_month_index(month_str)
233
+ if month != -1:
234
+ year = _infer_reset_year(month, day)
235
+ return f"{MONTH_ABBR[month]} {day} {pad(hours)}:{pad(minutes)}"
236
+
237
+ # Claude: "Thursday, April 17 at 5:00 AM" or "April 17, 2026, 5 PM"
238
+ m = re.match(
239
+ r"(?:[A-Za-z]+,\s+)?([A-Za-z]+)\s+(\d{1,2})(?:,\s+(\d{4}))?"
240
+ r"(?:\s*(?:,|at)\s*(\d{1,2})(?::(\d{2}))?\s*([ap]\.?m\.?))?$",
241
+ raw,
242
+ re.I,
243
+ )
244
+ if m:
245
+ month = _parse_month_index(m[1])
246
+ if month != -1:
247
+ day = int(m[2])
248
+ year = int(m[3]) if m[3] else _infer_reset_year(month, day)
249
+ if m[4]:
250
+ hours, minutes = parse_ampm(int(m[4]), int(m[5] or 0), m[6])
251
+ return f"{MONTH_ABBR[month]} {day} {format_time(hours, minutes)}"
252
+ return f"{MONTH_ABBR[month]} {day}"
253
+
254
+ # Claude session reset: "at 5:00 AM", "5:00 AM", or "today at 5 PM"
255
+ m = re.match(r"(?:today\s+)?(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*([ap]\.?m\.?)$", raw, re.I)
256
+ if m:
257
+ hours, minutes = parse_ampm(int(m[1]), int(m[2] or 0), m[3])
258
+ now = datetime.now().astimezone()
259
+ from datetime import timedelta
260
+ candidate = datetime(now.year, now.month, now.day, hours, minutes, tzinfo=now.tzinfo)
261
+ if candidate <= now:
262
+ candidate = candidate + timedelta(days=1)
263
+ return f"{MONTH_ABBR[candidate.month - 1]} {candidate.day} {format_time(hours, minutes)}"
264
+
265
+ # Codex time-only: "21:51"
266
+ m = re.match(r"^(\d{1,2}):(\d{2})$", raw)
267
+ if m:
268
+ hours, minutes = int(m[1]), int(m[2])
269
+ now = datetime.now().astimezone()
270
+ from datetime import timedelta
271
+ candidate = datetime(now.year, now.month, now.day, hours, minutes, tzinfo=now.tzinfo)
272
+ if candidate <= now:
273
+ candidate = candidate + timedelta(days=1)
274
+ return f"{MONTH_ABBR[candidate.month - 1]} {candidate.day} {format_time(hours, minutes)}"
275
+
276
+ # Claude: "Thursday, April 17" or "April 17" or "April 17, 2026"
277
+ m = re.match(r"(?:[A-Za-z]+,\s+)?([A-Za-z]+)\s+(\d{1,2})(?:,\s+(\d{4}))?", raw, re.I)
278
+ if m:
279
+ month = _parse_month_index(m[1])
280
+ if month != -1:
281
+ day = int(m[2])
282
+ year = int(m[3]) if m[3] else _infer_reset_year(month, day)
283
+ return f"{MONTH_ABBR[month]} {day}"
284
+
285
+ return None
286
+
287
+
288
+ def _extract_account_identity(text):
289
+ normalized = _normalize_terminal_transcript(text)
290
+ for line in normalized.split("\n"):
291
+ m = re.match(r"^\s*(?:[│|]\s*)?Account:\s*(.+?)\s*$", line, re.I)
292
+ if not m:
293
+ continue
294
+ value = m.group(1).strip().lower()
295
+ if value:
296
+ return value
297
+ return None
298
+
299
+
300
+ def _account_matches_expected(block_text, expected_account_email):
301
+ if not expected_account_email:
302
+ return True
303
+ actual = _extract_account_identity(block_text)
304
+ if not actual:
305
+ return True
306
+
307
+ expected = str(expected_account_email).strip().lower()
308
+ actual_email = re.split(r"\s|\(", actual, maxsplit=1)[0]
309
+ expected_email = re.split(r"\s|\(", expected, maxsplit=1)[0]
310
+
311
+ if actual_email == expected_email:
312
+ return True
313
+ if actual_email.startswith(expected_email) or expected_email.startswith(actual_email):
314
+ return min(len(actual_email), len(expected_email)) >= 8
315
+ return False
316
+
317
+
318
+ def extract_named_statuses_from_text(text):
319
+ normalized = _normalize_terminal_transcript(text)
320
+ lines = [l.strip() for l in normalized.split("\n") if l.strip()]
321
+ result = {}
322
+
323
+ key_value_patterns = [
324
+ ("usage_pct", re.compile(r"usage_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
325
+ ("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
326
+ ("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
327
+ ("credits", re.compile(r"credits?\s*[:=]\s*([\d, ]*\d[\d, ]*)\s*(?:credits?)?", re.I)),
328
+ ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
329
+ ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
330
+ ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
331
+ ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
332
+ ("usage_pct", re.compile(r"usage\s*[:=]\s*(\d{1,3})%", re.I)),
333
+ ("usage_pct", re.compile(r"current\s*[:=]\s*(\d{1,3})%", re.I)),
334
+ ("remaining_5h_pct", re.compile(r"5h(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
335
+ ("remaining_5h_pct", re.compile(r"remaining\s+5h\s*[:=]\s*(\d{1,3})%", re.I)),
336
+ ("remaining_week_pct", re.compile(r"week(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
337
+ ("remaining_week_pct", re.compile(r"remaining\s+week\s*[:=]\s*(\d{1,3})%", re.I)),
338
+ ]
339
+ for field, pattern in key_value_patterns:
340
+ if field not in result:
341
+ m = pattern.search(normalized)
342
+ if m:
343
+ result[field] = int(re.sub(r"\D", "", m[1]))
344
+
345
+ # Claude "Current session / Current week" block
346
+ def extract_following_percent(anchor_pattern):
347
+ idx = next((i for i, l in enumerate(lines) if anchor_pattern.search(l)), -1)
348
+ if idx == -1:
349
+ return None
350
+ for i in range(idx + 1, min(len(lines), idx + 8)):
351
+ m = re.search(r"(\d{1,3})%\s+used", lines[i], re.I)
352
+ if m:
353
+ return int(m[1])
354
+ return None
355
+
356
+ session_used = extract_following_percent(re.compile(r"\bCurrent session\b", re.I))
357
+ week_used = extract_following_percent(re.compile(r"\bCurrent week\b", re.I))
358
+ if session_used is not None or week_used is not None:
359
+ if "usage_pct" not in result and session_used is not None:
360
+ result["usage_pct"] = session_used
361
+ if "remaining_5h_pct" not in result and session_used is not None:
362
+ result["remaining_5h_pct"] = max(0, 100 - session_used)
363
+ if "remaining_week_pct" not in result and week_used is not None:
364
+ result["remaining_week_pct"] = max(0, 100 - week_used)
365
+
366
+ # Table header row
367
+ header_idx = next(
368
+ (i for i, l in enumerate(lines)
369
+ if re.search(r"\bSESSION\b", l, re.I)
370
+ and re.search(r"\bUSAGE\b", l, re.I)
371
+ and re.search(r"\b5H\b", l, re.I)
372
+ and re.search(r"\bWEEK\b", l, re.I)),
373
+ -1,
374
+ )
375
+ if header_idx != -1:
376
+ for line in lines[header_idx + 1:]:
377
+ pcts = [int(m) for m in re.findall(r"(\d{1,3})%", line)]
378
+ if len(pcts) >= 3:
379
+ result.setdefault("usage_pct", pcts[0])
380
+ result.setdefault("remaining_5h_pct", pcts[1])
381
+ result.setdefault("remaining_week_pct", pcts[2])
382
+ break
383
+
384
+ for line in lines:
385
+ m = re.search(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", line, re.I)
386
+ if m and "remaining_5h_pct" not in result:
387
+ result["remaining_5h_pct"] = int(m[1])
388
+ m = re.search(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", line, re.I)
389
+ if m and "remaining_5h_pct" not in result:
390
+ result["remaining_5h_pct"] = int(m[1])
391
+ m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", line, re.I)
392
+ if m and "remaining_week_pct" not in result:
393
+ result["remaining_week_pct"] = int(m[1])
394
+ m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", line, re.I)
395
+ if m and "remaining_week_pct" not in result:
396
+ result["remaining_week_pct"] = int(m[1])
397
+
398
+ if "remaining_5h_pct" in result and "usage_pct" not in result:
399
+ result["usage_pct"] = max(0, 100 - result["remaining_5h_pct"])
400
+ if "remaining_week_pct" in result and "usage_pct" not in result:
401
+ result["usage_pct"] = max(0, 100 - result["remaining_week_pct"])
402
+
403
+ def _extract_reset_near(anchor_pattern, stop_pattern=None, max_span=8):
404
+ idx = next((i for i, l in enumerate(lines) if anchor_pattern.search(l)), -1)
405
+ if idx == -1:
406
+ return None
407
+ for i in range(idx, min(len(lines), idx + max_span)):
408
+ if i > idx and stop_pattern and stop_pattern.search(lines[i]):
409
+ break
410
+ m = re.search(r"\(resets\s+(.+?)\)", lines[i], re.I)
411
+ if m:
412
+ return m[1].strip()
413
+ m = re.match(r"resets?\s*:?\s*(.+)", lines[i], re.I)
414
+ if m:
415
+ return m[1].strip()
416
+ return None
417
+
418
+ reset_5h_at = _extract_reset_near(
419
+ re.compile(r"\b5h\s+limit\b", re.I),
420
+ stop_pattern=re.compile(r"\bweekly\s+limit\b", re.I),
421
+ max_span=4,
422
+ )
423
+ if not reset_5h_at:
424
+ reset_5h_at = _extract_reset_near(
425
+ re.compile(r"\bCurrent session\b", re.I),
426
+ stop_pattern=re.compile(r"\bCurrent week\b", re.I),
427
+ )
428
+ reset_week_at = _extract_reset_near(
429
+ re.compile(r"\bweekly\s+limit\b", re.I),
430
+ max_span=4,
431
+ )
432
+ if not reset_week_at:
433
+ reset_week_at = _extract_reset_near(re.compile(r"\bCurrent week\b", re.I))
434
+
435
+ if not reset_week_at:
436
+ for line in lines:
437
+ m = re.match(r"(?:(?:weekly\s+)?resets?)\s*:?\s*(.+)", line, re.I)
438
+ if m:
439
+ reset_week_at = m[1].strip()
440
+ break
441
+
442
+ if not reset_week_at:
443
+ all_resets = list(re.finditer(r"\(resets\s+(.+?)\)", normalized, re.I))
444
+ if all_resets:
445
+ reset_week_at = all_resets[-1].group(1).strip()
446
+
447
+ if reset_5h_at:
448
+ reset_5h_at = _normalize_reset_date(reset_5h_at) or reset_5h_at
449
+ if reset_week_at:
450
+ reset_week_at = _normalize_reset_date(reset_week_at) or reset_week_at
451
+
452
+ if not result:
453
+ return None
454
+
455
+ return {
456
+ "usage_pct": result.get("usage_pct"),
457
+ "remaining_5h_pct": result.get("remaining_5h_pct"),
458
+ "remaining_week_pct": result.get("remaining_week_pct"),
459
+ "credits": result.get("credits"),
460
+ "reset_5h_at": reset_5h_at,
461
+ "reset_week_at": reset_week_at,
462
+ "reset_at": reset_week_at or reset_5h_at,
463
+ "raw_status_text": normalized.strip() or None,
464
+ }
465
+
466
+
467
+ def _collect_candidate_files(root_dir):
468
+ priority_candidates = []
469
+ history_candidates = []
470
+ direct = [
471
+ os.path.join(root_dir, "history.jsonl"),
472
+ os.path.join(root_dir, "session_index.jsonl"),
473
+ os.path.join(root_dir, "log", "codex-tui.log"),
474
+ ]
475
+ for fp in direct:
476
+ if _safe_stat(fp):
477
+ priority_candidates.append(fp)
478
+
479
+ log_dir = os.path.join(root_dir, "log")
480
+ log_dir_stat = _safe_stat(log_dir)
481
+ if log_dir_stat and os.path.isdir(log_dir):
482
+ for fname in os.listdir(log_dir):
483
+ if fname.startswith("cdx-session") and fname.endswith(".log"):
484
+ fp = os.path.join(log_dir, fname)
485
+ if _safe_stat(fp):
486
+ priority_candidates.append(fp)
487
+
488
+ sessions_dir = os.path.join(root_dir, "sessions")
489
+ if not _safe_stat(sessions_dir):
490
+ return priority_candidates, history_candidates
491
+
492
+ skip = {"cache", "plugins", "skills", "memories", "sqlite", "shell_snapshots", "tmp"}
493
+
494
+ for dirpath, dirnames, filenames in os.walk(sessions_dir):
495
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d not in skip]
496
+ for fname in filenames:
497
+ if fname.endswith(".jsonl") or fname.endswith(".log"):
498
+ history_candidates.append(os.path.join(dirpath, fname))
499
+
500
+ return priority_candidates, history_candidates
501
+
502
+
503
+ def _sort_recent(paths):
504
+ candidate_stats = {
505
+ fp: stat
506
+ for fp, stat in ((candidate, _safe_stat(candidate)) for candidate in set(paths))
507
+ if stat
508
+ }
509
+ return sorted(
510
+ candidate_stats,
511
+ key=lambda fp: candidate_stats[fp].st_mtime,
512
+ reverse=True,
513
+ )
514
+
515
+
516
+ def find_latest_status_artifact(root_dir, provider=None, expected_account_email=None):
517
+ priority_candidates, history_candidates = _collect_candidate_files(root_dir)
518
+ candidates = (
519
+ _sort_recent(priority_candidates)
520
+ + _sort_recent(history_candidates)[:MAX_STATUS_CANDIDATE_FILES]
521
+ )
522
+ records = []
523
+ for fp in candidates:
524
+ normalized_fp = fp.replace(os.sep, "/")
525
+ if "/sessions/" in normalized_fp and os.path.basename(fp).startswith("rollout"):
526
+ continue
527
+ if fp.endswith(".jsonl"):
528
+ records.extend(_extract_jsonl_texts(fp, provider))
529
+ elif fp.endswith(".log"):
530
+ records.extend(_extract_log_block(fp, provider))
531
+
532
+ best = None
533
+ for candidate in records:
534
+ if provider == "codex" and not _account_matches_expected(
535
+ candidate["text"], expected_account_email
536
+ ):
537
+ continue
538
+ parsed = extract_named_statuses_from_text(candidate["text"])
539
+ if not parsed:
540
+ continue
541
+ ts = candidate.get("timestamp")
542
+ try:
543
+ score = float(ts) if ts else 0
544
+ except (TypeError, ValueError):
545
+ score = 0
546
+ src_file = re.sub(r":\d+$", "", candidate["source_ref"])
547
+ stat = _safe_stat(src_file)
548
+ if not score and stat:
549
+ score = stat.st_mtime
550
+ priority = 2 if src_file.endswith(".log") else 1
551
+
552
+ if best is None or (priority, score) >= (best["priority"], best["score"]):
553
+ best = {
554
+ "priority": priority,
555
+ "score": score,
556
+ "source_ref": candidate["source_ref"],
557
+ **parsed,
558
+ }
559
+ if ts:
560
+ try:
561
+ best["updated_at"] = datetime.fromtimestamp(
562
+ float(ts) / 1000, tz=timezone.utc
563
+ ).astimezone().isoformat()
564
+ except (TypeError, ValueError):
565
+ pass
566
+ if "updated_at" not in best:
567
+ if stat:
568
+ best["updated_at"] = datetime.fromtimestamp(
569
+ stat.st_mtime, tz=timezone.utc
570
+ ).astimezone().isoformat()
571
+
572
+ return best