cdx-manager 0.12.3 → 0.14.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,534 @@
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 _merge_status_payload(current, candidate):
150
+ if not current:
151
+ return candidate
152
+ if not candidate:
153
+ return current
154
+
155
+ merged = dict(current)
156
+ for field in [
157
+ "usage_pct",
158
+ "remaining_5h_pct",
159
+ "remaining_week_pct",
160
+ "credits",
161
+ "reset_credits_available",
162
+ "reset_credits",
163
+ "reset_5h_at",
164
+ "reset_week_at",
165
+ "reset_at",
166
+ "raw_status_text",
167
+ "source_ref",
168
+ ]:
169
+ if merged.get(field) is None and candidate.get(field) is not None:
170
+ merged[field] = candidate[field]
171
+
172
+ # The structured marker qualifies the source_ref; keep them in sync when
173
+ # the merge adopts the candidate's ref, else a structured rollout ref
174
+ # would read as low-confidence scraped text.
175
+ if merged.get("source_ref") is not None and merged["source_ref"] == candidate.get("source_ref"):
176
+ merged["structured"] = bool(candidate.get("structured"))
177
+
178
+ merged["updated_at"] = candidate.get("updated_at") or current.get("updated_at")
179
+ return merged
180
+
181
+ def _compute_available_pct(status):
182
+ if not status:
183
+ return None
184
+ values = [
185
+ _normalize_pct_value(status.get("remaining_5h_pct")),
186
+ _normalize_pct_value(status.get("remaining_week_pct")),
187
+ ]
188
+ values = [value for value in values if value is not None]
189
+ if not values:
190
+ return None
191
+ return min(values)
192
+
193
+ def _is_low_confidence_status_source(status):
194
+ if not status:
195
+ return False
196
+ if status.get("structured"):
197
+ # Exact rate_limits API data: trustworthy even when it comes from a
198
+ # sessions/rollout path, which otherwise flags scraped-text noise.
199
+ return False
200
+ source_ref = str(status.get("source_ref") or "").replace(os.sep, "/")
201
+ return "/sessions/" in source_ref and "/rollout" in source_ref
202
+
203
+ def _read_expected_account_email(auth_home):
204
+ auth_path = os.path.join(auth_home, "auth.json")
205
+ try:
206
+ with open(auth_path, encoding="utf-8") as handle:
207
+ auth = json.load(handle)
208
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
209
+ return None
210
+
211
+ tokens = auth.get("tokens") or {}
212
+ for token_name in ("id_token", "access_token"):
213
+ claims = _decode_jwt_claims(tokens.get(token_name))
214
+ email = claims.get("email")
215
+ if not email and token_name == "access_token":
216
+ profile = claims.get("https://api.openai.com/profile") or {}
217
+ email = profile.get("email")
218
+ if email:
219
+ return str(email).strip().lower()
220
+ return None
221
+
222
+ def _status_cache_hit(s, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS, cache_only=False):
223
+ return (
224
+ cache_only
225
+ or s.get("enabled", True) is False
226
+ or (
227
+ s.get("lastStatus")
228
+ and not force_refresh
229
+ and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
230
+ )
231
+ )
232
+
233
+ def record_status(store, name, payload):
234
+ normalized = _normalize_status_payload(payload)
235
+ updated = store["update_session"](name, lambda s: {
236
+ **s,
237
+ "lastStatus": normalized,
238
+ "lastStatusAt": normalized["updated_at"],
239
+ })
240
+ if not updated:
241
+ raise CdxError(f"Unknown session: {name}")
242
+ return updated
243
+
244
+ def _fetch_codex_status(codex_status_fetcher, custom_codex_status_fetcher, default_status_timeout_seconds, session, timeout_seconds=None):
245
+ if custom_codex_status_fetcher:
246
+ return custom_codex_status_fetcher(session)
247
+ timeout = timeout_seconds or default_status_timeout_seconds
248
+ return codex_status_fetcher(session, timeout=timeout)
249
+
250
+ def _status_row_from_session(store, s):
251
+ status = s.get("lastStatus")
252
+ enabled = s.get("enabled", True) is not False
253
+ row_status = status if enabled else None
254
+ return {
255
+ "session_name": s["name"],
256
+ "label": s.get("label"),
257
+ "provider": s["provider"],
258
+ "enabled": enabled,
259
+ # Carried so the ranking can honor the user's `--priority` and the
260
+ # configured effort wherever it runs. Without these on the row, the
261
+ # recommendation path could not see either, which is why `cdx next`
262
+ # ignored `--priority` entirely.
263
+ "priority": _row_priority(s),
264
+ "reasoning_effort": _row_reasoning_effort(s),
265
+ "active": bool(_session_runtime(store, s["name"])) if enabled else False,
266
+ "status": "enabled" if enabled else "disabled",
267
+ "auth_status": (s.get("auth") or {}).get("status") or "unknown",
268
+ "auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
269
+ "remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
270
+ "remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
271
+ "credits": row_status.get("credits") if row_status else None,
272
+ "reset_credits_available": row_status.get("reset_credits_available") if row_status else None,
273
+ "reset_credits": row_status.get("reset_credits") if row_status else None,
274
+ "available_pct": _compute_available_pct(row_status),
275
+ "reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
276
+ "reset_week_at": row_status.get("reset_week_at") if row_status else None,
277
+ "reset_at": row_status.get("reset_at") if row_status else None,
278
+ "updated_at": _to_local_iso(s.get("lastStatusAt")),
279
+ "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
280
+ }
281
+
282
+ def format_list_rows(store):
283
+ sessions = list_sessions(store)
284
+ providers = {s["provider"] for s in sessions}
285
+ has_multiple = len(providers) > 1
286
+ sessions = sorted(
287
+ sessions,
288
+ key=lambda s: (
289
+ 1 if s.get("enabled", True) is False else 0,
290
+ s.get("name", ""),
291
+ ),
292
+ )
293
+ return [{
294
+ "name": s["name"],
295
+ "label": s.get("label"),
296
+ "provider": s["provider"] if has_multiple else None,
297
+ "enabled": s.get("enabled", True) is not False,
298
+ "active": bool(_session_runtime(store, s["name"])) if s.get("enabled", True) is not False else False,
299
+ "enabled_status": "disabled" if s.get("enabled", True) is False else "enabled",
300
+ "status": s.get("lastStatus"),
301
+ "launch": s.get("launch") or {},
302
+ "updated_at": _to_local_iso(s.get("updatedAt")),
303
+ } for s in sessions]
304
+
305
+ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
306
+ force_refresh=False,
307
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
308
+ cache_only=False,
309
+ status_timeout_seconds=None,):
310
+ current_status = session.get("lastStatus")
311
+ if session.get("enabled", True) is False:
312
+ return current_status
313
+ if cache_only:
314
+ return current_status
315
+ if current_status and not force_refresh and _is_status_cache_fresh(session, ttl_seconds=cache_ttl_seconds):
316
+ return current_status
317
+ source_root = session.get("authHome") or _get_session_auth_home(base_dir,
318
+ session["name"], session["provider"]
319
+ )
320
+ if session["provider"] == PROVIDER_CODEX and fetch_codex_status:
321
+ live_status = fetch_codex_status(
322
+ {**session, "authHome": source_root},
323
+ timeout_seconds=status_timeout_seconds,
324
+ )
325
+ if live_status:
326
+ record_status(store, session["name"], live_status)
327
+ return live_status
328
+
329
+ expected_account_email = (
330
+ _read_expected_account_email(source_root)
331
+ if session["provider"] == PROVIDER_CODEX
332
+ else None
333
+ )
334
+ artifact = find_latest_status_artifact(
335
+ source_root,
336
+ session["provider"],
337
+ expected_account_email=expected_account_email,
338
+ )
339
+ if (
340
+ session["provider"] == PROVIDER_CODEX
341
+ and not artifact
342
+ and os.path.abspath(base_dir) == os.path.abspath(get_cdx_home(env))
343
+ ):
344
+ global_root = _get_global_codex_home(env)
345
+ if global_root and os.path.abspath(global_root) != os.path.abspath(source_root):
346
+ artifact = find_latest_status_artifact(
347
+ global_root,
348
+ session["provider"],
349
+ expected_account_email=expected_account_email,
350
+ # The shared codex home can hold other accounts' rollouts;
351
+ # structured payloads there cannot be attributed to ours.
352
+ trust_unattributed_structured=False,
353
+ )
354
+ if not artifact:
355
+ if _is_low_confidence_status_source(current_status):
356
+ return None
357
+ return current_status
358
+ resolved = _normalize_status_payload({
359
+ "usage_pct": artifact.get("usage_pct"),
360
+ "remaining_5h_pct": artifact.get("remaining_5h_pct"),
361
+ "remaining_week_pct": artifact.get("remaining_week_pct"),
362
+ "credits": artifact.get("credits"),
363
+ "reset_5h_at": artifact.get("reset_5h_at"),
364
+ "reset_week_at": artifact.get("reset_week_at"),
365
+ "reset_at": artifact.get("reset_at"),
366
+ "updated_at": artifact.get("updated_at"),
367
+ "raw_status_text": artifact.get("raw_status_text"),
368
+ "source_ref": artifact.get("source_ref"),
369
+ "structured": artifact.get("structured"),
370
+ })
371
+ if _is_low_confidence_status_source(current_status) and not _is_low_confidence_status_source(resolved):
372
+ record_status(store, session["name"], resolved)
373
+ return resolved
374
+ if _is_status_newer(resolved, current_status):
375
+ record_status(store, session["name"], resolved)
376
+ return resolved
377
+ if _status_has_more_detail(resolved, current_status):
378
+ merged = _merge_status_payload(current_status, resolved)
379
+ record_status(store, session["name"], merged)
380
+ return merged
381
+ return current_status or resolved
382
+
383
+ def _resolve_row_session(store, base_dir, env, fetch_codex_status, s,
384
+ force_refresh=False,
385
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
386
+ cache_only=False,
387
+ status_timeout_seconds=None,):
388
+ status = _resolve_session_status(store, base_dir, env, fetch_codex_status,
389
+ s,
390
+ force_refresh=force_refresh,
391
+ cache_ttl_seconds=cache_ttl_seconds,
392
+ cache_only=cache_only,
393
+ status_timeout_seconds=status_timeout_seconds,
394
+ )
395
+ return {
396
+ **s,
397
+ "lastStatus": status,
398
+ "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
399
+ }
400
+
401
+ def get_status_row(store, base_dir, env, fetch_codex_status, name,
402
+ progress_callback=None,
403
+ force_refresh=False,
404
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
405
+ cache_only=False,
406
+ status_timeout_seconds=None,):
407
+ session = store["get_session"](name)
408
+ if not session:
409
+ raise CdxError(f"Unknown session: {name}")
410
+ cache_hit = _status_cache_hit(
411
+ session,
412
+ force_refresh=force_refresh,
413
+ cache_ttl_seconds=cache_ttl_seconds,
414
+ cache_only=cache_only,
415
+ )
416
+ if progress_callback:
417
+ progress_callback({
418
+ "event": "status_started",
419
+ "session_count": 1,
420
+ "check_count": 0 if cache_hit else 1,
421
+ })
422
+ if not cache_hit:
423
+ progress_callback({
424
+ "event": "session_started",
425
+ "session_name": session["name"],
426
+ "provider": session["provider"],
427
+ })
428
+ resolved = _resolve_row_session(store, base_dir, env, fetch_codex_status,
429
+ session,
430
+ force_refresh=force_refresh,
431
+ cache_ttl_seconds=cache_ttl_seconds,
432
+ cache_only=cache_only,
433
+ status_timeout_seconds=status_timeout_seconds,
434
+ )
435
+ if progress_callback:
436
+ progress_callback({
437
+ "event": "session_finished",
438
+ "session_name": session["name"],
439
+ "has_status": bool(resolved.get("lastStatus")),
440
+ "cache_hit": cache_hit,
441
+ })
442
+ progress_callback({
443
+ "event": "status_finished",
444
+ "row_count": 1,
445
+ })
446
+ return _status_row_from_session(store, resolved)
447
+
448
+ def get_status_rows(store, base_dir, env, fetch_codex_status, progress_callback=None,
449
+ force_refresh=False,
450
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
451
+ cache_only=False,
452
+ status_timeout_seconds=None,):
453
+ sessions = list_sessions(store)
454
+
455
+ cache_hits = {
456
+ s["name"]: _status_cache_hit(
457
+ s,
458
+ force_refresh=force_refresh,
459
+ cache_ttl_seconds=cache_ttl_seconds,
460
+ cache_only=cache_only,
461
+ )
462
+ for s in sessions
463
+ }
464
+ if progress_callback:
465
+ progress_callback({
466
+ "event": "status_started",
467
+ "session_count": len(sessions),
468
+ "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
469
+ })
470
+
471
+ resolved_by_name = {}
472
+ if sessions:
473
+ max_workers = min(MAX_STATUS_WORKERS, len(sessions))
474
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
475
+ futures = {}
476
+ for s in sessions:
477
+ cache_hit = cache_hits[s["name"]]
478
+ if progress_callback and not cache_hit:
479
+ progress_callback({
480
+ "event": "session_started",
481
+ "session_name": s["name"],
482
+ "provider": s["provider"],
483
+ })
484
+ futures[executor.submit(
485
+ _resolve_row_session,
486
+ store,
487
+ base_dir,
488
+ env,
489
+ fetch_codex_status,
490
+ s,
491
+ force_refresh=force_refresh,
492
+ cache_ttl_seconds=cache_ttl_seconds,
493
+ cache_only=cache_only,
494
+ status_timeout_seconds=status_timeout_seconds,
495
+ )] = s
496
+ for future in as_completed(futures):
497
+ s = futures[future]
498
+ try:
499
+ resolved = future.result()
500
+ except CdxError as error:
501
+ if not str(error).startswith("Unknown session:"):
502
+ raise
503
+ continue
504
+ resolved_by_name[s["name"]] = resolved
505
+ if progress_callback:
506
+ progress_callback({
507
+ "event": "session_finished",
508
+ "session_name": s["name"],
509
+ "has_status": bool(resolved.get("lastStatus")),
510
+ "cache_hit": cache_hits[s["name"]],
511
+ })
512
+ resolved = [resolved_by_name[s["name"]] for s in sessions if s["name"] in resolved_by_name]
513
+
514
+ def sort_key(s):
515
+ at = s.get("lastStatusAt") or ""
516
+ disabled_rank = 1 if s.get("enabled", True) is False else 0
517
+ return (disabled_rank, "" if at else "\xff", at, s["name"])
518
+
519
+ resolved.sort(key=sort_key)
520
+ enabled = [s for s in resolved if s.get("enabled", True) is not False]
521
+ disabled = [s for s in resolved if s.get("enabled", True) is False]
522
+ enabled.reverse()
523
+ disabled.sort(key=lambda s: s["name"])
524
+ resolved = enabled + disabled
525
+
526
+ rows = []
527
+ for s in resolved:
528
+ rows.append(_status_row_from_session(store, s))
529
+ if progress_callback:
530
+ progress_callback({
531
+ "event": "status_finished",
532
+ "row_count": len(rows),
533
+ })
534
+ return rows
@@ -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):