cdx-manager 0.9.3 → 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.1-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.
@@ -329,6 +330,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
329
330
  | `cdx --json` | List all sessions as a machine-readable JSON payload |
330
331
  | `cdx <name>` | Launch a session (checks auth first) |
331
332
  | `cdx <name> [--json]` | Launch a session; `--json` returns a structured success payload after the interactive run ends |
333
+ | `cdx <name> -r` / `cdx <name> --resume` | Resume the provider-native conversation for a session when supported |
332
334
  | `cdx add [provider] <name> [--model MODEL] [--json]` | Register a new session (`provider`: `codex`, `claude`, `antigravity`, or `ollama`; Ollama requires `--model`) |
333
335
  | `cdx cp <source> <dest> [--json]` | Copy a session into another session name, overwriting the destination if it exists |
334
336
  | `cdx ren <source> <dest> [--json]` | Rename a session and move its auth data |
@@ -343,6 +345,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
343
345
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--logics\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
344
346
  | `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
345
347
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
348
+ | `cdx resume <name> [--json]` | Resume the provider-native conversation for a session using the named command form |
349
+ | `cdx can-resume <name> [--json]` | Check whether a session supports native resume without launching the provider |
346
350
  | `cdx context show\|path\|init\|edit\|clear\|set [text...] [--json]` | Manage the shared Markdown context for the current workspace |
347
351
  | `cdx handoff <name> [--json]` | Install the current workspace context into a target session and launch it unless `--json` is used |
348
352
  | `cdx handoff <source> <target> [--json]` | Build shared context from the source session's latest launch transcript, install it into the target session, and launch the target unless `--json` is used; supports cross-provider handoff |
@@ -361,9 +365,9 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
361
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 |
362
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 |
363
367
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
364
- | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
365
- | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
366
- | `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 |
367
371
  | `cdx --help` | Show usage |
368
372
  | `cdx --version` | Show version |
369
373
 
@@ -426,7 +430,7 @@ Most commands use a shared stderr JSON envelope for errors whenever `--json` is
426
430
  "ok": false,
427
431
  "error": {
428
432
  "code": "invalid_usage",
429
- "message": "Usage: cdx status [--json] [--refresh] | ...",
433
+ "message": "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | ...",
430
434
  "exit_code": 1
431
435
  }
432
436
  }
@@ -0,0 +1,43 @@
1
+ # CDX Manager 0.9.4
2
+
3
+ ## Highlights
4
+
5
+ - Added provider-native resume commands for named sessions.
6
+ - Added a non-launching capability check for resume support.
7
+ - Closed the Logics workflow for the resume command delivery.
8
+
9
+ ## Changes
10
+
11
+ ### Provider-native resume commands
12
+
13
+ `cdx` can now resume supported provider conversations without requiring users to remember provider-specific commands:
14
+
15
+ ```
16
+ cdx main -r
17
+ cdx main --resume
18
+ cdx resume main
19
+ ```
20
+
21
+ Codex sessions resume with `codex resume --last --cd <cwd>` inside the named session's isolated `CODEX_HOME`. Claude sessions resume with `claude --continue --name <name>` inside the named session's isolated `HOME`.
22
+
23
+ Providers without a verified native resume mode, currently Antigravity and Ollama, return a clear unsupported result instead of falling back to a normal launch.
24
+
25
+ ### Resume capability checks
26
+
27
+ `cdx can-resume <name>` reports whether a session can resume without launching an interactive provider. JSON mode exposes a provider-neutral payload with the session name, provider, resumable state, strategy, reason, and command preview.
28
+
29
+ ### Workflow traceability
30
+
31
+ The Logics request, backlog item, task, and ADR for provider-native resume are complete, with validation evidence attached to the delivery task.
32
+
33
+ ## Validation
34
+
35
+ - `python -m unittest discover -s test -p 'test_runtime_py.py' -k resume`
36
+ - `python -m unittest discover -s test -p 'test_cli_py.py' -k resume`
37
+ - `python -m unittest discover -s test -p 'test_cli_py.py' -k help`
38
+ - `python -m unittest discover -s test -p 'test_*_py.py' -k resume`
39
+ - `npm run lint`
40
+ - `npm test`
41
+ - `logics-manager lint --require-status`
42
+ - `logics-manager audit`
43
+ - `git diff --check`
@@ -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/*`
@@ -76,6 +76,14 @@
76
76
  "v0.9.2": {
77
77
  "github_tarball_sha256": "3e3ae4e4efc63a97dc6623ae8ddff36f04f4b7a0b531878a28c041298223d0b8",
78
78
  "github_zip_sha256": "09acd2866770f4dc7f9aaba6aff8308766bb211dabddd7f28bdfde9ace427e78"
79
+ },
80
+ "v0.9.3": {
81
+ "github_tarball_sha256": "cb3cfd9134447d1049b510836014001fb6c38a89de588adc8cdbb1fe5deb4d6d",
82
+ "github_zip_sha256": "056eb2d1a3f0fa721a7a0e597cd0409e496618cfa35791a78d4e184397596754"
83
+ },
84
+ "v0.9.4": {
85
+ "github_tarball_sha256": "fe1c96e612392130a98ec6d9856ec41fb0c055c1a274ee77f143b5e4b8c1fb2f",
86
+ "github_zip_sha256": "494b84aef93f796d78ef164c7b7670ba3dec1d81f66be966cb7a84d77d5ed6e3"
79
87
  }
80
88
  }
81
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.3",
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.3"
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
@@ -11,6 +11,7 @@ from .cli_commands import (
11
11
  handle_clean,
12
12
  handle_config,
13
13
  handle_configs,
14
+ handle_can_resume,
14
15
  handle_context,
15
16
  handle_copy,
16
17
  handle_doctor,
@@ -30,6 +31,7 @@ from .cli_commands import (
30
31
  handle_remove,
31
32
  handle_repair,
32
33
  handle_rename,
34
+ handle_resume,
33
35
  handle_run,
34
36
  handle_run_report,
35
37
  handle_run_status,
@@ -64,7 +66,7 @@ from .status_view import (
64
66
  )
65
67
  from .update_check import check_for_update, check_logics_manager_for_update
66
68
 
67
- VERSION = "0.9.3"
69
+ VERSION = "0.9.5"
68
70
 
69
71
 
70
72
  # ---------------------------------------------------------------------------
@@ -78,9 +80,9 @@ def _print_help(use_color=False):
78
80
  _style("Usage:", "1", use_color),
79
81
  f" {_style('cdx', '36', use_color)}",
80
82
  f" {_style('cdx --json', '36', use_color)}",
81
- f" {_style('cdx status [--json] [--refresh]', '36', use_color)}",
82
- f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
83
- 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)}",
84
86
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
85
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)}",
86
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)}",
@@ -96,6 +98,8 @@ def _print_help(use_color=False):
96
98
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
97
99
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
98
100
  f" {_style('cdx last [--json]', '36', use_color)}",
101
+ f" {_style('cdx resume <name> [--json]', '36', use_color)}",
102
+ f" {_style('cdx can-resume <name> [--json]', '36', use_color)}",
99
103
  f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
100
104
  f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
101
105
  f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
@@ -283,7 +287,7 @@ def main(argv, options=None):
283
287
  "version": VERSION,
284
288
  "cwd": options.get("cwd") or os.getcwd(),
285
289
  "update_notices": _get_update_notices(service, env, options) if command not in (
286
- "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
290
+ "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "resume", "can-resume", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
287
291
  ) else None,
288
292
  "use_color": use_color,
289
293
  }
@@ -365,6 +369,12 @@ def main(argv, options=None):
365
369
  if command == "last":
366
370
  return handle_last(rest, ctx)
367
371
 
372
+ if command == "resume":
373
+ return handle_resume(rest, ctx)
374
+
375
+ if command == "can-resume":
376
+ return handle_can_resume(rest, ctx)
377
+
368
378
  if command == "handoff":
369
379
  return handle_handoff(rest, ctx)
370
380
 
@@ -400,8 +410,8 @@ def main(argv, options=None):
400
410
  out(f"{_print_version()}\n")
401
411
  return 0
402
412
 
403
- if not rest or rest == ["--json"]:
404
- return handle_launch(command, ctx)
413
+ if all(arg in ("--json", "-r", "--resume") for arg in rest):
414
+ return handle_launch(command, ctx, resume=("-r" in rest or "--resume" in rest))
405
415
 
406
416
  raise CdxError(f"Unknown command: {command}. Use cdx --help.")
407
417
 
@@ -3,6 +3,7 @@ import getpass
3
3
  import json
4
4
  import os
5
5
  import re
6
+ import shlex
6
7
  import sys
7
8
  import time
8
9
  from datetime import datetime, timedelta
@@ -37,6 +38,7 @@ from .provider_runtime import (
37
38
  _list_launch_transcript_paths,
38
39
  _normalize_reasoning_effort,
39
40
  _probe_provider_auth,
41
+ get_resume_capability,
40
42
  _run_headless_provider_command,
41
43
  _run_interactive_provider_command,
42
44
  )
@@ -50,7 +52,7 @@ from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_l
50
52
  from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
51
53
 
52
54
 
53
- 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]"
54
56
  DOCTOR_USAGE = "Usage: cdx doctor [--json]"
55
57
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
56
58
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
@@ -66,6 +68,8 @@ CONFIGS_USAGE = "Usage: cdx configs [--json]"
66
68
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
67
69
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
68
70
  LAST_USAGE = "Usage: cdx last [--json]"
71
+ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
72
+ CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
69
73
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
70
74
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
71
75
  RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--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"
@@ -668,13 +672,13 @@ def _parse_next_args(args):
668
672
  return {"json": parsed["json"], "refresh": parsed["refresh"]}
669
673
 
670
674
 
671
- def _parse_timeout_seconds(value):
675
+ def _parse_timeout_seconds(value, usage=RUN_USAGE):
672
676
  try:
673
677
  parsed = float(value)
674
678
  except (TypeError, ValueError) as error:
675
- raise CdxError(RUN_USAGE) from error
679
+ raise CdxError(usage) from error
676
680
  if parsed <= 0:
677
- raise CdxError(RUN_USAGE)
681
+ raise CdxError(usage)
678
682
  return parsed
679
683
 
680
684
 
@@ -2483,18 +2487,26 @@ def handle_status(rest, ctx):
2483
2487
  "--small": {"key": "small", "type": "bool", "default": False},
2484
2488
  "-s": {"key": "small", "type": "bool", "default": False},
2485
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)},
2486
2492
  }, STATUS_USAGE, positionals_key="args", max_positionals=1)
2487
2493
  if parsed["json"] and parsed["small"]:
2488
2494
  raise CdxError(STATUS_USAGE)
2495
+ if parsed["refresh"] and parsed["cached"]:
2496
+ raise CdxError(STATUS_USAGE)
2489
2497
  args = parsed["args"]
2490
2498
  if len(args) == 1 and parsed["small"]:
2491
2499
  raise CdxError(STATUS_USAGE)
2492
2500
 
2493
- auth_refresh = _refresh_claude_auth_states(
2494
- ctx["service"],
2495
- target_names=args if len(args) == 1 else None,
2496
- spawn_sync=ctx.get("spawn_sync"),
2497
- 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
+ )
2498
2510
  )
2499
2511
  warnings = [
2500
2512
  {
@@ -2504,11 +2516,15 @@ def handle_status(rest, ctx):
2504
2516
  }
2505
2517
  for item in auth_refresh.get("errors", [])
2506
2518
  ]
2507
- refresh_result = _refresh_claude_sessions(
2508
- ctx["service"],
2509
- ctx.get("refresh_fn"),
2510
- target_names=args if len(args) == 1 else None,
2511
- 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
+ )
2512
2528
  )
2513
2529
  refresh_errors = [
2514
2530
  {
@@ -2528,26 +2544,32 @@ def handle_status(rest, ctx):
2528
2544
  warnings.extend(_update_notice_warnings(ctx))
2529
2545
 
2530
2546
  status_progress = None if parsed["json"] else _make_status_progress(ctx)
2531
- rows = ctx["service"]["get_status_rows"](
2532
- progress_callback=status_progress,
2533
- force_refresh=parsed["refresh"],
2534
- )
2535
- 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
+ )
2536
2555
  if parsed["json"]:
2537
- _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))
2538
2557
  return 0
2539
- 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")
2540
2559
  _write_refresh_warnings(refresh_errors, ctx)
2541
2560
  _write_update_notice(ctx)
2542
2561
  return 0
2543
2562
 
2544
- row = next((r for r in rows if r["session_name"] == args[0]), None)
2545
- if not row:
2546
- 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
+ )
2547
2569
  if parsed["json"]:
2548
- _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))
2549
2571
  return 0
2550
- 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")
2551
2573
  _write_refresh_warnings(refresh_errors, ctx)
2552
2574
  _write_update_notice(ctx)
2553
2575
  return 0
@@ -2844,10 +2866,83 @@ def handle_update(rest, ctx):
2844
2866
  return 0
2845
2867
 
2846
2868
 
2847
- def handle_launch(command, ctx, initial_prompt=None):
2869
+ def _resume_capability_for_session(session, ctx):
2870
+ capability = get_resume_capability(session, cwd=ctx.get("cwd") or os.getcwd())
2871
+ return {
2872
+ "session": session["name"],
2873
+ "provider": session["provider"],
2874
+ "resumable": bool(capability.get("resumable")),
2875
+ "strategy": capability.get("strategy"),
2876
+ "reason": capability.get("reason"),
2877
+ "command_preview": capability.get("command_preview") or [],
2878
+ }
2879
+
2880
+
2881
+ def _format_resume_capability(capability, use_color=False):
2882
+ name = capability["session"]
2883
+ provider = capability["provider"]
2884
+ if capability["resumable"]:
2885
+ preview = shlex.join(capability.get("command_preview") or [])
2886
+ detail = f"({provider}, {capability['strategy']})"
2887
+ return (
2888
+ f"{_success(f'{name} can resume', use_color)} "
2889
+ f"{_dim(detail, use_color)}\n"
2890
+ f"{_dim(preview, use_color)}"
2891
+ )
2892
+ reason = capability.get("reason") or "not_supported"
2893
+ return (
2894
+ f"{_warn(f'{name} cannot resume', use_color)} "
2895
+ f"{_dim(f'({provider}, {reason})', use_color)}"
2896
+ )
2897
+
2898
+
2899
+ def handle_can_resume(rest, ctx):
2900
+ json_flag, args = _parse_json_flag(rest)
2901
+ if len(args) != 1:
2902
+ raise CdxError(CAN_RESUME_USAGE)
2903
+ session = ctx["service"]["get_session"](args[0])
2904
+ if not session:
2905
+ raise CdxError(f"Unknown session: {args[0]}")
2906
+ if session.get("enabled", True) is False:
2907
+ capability = {
2908
+ "session": session["name"],
2909
+ "provider": session["provider"],
2910
+ "resumable": False,
2911
+ "strategy": "session_disabled",
2912
+ "reason": "session_disabled",
2913
+ "command_preview": [],
2914
+ }
2915
+ else:
2916
+ capability = _resume_capability_for_session(session, ctx)
2917
+ if json_flag:
2918
+ _write_json(ctx, {
2919
+ "schema_version": API_SCHEMA_VERSION,
2920
+ "ok": True,
2921
+ **capability,
2922
+ })
2923
+ return 0
2924
+ ctx["out"](f"{_format_resume_capability(capability, ctx['use_color'])}\n")
2925
+ return 0
2926
+
2927
+
2928
+ def handle_resume(rest, ctx):
2929
+ json_flag, args = _parse_json_flag(rest)
2930
+ if len(args) != 1:
2931
+ raise CdxError(RESUME_USAGE)
2932
+ return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
2933
+
2934
+
2935
+ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
2848
2936
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
2937
+ if force_json is not None:
2938
+ json_flag = force_json
2849
2939
  warnings = _update_notice_warnings(ctx)
2850
2940
  session = ctx["service"]["launch_session"](command)
2941
+ capability = _resume_capability_for_session(session, ctx) if resume else None
2942
+ if capability and not capability["resumable"]:
2943
+ raise CdxError(
2944
+ f"Provider {session['provider']} does not support native resume through cdx."
2945
+ )
2851
2946
  _ensure_session_authentication(
2852
2947
  session,
2853
2948
  ctx["service"],
@@ -2859,7 +2954,11 @@ def handle_launch(command, ctx, initial_prompt=None):
2859
2954
  signal_emitter=ctx.get("signal_emitter"),
2860
2955
  trust_local_credentials=False,
2861
2956
  )
2862
- message = f"Launching {session['provider']} session {session['name']}"
2957
+ message = (
2958
+ f"Resuming {session['provider']} session {session['name']}"
2959
+ if resume else
2960
+ f"Launching {session['provider']} session {session['name']}"
2961
+ )
2863
2962
  if not json_flag:
2864
2963
  ctx["out"](f"{_info(message, ctx['use_color'])}\n")
2865
2964
  _write_update_notice(ctx)
@@ -2880,7 +2979,7 @@ def handle_launch(command, ctx, initial_prompt=None):
2880
2979
 
2881
2980
  try:
2882
2981
  run_info = _run_interactive_provider_command(
2883
- session, "launch", spawn=ctx.get("spawn"), cwd=cwd, env_override=ctx.get("env"),
2982
+ session, "resume" if resume else "launch", spawn=ctx.get("spawn"), cwd=cwd, env_override=ctx.get("env"),
2884
2983
  signal_emitter=ctx.get("signal_emitter"), initial_prompt=initial_prompt,
2885
2984
  lifecycle_callback=runtime_lifecycle,
2886
2985
  )
@@ -2903,12 +3002,16 @@ def handle_launch(command, ctx, initial_prompt=None):
2903
3002
  raise
2904
3003
  ctx["service"]["record_launch_history"](session["name"], {
2905
3004
  "status": "success",
3005
+ "action": "resume" if resume else "launch",
2906
3006
  "cwd": cwd,
2907
3007
  "exit_code": 0,
2908
3008
  **run_info,
2909
3009
  })
2910
3010
  if json_flag:
2911
- _write_json(ctx, _json_success("launch", message, warnings=warnings, session=ctx["service"]["get_session"](session["name"])))
3011
+ extra = {"session": ctx["service"]["get_session"](session["name"])}
3012
+ if capability:
3013
+ extra["resume"] = capability
3014
+ _write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))
2912
3015
  return 0
2913
3016
 
2914
3017
 
@@ -511,6 +511,90 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
511
511
  }, capture_transcript=capture_transcript, env=env)
512
512
 
513
513
 
514
+ def _redacted_resume_command_preview(session, cwd=None):
515
+ capability = get_resume_capability(session, cwd=cwd)
516
+ return capability.get("command_preview")
517
+
518
+
519
+ def get_resume_capability(session, cwd=None):
520
+ provider = session.get("provider")
521
+ cwd = cwd or os.getcwd()
522
+ if provider == PROVIDER_CODEX:
523
+ return {
524
+ "resumable": True,
525
+ "provider": provider,
526
+ "strategy": "provider_last",
527
+ "reason": "supported",
528
+ "command_preview": ["codex", "resume", "--last", "--cd", cwd],
529
+ }
530
+ if provider == PROVIDER_CLAUDE:
531
+ return {
532
+ "resumable": True,
533
+ "provider": provider,
534
+ "strategy": "provider_continue",
535
+ "reason": "supported",
536
+ "command_preview": ["claude", "--continue"],
537
+ }
538
+ return {
539
+ "resumable": False,
540
+ "provider": provider,
541
+ "strategy": "not_supported",
542
+ "reason": "not_supported",
543
+ "command_preview": [],
544
+ }
545
+
546
+
547
+ def _build_resume_spec(session, cwd=None, env_override=None, capture_transcript=True):
548
+ cwd = cwd or os.getcwd()
549
+ env_override = env_override or {}
550
+ env = {**os.environ, **env_override}
551
+ capability = get_resume_capability(session, cwd=cwd)
552
+ if not capability["resumable"]:
553
+ raise CdxError(f"Provider {session.get('provider')} does not support native resume through cdx.")
554
+
555
+ resume_prompt = _with_launch_preferences(session, env=env)
556
+ _validate_initial_prompt(resume_prompt)
557
+
558
+ if session["provider"] == PROVIDER_CLAUDE:
559
+ launch = session.get("launch") or {}
560
+ args = ["--continue", "--name", session["name"]]
561
+ if launch.get("model"):
562
+ args += ["--model", _claude_cli_model(launch["model"])]
563
+ args += _launch_config_args(session)
564
+ if resume_prompt:
565
+ args.append(resume_prompt)
566
+ auth_home = _get_auth_home(session)
567
+ claude_env = _claude_env(env, auth_home)
568
+ oauth_token = _read_claude_launch_oauth_token(auth_home)
569
+ if oauth_token:
570
+ claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
571
+ return _wrap_launch_with_transcript(session, {
572
+ "command": "claude",
573
+ "args": args,
574
+ "options": {
575
+ "cwd": cwd,
576
+ "env": claude_env,
577
+ },
578
+ "label": "claude resume",
579
+ }, capture_transcript=capture_transcript, env=env)
580
+
581
+ launch = session.get("launch") or {}
582
+ args = ["resume", "--last", "--cd", cwd]
583
+ if launch.get("model"):
584
+ args += ["--model", launch["model"]]
585
+ args += _launch_config_args(session)
586
+ if resume_prompt:
587
+ args.append(resume_prompt)
588
+ return _wrap_launch_with_transcript(session, {
589
+ "command": "codex",
590
+ "args": args,
591
+ "options": {
592
+ "env": {**env, "CODEX_HOME": _get_auth_home(session)},
593
+ },
594
+ "label": "codex resume",
595
+ }, capture_transcript=capture_transcript, env=env)
596
+
597
+
514
598
  def _validate_initial_prompt(initial_prompt):
515
599
  if initial_prompt is not None:
516
600
  if not isinstance(initial_prompt, str):
@@ -846,11 +930,14 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
846
930
  env_override=None, signal_emitter=None,
847
931
  initial_prompt=None, lifecycle_callback=None):
848
932
  spawn = spawn or subprocess.Popen
849
- spec = (
850
- _build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
851
- if action == "launch"
852
- else _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
853
- )
933
+ if action == "launch":
934
+ spec = _build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
935
+ elif action == "resume":
936
+ if initial_prompt is not None:
937
+ raise CdxError("initial_prompt is not supported for resume.")
938
+ spec = _build_resume_spec(session, cwd=cwd, env_override=env_override)
939
+ else:
940
+ spec = _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
854
941
  def start_child(current_spec):
855
942
  command = current_spec["command"]
856
943
  if spawn is subprocess.Popen:
@@ -21,6 +21,7 @@ ALLOWED_PROVIDERS = set(PROVIDERS)
21
21
  MAX_SESSION_NAME_LENGTH = 64
22
22
  RESERVED_SESSION_NAMES = {
23
23
  "add",
24
+ "can-resume",
24
25
  "clean",
25
26
  "context",
26
27
  "configs",
@@ -45,6 +46,7 @@ RESERVED_SESSION_NAMES = {
45
46
  "power",
46
47
  "ready",
47
48
  "repair",
49
+ "resume",
48
50
  "ren",
49
51
  "rename",
50
52
  "rmv",
@@ -65,6 +67,7 @@ RESERVED_SESSION_NAMES = {
65
67
  }
66
68
  STATUS_CACHE_TTL_SECONDS = 60
67
69
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
70
+ STATUS_PROBE_TIMEOUT_SECONDS = 5
68
71
  MAX_STATUS_WORKERS = 8
69
72
  LAUNCH_POWER_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
70
73
  LAUNCH_REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
@@ -276,6 +279,18 @@ def _status_cache_ttl_seconds(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
276
279
  return ttl_seconds
277
280
 
278
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
+
279
294
  def _is_status_cache_fresh(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
280
295
  status = session.get("lastStatus") or {}
281
296
  if _is_low_confidence_status_source(status):
@@ -431,7 +446,19 @@ def create_session_service(options=None):
431
446
  env = options.get("env", os.environ)
432
447
  base_dir = options.get("base_dir") or get_cdx_home(env)
433
448
  store = options.get("store") or create_session_store(base_dir)
434
- 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)
435
462
 
436
463
  def _get_session_root(name):
437
464
  return os.path.join(base_dir, "profiles", _encode(name))
@@ -905,17 +932,28 @@ def create_session_service(options=None):
905
932
  raise CdxError(f"Unknown session: {name}")
906
933
  return store["list_launch_history"](session_name=name, limit=limit)
907
934
 
908
- 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
+ ):
909
942
  current_status = session.get("lastStatus")
910
943
  if session.get("enabled", True) is False:
911
944
  return current_status
945
+ if cache_only:
946
+ return current_status
912
947
  if current_status and not force_refresh and _is_status_cache_fresh(session, ttl_seconds=cache_ttl_seconds):
913
948
  return current_status
914
949
  source_root = session.get("authHome") or _get_session_auth_home(
915
950
  session["name"], session["provider"]
916
951
  )
917
952
  if session["provider"] == PROVIDER_CODEX and codex_status_fetcher:
918
- 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
+ )
919
957
  if live_status:
920
958
  record_status(session["name"], live_status)
921
959
  return live_status
@@ -981,41 +1019,133 @@ def create_session_service(options=None):
981
1019
  raise CdxError(f"Unknown session: {name}")
982
1020
  return updated
983
1021
 
984
- def get_status_rows(progress_callback=None, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
985
- sessions = list_sessions()
986
-
987
- def _status_cache_hit(s):
988
- return (
989
- s.get("enabled", True) is False
990
- or (
991
- s.get("lastStatus")
992
- and not force_refresh
993
- and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
994
- )
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)
995
1030
  )
1031
+ )
996
1032
 
997
- cache_hits = {
998
- s["name"]: _status_cache_hit(s)
999
- 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"),
1000
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
+ )
1001
1093
  if progress_callback:
1002
1094
  progress_callback({
1003
1095
  "event": "status_started",
1004
- "session_count": len(sessions),
1005
- "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,
1006
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()
1007
1133
 
1008
- def _resolve_row_session(s):
1009
- status = _resolve_session_status(
1134
+ cache_hits = {
1135
+ s["name"]: _status_cache_hit(
1010
1136
  s,
1011
1137
  force_refresh=force_refresh,
1012
1138
  cache_ttl_seconds=cache_ttl_seconds,
1139
+ cache_only=cache_only,
1013
1140
  )
1014
- return {
1015
- **s,
1016
- "lastStatus": status,
1017
- "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
1018
- }
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
+ })
1019
1149
 
1020
1150
  resolved_by_name = {}
1021
1151
  if sessions:
@@ -1030,7 +1160,14 @@ def create_session_service(options=None):
1030
1160
  "session_name": s["name"],
1031
1161
  "provider": s["provider"],
1032
1162
  })
1033
- 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
1034
1171
  for future in as_completed(futures):
1035
1172
  s = futures[future]
1036
1173
  resolved = future.result()
@@ -1058,27 +1195,7 @@ def create_session_service(options=None):
1058
1195
 
1059
1196
  rows = []
1060
1197
  for s in resolved:
1061
- status = s.get("lastStatus")
1062
- enabled = s.get("enabled", True) is not False
1063
- row_status = status if enabled else None
1064
- rows.append({
1065
- "session_name": s["name"],
1066
- "provider": s["provider"],
1067
- "enabled": enabled,
1068
- "active": bool(_session_runtime(s["name"])) if enabled else False,
1069
- "status": "enabled" if enabled else "disabled",
1070
- "auth_status": (s.get("auth") or {}).get("status") or "unknown",
1071
- "auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
1072
- "remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
1073
- "remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
1074
- "credits": row_status.get("credits") if row_status else None,
1075
- "available_pct": _compute_available_pct(row_status),
1076
- "reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
1077
- "reset_week_at": row_status.get("reset_week_at") if row_status else None,
1078
- "reset_at": row_status.get("reset_at") if row_status else None,
1079
- "updated_at": _to_local_iso(s.get("lastStatusAt")),
1080
- "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
1081
- })
1198
+ rows.append(_status_row_from_session(s))
1082
1199
  if progress_callback:
1083
1200
  progress_callback({
1084
1201
  "event": "status_finished",
@@ -1293,6 +1410,7 @@ def create_session_service(options=None):
1293
1410
  "record_launch_history": record_launch_history,
1294
1411
  "get_launch_history": get_launch_history,
1295
1412
  "update_auth_state": update_auth_state,
1413
+ "get_status_row": get_status_row,
1296
1414
  "get_status_rows": get_status_rows,
1297
1415
  "format_list_rows": format_list_rows,
1298
1416
  "get_session_auth_home": get_session_auth_home,