cdx-manager 0.10.0 → 0.11.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.
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import binascii
2
3
  import json
3
4
  import os
4
5
  import shutil
@@ -21,6 +22,7 @@ from .status_source import find_latest_status_artifact
21
22
  DEFAULT_PROVIDER = PROVIDER_CODEX
22
23
  ALLOWED_PROVIDERS = set(PROVIDERS)
23
24
  MAX_SESSION_NAME_LENGTH = 64
25
+ MAX_SESSION_LABEL_LENGTH = 64
24
26
  RESERVED_SESSION_NAMES = {
25
27
  "add",
26
28
  "can-resume",
@@ -39,6 +41,7 @@ RESERVED_SESSION_NAMES = {
39
41
  "history",
40
42
  "import",
41
43
  "last",
44
+ "label",
42
45
  "login",
43
46
  "logout",
44
47
  "model",
@@ -533,10 +536,21 @@ def create_session_service(options=None):
533
536
  if name in RESERVED_SESSION_NAMES:
534
537
  raise CdxError(f"Session name is reserved: {name}")
535
538
 
539
+ def _normalize_session_label(label):
540
+ text = str(label or "").strip()
541
+ if not text:
542
+ raise CdxError("Session label is required")
543
+ if len(text) > MAX_SESSION_LABEL_LENGTH:
544
+ raise CdxError(f"Session label is too long (max {MAX_SESSION_LABEL_LENGTH} characters)")
545
+ if any(ord(ch) < 32 or ord(ch) == 127 for ch in text):
546
+ raise CdxError("Session label cannot contain control characters")
547
+ return text
548
+
536
549
  def _build_export_session_record(session):
537
550
  return {
538
551
  "name": session["name"],
539
552
  "provider": session["provider"],
553
+ "label": session.get("label"),
540
554
  "enabled": session.get("enabled", True) is not False,
541
555
  "createdAt": session.get("createdAt"),
542
556
  "updatedAt": session.get("updatedAt"),
@@ -697,6 +711,7 @@ def create_session_service(options=None):
697
711
  "lastStatusAt": None,
698
712
  "lastStatus": None,
699
713
  **({"launch": source.get("launch")} if source.get("launch") else {}),
714
+ **({"label": source.get("label")} if source.get("label") else {}),
700
715
  "auth": {
701
716
  "status": "unknown",
702
717
  "lastCheckedAt": None,
@@ -887,6 +902,31 @@ def create_session_service(options=None):
887
902
  "updatedAt": now,
888
903
  })
889
904
 
905
+ def set_session_label(name, label):
906
+ session = store["get_session"](name)
907
+ if not session:
908
+ raise CdxError(f"Unknown session: {name}")
909
+ normalized = _normalize_session_label(label)
910
+ now = _local_now_iso()
911
+ return store["update_session"](name, lambda s: {
912
+ **s,
913
+ "label": normalized,
914
+ "updatedAt": now,
915
+ })
916
+
917
+ def clear_session_label(name):
918
+ session = store["get_session"](name)
919
+ if not session:
920
+ raise CdxError(f"Unknown session: {name}")
921
+ now = _local_now_iso()
922
+
923
+ def updater(s):
924
+ updated = {**s, "updatedAt": now}
925
+ updated.pop("label", None)
926
+ return updated
927
+
928
+ return store["update_session"](name, updater)
929
+
890
930
  def set_launch_settings(name, settings):
891
931
  session = store["get_session"](name)
892
932
  if not session:
@@ -901,6 +941,11 @@ def create_session_service(options=None):
901
941
  # lock; a pre-lock snapshot loses a concurrent setting change.
902
942
  current = _normalize_launch_settings(s.get("launch") or {}, mark_fast_service_tier=False)
903
943
  launch = {**current, **updates}
944
+ if "power" in updates:
945
+ launch.pop("reasoning_effort", None)
946
+ launch.pop("reasoningEffort", None)
947
+ if "reasoning_effort" in updates:
948
+ launch.pop("power", None)
904
949
  explicit_power = "power" in updates or "reasoning_effort" in updates
905
950
  if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
906
951
  launch["fast"] = False
@@ -923,7 +968,7 @@ def create_session_service(options=None):
923
968
  raise CdxError(f"Unknown session: {name}")
924
969
  if not keys:
925
970
  raise CdxError("At least one launch setting is required.")
926
- allowed = {"power", "permission", "fast", "rtk", "logics", "model", "priority"}
971
+ allowed = {"power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "model", "priority"}
927
972
  unknown = [key for key in keys if key not in allowed]
928
973
  if unknown:
929
974
  raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
@@ -1100,6 +1145,7 @@ def create_session_service(options=None):
1100
1145
  row_status = status if enabled else None
1101
1146
  return {
1102
1147
  "session_name": s["name"],
1148
+ "label": s.get("label"),
1103
1149
  "provider": s["provider"],
1104
1150
  "enabled": enabled,
1105
1151
  "active": bool(_session_runtime(s["name"])) if enabled else False,
@@ -1216,7 +1262,12 @@ def create_session_service(options=None):
1216
1262
  )] = s
1217
1263
  for future in as_completed(futures):
1218
1264
  s = futures[future]
1219
- resolved = future.result()
1265
+ try:
1266
+ resolved = future.result()
1267
+ except CdxError as error:
1268
+ if not str(error).startswith("Unknown session:"):
1269
+ raise
1270
+ continue
1220
1271
  resolved_by_name[s["name"]] = resolved
1221
1272
  if progress_callback:
1222
1273
  progress_callback({
@@ -1225,7 +1276,7 @@ def create_session_service(options=None):
1225
1276
  "has_status": bool(resolved.get("lastStatus")),
1226
1277
  "cache_hit": cache_hits[s["name"]],
1227
1278
  })
1228
- resolved = [resolved_by_name[s["name"]] for s in sessions]
1279
+ resolved = [resolved_by_name[s["name"]] for s in sessions if s["name"] in resolved_by_name]
1229
1280
 
1230
1281
  def sort_key(s):
1231
1282
  at = s.get("lastStatusAt") or ""
@@ -1262,6 +1313,7 @@ def create_session_service(options=None):
1262
1313
  )
1263
1314
  return [{
1264
1315
  "name": s["name"],
1316
+ "label": s.get("label"),
1265
1317
  "provider": s["provider"] if has_multiple else None,
1266
1318
  "enabled": s.get("enabled", True) is not False,
1267
1319
  "active": bool(_session_runtime(s["name"])) if s.get("enabled", True) is not False else False,
@@ -1336,7 +1388,52 @@ def create_session_service(options=None):
1336
1388
  "bundle_size_bytes": len(bundle_bytes),
1337
1389
  }
1338
1390
 
1339
- def import_bundle(file_path, passphrase=None, session_names=None, force=False, merge=False):
1391
+ def _validate_import_profile_files(payload, imported_sessions):
1392
+ profiles = payload.get("profiles") or {}
1393
+ if not isinstance(profiles, dict):
1394
+ raise CdxError("Bundle contains invalid profile data.")
1395
+ decoded_profiles = {}
1396
+ for session_payload in imported_sessions:
1397
+ name = session_payload["name"]
1398
+ files = []
1399
+ profile_items = profiles.get(name, [])
1400
+ if not isinstance(profile_items, list):
1401
+ raise CdxError(f"Bundle contains invalid profile data for session {name}.")
1402
+ for item in profile_items:
1403
+ if not isinstance(item, dict):
1404
+ raise CdxError(f"Bundle contains invalid profile entry for session {name}.")
1405
+ rel_path = _safe_relpath(item.get("path"))
1406
+ data_b64 = item.get("data_b64")
1407
+ if not isinstance(data_b64, str):
1408
+ raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}")
1409
+ try:
1410
+ content = base64.b64decode(data_b64.encode("ascii"), validate=True)
1411
+ except (binascii.Error, UnicodeEncodeError) as error:
1412
+ raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
1413
+ files.append({"path": rel_path, "content": content})
1414
+ decoded_profiles[name] = files
1415
+ return decoded_profiles
1416
+
1417
+ def _restore_import_backup(name, backup_root, session_root, old_record, old_state):
1418
+ if os.path.exists(session_root):
1419
+ remove_tree(session_root, ignore_errors=True)
1420
+ if backup_root and os.path.exists(backup_root):
1421
+ os.rename(backup_root, session_root)
1422
+ if old_record:
1423
+ store["replace_session"](name, old_record)
1424
+ if old_state is not None:
1425
+ store["write_session_state"](name, old_state)
1426
+ else:
1427
+ store["remove_session"](name)
1428
+
1429
+ def import_bundle(
1430
+ file_path,
1431
+ passphrase=None,
1432
+ session_names=None,
1433
+ force=False,
1434
+ merge=False,
1435
+ allow_authless_force=False,
1436
+ ):
1340
1437
  if not file_path or not os.path.isfile(file_path):
1341
1438
  raise CdxError(f"Bundle file not found: {file_path}")
1342
1439
  with open(file_path, "rb") as handle:
@@ -1358,73 +1455,91 @@ def create_session_service(options=None):
1358
1455
  conflicts = [name for name in names if name in existing]
1359
1456
  if conflicts and not force and not merge:
1360
1457
  raise CdxError(f"Import would overwrite existing sessions: {', '.join(conflicts)}")
1458
+ if conflicts and force and not decoded["meta"].get("include_auth") and not allow_authless_force:
1459
+ raise CdxError(
1460
+ "Import --force would overwrite existing session credentials, but this bundle has no auth payloads. "
1461
+ "Re-export with --include-auth, use --merge, or pass --allow-authless-force to discard local credentials."
1462
+ )
1463
+
1464
+ for session_payload in imported_sessions:
1465
+ _validate_new_session_name(session_payload["name"])
1466
+ _normalize_provider(session_payload["provider"])
1467
+ decoded_profiles = _validate_import_profile_files(payload, imported_sessions)
1361
1468
 
1362
1469
  for session_payload in imported_sessions:
1363
1470
  name = session_payload["name"]
1364
- _validate_new_session_name(name)
1365
1471
  provider = _normalize_provider(session_payload["provider"])
1366
1472
  is_existing = name in existing
1367
1473
 
1368
- if is_existing and force:
1369
- remove_session(name)
1370
- is_existing = False
1371
-
1372
1474
  session_root = _get_session_root(name)
1373
1475
  auth_home = _get_session_auth_home(name, provider)
1374
1476
  _ensure_private_dir(base_dir)
1375
1477
  _ensure_private_dir(os.path.join(base_dir, "profiles"))
1478
+ backup_root = None
1479
+ old_record = None
1480
+ old_state = None
1481
+ if is_existing and force:
1482
+ old_record = store["get_session"](name)
1483
+ old_state = store["read_session_state"](name)
1484
+ if os.path.exists(session_root):
1485
+ backup_root = tempfile.mkdtemp(prefix=f".{_encode(name)}.import.", dir=os.path.dirname(session_root))
1486
+ os.rmdir(backup_root)
1487
+ os.rename(session_root, backup_root)
1488
+ is_existing = False
1376
1489
  _ensure_private_dir(session_root)
1377
1490
  _ensure_private_dir(auth_home)
1378
1491
 
1379
1492
  existing_state_before = None
1380
- if is_existing and merge:
1381
- existing_record = store["get_session"](name) or {}
1382
- # replace_session resets the state file to defaults, so the
1383
- # local state must be captured before it runs.
1384
- existing_state_before = store["read_session_state"](name) or {}
1385
- bundle_record = {
1386
- **session_payload,
1387
- "provider": provider,
1388
- "enabled": session_payload.get("enabled", True) is not False,
1389
- "sessionRoot": session_root,
1390
- "authHome": auth_home,
1391
- }
1392
- # Existing values take precedence; bundle fills in missing keys only.
1393
- merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
1394
- merged_record["sessionRoot"] = session_root
1395
- merged_record["authHome"] = auth_home
1396
- store["replace_session"](name, merged_record)
1397
- else:
1398
- session_record = {
1399
- **session_payload,
1400
- "provider": provider,
1401
- "enabled": session_payload.get("enabled", True) is not False,
1402
- "sessionRoot": session_root,
1403
- "authHome": auth_home,
1404
- }
1405
- store["replace_session"](name, session_record)
1406
-
1407
- state = (payload.get("states") or {}).get(name)
1408
- if is_existing and merge:
1409
- merged_state = {**(state or {}), **{k: v for k, v in (existing_state_before or {}).items() if v is not None}}
1410
- if merged_state:
1411
- store["write_session_state"](name, merged_state)
1412
- elif state is not None:
1413
- store["write_session_state"](name, state)
1414
-
1415
- for item in (payload.get("profiles") or {}).get(name, []):
1416
- rel_path = _safe_relpath(item.get("path"))
1417
- try:
1418
- content = base64.b64decode(item.get("data_b64", "").encode("ascii"))
1419
- except (AttributeError, ValueError, UnicodeEncodeError) as error:
1420
- raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
1421
- dest_path = os.path.join(session_root, rel_path)
1422
- # In merge mode, skip files that already exist locally.
1423
- if is_existing and merge and os.path.exists(dest_path):
1424
- continue
1425
- _ensure_private_dir(os.path.dirname(dest_path))
1426
- # Decrypted credentials: 0o600 from creation, no umask window.
1427
- atomic_write(dest_path, content, mode=0o600)
1493
+ try:
1494
+ if is_existing and merge:
1495
+ existing_record = store["get_session"](name) or {}
1496
+ # replace_session resets the state file to defaults, so the
1497
+ # local state must be captured before it runs.
1498
+ existing_state_before = store["read_session_state"](name) or {}
1499
+ bundle_record = {
1500
+ **session_payload,
1501
+ "provider": provider,
1502
+ "enabled": session_payload.get("enabled", True) is not False,
1503
+ "sessionRoot": session_root,
1504
+ "authHome": auth_home,
1505
+ }
1506
+ # Existing values take precedence; bundle fills in missing keys only.
1507
+ merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
1508
+ merged_record["sessionRoot"] = session_root
1509
+ merged_record["authHome"] = auth_home
1510
+ store["replace_session"](name, merged_record)
1511
+ else:
1512
+ session_record = {
1513
+ **session_payload,
1514
+ "provider": provider,
1515
+ "enabled": session_payload.get("enabled", True) is not False,
1516
+ "sessionRoot": session_root,
1517
+ "authHome": auth_home,
1518
+ }
1519
+ store["replace_session"](name, session_record)
1520
+
1521
+ state = (payload.get("states") or {}).get(name)
1522
+ if is_existing and merge:
1523
+ merged_state = {**(state or {}), **{k: v for k, v in (existing_state_before or {}).items() if v is not None}}
1524
+ if merged_state:
1525
+ store["write_session_state"](name, merged_state)
1526
+ elif state is not None:
1527
+ store["write_session_state"](name, state)
1528
+
1529
+ for item in decoded_profiles.get(name, []):
1530
+ dest_path = os.path.join(session_root, item["path"])
1531
+ # In merge mode, skip files that already exist locally.
1532
+ if is_existing and merge and os.path.exists(dest_path):
1533
+ continue
1534
+ _ensure_private_dir(os.path.dirname(dest_path))
1535
+ # Decrypted credentials: 0o600 from creation, no umask window.
1536
+ atomic_write(dest_path, item["content"], mode=0o600)
1537
+ except Exception:
1538
+ _restore_import_backup(name, backup_root, session_root, old_record, old_state)
1539
+ raise
1540
+ finally:
1541
+ if backup_root and os.path.exists(backup_root):
1542
+ remove_tree(backup_root, ignore_errors=True)
1428
1543
 
1429
1544
  return {
1430
1545
  "path": file_path,
@@ -1445,6 +1560,8 @@ def create_session_service(options=None):
1445
1560
  "list_sessions": list_sessions,
1446
1561
  "get_session": get_session,
1447
1562
  "set_session_enabled": set_session_enabled,
1563
+ "set_session_label": set_session_label,
1564
+ "clear_session_label": clear_session_label,
1448
1565
  "set_launch_settings": set_launch_settings,
1449
1566
  "unset_launch_settings": unset_launch_settings,
1450
1567
  "record_status": record_status,
@@ -294,8 +294,8 @@ def create_session_store(base_dir):
294
294
  continue
295
295
  try:
296
296
  entry = json.loads(line)
297
- except json.JSONDecodeError as error:
298
- raise CdxError(f"Corrupt JSONL file: {launch_history_file}") from error
297
+ except json.JSONDecodeError:
298
+ continue
299
299
  if session_name and entry.get("session_name") != session_name:
300
300
  continue
301
301
  entries.append(entry)
@@ -20,10 +20,6 @@ _KEY_VALUE_PATTERNS = [
20
20
  ("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
21
21
  ("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
22
22
  ("credits", re.compile(r"credits?\s*[:=]\s*(\d[\d, ]*(?:\.\d+)?)\s*(?:credits?)?", re.I)),
23
- ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
24
- ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
25
- ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
26
- ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
27
23
  ("usage_pct", re.compile(r"usage\s*[:=]\s*(\d{1,3})%", re.I)),
28
24
  ("usage_pct", re.compile(r"current\s*[:=]\s*(\d{1,3})%", re.I)),
29
25
  ("remaining_5h_pct", re.compile(r"5h(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
@@ -32,6 +28,14 @@ _KEY_VALUE_PATTERNS = [
32
28
  ("remaining_week_pct", re.compile(r"remaining\s+week\s*[:=]\s*(\d{1,3})%", re.I)),
33
29
  ]
34
30
 
31
+ _CODEX_LIMIT_PERCENT_PATTERNS = [
32
+ ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
33
+ ("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
34
+ ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
35
+ ("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
36
+ ]
37
+ _KEY_VALUE_PATTERNS.extend(_CODEX_LIMIT_PERCENT_PATTERNS)
38
+
35
39
 
36
40
  def _strip_ansi(text):
37
41
  return _ANSI_ESCAPE.sub("", str(text or ""))
@@ -103,6 +107,27 @@ def _is_zero_credit_balance(value):
103
107
  return False
104
108
 
105
109
 
110
+ def _get_structured_rate_limit_window(rate_limits, duration_mins, fallback_key):
111
+ for key in ("primary", "secondary"):
112
+ window = rate_limits.get(key) or {}
113
+ if window.get("windowDurationMins") == duration_mins:
114
+ return window
115
+ if window.get("window_minutes") == duration_mins:
116
+ return window
117
+ fallback = rate_limits.get(fallback_key) or {}
118
+ if "windowDurationMins" in fallback or "window_minutes" in fallback:
119
+ return {}
120
+ return fallback
121
+
122
+
123
+ def _get_rate_limit_used_percent(window):
124
+ return window.get("used_percent", window.get("usedPercent"))
125
+
126
+
127
+ def _get_rate_limit_reset_at(window):
128
+ return window.get("resets_at", window.get("resetsAt"))
129
+
130
+
106
131
  def _extract_structured_rate_limits(record):
107
132
  if not isinstance(record, dict):
108
133
  return None
@@ -111,25 +136,27 @@ def _extract_structured_rate_limits(record):
111
136
  if not isinstance(rate_limits, dict):
112
137
  return None
113
138
 
114
- primary = rate_limits.get("primary") or {}
115
- secondary = rate_limits.get("secondary") or {}
139
+ five_hour = _get_structured_rate_limit_window(rate_limits, 300, "primary")
140
+ weekly = _get_structured_rate_limit_window(rate_limits, 10080, "secondary")
116
141
  credits = rate_limits.get("credits") or {}
117
142
 
118
- primary_used = _coerce_percentage(primary.get("used_percent"))
119
- secondary_used = _coerce_percentage(secondary.get("used_percent"))
120
- remaining_5h_pct = None if primary_used is None else max(0, 100 - primary_used)
121
- remaining_week_pct = None if secondary_used is None else max(0, 100 - secondary_used)
143
+ five_hour_used = _coerce_percentage(_get_rate_limit_used_percent(five_hour))
144
+ weekly_used = _coerce_percentage(_get_rate_limit_used_percent(weekly))
145
+ remaining_5h_pct = None if five_hour_used is None else max(0, 100 - five_hour_used)
146
+ remaining_week_pct = None if weekly_used is None else max(0, 100 - weekly_used)
122
147
 
123
148
  result = {
124
- "usage_pct": primary_used if primary_used is not None else secondary_used,
149
+ "usage_pct": five_hour_used if five_hour_used is not None else weekly_used,
125
150
  "remaining_5h_pct": remaining_5h_pct,
126
151
  "remaining_week_pct": remaining_week_pct,
127
152
  "credits": None,
128
- "reset_5h_at": _normalize_timestamp_value(primary.get("resets_at")),
129
- "reset_week_at": _normalize_timestamp_value(secondary.get("resets_at")),
153
+ "reset_5h_at": _normalize_timestamp_value(_get_rate_limit_reset_at(five_hour)),
154
+ "reset_week_at": _normalize_timestamp_value(_get_rate_limit_reset_at(weekly)),
130
155
  "reset_at": None,
131
156
  "raw_status_text": json.dumps(rate_limits, sort_keys=True),
132
157
  }
158
+ if not five_hour and weekly:
159
+ result["_skip_5h_backfill"] = True
133
160
 
134
161
  balance = credits.get("balance")
135
162
  if balance not in (None, "") and not (
@@ -496,18 +523,10 @@ def extract_named_statuses_from_text(text):
496
523
  break
497
524
 
498
525
  for line in lines:
499
- m = re.search(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", line, re.I)
500
- if m and "remaining_5h_pct" not in result:
501
- result["remaining_5h_pct"] = int(m[1])
502
- m = re.search(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", line, re.I)
503
- if m and "remaining_5h_pct" not in result:
504
- result["remaining_5h_pct"] = int(m[1])
505
- m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", line, re.I)
506
- if m and "remaining_week_pct" not in result:
507
- result["remaining_week_pct"] = int(m[1])
508
- m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", line, re.I)
509
- if m and "remaining_week_pct" not in result:
510
- result["remaining_week_pct"] = int(m[1])
526
+ for field, pattern in _CODEX_LIMIT_PERCENT_PATTERNS:
527
+ m = pattern.search(line)
528
+ if m and field not in result:
529
+ result[field] = int(m[1])
511
530
 
512
531
  if "remaining_5h_pct" in result and "usage_pct" not in result:
513
532
  result["usage_pct"] = max(0, 100 - result["remaining_5h_pct"])
@@ -640,6 +659,8 @@ _STATUS_VALUE_FIELDS = (
640
659
 
641
660
  def _backfill_missing_fields(target, source):
642
661
  for field in _STATUS_VALUE_FIELDS:
662
+ if target.get("_skip_5h_backfill") and field in ("remaining_5h_pct", "reset_5h_at"):
663
+ continue
643
664
  if target.get(field) is None and source.get(field) is not None:
644
665
  target[field] = source[field]
645
666
 
@@ -694,6 +715,7 @@ def find_latest_status_artifact(
694
715
  "reset_week_at": candidate["structured"].get("reset_week_at"),
695
716
  "reset_at": candidate["structured"].get("reset_at"),
696
717
  "raw_status_text": candidate["structured"].get("raw_status_text"),
718
+ "_skip_5h_backfill": candidate["structured"].get("_skip_5h_backfill"),
697
719
  }
698
720
  if not parsed:
699
721
  continue
@@ -75,12 +75,19 @@ def _format_credits(value, empty="n/a"):
75
75
 
76
76
  def _format_status_rows(rows, use_color=False, small=False):
77
77
  has_provider = len({r["provider"] for r in rows}) > 1 and not small
78
+ has_label = any(r.get("label") for r in rows) and not small
78
79
  if small:
79
80
  headers = ["SESSION", "STATUS", "OK", "5H", "WEEK", "RESETS", "RESET 5H", "RESET WEEK"]
80
81
  elif has_provider:
81
- headers = ["SESSION", "PROV.", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
82
+ headers = ["SESSION", "PROV."]
83
+ if has_label:
84
+ headers.append("LABEL")
85
+ headers += ["STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
82
86
  else:
83
- headers = ["SESSION", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
87
+ headers = ["SESSION"]
88
+ if has_label:
89
+ headers.append("LABEL")
90
+ headers += ["STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
84
91
  if not rows:
85
92
  if small:
86
93
  return "SESSION STATUS OK 5H WEEK RESETS RESET 5H RESET WEEK\nNo saved sessions yet."
@@ -102,6 +109,8 @@ def _format_status_rows(rows, use_color=False, small=False):
102
109
  base = [_format_session_name(r)]
103
110
  if has_provider:
104
111
  base.append(r.get("provider") or "n/a")
112
+ if has_label:
113
+ base.append(r.get("label") or "-")
105
114
  status = r.get("status") or ("enabled" if r.get("enabled", True) else "disabled")
106
115
  base.append(_style(status, "2" if status == "disabled" else "32", use_color))
107
116
  if not small: