cdx-manager 0.12.4 → 0.15.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.
@@ -0,0 +1,550 @@
1
+ """Status domain: reading, caching, merging and reporting session status.
2
+
3
+ Split out of session_service.py along one of the two seams the req_027
4
+ coupling measurement supports. Moved verbatim; re-exported by session_service.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ from datetime import datetime, timezone
11
+
12
+ from .claude_usage import _decode_jwt_claims
13
+ from .config import PROVIDER_CLAUDE, PROVIDER_CODEX, get_cdx_home
14
+ from .errors import CdxError
15
+ from .session_helpers import (
16
+ _get_global_codex_home,
17
+ _get_session_auth_home,
18
+ _local_now_iso,
19
+ _session_runtime,
20
+ list_sessions,
21
+ )
22
+ from .status_source import find_latest_status_artifact
23
+
24
+ STATUS_CACHE_TTL_SECONDS = 60
25
+
26
+ CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
27
+
28
+ CODEX_STATUS_CACHE_TTL_SECONDS = 5 * 60
29
+
30
+ MAX_STATUS_WORKERS = 8
31
+
32
+ def _to_local_iso(value):
33
+ if not value:
34
+ return value
35
+ try:
36
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
37
+ except (TypeError, ValueError):
38
+ return value
39
+ if parsed.tzinfo is None:
40
+ parsed = parsed.replace(tzinfo=timezone.utc)
41
+ return parsed.astimezone().isoformat()
42
+
43
+ def _row_priority(session):
44
+ launch = session.get("launch") or {}
45
+ try:
46
+ return int(launch.get("priority") or 0)
47
+ except (TypeError, ValueError):
48
+ return 0
49
+
50
+ def _row_reasoning_effort(session):
51
+ launch = session.get("launch") or {}
52
+ return (
53
+ launch.get("reasoning_effort")
54
+ or launch.get("reasoningEffort")
55
+ or launch.get("power")
56
+ or ("low" if launch.get("fast") is True and launch.get("fastMode") != "service_tier" else None)
57
+ or "low"
58
+ )
59
+
60
+ def _normalize_pct_value(value):
61
+ if value is None:
62
+ return None
63
+ try:
64
+ return max(0, min(100, round(float(value))))
65
+ except (TypeError, ValueError):
66
+ return None
67
+
68
+ def _normalize_status_payload(payload=None):
69
+ if payload is None:
70
+ payload = {}
71
+ now = _local_now_iso()
72
+ return {
73
+ "usage_pct": _normalize_pct_value(payload.get("usage_pct")),
74
+ "remaining_5h_pct": _normalize_pct_value(payload.get("remaining_5h_pct")),
75
+ "remaining_week_pct": _normalize_pct_value(payload.get("remaining_week_pct")),
76
+ "credits": payload.get("credits"),
77
+ "reset_credits_available": payload.get("reset_credits_available"),
78
+ "reset_credits": payload.get("reset_credits"),
79
+ "reset_5h_at": payload.get("reset_5h_at"),
80
+ "reset_week_at": payload.get("reset_week_at"),
81
+ "reset_at": payload.get("reset_at") or payload.get("reset_week_at") or payload.get("reset_5h_at"),
82
+ "updated_at": _to_local_iso(payload.get("updated_at") or payload.get("captured_at") or now),
83
+ "raw_status_text": payload.get("raw_status_text"),
84
+ "source_ref": payload.get("source_ref"),
85
+ "structured": bool(payload.get("structured")),
86
+ }
87
+
88
+ def _parse_status_timestamp(value):
89
+ if not value:
90
+ return None
91
+ try:
92
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
93
+ except (TypeError, ValueError):
94
+ return None
95
+
96
+ def _status_cache_ttl_seconds(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
97
+ if ttl_seconds == STATUS_CACHE_TTL_SECONDS:
98
+ if session.get("provider") == PROVIDER_CLAUDE:
99
+ return CLAUDE_STATUS_CACHE_TTL_SECONDS
100
+ if session.get("provider") == PROVIDER_CODEX:
101
+ return CODEX_STATUS_CACHE_TTL_SECONDS
102
+ return ttl_seconds
103
+
104
+ def _is_status_cache_fresh(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
105
+ status = session.get("lastStatus") or {}
106
+ if _is_low_confidence_status_source(status):
107
+ return False
108
+ updated_at = _parse_status_timestamp(status.get("updated_at") or session.get("lastStatusAt"))
109
+ if not updated_at:
110
+ return False
111
+ now = datetime.now(timezone.utc).astimezone()
112
+ ttl_seconds = _status_cache_ttl_seconds(session, ttl_seconds)
113
+ return (now - updated_at.astimezone(now.tzinfo)).total_seconds() < ttl_seconds
114
+
115
+ def _is_status_newer(candidate, current):
116
+ if not candidate:
117
+ return False
118
+ if not current:
119
+ return True
120
+ candidate_at = _parse_status_timestamp(candidate.get("updated_at"))
121
+ current_at = _parse_status_timestamp(current.get("updated_at"))
122
+ if candidate_at and current_at:
123
+ return candidate_at > current_at
124
+ if candidate_at:
125
+ return True
126
+ return False
127
+
128
+ def _status_has_more_detail(candidate, current):
129
+ if not candidate:
130
+ return False
131
+ if not current:
132
+ return True
133
+
134
+ fields = [
135
+ "usage_pct",
136
+ "remaining_5h_pct",
137
+ "remaining_week_pct",
138
+ "credits",
139
+ "reset_credits_available",
140
+ "reset_credits",
141
+ "reset_5h_at",
142
+ "reset_week_at",
143
+ "reset_at",
144
+ "raw_status_text",
145
+ "source_ref",
146
+ ]
147
+ return any(current.get(field) is None and candidate.get(field) is not None for field in fields)
148
+
149
+ def _later_status_timestamp(current_at, candidate_at):
150
+ if not candidate_at:
151
+ return current_at
152
+ if not current_at:
153
+ return candidate_at
154
+ parsed_current = _parse_status_timestamp(current_at)
155
+ parsed_candidate = _parse_status_timestamp(candidate_at)
156
+ if not parsed_current or not parsed_candidate:
157
+ return current_at
158
+ return candidate_at if parsed_candidate > parsed_current else current_at
159
+
160
+
161
+ def _merge_status_payload(current, candidate):
162
+ if not current:
163
+ return candidate
164
+ if not candidate:
165
+ return current
166
+
167
+ merged = dict(current)
168
+ for field in [
169
+ "usage_pct",
170
+ "remaining_5h_pct",
171
+ "remaining_week_pct",
172
+ "credits",
173
+ "reset_credits_available",
174
+ "reset_credits",
175
+ "reset_5h_at",
176
+ "reset_week_at",
177
+ "reset_at",
178
+ "raw_status_text",
179
+ "source_ref",
180
+ ]:
181
+ if merged.get(field) is None and candidate.get(field) is not None:
182
+ merged[field] = candidate[field]
183
+
184
+ # The structured marker qualifies the source_ref; keep them in sync when
185
+ # the merge adopts the candidate's ref, else a structured rollout ref
186
+ # would read as low-confidence scraped text.
187
+ if merged.get("source_ref") is not None and merged["source_ref"] == candidate.get("source_ref"):
188
+ merged["structured"] = bool(candidate.get("structured"))
189
+
190
+ # The merge is only reached when the candidate is not newer, so adopting its
191
+ # timestamp would date the record by its stalest contributor. That is not
192
+ # cosmetic: updated_at drives the freshness check, so a regressed stamp
193
+ # makes a just-resolved status read as expired and re-probes every call.
194
+ merged["updated_at"] = _later_status_timestamp(current.get("updated_at"), candidate.get("updated_at"))
195
+ return merged
196
+
197
+ def _compute_available_pct(status):
198
+ if not status:
199
+ return None
200
+ values = [
201
+ _normalize_pct_value(status.get("remaining_5h_pct")),
202
+ _normalize_pct_value(status.get("remaining_week_pct")),
203
+ ]
204
+ values = [value for value in values if value is not None]
205
+ if not values:
206
+ return None
207
+ return min(values)
208
+
209
+ def _is_low_confidence_status_source(status):
210
+ if not status:
211
+ return False
212
+ if status.get("structured"):
213
+ # Exact rate_limits API data: trustworthy even when it comes from a
214
+ # sessions/rollout path, which otherwise flags scraped-text noise.
215
+ return False
216
+ source_ref = str(status.get("source_ref") or "").replace(os.sep, "/")
217
+ return "/sessions/" in source_ref and "/rollout" in source_ref
218
+
219
+ def _read_expected_account_email(auth_home):
220
+ auth_path = os.path.join(auth_home, "auth.json")
221
+ try:
222
+ with open(auth_path, encoding="utf-8") as handle:
223
+ auth = json.load(handle)
224
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
225
+ return None
226
+
227
+ tokens = auth.get("tokens") or {}
228
+ for token_name in ("id_token", "access_token"):
229
+ claims = _decode_jwt_claims(tokens.get(token_name))
230
+ email = claims.get("email")
231
+ if not email and token_name == "access_token":
232
+ profile = claims.get("https://api.openai.com/profile") or {}
233
+ email = profile.get("email")
234
+ if email:
235
+ return str(email).strip().lower()
236
+ return None
237
+
238
+ def _status_cache_hit(s, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS, cache_only=False):
239
+ return (
240
+ cache_only
241
+ or s.get("enabled", True) is False
242
+ or (
243
+ s.get("lastStatus")
244
+ and not force_refresh
245
+ and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
246
+ )
247
+ )
248
+
249
+ def record_status(store, name, payload):
250
+ normalized = _normalize_status_payload(payload)
251
+ updated = store["update_session"](name, lambda s: {
252
+ **s,
253
+ "lastStatus": normalized,
254
+ "lastStatusAt": normalized["updated_at"],
255
+ })
256
+ if not updated:
257
+ raise CdxError(f"Unknown session: {name}")
258
+ return updated
259
+
260
+ def _fetch_codex_status(codex_status_fetcher, custom_codex_status_fetcher, default_status_timeout_seconds, session, timeout_seconds=None):
261
+ if custom_codex_status_fetcher:
262
+ return custom_codex_status_fetcher(session)
263
+ timeout = timeout_seconds or default_status_timeout_seconds
264
+ return codex_status_fetcher(session, timeout=timeout)
265
+
266
+ def _status_row_from_session(store, s):
267
+ status = s.get("lastStatus")
268
+ enabled = s.get("enabled", True) is not False
269
+ row_status = status if enabled else None
270
+ return {
271
+ "session_name": s["name"],
272
+ "label": s.get("label"),
273
+ "provider": s["provider"],
274
+ "enabled": enabled,
275
+ # Carried so the ranking can honor the user's `--priority` and the
276
+ # configured effort wherever it runs. Without these on the row, the
277
+ # recommendation path could not see either, which is why `cdx next`
278
+ # ignored `--priority` entirely.
279
+ "priority": _row_priority(s),
280
+ "reasoning_effort": _row_reasoning_effort(s),
281
+ "active": bool(_session_runtime(store, s["name"])) if enabled else False,
282
+ "status": "enabled" if enabled else "disabled",
283
+ "auth_status": (s.get("auth") or {}).get("status") or "unknown",
284
+ "auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
285
+ "remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
286
+ "remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
287
+ "credits": row_status.get("credits") if row_status else None,
288
+ "reset_credits_available": row_status.get("reset_credits_available") if row_status else None,
289
+ "reset_credits": row_status.get("reset_credits") if row_status else None,
290
+ "available_pct": _compute_available_pct(row_status),
291
+ "reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
292
+ "reset_week_at": row_status.get("reset_week_at") if row_status else None,
293
+ "reset_at": row_status.get("reset_at") if row_status else None,
294
+ "updated_at": _to_local_iso(s.get("lastStatusAt")),
295
+ "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
296
+ }
297
+
298
+ def format_list_rows(store):
299
+ sessions = list_sessions(store)
300
+ providers = {s["provider"] for s in sessions}
301
+ has_multiple = len(providers) > 1
302
+ sessions = sorted(
303
+ sessions,
304
+ key=lambda s: (
305
+ 1 if s.get("enabled", True) is False else 0,
306
+ s.get("name", ""),
307
+ ),
308
+ )
309
+ return [{
310
+ "name": s["name"],
311
+ "label": s.get("label"),
312
+ "provider": s["provider"] if has_multiple else None,
313
+ "enabled": s.get("enabled", True) is not False,
314
+ "active": bool(_session_runtime(store, s["name"])) if s.get("enabled", True) is not False else False,
315
+ "enabled_status": "disabled" if s.get("enabled", True) is False else "enabled",
316
+ "status": s.get("lastStatus"),
317
+ "launch": s.get("launch") or {},
318
+ "updated_at": _to_local_iso(s.get("updatedAt")),
319
+ } for s in sessions]
320
+
321
+ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
322
+ force_refresh=False,
323
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
324
+ cache_only=False,
325
+ status_timeout_seconds=None,):
326
+ current_status = session.get("lastStatus")
327
+ if session.get("enabled", True) is False:
328
+ return current_status
329
+ if cache_only:
330
+ return current_status
331
+ if current_status and not force_refresh and _is_status_cache_fresh(session, ttl_seconds=cache_ttl_seconds):
332
+ return current_status
333
+ source_root = session.get("authHome") or _get_session_auth_home(base_dir,
334
+ session["name"], session["provider"]
335
+ )
336
+ if session["provider"] == PROVIDER_CODEX and fetch_codex_status:
337
+ live_status = fetch_codex_status(
338
+ {**session, "authHome": source_root},
339
+ timeout_seconds=status_timeout_seconds,
340
+ )
341
+ if live_status:
342
+ record_status(store, session["name"], live_status)
343
+ return live_status
344
+
345
+ expected_account_email = (
346
+ _read_expected_account_email(source_root)
347
+ if session["provider"] == PROVIDER_CODEX
348
+ else None
349
+ )
350
+ artifact = find_latest_status_artifact(
351
+ source_root,
352
+ session["provider"],
353
+ expected_account_email=expected_account_email,
354
+ )
355
+ if (
356
+ session["provider"] == PROVIDER_CODEX
357
+ and not artifact
358
+ and os.path.abspath(base_dir) == os.path.abspath(get_cdx_home(env))
359
+ ):
360
+ global_root = _get_global_codex_home(env)
361
+ if global_root and os.path.abspath(global_root) != os.path.abspath(source_root):
362
+ artifact = find_latest_status_artifact(
363
+ global_root,
364
+ session["provider"],
365
+ expected_account_email=expected_account_email,
366
+ # The shared codex home can hold other accounts' rollouts;
367
+ # structured payloads there cannot be attributed to ours.
368
+ trust_unattributed_structured=False,
369
+ )
370
+ if not artifact:
371
+ if _is_low_confidence_status_source(current_status):
372
+ return None
373
+ return current_status
374
+ resolved = _normalize_status_payload({
375
+ "usage_pct": artifact.get("usage_pct"),
376
+ "remaining_5h_pct": artifact.get("remaining_5h_pct"),
377
+ "remaining_week_pct": artifact.get("remaining_week_pct"),
378
+ "credits": artifact.get("credits"),
379
+ "reset_5h_at": artifact.get("reset_5h_at"),
380
+ "reset_week_at": artifact.get("reset_week_at"),
381
+ "reset_at": artifact.get("reset_at"),
382
+ "updated_at": artifact.get("updated_at"),
383
+ "raw_status_text": artifact.get("raw_status_text"),
384
+ "source_ref": artifact.get("source_ref"),
385
+ "structured": artifact.get("structured"),
386
+ })
387
+ if _is_low_confidence_status_source(current_status) and not _is_low_confidence_status_source(resolved):
388
+ record_status(store, session["name"], resolved)
389
+ return resolved
390
+ if _is_status_newer(resolved, current_status):
391
+ record_status(store, session["name"], resolved)
392
+ return resolved
393
+ if _status_has_more_detail(resolved, current_status):
394
+ merged = _merge_status_payload(current_status, resolved)
395
+ record_status(store, session["name"], merged)
396
+ return merged
397
+ return current_status or resolved
398
+
399
+ def _resolve_row_session(store, base_dir, env, fetch_codex_status, s,
400
+ force_refresh=False,
401
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
402
+ cache_only=False,
403
+ status_timeout_seconds=None,):
404
+ status = _resolve_session_status(store, base_dir, env, fetch_codex_status,
405
+ s,
406
+ force_refresh=force_refresh,
407
+ cache_ttl_seconds=cache_ttl_seconds,
408
+ cache_only=cache_only,
409
+ status_timeout_seconds=status_timeout_seconds,
410
+ )
411
+ return {
412
+ **s,
413
+ "lastStatus": status,
414
+ "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
415
+ }
416
+
417
+ def get_status_row(store, base_dir, env, fetch_codex_status, name,
418
+ progress_callback=None,
419
+ force_refresh=False,
420
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
421
+ cache_only=False,
422
+ status_timeout_seconds=None,):
423
+ session = store["get_session"](name)
424
+ if not session:
425
+ raise CdxError(f"Unknown session: {name}")
426
+ cache_hit = _status_cache_hit(
427
+ session,
428
+ force_refresh=force_refresh,
429
+ cache_ttl_seconds=cache_ttl_seconds,
430
+ cache_only=cache_only,
431
+ )
432
+ if progress_callback:
433
+ progress_callback({
434
+ "event": "status_started",
435
+ "session_count": 1,
436
+ "check_count": 0 if cache_hit else 1,
437
+ })
438
+ if not cache_hit:
439
+ progress_callback({
440
+ "event": "session_started",
441
+ "session_name": session["name"],
442
+ "provider": session["provider"],
443
+ })
444
+ resolved = _resolve_row_session(store, base_dir, env, fetch_codex_status,
445
+ session,
446
+ force_refresh=force_refresh,
447
+ cache_ttl_seconds=cache_ttl_seconds,
448
+ cache_only=cache_only,
449
+ status_timeout_seconds=status_timeout_seconds,
450
+ )
451
+ if progress_callback:
452
+ progress_callback({
453
+ "event": "session_finished",
454
+ "session_name": session["name"],
455
+ "has_status": bool(resolved.get("lastStatus")),
456
+ "cache_hit": cache_hit,
457
+ })
458
+ progress_callback({
459
+ "event": "status_finished",
460
+ "row_count": 1,
461
+ })
462
+ return _status_row_from_session(store, resolved)
463
+
464
+ def get_status_rows(store, base_dir, env, fetch_codex_status, progress_callback=None,
465
+ force_refresh=False,
466
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
467
+ cache_only=False,
468
+ status_timeout_seconds=None,):
469
+ sessions = list_sessions(store)
470
+
471
+ cache_hits = {
472
+ s["name"]: _status_cache_hit(
473
+ s,
474
+ force_refresh=force_refresh,
475
+ cache_ttl_seconds=cache_ttl_seconds,
476
+ cache_only=cache_only,
477
+ )
478
+ for s in sessions
479
+ }
480
+ if progress_callback:
481
+ progress_callback({
482
+ "event": "status_started",
483
+ "session_count": len(sessions),
484
+ "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
485
+ })
486
+
487
+ resolved_by_name = {}
488
+ if sessions:
489
+ max_workers = min(MAX_STATUS_WORKERS, len(sessions))
490
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
491
+ futures = {}
492
+ for s in sessions:
493
+ cache_hit = cache_hits[s["name"]]
494
+ if progress_callback and not cache_hit:
495
+ progress_callback({
496
+ "event": "session_started",
497
+ "session_name": s["name"],
498
+ "provider": s["provider"],
499
+ })
500
+ futures[executor.submit(
501
+ _resolve_row_session,
502
+ store,
503
+ base_dir,
504
+ env,
505
+ fetch_codex_status,
506
+ s,
507
+ force_refresh=force_refresh,
508
+ cache_ttl_seconds=cache_ttl_seconds,
509
+ cache_only=cache_only,
510
+ status_timeout_seconds=status_timeout_seconds,
511
+ )] = s
512
+ for future in as_completed(futures):
513
+ s = futures[future]
514
+ try:
515
+ resolved = future.result()
516
+ except CdxError as error:
517
+ if not str(error).startswith("Unknown session:"):
518
+ raise
519
+ continue
520
+ resolved_by_name[s["name"]] = resolved
521
+ if progress_callback:
522
+ progress_callback({
523
+ "event": "session_finished",
524
+ "session_name": s["name"],
525
+ "has_status": bool(resolved.get("lastStatus")),
526
+ "cache_hit": cache_hits[s["name"]],
527
+ })
528
+ resolved = [resolved_by_name[s["name"]] for s in sessions if s["name"] in resolved_by_name]
529
+
530
+ def sort_key(s):
531
+ at = s.get("lastStatusAt") or ""
532
+ disabled_rank = 1 if s.get("enabled", True) is False else 0
533
+ return (disabled_rank, "" if at else "\xff", at, s["name"])
534
+
535
+ resolved.sort(key=sort_key)
536
+ enabled = [s for s in resolved if s.get("enabled", True) is not False]
537
+ disabled = [s for s in resolved if s.get("enabled", True) is False]
538
+ enabled.reverse()
539
+ disabled.sort(key=lambda s: s["name"])
540
+ resolved = enabled + disabled
541
+
542
+ rows = []
543
+ for s in resolved:
544
+ rows.append(_status_row_from_session(store, s))
545
+ if progress_callback:
546
+ progress_callback({
547
+ "event": "status_finished",
548
+ "row_count": len(rows),
549
+ })
550
+ return rows
@@ -597,6 +597,51 @@ def extract_named_statuses_from_text(text):
597
597
  }
598
598
 
599
599
 
600
+ _ROLLOUT_NAME = re.compile(r"^rollout-.*-([0-9a-fA-F-]{36})\.jsonl$")
601
+
602
+
603
+ def find_latest_codex_conversation_id(root_dir):
604
+ """The conversation id of the newest rollout under a Codex home, or None.
605
+
606
+ Codex exposes no flag that imposes a conversation id, so the only way to
607
+ learn one is to read it back after the run. Each rollout opens with a
608
+ session-meta record carrying `payload.session_id`, and the filename repeats
609
+ it; the record is preferred and the filename is the fallback for a file
610
+ whose first line is truncated.
611
+
612
+ Returns None rather than guessing whenever the newest rollout yields
613
+ nothing usable: a session left on the recency path behaves as it does
614
+ today, while a wrong id would resume somebody else's conversation.
615
+ """
616
+ sessions_dir = os.path.join(root_dir, "sessions")
617
+ if not os.path.isdir(sessions_dir):
618
+ return None
619
+ newest = None
620
+ for dirpath, _dirnames, filenames in os.walk(sessions_dir):
621
+ for fname in filenames:
622
+ if not _ROLLOUT_NAME.match(fname):
623
+ continue
624
+ fp = os.path.join(dirpath, fname)
625
+ stat = _safe_stat(fp)
626
+ if stat and (newest is None or stat.st_mtime > newest[0]):
627
+ newest = (stat.st_mtime, fp)
628
+ if not newest:
629
+ return None
630
+
631
+ path = newest[1]
632
+ try:
633
+ with open(path, encoding="utf-8", errors="replace") as handle:
634
+ record = json.loads(handle.readline())
635
+ identifier = (record.get("payload") or {}).get("session_id")
636
+ if isinstance(identifier, str) and identifier.strip():
637
+ return identifier.strip()
638
+ except (OSError, ValueError):
639
+ pass
640
+
641
+ match = _ROLLOUT_NAME.match(os.path.basename(path))
642
+ return match.group(1) if match else None
643
+
644
+
600
645
  def _collect_candidate_files(root_dir):
601
646
  priority_candidates = []
602
647
  history_candidates = []
@@ -9,6 +9,7 @@ from .cli_render import (
9
9
  _style,
10
10
  _style_pct,
11
11
  )
12
+ from .session_ranking import rank_sessions
12
13
 
13
14
  RESET_COUNTDOWN_SAFETY_SECONDS = 60
14
15
  PRIORITY_EMPTY_AVAILABLE_THRESHOLD = 5
@@ -140,13 +141,17 @@ def _format_status_rows(rows, use_color=False, small=False):
140
141
  _style(_format_relative_age(r.get("updated_at")), "2", use_color),
141
142
  ]
142
143
  table_rows.append(base)
144
+ # "Recommended", not "Priority": the `--priority` setting is one input to
145
+ # this ranking among several, and labelling the whole recommendation with
146
+ # its name made users tune `--priority` and conclude it did nothing when
147
+ # availability or resets had decided instead.
143
148
  priority_line = (
144
- f"Priority: {_priority_instruction(priority[0], 'first')}"
149
+ f"Recommended: {_priority_instruction(priority[0], 'first')}"
145
150
  + (
146
151
  f", {_priority_instruction(priority[1], 'next')}."
147
152
  if len(priority) > 1 else "."
148
153
  )
149
- ) if priority else "Priority: no usable session status yet."
154
+ ) if priority else "Recommended: no usable session status yet."
150
155
  current_line = _format_current_session_line(rows)
151
156
  return "\n".join([
152
157
  _pad_table([headers] + table_rows),
@@ -187,44 +192,20 @@ def _format_auth_status(row):
187
192
 
188
193
 
189
194
  def recommend_priority_rows(rows):
190
- active_rows = [r for r in rows if r.get("enabled", True) is not False]
191
- priority_candidates = [
192
- r for r in active_rows
193
- if _format_auth_status(r) != "logged out"
194
- ]
195
- return _recommend_priority_sessions(priority_candidates)
195
+ """Sessions to recommend, best first.
196
+
197
+ Delegates to the one ranking every selector shares, so `cdx next`, `cdx
198
+ status`, and `cdx ready` cannot disagree with `cdx select` and `cdx run
199
+ --provider` about which session is best.
200
+ """
201
+ ordered, _decision = rank_sessions(rows, _now_timestamp(), _priority_reset_timestamp)
202
+ return ordered
196
203
 
197
204
 
198
205
  def format_priority_instruction(row, position="first"):
199
206
  return _priority_instruction(row, position)
200
207
 
201
208
 
202
- def _recommend_priority_sessions(rows):
203
- if not rows:
204
- return []
205
-
206
- def rank(row):
207
- has_credits = row.get("credits") is not None
208
- credit_rank = 0 if has_credits else 1
209
- available = row.get("available_pct")
210
- usable_now = available is not None and available > PRIORITY_EMPTY_AVAILABLE_THRESHOLD
211
- known_available = available is not None
212
- reset_timestamp = _priority_reset_timestamp(row)
213
- reset_is_future = reset_timestamp is not None and reset_timestamp >= _now_timestamp()
214
- blocked_future = not usable_now and reset_is_future
215
- reset_is_known = reset_timestamp is not None
216
- reset_rank = -reset_timestamp if reset_is_known else float("-inf")
217
- available_rank = available if available is not None else -1
218
- name_rank = row.get("session_name") or ""
219
- if usable_now:
220
- return (3, credit_rank, 1 if known_available else 0, available_rank, reset_rank, name_rank)
221
- if blocked_future:
222
- return (2, 1 if reset_is_known else 0, reset_rank, credit_rank, available_rank, name_rank)
223
- if reset_is_known:
224
- return (1, reset_rank, credit_rank, 1 if known_available else 0, available_rank, name_rank)
225
- return (0, credit_rank, 1 if known_available else 0, available_rank, name_rank)
226
-
227
- return sorted(rows, key=rank, reverse=True)
228
209
 
229
210
 
230
211
  def _format_blocking_quota(row):