cdx-manager 0.9.4 → 0.9.5

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.9.4-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.9.5-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -69,6 +69,7 @@ One command to launch any session. Zero auth juggling.
69
69
  - Primary source: recorded status fields on the session record.
70
70
  - Codex live source: `codex app-server` JSON-RPC `account/rateLimits/read`, normalized into 5-hour, weekly, reset, credit, and plan fields.
71
71
  - Fallback: `status-source` scans provider JSONL history files and terminal log transcripts, strips ANSI/OSC sequences, and extracts `usage%`, `5h remaining%`, and `week remaining%` via pattern matching.
72
+ - Status latency controls: use `cdx status --cached` to skip live probes, `cdx status --timeout SECONDS` for one command, or `CDX_STATUS_TIMEOUT_SECONDS` to lower the default Codex live-probe timeout.
72
73
  - Claude status refreshes are cached briefly by default; pass `--refresh` to force a live rate-limit probe.
73
74
  - On Linux, transcript capture uses the `util-linux` `script -c` command form.
74
75
  - If `script` is unavailable, Codex launch falls back to running without transcript capture.
@@ -364,9 +365,9 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
364
365
  | `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
365
366
  | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
366
367
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
367
- | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
368
- | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
369
- | `cdx status <name> [--json] [--refresh]` | Show detailed usage breakdown for one session |
368
+ | `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; `--cached` skips live provider probes and returns only stored status |
369
+ | `cdx status --small [--refresh\|--cached] [--timeout SECONDS]` / `cdx status -s [--refresh\|--cached] [--timeout SECONDS]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
370
+ | `cdx status <name> [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show detailed usage breakdown for one session; `--cached` avoids live provider refreshes |
370
371
  | `cdx --help` | Show usage |
371
372
  | `cdx --version` | Show version |
372
373
 
@@ -429,7 +430,7 @@ Most commands use a shared stderr JSON envelope for errors whenever `--json` is
429
430
  "ok": false,
430
431
  "error": {
431
432
  "code": "invalid_usage",
432
- "message": "Usage: cdx status [--json] [--refresh] | ...",
433
+ "message": "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | ...",
433
434
  "exit_code": 1
434
435
  }
435
436
  }
@@ -0,0 +1,45 @@
1
+ # CDX Manager 0.9.5
2
+
3
+ ## Highlights
4
+
5
+ - Made per-session status lookup targeted instead of resolving every configured session.
6
+ - Added fast status controls for automation-heavy workspaces.
7
+ - Added a Logics release contract for the `cdx-manager` release workflow.
8
+
9
+ ## Changes
10
+
11
+ ### Faster per-session status
12
+
13
+ `cdx status <name>` now resolves only the named session. Previously the command collected all status rows and filtered afterward, so a detail lookup could still trigger live probes for every configured session.
14
+
15
+ ### Cached and bounded status probes
16
+
17
+ `cdx status --cached` reads stored status only and skips live provider probes. This is intended for AGENTS prompts, health checks, and project scripts that need a quick snapshot without risking provider timeouts.
18
+
19
+ Live Codex status probes can also be bounded with:
20
+
21
+ ```
22
+ cdx status --timeout 1
23
+ CDX_STATUS_TIMEOUT_SECONDS=1 cdx status
24
+ ```
25
+
26
+ The timeout applies to Codex live rate-limit probes while preserving the existing cached and fallback behavior.
27
+
28
+ ### Release workflow contract
29
+
30
+ The repository now includes `logics/release/contract.json` and `logics/release/release-contract.v1.schema.json`. The contract records the expected version sources, changelog path, checksum gate, validation commands, GitHub release gate, npm publication, and PyPI publication checks.
31
+
32
+ ## Validation
33
+
34
+ - `python -m unittest discover -s test -p 'test_session_service_py.py'`
35
+ - `python -m unittest discover -s test -p 'test_cli_py.py' -k 'status'`
36
+ - `npm run lint`
37
+ - JSON Schema validation for `logics/release/contract.json`
38
+ - `logics-manager lint --require-status`
39
+ - `logics-manager audit`
40
+ - `git diff --check`
41
+ - `node bin/cdx.js --version`
42
+ - `python3 bin/cdx --version`
43
+ - `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
44
+ - `python -m build`
45
+ - `python -m twine check dist/*`
@@ -80,6 +80,10 @@
80
80
  "v0.9.3": {
81
81
  "github_tarball_sha256": "cb3cfd9134447d1049b510836014001fb6c38a89de588adc8cdbb1fe5deb4d6d",
82
82
  "github_zip_sha256": "056eb2d1a3f0fa721a7a0e597cd0409e496618cfa35791a78d4e184397596754"
83
+ },
84
+ "v0.9.4": {
85
+ "github_tarball_sha256": "fe1c96e612392130a98ec6d9856ec41fb0c055c1a274ee77f143b5e4b8c1fb2f",
86
+ "github_zip_sha256": "494b84aef93f796d78ef164c7b7670ba3dec1d81f66be966cb7a84d77d5ed6e3"
83
87
  }
84
88
  }
85
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.9.4"
7
+ version = "0.9.5"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -66,7 +66,7 @@ from .status_view import (
66
66
  )
67
67
  from .update_check import check_for_update, check_logics_manager_for_update
68
68
 
69
- VERSION = "0.9.4"
69
+ VERSION = "0.9.5"
70
70
 
71
71
 
72
72
  # ---------------------------------------------------------------------------
@@ -80,9 +80,9 @@ def _print_help(use_color=False):
80
80
  _style("Usage:", "1", use_color),
81
81
  f" {_style('cdx', '36', use_color)}",
82
82
  f" {_style('cdx --json', '36', use_color)}",
83
- f" {_style('cdx status [--json] [--refresh]', '36', use_color)}",
84
- f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
85
- f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
83
+ f" {_style('cdx status [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
84
+ f" {_style('cdx status --small|-s [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
85
+ f" {_style('cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
86
86
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
87
87
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
88
88
  f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
@@ -52,7 +52,7 @@ from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_l
52
52
  from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
53
53
 
54
54
 
55
- STATUS_USAGE = "Usage: cdx status [--json] [--refresh] | cdx status --small|-s [--refresh] | cdx status <name> [--json] [--refresh]"
55
+ STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | cdx status --small|-s [--refresh|--cached] [--timeout SECONDS] | cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]"
56
56
  DOCTOR_USAGE = "Usage: cdx doctor [--json]"
57
57
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
58
58
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
@@ -672,13 +672,13 @@ def _parse_next_args(args):
672
672
  return {"json": parsed["json"], "refresh": parsed["refresh"]}
673
673
 
674
674
 
675
- def _parse_timeout_seconds(value):
675
+ def _parse_timeout_seconds(value, usage=RUN_USAGE):
676
676
  try:
677
677
  parsed = float(value)
678
678
  except (TypeError, ValueError) as error:
679
- raise CdxError(RUN_USAGE) from error
679
+ raise CdxError(usage) from error
680
680
  if parsed <= 0:
681
- raise CdxError(RUN_USAGE)
681
+ raise CdxError(usage)
682
682
  return parsed
683
683
 
684
684
 
@@ -2487,18 +2487,26 @@ def handle_status(rest, ctx):
2487
2487
  "--small": {"key": "small", "type": "bool", "default": False},
2488
2488
  "-s": {"key": "small", "type": "bool", "default": False},
2489
2489
  "--refresh": {"key": "refresh", "type": "bool", "default": False},
2490
+ "--cached": {"key": "cached", "type": "bool", "default": False},
2491
+ "--timeout": {"key": "timeout", "type": "str", "default": None, "transform": lambda value: _parse_timeout_seconds(value, STATUS_USAGE)},
2490
2492
  }, STATUS_USAGE, positionals_key="args", max_positionals=1)
2491
2493
  if parsed["json"] and parsed["small"]:
2492
2494
  raise CdxError(STATUS_USAGE)
2495
+ if parsed["refresh"] and parsed["cached"]:
2496
+ raise CdxError(STATUS_USAGE)
2493
2497
  args = parsed["args"]
2494
2498
  if len(args) == 1 and parsed["small"]:
2495
2499
  raise CdxError(STATUS_USAGE)
2496
2500
 
2497
- auth_refresh = _refresh_claude_auth_states(
2498
- ctx["service"],
2499
- target_names=args if len(args) == 1 else None,
2500
- spawn_sync=ctx.get("spawn_sync"),
2501
- env_override=ctx.get("env"),
2501
+ auth_refresh = (
2502
+ {"errors": []}
2503
+ if parsed["cached"]
2504
+ else _refresh_claude_auth_states(
2505
+ ctx["service"],
2506
+ target_names=args if len(args) == 1 else None,
2507
+ spawn_sync=ctx.get("spawn_sync"),
2508
+ env_override=ctx.get("env"),
2509
+ )
2502
2510
  )
2503
2511
  warnings = [
2504
2512
  {
@@ -2508,11 +2516,15 @@ def handle_status(rest, ctx):
2508
2516
  }
2509
2517
  for item in auth_refresh.get("errors", [])
2510
2518
  ]
2511
- refresh_result = _refresh_claude_sessions(
2512
- ctx["service"],
2513
- ctx.get("refresh_fn"),
2514
- target_names=args if len(args) == 1 else None,
2515
- force=parsed["refresh"],
2519
+ refresh_result = (
2520
+ {"errors": []}
2521
+ if parsed["cached"]
2522
+ else _refresh_claude_sessions(
2523
+ ctx["service"],
2524
+ ctx.get("refresh_fn"),
2525
+ target_names=args if len(args) == 1 else None,
2526
+ force=parsed["refresh"],
2527
+ )
2516
2528
  )
2517
2529
  refresh_errors = [
2518
2530
  {
@@ -2532,26 +2544,32 @@ def handle_status(rest, ctx):
2532
2544
  warnings.extend(_update_notice_warnings(ctx))
2533
2545
 
2534
2546
  status_progress = None if parsed["json"] else _make_status_progress(ctx)
2535
- rows = ctx["service"]["get_status_rows"](
2536
- progress_callback=status_progress,
2537
- force_refresh=parsed["refresh"],
2538
- )
2539
- if len(args) == 0:
2547
+ if len(args) == 1:
2548
+ row = ctx["service"]["get_status_row"](
2549
+ args[0],
2550
+ progress_callback=status_progress,
2551
+ force_refresh=parsed["refresh"],
2552
+ cache_only=parsed["cached"],
2553
+ status_timeout_seconds=parsed["timeout"],
2554
+ )
2540
2555
  if parsed["json"]:
2541
- _write_json(ctx, _json_success("status", "Collected session status rows", warnings=warnings, rows=rows))
2556
+ _write_json(ctx, _json_success("status", f"Collected status for {args[0]}", warnings=warnings, session=row))
2542
2557
  return 0
2543
- ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=parsed['small'])}\n")
2558
+ ctx["out"](f"{_format_status_detail(row, use_color=ctx['use_color'])}\n")
2544
2559
  _write_refresh_warnings(refresh_errors, ctx)
2545
2560
  _write_update_notice(ctx)
2546
2561
  return 0
2547
2562
 
2548
- row = next((r for r in rows if r["session_name"] == args[0]), None)
2549
- if not row:
2550
- raise CdxError(f"Unknown session: {args[0]}")
2563
+ rows = ctx["service"]["get_status_rows"](
2564
+ progress_callback=status_progress,
2565
+ force_refresh=parsed["refresh"],
2566
+ cache_only=parsed["cached"],
2567
+ status_timeout_seconds=parsed["timeout"],
2568
+ )
2551
2569
  if parsed["json"]:
2552
- _write_json(ctx, _json_success("status", f"Collected status for {args[0]}", warnings=warnings, session=row))
2570
+ _write_json(ctx, _json_success("status", "Collected session status rows", warnings=warnings, rows=rows))
2553
2571
  return 0
2554
- ctx["out"](f"{_format_status_detail(row, use_color=ctx['use_color'])}\n")
2572
+ ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=parsed['small'])}\n")
2555
2573
  _write_refresh_warnings(refresh_errors, ctx)
2556
2574
  _write_update_notice(ctx)
2557
2575
  return 0
@@ -67,6 +67,7 @@ RESERVED_SESSION_NAMES = {
67
67
  }
68
68
  STATUS_CACHE_TTL_SECONDS = 60
69
69
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
70
+ STATUS_PROBE_TIMEOUT_SECONDS = 5
70
71
  MAX_STATUS_WORKERS = 8
71
72
  LAUNCH_POWER_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
72
73
  LAUNCH_REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
@@ -278,6 +279,18 @@ def _status_cache_ttl_seconds(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
278
279
  return ttl_seconds
279
280
 
280
281
 
282
+ def _parse_status_timeout_seconds(value):
283
+ if value in (None, ""):
284
+ return None
285
+ try:
286
+ parsed = float(value)
287
+ except (TypeError, ValueError):
288
+ return None
289
+ if parsed <= 0:
290
+ return None
291
+ return parsed
292
+
293
+
281
294
  def _is_status_cache_fresh(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
282
295
  status = session.get("lastStatus") or {}
283
296
  if _is_low_confidence_status_source(status):
@@ -433,7 +446,19 @@ def create_session_service(options=None):
433
446
  env = options.get("env", os.environ)
434
447
  base_dir = options.get("base_dir") or get_cdx_home(env)
435
448
  store = options.get("store") or create_session_store(base_dir)
436
- codex_status_fetcher = options.get("fetchCodexRateLimits") or fetch_codex_rate_limits
449
+ custom_codex_status_fetcher = options.get("fetchCodexRateLimits")
450
+ codex_status_fetcher = custom_codex_status_fetcher or fetch_codex_rate_limits
451
+ default_status_timeout_seconds = (
452
+ _parse_status_timeout_seconds(options.get("statusTimeoutSeconds"))
453
+ or _parse_status_timeout_seconds(env.get("CDX_STATUS_TIMEOUT_SECONDS"))
454
+ or STATUS_PROBE_TIMEOUT_SECONDS
455
+ )
456
+
457
+ def _fetch_codex_status(session, timeout_seconds=None):
458
+ if custom_codex_status_fetcher:
459
+ return custom_codex_status_fetcher(session)
460
+ timeout = timeout_seconds or default_status_timeout_seconds
461
+ return codex_status_fetcher(session, timeout=timeout)
437
462
 
438
463
  def _get_session_root(name):
439
464
  return os.path.join(base_dir, "profiles", _encode(name))
@@ -907,17 +932,28 @@ def create_session_service(options=None):
907
932
  raise CdxError(f"Unknown session: {name}")
908
933
  return store["list_launch_history"](session_name=name, limit=limit)
909
934
 
910
- def _resolve_session_status(session, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
935
+ def _resolve_session_status(
936
+ session,
937
+ force_refresh=False,
938
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
939
+ cache_only=False,
940
+ status_timeout_seconds=None,
941
+ ):
911
942
  current_status = session.get("lastStatus")
912
943
  if session.get("enabled", True) is False:
913
944
  return current_status
945
+ if cache_only:
946
+ return current_status
914
947
  if current_status and not force_refresh and _is_status_cache_fresh(session, ttl_seconds=cache_ttl_seconds):
915
948
  return current_status
916
949
  source_root = session.get("authHome") or _get_session_auth_home(
917
950
  session["name"], session["provider"]
918
951
  )
919
952
  if session["provider"] == PROVIDER_CODEX and codex_status_fetcher:
920
- live_status = codex_status_fetcher({**session, "authHome": source_root})
953
+ live_status = _fetch_codex_status(
954
+ {**session, "authHome": source_root},
955
+ timeout_seconds=status_timeout_seconds,
956
+ )
921
957
  if live_status:
922
958
  record_status(session["name"], live_status)
923
959
  return live_status
@@ -983,41 +1019,133 @@ def create_session_service(options=None):
983
1019
  raise CdxError(f"Unknown session: {name}")
984
1020
  return updated
985
1021
 
986
- def get_status_rows(progress_callback=None, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
987
- sessions = list_sessions()
988
-
989
- def _status_cache_hit(s):
990
- return (
991
- s.get("enabled", True) is False
992
- or (
993
- s.get("lastStatus")
994
- and not force_refresh
995
- and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
996
- )
1022
+ def _status_cache_hit(s, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS, cache_only=False):
1023
+ return (
1024
+ cache_only
1025
+ or s.get("enabled", True) is False
1026
+ or (
1027
+ s.get("lastStatus")
1028
+ and not force_refresh
1029
+ and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
997
1030
  )
1031
+ )
998
1032
 
999
- cache_hits = {
1000
- s["name"]: _status_cache_hit(s)
1001
- for s in sessions
1033
+ def _resolve_row_session(
1034
+ s,
1035
+ force_refresh=False,
1036
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
1037
+ cache_only=False,
1038
+ status_timeout_seconds=None,
1039
+ ):
1040
+ status = _resolve_session_status(
1041
+ s,
1042
+ force_refresh=force_refresh,
1043
+ cache_ttl_seconds=cache_ttl_seconds,
1044
+ cache_only=cache_only,
1045
+ status_timeout_seconds=status_timeout_seconds,
1046
+ )
1047
+ return {
1048
+ **s,
1049
+ "lastStatus": status,
1050
+ "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
1002
1051
  }
1052
+
1053
+ def _status_row_from_session(s):
1054
+ status = s.get("lastStatus")
1055
+ enabled = s.get("enabled", True) is not False
1056
+ row_status = status if enabled else None
1057
+ return {
1058
+ "session_name": s["name"],
1059
+ "provider": s["provider"],
1060
+ "enabled": enabled,
1061
+ "active": bool(_session_runtime(s["name"])) if enabled else False,
1062
+ "status": "enabled" if enabled else "disabled",
1063
+ "auth_status": (s.get("auth") or {}).get("status") or "unknown",
1064
+ "auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
1065
+ "remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
1066
+ "remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
1067
+ "credits": row_status.get("credits") if row_status else None,
1068
+ "available_pct": _compute_available_pct(row_status),
1069
+ "reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
1070
+ "reset_week_at": row_status.get("reset_week_at") if row_status else None,
1071
+ "reset_at": row_status.get("reset_at") if row_status else None,
1072
+ "updated_at": _to_local_iso(s.get("lastStatusAt")),
1073
+ "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
1074
+ }
1075
+
1076
+ def get_status_row(
1077
+ name,
1078
+ progress_callback=None,
1079
+ force_refresh=False,
1080
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
1081
+ cache_only=False,
1082
+ status_timeout_seconds=None,
1083
+ ):
1084
+ session = store["get_session"](name)
1085
+ if not session:
1086
+ raise CdxError(f"Unknown session: {name}")
1087
+ cache_hit = _status_cache_hit(
1088
+ session,
1089
+ force_refresh=force_refresh,
1090
+ cache_ttl_seconds=cache_ttl_seconds,
1091
+ cache_only=cache_only,
1092
+ )
1003
1093
  if progress_callback:
1004
1094
  progress_callback({
1005
1095
  "event": "status_started",
1006
- "session_count": len(sessions),
1007
- "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
1096
+ "session_count": 1,
1097
+ "check_count": 0 if cache_hit else 1,
1098
+ })
1099
+ if not cache_hit:
1100
+ progress_callback({
1101
+ "event": "session_started",
1102
+ "session_name": session["name"],
1103
+ "provider": session["provider"],
1104
+ })
1105
+ resolved = _resolve_row_session(
1106
+ session,
1107
+ force_refresh=force_refresh,
1108
+ cache_ttl_seconds=cache_ttl_seconds,
1109
+ cache_only=cache_only,
1110
+ status_timeout_seconds=status_timeout_seconds,
1111
+ )
1112
+ if progress_callback:
1113
+ progress_callback({
1114
+ "event": "session_finished",
1115
+ "session_name": session["name"],
1116
+ "has_status": bool(resolved.get("lastStatus")),
1117
+ "cache_hit": cache_hit,
1118
+ })
1119
+ progress_callback({
1120
+ "event": "status_finished",
1121
+ "row_count": 1,
1008
1122
  })
1123
+ return _status_row_from_session(resolved)
1124
+
1125
+ def get_status_rows(
1126
+ progress_callback=None,
1127
+ force_refresh=False,
1128
+ cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS,
1129
+ cache_only=False,
1130
+ status_timeout_seconds=None,
1131
+ ):
1132
+ sessions = list_sessions()
1009
1133
 
1010
- def _resolve_row_session(s):
1011
- status = _resolve_session_status(
1134
+ cache_hits = {
1135
+ s["name"]: _status_cache_hit(
1012
1136
  s,
1013
1137
  force_refresh=force_refresh,
1014
1138
  cache_ttl_seconds=cache_ttl_seconds,
1139
+ cache_only=cache_only,
1015
1140
  )
1016
- return {
1017
- **s,
1018
- "lastStatus": status,
1019
- "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
1020
- }
1141
+ for s in sessions
1142
+ }
1143
+ if progress_callback:
1144
+ progress_callback({
1145
+ "event": "status_started",
1146
+ "session_count": len(sessions),
1147
+ "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
1148
+ })
1021
1149
 
1022
1150
  resolved_by_name = {}
1023
1151
  if sessions:
@@ -1032,7 +1160,14 @@ def create_session_service(options=None):
1032
1160
  "session_name": s["name"],
1033
1161
  "provider": s["provider"],
1034
1162
  })
1035
- futures[executor.submit(_resolve_row_session, s)] = s
1163
+ futures[executor.submit(
1164
+ _resolve_row_session,
1165
+ s,
1166
+ force_refresh=force_refresh,
1167
+ cache_ttl_seconds=cache_ttl_seconds,
1168
+ cache_only=cache_only,
1169
+ status_timeout_seconds=status_timeout_seconds,
1170
+ )] = s
1036
1171
  for future in as_completed(futures):
1037
1172
  s = futures[future]
1038
1173
  resolved = future.result()
@@ -1060,27 +1195,7 @@ def create_session_service(options=None):
1060
1195
 
1061
1196
  rows = []
1062
1197
  for s in resolved:
1063
- status = s.get("lastStatus")
1064
- enabled = s.get("enabled", True) is not False
1065
- row_status = status if enabled else None
1066
- rows.append({
1067
- "session_name": s["name"],
1068
- "provider": s["provider"],
1069
- "enabled": enabled,
1070
- "active": bool(_session_runtime(s["name"])) if enabled else False,
1071
- "status": "enabled" if enabled else "disabled",
1072
- "auth_status": (s.get("auth") or {}).get("status") or "unknown",
1073
- "auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
1074
- "remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
1075
- "remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
1076
- "credits": row_status.get("credits") if row_status else None,
1077
- "available_pct": _compute_available_pct(row_status),
1078
- "reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
1079
- "reset_week_at": row_status.get("reset_week_at") if row_status else None,
1080
- "reset_at": row_status.get("reset_at") if row_status else None,
1081
- "updated_at": _to_local_iso(s.get("lastStatusAt")),
1082
- "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
1083
- })
1198
+ rows.append(_status_row_from_session(s))
1084
1199
  if progress_callback:
1085
1200
  progress_callback({
1086
1201
  "event": "status_finished",
@@ -1295,6 +1410,7 @@ def create_session_service(options=None):
1295
1410
  "record_launch_history": record_launch_history,
1296
1411
  "get_launch_history": get_launch_history,
1297
1412
  "update_auth_state": update_auth_state,
1413
+ "get_status_row": get_status_row,
1298
1414
  "get_status_rows": get_status_rows,
1299
1415
  "format_list_rows": format_list_rows,
1300
1416
  "get_session_auth_home": get_session_auth_home,