cdx-manager 0.26.6 → 0.26.8

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,45 @@
1
+ """Additive quota evidence; never infer a measurement from a missing value."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ SOURCES = {"codex_rate_limits", "claude_probe", "claude_statusline", "cache", "unknown"}
6
+
7
+
8
+ def quota_windows(status, provider, reason=None, now=None):
9
+ status = status or {}
10
+ now = now or datetime.now(timezone.utc)
11
+ ttl = {"codex": 300, "claude": 600}.get(provider, 60)
12
+ source = {
13
+ "api:codex-app-server-rate-limits": "codex_rate_limits",
14
+ "api:anthropic-ratelimit-headers": "claude_probe",
15
+ "claude:statusline": "claude_statusline",
16
+ }.get(status.get("source_ref"), "cache")
17
+ windows = {}
18
+ for name, field in (("5h", "remaining_5h_pct"), ("weekly", "remaining_week_pct")):
19
+ evidence = (status.get("quota_windows") or {}).get(name, {})
20
+ value = status.get(field)
21
+ observed_at = evidence.get("observed_at", status.get("updated_at")) if value is not None else None
22
+ try:
23
+ observed = datetime.fromisoformat(str(observed_at).replace("Z", "+00:00"))
24
+ if observed.tzinfo is None:
25
+ observed = observed.replace(tzinfo=timezone.utc)
26
+ observed_at = observed.isoformat()
27
+ except (TypeError, ValueError):
28
+ observed, observed_at = None, None
29
+ window_reason = reason or ("missing_window" if value is None else None)
30
+ availability = "unavailable" if window_reason else "measured"
31
+ if not window_reason and (observed is None or (now - observed).total_seconds() >= ttl):
32
+ availability = "stale"
33
+ window_source = evidence.get("source", source) if value is not None else "unknown"
34
+ windows[name] = {
35
+ "source": window_source if window_source in SOURCES else "unknown",
36
+ "observed_at": observed_at,
37
+ "availability": availability,
38
+ "reason": window_reason,
39
+ }
40
+ return windows
41
+
42
+
43
+ def mark_quota_unavailable(row, reason):
44
+ for window in row.get("quota_windows", {}).values():
45
+ window.update(availability="unavailable", reason=reason)
@@ -441,7 +441,11 @@ def import_bundle(base_dir, store, file_path,
441
441
  if recovery_dir:
442
442
  remove_tree(recovery_dir, ignore_errors=True)
443
443
 
444
+ from .claude_status_line import provision
445
+
446
+ status_line_warnings = [notice for name in names if (notice := provision(store["get_session"](name), base_dir))]
444
447
  return {
448
+ "status_line_warnings": status_line_warnings,
445
449
  "path": file_path,
446
450
  "session_names": names,
447
451
  "include_auth": bool(decoded["meta"].get("include_auth")),
@@ -149,7 +149,7 @@ def _seed_codex_auth_from_global(auth_home, env=None):
149
149
  return True
150
150
 
151
151
 
152
- _ON_OFF_SETTINGS = {"fast": "fast", "rtk": "rtk", "logics": "logics", "notify": "notify", "agent_state": "agent state", "notify_preview": "notify preview", "scrub_env": "scrub env", "shared_memory": "shared memory", "retro": "retro"}
152
+ _ON_OFF_SETTINGS = {"fast": "fast", "rtk": "rtk", "logics": "logics", "notify": "notify", "agent_state": "agent state", "status_line": "status line", "notify_preview": "notify preview", "scrub_env": "scrub env", "shared_memory": "shared memory", "retro": "retro"}
153
153
 
154
154
 
155
155
  def _has_control_chars(text):
@@ -284,7 +284,7 @@ def _normalize_session_label(label):
284
284
  return text
285
285
 
286
286
 
287
- def set_launch_settings(store, name, settings):
287
+ def set_launch_settings(store, name, settings, base_dir=None):
288
288
  session = store["get_session"](name)
289
289
  if not session:
290
290
  raise CdxError(f"Unknown session: {name}")
@@ -317,16 +317,21 @@ def set_launch_settings(store, name, settings):
317
317
  "updatedAt": now,
318
318
  }
319
319
 
320
- return store["update_session"](name, updater)
320
+ updated = store["update_session"](name, updater)
321
+ if base_dir is not None:
322
+ from .claude_status_line import provision
323
+
324
+ provision(updated, base_dir)
325
+ return updated
321
326
 
322
- def unset_launch_settings(store, name, keys):
327
+ def unset_launch_settings(store, name, keys, base_dir=None):
323
328
  session = store["get_session"](name)
324
329
  if not session:
325
330
  raise CdxError(f"Unknown session: {name}")
326
331
  if not keys:
327
332
  raise CdxError("At least one launch setting is required.")
328
333
  allowed = {
329
- "power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview",
334
+ "power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "status_line", "notify_preview",
330
335
  "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
331
336
  }
332
337
  unknown = [key for key in keys if key not in allowed]
@@ -345,7 +350,12 @@ def unset_launch_settings(store, name, keys):
345
350
  updated.pop("launch", None)
346
351
  return updated
347
352
 
348
- return store["update_session"](name, updater)
353
+ updated = store["update_session"](name, updater)
354
+ if base_dir is not None:
355
+ from .claude_status_line import provision
356
+
357
+ provision(updated, base_dir)
358
+ return updated
349
359
 
350
360
  def update_auth_state(store, name, updater):
351
361
  now = _local_now_iso()
@@ -555,6 +565,9 @@ def copy_session(base_dir, store, source_name, dest_name):
555
565
  remove_tree(backup_root, ignore_errors=True)
556
566
  finally:
557
567
  remove_tree(temp_parent, ignore_errors=True)
568
+ from .claude_status_line import provision
569
+
570
+ provision(result["session"], base_dir)
558
571
  return {"session": result["session"], "overwritten": overwritten}
559
572
 
560
573
 
@@ -604,6 +617,9 @@ def rename_session(base_dir, store, source_name, dest_name):
604
617
  delete_keychain_credentials(source_auth_home)
605
618
  except CdxError:
606
619
  raise CdxError(f"Renamed session to {dest_name}, but old keychain credential cleanup failed; the new session is usable and the old entry was retained.") from None
620
+ from .claude_status_line import provision
621
+
622
+ provision(result["session"], base_dir)
607
623
  return result["session"]
608
624
 
609
625
 
@@ -647,6 +663,9 @@ def create_session(base_dir, env, store, name, provider=DEFAULT_PROVIDER):
647
663
  _seed_codex_auth_from_global(auth_home, env=env)
648
664
  if normalized_provider == PROVIDER_CLAUDE:
649
665
  _ensure_claude_attribution_disabled(auth_home)
666
+ from .claude_status_line import provision
667
+
668
+ provision({"provider": normalized_provider, "authHome": auth_home, "name": name}, base_dir)
650
669
  if normalized_provider == "antigravity":
651
670
  from .antigravity_status import install
652
671
 
@@ -1034,8 +1053,8 @@ def create_session_service(options=None):
1034
1053
  "set_session_enabled": partial(set_session_enabled, store),
1035
1054
  "set_session_label": partial(set_session_label, store),
1036
1055
  "clear_session_label": partial(clear_session_label, store),
1037
- "set_launch_settings": partial(set_launch_settings, store),
1038
- "unset_launch_settings": partial(unset_launch_settings, store),
1056
+ "set_launch_settings": partial(set_launch_settings, store, base_dir=base_dir),
1057
+ "unset_launch_settings": partial(unset_launch_settings, store, base_dir=base_dir),
1039
1058
  "record_status": partial(record_status, store),
1040
1059
  "record_launch_history": partial(record_launch_history, store),
1041
1060
  "get_launch_history": partial(get_launch_history, store),
@@ -13,6 +13,7 @@ from .claude_credentials import claude_auth_type, setup_token_days_left
13
13
  from .claude_usage import _decode_jwt_claims
14
14
  from .config import PROVIDER_CLAUDE, PROVIDER_CODEX, get_cdx_home
15
15
  from .errors import CdxError
16
+ from .quota_provenance import quota_windows
16
17
  from .session_helpers import (
17
18
  _get_global_codex_home,
18
19
  _get_session_auth_home,
@@ -158,6 +159,7 @@ def _normalize_status_payload(payload=None):
158
159
  "raw_status_text": payload.get("raw_status_text"),
159
160
  "source_ref": payload.get("source_ref"),
160
161
  "structured": bool(payload.get("structured")),
162
+ "quota_windows": payload.get("quota_windows"),
161
163
  }
162
164
 
163
165
  def _parse_status_timestamp(value):
@@ -240,6 +242,11 @@ def _merge_status_payload(current, candidate):
240
242
  return current
241
243
 
242
244
  merged = dict(current)
245
+ merged["quota_windows"] = dict(current.get("quota_windows") or quota_windows(current, "unknown"))
246
+ candidate_windows = candidate.get("quota_windows") or quota_windows(candidate, "unknown")
247
+ for name, field in (("5h", "remaining_5h_pct"), ("weekly", "remaining_week_pct")):
248
+ if current.get(field) is None and candidate.get(field) is not None:
249
+ merged["quota_windows"][name] = candidate_windows[name]
243
250
  for field in [
244
251
  "usage_pct",
245
252
  "remaining_5h_pct",
@@ -336,7 +343,7 @@ def _fetch_codex_status(codex_status_fetcher, custom_codex_status_fetcher, defau
336
343
  if custom_codex_status_fetcher:
337
344
  return custom_codex_status_fetcher(session)
338
345
  timeout = timeout_seconds or default_status_timeout_seconds
339
- return codex_status_fetcher(session, timeout=timeout)
346
+ return codex_status_fetcher(session, timeout=timeout, diagnostic=True)
340
347
 
341
348
  def session_paths(session):
342
349
  """Where a session's files live, so a caller never rebuilds cdx's layout.
@@ -415,6 +422,12 @@ def _status_row_from_session(store, s):
415
422
  )})
416
423
  if enabled and antigravity["quota_stale"]:
417
424
  row["status"] = "stale"
425
+ reason = s.get("_quota_error")
426
+ if not enabled:
427
+ reason = "profile_disabled"
428
+ elif row["auth_status"] == "logged_out":
429
+ reason = "login_required"
430
+ row["quota_windows"] = quota_windows(row_status, s["provider"], reason=reason)
418
431
  row["runs_out_at_iso"] = _runs_out(row)
419
432
  return row
420
433
 
@@ -465,9 +478,13 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
465
478
  {**session, "authHome": source_root},
466
479
  timeout_seconds=status_timeout_seconds,
467
480
  )
468
- if live_status:
481
+ if live_status and live_status.get("_quota_error"):
482
+ session["_quota_error"] = live_status["_quota_error"]
483
+ elif live_status:
469
484
  record_status(store, session["name"], live_status)
470
485
  return live_status
486
+ else:
487
+ session["_quota_error"] = "quota_read_failed"
471
488
 
472
489
  expected_account_email = (
473
490
  _read_expected_account_email(source_root)
@@ -494,6 +511,12 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
494
511
  # structured payloads there cannot be attributed to ours.
495
512
  trust_unattributed_structured=False,
496
513
  )
514
+ if artifact and (artifact.get("structured") or not expected_account_email):
515
+ # Shared artifacts without verified identity cannot date this profile.
516
+ artifact["quota_windows"] = {
517
+ name: {"source": "cache", "observed_at": None}
518
+ for name in ("5h", "weekly")
519
+ }
497
520
  if not artifact:
498
521
  if _is_low_confidence_status_source(current_status):
499
522
  return None
@@ -511,6 +534,7 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
511
534
  "raw_status_text": artifact.get("raw_status_text"),
512
535
  "source_ref": artifact.get("source_ref"),
513
536
  "structured": artifact.get("structured"),
537
+ "quota_windows": artifact.get("quota_windows"),
514
538
  })
515
539
  if _is_low_confidence_status_source(current_status) and not _is_low_confidence_status_source(resolved):
516
540
  record_status(store, session["name"], resolved)
@@ -4,6 +4,7 @@ import re
4
4
  from datetime import datetime, timezone
5
5
 
6
6
  from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
7
+ from .quota_provenance import quota_windows
7
8
 
8
9
  _ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
9
10
  _ANSI_TERMINAL_CONTROL = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
@@ -513,11 +514,6 @@ def extract_named_statuses_from_text(text):
513
514
  if "remaining_week_pct" not in result and week_used is not None:
514
515
  result["remaining_week_pct"] = max(0, 100 - week_used)
515
516
 
516
- if re.search(r"No stats available yet\.?\s+Start using Claude Code", normalized, re.I):
517
- result.setdefault("usage_pct", 0)
518
- result.setdefault("remaining_5h_pct", 100)
519
- result.setdefault("remaining_week_pct", 100)
520
-
521
517
  # Table header row
522
518
  header_idx = next(
523
519
  (i for i, l in enumerate(lines)
@@ -722,6 +718,9 @@ def _backfill_missing_fields(target, source):
722
718
  continue
723
719
  if target.get(field) is None and source.get(field) is not None:
724
720
  target[field] = source[field]
721
+ name = {"remaining_5h_pct": "5h", "remaining_week_pct": "weekly"}.get(field)
722
+ if name:
723
+ target.setdefault("quota_windows", {})[name] = (source.get("quota_windows") or quota_windows(source, "unknown"))[name]
725
724
 
726
725
 
727
726
  def _record_timestamp_epoch(value):
@@ -792,9 +791,9 @@ def find_latest_status_artifact(
792
791
  score = stat.st_mtime
793
792
  else:
794
793
  score = 0
795
- # Each record producer stamps "trusted" where it knows the source kind.
796
- # Untrusted records are text scraped from jsonl payloads: they can be
797
- # conversational noise quoting an old status block.
794
+ parsed["updated_at"] = datetime.fromtimestamp(score, tz=timezone.utc).isoformat() if score else None
795
+ parsed["quota_windows"] = quota_windows(parsed, provider)
796
+ # Trust comes from the record producer, not conversational text.
798
797
  trusted = bool(candidate.get("trusted"))
799
798
  # Structured payloads carry no account identity, so the email guard
800
799
  # above cannot filter them. In roots shared by several accounts the