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 +9 -5
- package/changelogs/CHANGELOGS_0_9_4.md +43 -0
- package/changelogs/CHANGELOGS_0_9_5.md +45 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +17 -7
- package/src/cli_commands.py +133 -30
- package/src/provider_runtime.py +92 -5
- package/src/session_service.py +166 -48
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
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;
|
|
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
package/pyproject.toml
CHANGED
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.
|
|
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
|
|
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
|
|
package/src/cli_commands.py
CHANGED
|
@@ -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(
|
|
679
|
+
raise CdxError(usage) from error
|
|
676
680
|
if parsed <= 0:
|
|
677
|
-
raise CdxError(
|
|
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 =
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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 =
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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
|
|
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"{
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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",
|
|
2570
|
+
_write_json(ctx, _json_success("status", "Collected session status rows", warnings=warnings, rows=rows))
|
|
2549
2571
|
return 0
|
|
2550
|
-
ctx["out"](f"{
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
package/src/provider_runtime.py
CHANGED
|
@@ -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
|
-
|
|
850
|
-
_build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
|
|
851
|
-
|
|
852
|
-
|
|
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:
|
package/src/session_service.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
s.get("
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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":
|
|
1005
|
-
"check_count":
|
|
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
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|