cdx-manager 0.22.0 → 0.23.0
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 +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_23_0.md +19 -0
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +5 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +6 -3
- package/src/cli_args.py +32 -3
- package/src/cli_commands.py +3 -0
- package/src/commands/launch.py +25 -4
- package/src/commands/runs.py +3 -2
- package/src/commands/status.py +38 -0
- package/src/provider_runtime.py +12 -0
- package/src/run_registry.py +51 -2
- package/src/session_status.py +19 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.23.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CDX Manager 0.23.0
|
|
2
|
+
|
|
3
|
+
This release gives external orchestrators a stable way to read runs, live sessions and profile locations, so they no longer need to scan processes, parse provider output or rely on cdx's directory layout.
|
|
4
|
+
|
|
5
|
+
## Runs
|
|
6
|
+
|
|
7
|
+
- `cdx runs --running --json` returns every run still in progress, without the `--limit` cap, so a concurrency check always sees the full count.
|
|
8
|
+
- `cdx run-report` now includes `final_message`: the run's last assistant message, read from Claude's result or Codex's last agent message.
|
|
9
|
+
- The accepted run `kind` values are documented with `cdx runs`; `cdx schema --json` remains their source.
|
|
10
|
+
|
|
11
|
+
## Sessions and profiles
|
|
12
|
+
|
|
13
|
+
- `cdx sessions --json` lists interactive sessions launched through cdx that are still running, with their process id, provider, working directory, start time and conversation id.
|
|
14
|
+
- `cdx sessions --all --json` adds idle accounts. Each row includes `profile_dir` and `transcripts_dir`; the credential directory stays hidden.
|
|
15
|
+
|
|
16
|
+
## Resume
|
|
17
|
+
|
|
18
|
+
- `cdx can-resume <name> --conversation <id>` checks one exact conversation on Claude or Codex.
|
|
19
|
+
- `cdx resume <name> --conversation <id>` reopens that conversation. A missing conversation is refused with `conversation_not_found` rather than replaced by the most recent one.
|
|
@@ -551,6 +551,17 @@
|
|
|
551
551
|
"x86_64-pc-windows-msvc": "65cb725a2ae55350b8d9500f4293cd52e709ee017c999d65de9a1e12b472f7ed",
|
|
552
552
|
"x86_64-unknown-linux-musl": "6ff5479d7d57f329e76ad633e963e0d45f059d28004a3777f406b5bdd0713abe"
|
|
553
553
|
}
|
|
554
|
+
},
|
|
555
|
+
"v0.22.0": {
|
|
556
|
+
"github_tarball_sha256": "06d623fad9ee339e6810792d94b925e3a27f62612b5ac015e8fb236093417bf1",
|
|
557
|
+
"github_zip_sha256": "fbfcc2748282f60968bbfb4127f97b2866890e1bf54e5d92e2b81e5526c92f6d",
|
|
558
|
+
"tray_assets": {
|
|
559
|
+
"aarch64-apple-darwin": "57ec5cec2876eecf62397b66f78cbb5028583e06deedab79b2af17bafc7a8d05",
|
|
560
|
+
"aarch64-unknown-linux-musl": "948110290f030caafe46eebf2db6e5565fc48e5fc4983a0c70d946336f39ec9e",
|
|
561
|
+
"x86_64-apple-darwin": "bb873f982b4af1a3cc53235e615fad2e2be8265defea0def0fde463467fa3495",
|
|
562
|
+
"x86_64-pc-windows-msvc": "37ddbc3464014a04a4fefee4cebeb24f16b3d06995844369bee8360526538d43",
|
|
563
|
+
"x86_64-unknown-linux-musl": "6602e175632b7a191434b627bfef1e430d32da90ce0e23379bdf38700ea9ba7c"
|
|
564
|
+
}
|
|
554
565
|
}
|
|
555
566
|
}
|
|
556
567
|
}
|
package/docs/cli.md
CHANGED
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
| `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--reasoning-effort\|--permission\|--fast\|--rtk\|--logics\|--notify\|--notify-preview\|--scrub-env\|--shared-memory\|--retro\|--policy\|--model\|--fallback-model\|--budget\|--extra-args\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
|
|
29
29
|
| `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 |
|
|
30
30
|
| `cdx last [--json]` | Launch the most recent existing session from launch history |
|
|
31
|
-
| `cdx resume <name> [--json]` | Resume the provider-native conversation for a session using the named command form |
|
|
32
|
-
| `cdx can-resume <name> [--json]` | Check whether a session supports native resume without launching the provider |
|
|
31
|
+
| `cdx resume <name> [--conversation <id>] [--json]` | Resume the provider-native conversation for a session using the named command form; `--conversation` reopens that exact conversation and refuses with `conversation_not_found` if the profile has no such transcript |
|
|
32
|
+
| `cdx can-resume <name> [--conversation <id>] [--json]` | Check whether a session supports native resume without launching the provider; `--conversation` checks that exact conversation and reports `resumable: false` with `conversation_not_found` instead of falling back to the most recent one |
|
|
33
|
+
| `cdx sessions [--all] --json` | Interactive sessions launched through cdx that are still running (`session`, `provider`, `pid`, `cwd`, `started_at`, `conversation_id`, `profile_dir`, `transcripts_dir`); `--all` adds idle enabled accounts so any account's directories can be resolved |
|
|
33
34
|
| `cdx context show\|path\|init\|edit\|clear\|set\|append [text...] [--json]` | Manage the shared Markdown context for the current workspace |
|
|
34
35
|
| `cdx memory [--global\|--project NAME_OR_PATH] [show\|view\|path\|init\|edit\|clear\|set\|append\|list] [text...] [--json]` | Manage explicit user-controlled memory for the current workspace, global scope, or a named/path project |
|
|
35
36
|
| `cdx handoff <name> [--json]` | Reuse the prepared entry for this target/workspace, or use explicitly labelled notes-only recovery if none exists; launch unless `--json` |
|
|
@@ -53,10 +54,10 @@
|
|
|
53
54
|
| `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
|
|
54
55
|
| `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 |
|
|
55
56
|
| `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant\|code-review\|retro] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--failover` continues the task on the next account when this one hits its rate limit, `--prompt-file -` reads the prompt from stdin, `--label` names the run in `cdx runs`, `--max-parallel N` (default `CDX_MAX_RUNS`; unset, invalid, 0 or negative means no limit) refuses to start with `too_many_runs` when N runs are already running, `--kind` (default `assistant`) labels the run record and `code-review` adds a review report to its final payload |
|
|
56
|
-
| `cdx run-report <run_id> --json` | Full report, transcript metadata, and final payload for a run |
|
|
57
|
+
| `cdx run-report <run_id> --json` | Full report, transcript metadata, and final payload for a run; `final_message` is the run's last assistant message, read from Claude's `result` envelope or Codex's last `agent_message` (null when neither exists) |
|
|
57
58
|
| `cdx run-status <run_id> --json` | Status of one run by id |
|
|
58
59
|
| `cdx run-tail <run_id> [--lines N] --json` | Last lines of a run's own output, while it is still running or after it finished |
|
|
59
|
-
| `cdx runs [--limit N] [--since 7d\|today\|DATE] --json` | List recent runs; `--since` returns every run completed after the cursor and ignores `--limit` |
|
|
60
|
+
| `cdx runs [--limit N] [--since 7d\|today\|DATE] [--running] --json` | List recent runs; `--since` returns every run completed after the cursor and ignores `--limit`; `--running` returns every run still running, uncapped. A run's `kind` is one of the values `cdx schema --json` lists under `enums.kind` |
|
|
60
61
|
| `cdx schema --json` | Publish the enums, mutually-exclusive argument groups, and error codes programmatic callers should validate against |
|
|
61
62
|
| `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--by session\|project\|model] [--json]` | Aggregate launch counts, duration, and known token usage by session, for headless and interactive runs alike. `--by project` groups runs by launch folder (labelled by the shortest trailing part that tells folders apart; `project` in JSON holds the full path), `--by model` by the model that served them (same totals, same pricing; default `session`). PROV. lists every provider a row saw, such as `claude+codex`. Without a session filter it also prints what rtk saved over the period (`rtk` in JSON), read with `rtk gain` from `RTK_DB_PATH` when set, else the shared history, and the older per-profile ones. |
|
|
62
63
|
| `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; empty credits, block, and reset columns are omitted; `--cached` skips live provider probes and returns only stored status; Claude rows also report the auth type (`setup-token` or `full-login`) in a TYPE column and as `auth_type` in JSON. |
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -50,6 +50,7 @@ from .cli_commands import (
|
|
|
50
50
|
handle_runs,
|
|
51
51
|
handle_schema,
|
|
52
52
|
handle_select,
|
|
53
|
+
handle_sessions,
|
|
53
54
|
handle_set,
|
|
54
55
|
handle_stats,
|
|
55
56
|
handle_status,
|
|
@@ -187,6 +188,7 @@ _COMMAND_HANDLERS = {
|
|
|
187
188
|
"runs": handle_runs,
|
|
188
189
|
"schema": handle_schema,
|
|
189
190
|
"select": handle_select,
|
|
191
|
+
"sessions": handle_sessions,
|
|
190
192
|
"set": handle_set,
|
|
191
193
|
"stats": handle_stats,
|
|
192
194
|
"status": handle_status,
|
|
@@ -241,9 +243,10 @@ def _print_help(use_color=False):
|
|
|
241
243
|
f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
|
|
242
244
|
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)}",
|
|
243
245
|
f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
|
|
244
|
-
f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] --json', '36', use_color)}",
|
|
246
|
+
f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json', '36', use_color)}",
|
|
245
247
|
f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
|
|
246
248
|
f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
|
|
249
|
+
f" {_style('cdx sessions [--all] --json', '36', use_color)}",
|
|
247
250
|
f" {_style('cdx run-tail <run_id> [--lines N] --json', '36', use_color)}",
|
|
248
251
|
f" {_style('cdx schema --json', '36', use_color)}",
|
|
249
252
|
f" {_style('cdx context show|path|init|edit|clear|set|append [text...] [--json]', '36', use_color)}",
|
|
@@ -257,8 +260,8 @@ def _print_help(use_color=False):
|
|
|
257
260
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
258
261
|
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
|
|
259
262
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
260
|
-
f" {_style('cdx resume <name> [--json]', '36', use_color)}",
|
|
261
|
-
f" {_style('cdx can-resume <name> [--json]', '36', use_color)}",
|
|
263
|
+
f" {_style('cdx resume <name> [--conversation <id>] [--json]', '36', use_color)}",
|
|
264
|
+
f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
|
|
262
265
|
f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
|
|
263
266
|
f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
|
|
264
267
|
f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
|
package/src/cli_args.py
CHANGED
|
@@ -42,8 +42,9 @@ HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|t
|
|
|
42
42
|
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
|
|
43
43
|
STATS_GROUPS = ("session", "project", "model")
|
|
44
44
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
45
|
-
RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
46
|
-
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
45
|
+
RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--json]"
|
|
46
|
+
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
|
|
47
|
+
SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
47
48
|
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"
|
|
48
49
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
49
50
|
RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--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] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json"
|
|
@@ -61,7 +62,7 @@ RUN_PERMISSION_VALUES = PERMISSION_INPUT_VALUES
|
|
|
61
62
|
RUN_PERMISSION_CANONICAL_VALUES = PERMISSION_VALUES
|
|
62
63
|
RUN_PERMISSION_ALIASES = PERMISSION_ALIASES
|
|
63
64
|
RUN_EFFORT_VALUES = REASONING_EFFORT_VALUES
|
|
64
|
-
RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] --json"
|
|
65
|
+
RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json"
|
|
65
66
|
RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
66
67
|
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
67
68
|
RUN_TAIL_USAGE = "Usage: cdx run-tail <run_id> [--lines N] --json"
|
|
@@ -80,6 +81,28 @@ ARG_CODE_OUT_OF_RANGE = "argument_value_out_of_range"
|
|
|
80
81
|
ARG_CODE_UNKNOWN = "unknown_argument"
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
def _pop_value_flag(args, flag, usage):
|
|
85
|
+
"""Remove `flag VALUE` / `flag=VALUE` from args; return (value or None, rest)."""
|
|
86
|
+
value, rest, index = None, [], 0
|
|
87
|
+
while index < len(args):
|
|
88
|
+
arg = args[index]
|
|
89
|
+
name, sep, inline = arg.partition("=")
|
|
90
|
+
if name != flag:
|
|
91
|
+
rest.append(arg)
|
|
92
|
+
index += 1
|
|
93
|
+
continue
|
|
94
|
+
if sep:
|
|
95
|
+
candidate, index = inline, index + 1
|
|
96
|
+
elif index + 1 < len(args):
|
|
97
|
+
candidate, index = args[index + 1], index + 2
|
|
98
|
+
else:
|
|
99
|
+
raise CdxError(usage)
|
|
100
|
+
if value is not None or not candidate:
|
|
101
|
+
raise CdxError(usage)
|
|
102
|
+
value = candidate
|
|
103
|
+
return value, rest
|
|
104
|
+
|
|
105
|
+
|
|
83
106
|
def _parse_json_flag(args):
|
|
84
107
|
json_flag = "--json" in args
|
|
85
108
|
cleaned = [arg for arg in args if arg != "--json"]
|
|
@@ -1033,8 +1056,13 @@ def _parse_runs_args(rest, now=None):
|
|
|
1033
1056
|
parsed = _parse_flag_args(rest, {
|
|
1034
1057
|
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
1035
1058
|
"--since": {"key": "since", "type": "str", "default": None},
|
|
1059
|
+
"--running": {"key": "running", "type": "bool", "default": False},
|
|
1036
1060
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
1037
1061
|
}, RUNS_USAGE, max_positionals=0)
|
|
1062
|
+
if parsed["running"] and parsed["since"] is not None:
|
|
1063
|
+
# --since returns completed runs only, --running the ones not completed:
|
|
1064
|
+
# together they can only ever answer "nothing".
|
|
1065
|
+
raise CdxError("cdx runs: --running and --since are mutually exclusive.")
|
|
1038
1066
|
since = None
|
|
1039
1067
|
if parsed["since"] is not None:
|
|
1040
1068
|
# Same cursor forms as `cdx history --since`, parsed by the same code:
|
|
@@ -1048,6 +1076,7 @@ def _parse_runs_args(rest, now=None):
|
|
|
1048
1076
|
# a watchdog behind by more than `limit` completions would never see
|
|
1049
1077
|
# the older ones at all.
|
|
1050
1078
|
"limit_given": any(arg == "--limit" or arg.startswith("--limit=") for arg in rest),
|
|
1079
|
+
"running": parsed["running"],
|
|
1051
1080
|
"json": parsed["json"],
|
|
1052
1081
|
}
|
|
1053
1082
|
|
package/src/cli_commands.py
CHANGED
|
@@ -265,6 +265,9 @@ from .commands.status import (
|
|
|
265
265
|
from .commands.status import (
|
|
266
266
|
handle_next as handle_next,
|
|
267
267
|
)
|
|
268
|
+
from .commands.status import (
|
|
269
|
+
handle_sessions as handle_sessions,
|
|
270
|
+
)
|
|
268
271
|
from .commands.status import (
|
|
269
272
|
handle_stats as handle_stats,
|
|
270
273
|
)
|
package/src/commands/launch.py
CHANGED
|
@@ -8,7 +8,7 @@ import shlex
|
|
|
8
8
|
|
|
9
9
|
from .. import retro, shared_memory
|
|
10
10
|
from ..agent_notify import notifications_enabled, provision, supports_agent_alerts
|
|
11
|
-
from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag
|
|
11
|
+
from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag, _pop_value_flag
|
|
12
12
|
from ..cli_helpers import (
|
|
13
13
|
API_SCHEMA_VERSION,
|
|
14
14
|
_json_failure,
|
|
@@ -30,6 +30,7 @@ from ..provider_runtime import (
|
|
|
30
30
|
_ensure_session_authentication,
|
|
31
31
|
_get_auth_home,
|
|
32
32
|
_run_interactive_provider_command,
|
|
33
|
+
with_requested_conversation,
|
|
33
34
|
)
|
|
34
35
|
from ..run_command import PROMPT_STDIN, read_run_prompt
|
|
35
36
|
from ..run_usage import estimate_cost
|
|
@@ -160,12 +161,14 @@ def _format_resume_capability(capability, use_color=False):
|
|
|
160
161
|
)
|
|
161
162
|
|
|
162
163
|
def handle_can_resume(rest, ctx):
|
|
164
|
+
conversation_id, rest = _pop_value_flag(rest, "--conversation", CAN_RESUME_USAGE)
|
|
163
165
|
json_flag, args = _parse_json_flag(rest)
|
|
164
166
|
if len(args) != 1:
|
|
165
167
|
raise CdxError(CAN_RESUME_USAGE)
|
|
166
168
|
session = ctx["service"]["get_session"](args[0])
|
|
167
169
|
if not session:
|
|
168
170
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
171
|
+
requested = with_requested_conversation(session, conversation_id) if conversation_id else session
|
|
169
172
|
if session.get("enabled", True) is False:
|
|
170
173
|
capability = {
|
|
171
174
|
"session": session["name"],
|
|
@@ -175,8 +178,19 @@ def handle_can_resume(rest, ctx):
|
|
|
175
178
|
"reason": "session_disabled",
|
|
176
179
|
"command_preview": [],
|
|
177
180
|
}
|
|
181
|
+
elif requested is None:
|
|
182
|
+
capability = {
|
|
183
|
+
"session": session["name"],
|
|
184
|
+
"provider": session["provider"],
|
|
185
|
+
"resumable": False,
|
|
186
|
+
"strategy": "provider_conversation_id",
|
|
187
|
+
"identity": conversation_id,
|
|
188
|
+
"provenance": "requested",
|
|
189
|
+
"reason": "conversation_not_found",
|
|
190
|
+
"command_preview": [],
|
|
191
|
+
}
|
|
178
192
|
else:
|
|
179
|
-
capability = _resume_capability_for_session(
|
|
193
|
+
capability = _resume_capability_for_session(requested, ctx)
|
|
180
194
|
if json_flag:
|
|
181
195
|
_write_json(ctx, {
|
|
182
196
|
"schema_version": API_SCHEMA_VERSION,
|
|
@@ -188,13 +202,15 @@ def handle_can_resume(rest, ctx):
|
|
|
188
202
|
return 0
|
|
189
203
|
|
|
190
204
|
def handle_resume(rest, ctx):
|
|
205
|
+
conversation_id, rest = _pop_value_flag(rest, "--conversation", RESUME_USAGE)
|
|
191
206
|
directory, rest, prompt = _parse_launch_args(rest)
|
|
192
207
|
if prompt["prompt"] is not None or prompt["prompt_file"] is not None:
|
|
193
208
|
raise CdxError("cdx resume: --prompt and --prompt-file are not supported when resuming.")
|
|
194
209
|
json_flag, args = _parse_json_flag(rest)
|
|
195
210
|
if len(args) != 1:
|
|
196
211
|
raise CdxError(RESUME_USAGE)
|
|
197
|
-
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory
|
|
212
|
+
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory,
|
|
213
|
+
conversation_id=conversation_id)
|
|
198
214
|
|
|
199
215
|
|
|
200
216
|
def _notification_provisioning_enabled(session, ctx, json_flag):
|
|
@@ -237,7 +253,8 @@ def _link_shared_memory(session, ctx, cwd, json_flag):
|
|
|
237
253
|
ctx["out"](f"{_warn(message, ctx['use_color'])} {detail}\n")
|
|
238
254
|
|
|
239
255
|
|
|
240
|
-
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None
|
|
256
|
+
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None,
|
|
257
|
+
conversation_id=None):
|
|
241
258
|
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
242
259
|
if force_json is not None:
|
|
243
260
|
json_flag = force_json
|
|
@@ -250,6 +267,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
250
267
|
raise CdxError(f"Unknown session: {command}")
|
|
251
268
|
if session.get("enabled", True) is False:
|
|
252
269
|
raise CdxError(f"Session is disabled: {command}")
|
|
270
|
+
if conversation_id and with_requested_conversation(session, conversation_id) is None:
|
|
271
|
+
raise CdxError(f"Conversation {conversation_id} not found in session {command}.", code="conversation_not_found")
|
|
253
272
|
capability = _resume_capability_for_session(session, ctx) if resume else None
|
|
254
273
|
if capability and not capability["resumable"]:
|
|
255
274
|
raise CdxError(
|
|
@@ -257,6 +276,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
257
276
|
)
|
|
258
277
|
cwd = _launch_directory(session, ctx, directory, json_flag)
|
|
259
278
|
session = ctx["service"]["launch_session"](command, resume=resume)
|
|
279
|
+
if conversation_id:
|
|
280
|
+
session = with_requested_conversation(session, conversation_id)
|
|
260
281
|
capability = _resume_capability_for_session(session, ctx) if resume else None
|
|
261
282
|
if not json_flag and not ctx["stdin_is_tty"]:
|
|
262
283
|
ctx["out"](f"Using directory: {cwd}\n")
|
package/src/commands/runs.py
CHANGED
|
@@ -43,7 +43,7 @@ from ..provider_runtime import (
|
|
|
43
43
|
)
|
|
44
44
|
from ..run_command import read_run_prompt, run_cdx_error_code, run_launch_payload, run_result_payload
|
|
45
45
|
from ..run_failover import MAX_FAILOVER_TRANSITIONS, failover_reason
|
|
46
|
-
from ..run_registry import RunRegistry, build_code_review_report
|
|
46
|
+
from ..run_registry import RunRegistry, build_code_review_report, final_message
|
|
47
47
|
from ..run_usage import extract_run_observation
|
|
48
48
|
from ..session_ranking import FACTOR_DESCRIPTIONS, rank_sessions, selection_policy
|
|
49
49
|
from ..status_view import _now_timestamp, _priority_reset_timestamp
|
|
@@ -415,7 +415,7 @@ def handle_runs(rest, ctx):
|
|
|
415
415
|
raise CdxError(RUNS_USAGE)
|
|
416
416
|
since = parsed.get("since")
|
|
417
417
|
_reconcile_background_runs(ctx)
|
|
418
|
-
runs = _run_registry(ctx).list(limit=parsed["limit"], since=since)
|
|
418
|
+
runs = _run_registry(ctx).list(limit=parsed["limit"], since=since, running=parsed.get("running", False))
|
|
419
419
|
warnings = []
|
|
420
420
|
if since is not None and parsed.get("limit_given"):
|
|
421
421
|
warnings.append({
|
|
@@ -503,6 +503,7 @@ def handle_run_report(rest, ctx):
|
|
|
503
503
|
_write_json(ctx, _json_success("run-report", "Run report loaded.", report={
|
|
504
504
|
"run": run,
|
|
505
505
|
"final_payload": run.get("final_payload"),
|
|
506
|
+
"final_message": final_message(run),
|
|
506
507
|
"task_report": run.get("task_report"),
|
|
507
508
|
"artifacts": run.get("artifacts") or {},
|
|
508
509
|
"usage": run.get("usage"),
|
package/src/commands/status.py
CHANGED
|
@@ -12,6 +12,7 @@ from datetime import datetime
|
|
|
12
12
|
from ..claude_refresh import _refresh_claude_sessions
|
|
13
13
|
from ..cli_args import (
|
|
14
14
|
LAST_USAGE,
|
|
15
|
+
SESSIONS_USAGE,
|
|
15
16
|
STATUS_USAGE,
|
|
16
17
|
_filter_history_period,
|
|
17
18
|
_has_history_period,
|
|
@@ -59,6 +60,7 @@ from ..run_usage import (
|
|
|
59
60
|
token_prices,
|
|
60
61
|
weighted_usage,
|
|
61
62
|
)
|
|
63
|
+
from ..session_status import session_paths
|
|
62
64
|
from ..status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
63
65
|
|
|
64
66
|
|
|
@@ -926,3 +928,39 @@ def _write_refresh_warnings(refresh_errors, ctx, stream="out", rows=None):
|
|
|
926
928
|
rows_by_name = _rows_by_session(rows)
|
|
927
929
|
for item in refresh_errors:
|
|
928
930
|
write(f"{_warn(_format_refresh_warning(item, rows_by_name), ctx['use_color'])}\n")
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def handle_sessions(rest, ctx):
|
|
934
|
+
"""Interactive sessions cdx launched that are still running, or every account.
|
|
935
|
+
|
|
936
|
+
Read from the runtime cdx records at launch, so a provider started outside
|
|
937
|
+
cdx is not listed. A runtime whose process has died is reported stale and
|
|
938
|
+
dropped by the same check `cdx status` uses for `active`. `--all` adds the
|
|
939
|
+
idle enabled accounts, so a caller can resolve any account's directories.
|
|
940
|
+
"""
|
|
941
|
+
json_flag, args = _parse_json_flag(rest)
|
|
942
|
+
include_all = "--all" in args
|
|
943
|
+
args = [arg for arg in args if arg != "--all"]
|
|
944
|
+
if not json_flag or args:
|
|
945
|
+
raise CdxError(SESSIONS_USAGE)
|
|
946
|
+
service = ctx["service"]
|
|
947
|
+
rows = []
|
|
948
|
+
for session in service["list_sessions"]():
|
|
949
|
+
if session.get("enabled", True) is False:
|
|
950
|
+
continue
|
|
951
|
+
runtime = service["active_session_runtime"](session["name"]) or {}
|
|
952
|
+
if not runtime and not include_all:
|
|
953
|
+
continue
|
|
954
|
+
rows.append({
|
|
955
|
+
"session": session["name"],
|
|
956
|
+
"provider": session["provider"],
|
|
957
|
+
"active": bool(runtime),
|
|
958
|
+
"pid": runtime.get("pid"),
|
|
959
|
+
"cwd": runtime.get("cwd"),
|
|
960
|
+
"started_at": runtime.get("startedAt"),
|
|
961
|
+
"runtime_id": runtime.get("runId"),
|
|
962
|
+
"conversation_id": (session.get("conversation") or {}).get("id"),
|
|
963
|
+
**session_paths(session),
|
|
964
|
+
})
|
|
965
|
+
_write_json(ctx, _json_success("sessions", "Sessions loaded.", sessions=rows))
|
|
966
|
+
return 0
|
package/src/provider_runtime.py
CHANGED
|
@@ -820,6 +820,18 @@ def _conversation_is_resumable(session, provider, conversation_id):
|
|
|
820
820
|
return False
|
|
821
821
|
|
|
822
822
|
|
|
823
|
+
def with_requested_conversation(session, conversation_id):
|
|
824
|
+
"""The session as if `conversation_id` were the one it last opened.
|
|
825
|
+
|
|
826
|
+
Returns None when that conversation does not exist in this session's
|
|
827
|
+
profile: an explicitly named resume must fail, not quietly fall back to
|
|
828
|
+
--last/--continue and open a different conversation.
|
|
829
|
+
"""
|
|
830
|
+
if not _conversation_is_resumable(session, session.get("provider"), conversation_id):
|
|
831
|
+
return None
|
|
832
|
+
return {**session, "conversation": {"id": conversation_id, "provenance": "requested"}}
|
|
833
|
+
|
|
834
|
+
|
|
823
835
|
def _redacted_resume_command_preview(session, cwd=None):
|
|
824
836
|
capability = get_resume_capability(session, cwd=cwd)
|
|
825
837
|
return capability.get("command_preview")
|
package/src/run_registry.py
CHANGED
|
@@ -7,7 +7,7 @@ from datetime import datetime, timezone
|
|
|
7
7
|
|
|
8
8
|
from .errors import CdxError
|
|
9
9
|
from .fs_utils import atomic_write
|
|
10
|
-
from .run_usage import coerce_usage
|
|
10
|
+
from .run_usage import _parse_json_records, coerce_usage
|
|
11
11
|
from .session_helpers import _process_is_running
|
|
12
12
|
|
|
13
13
|
|
|
@@ -367,7 +367,7 @@ class RunRegistry:
|
|
|
367
367
|
return run
|
|
368
368
|
return None
|
|
369
369
|
|
|
370
|
-
def list(self, limit=20, since=None):
|
|
370
|
+
def list(self, limit=20, since=None, running=False):
|
|
371
371
|
"""Recent runs, newest first.
|
|
372
372
|
|
|
373
373
|
`since` selects every run that *completed* strictly after that moment
|
|
@@ -384,6 +384,10 @@ class RunRegistry:
|
|
|
384
384
|
if changed:
|
|
385
385
|
_write_registry(self.path, data)
|
|
386
386
|
runs = data["runs"]
|
|
387
|
+
if running:
|
|
388
|
+
# Every in-flight run, uncapped: a caller counting live runs against
|
|
389
|
+
# a cap must not be handed a truncated count.
|
|
390
|
+
return [run for run in runs if run.get("status") == "running"]
|
|
387
391
|
if since is not None:
|
|
388
392
|
return [run for run in runs if _completed_after(run, since)]
|
|
389
393
|
return runs[: max(0, int(limit or 20))]
|
|
@@ -410,6 +414,51 @@ def read_text_file(path, limit=120000):
|
|
|
410
414
|
return ""
|
|
411
415
|
|
|
412
416
|
|
|
417
|
+
def _record_final_message(record):
|
|
418
|
+
"""The final assistant text one provider record carries, or None."""
|
|
419
|
+
if not isinstance(record, dict):
|
|
420
|
+
return None
|
|
421
|
+
# Claude: `--output-format json` ends with a `result` envelope.
|
|
422
|
+
if record.get("type") == "result" and isinstance(record.get("result"), str):
|
|
423
|
+
return record["result"]
|
|
424
|
+
# Codex `exec --json`: an `item.completed` event wrapping an agent_message.
|
|
425
|
+
item = record.get("item")
|
|
426
|
+
if isinstance(item, dict) and item.get("type") == "agent_message" and isinstance(item.get("text"), str):
|
|
427
|
+
return item["text"]
|
|
428
|
+
# Older Codex builds wrapped it as {"msg": {"type": "agent_message", "message": ...}}.
|
|
429
|
+
msg = record.get("msg")
|
|
430
|
+
if isinstance(msg, dict) and msg.get("type") == "agent_message" and isinstance(msg.get("message"), str):
|
|
431
|
+
return msg["message"]
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def final_message(run):
|
|
436
|
+
"""The run's last assistant message, read the same way for every provider.
|
|
437
|
+
|
|
438
|
+
A `--bg` run records it from the transcript as `final_payload.result`; any
|
|
439
|
+
other run left it in its stdout, in the provider's own event format. None
|
|
440
|
+
when neither holds one (a plain-text provider, a run that died early).
|
|
441
|
+
"""
|
|
442
|
+
result = (run.get("final_payload") or {}).get("result")
|
|
443
|
+
if isinstance(result, str) and result.strip():
|
|
444
|
+
return result
|
|
445
|
+
stdout_path = (run.get("artifacts") or {}).get("stdout_path")
|
|
446
|
+
if not stdout_path:
|
|
447
|
+
return None
|
|
448
|
+
try:
|
|
449
|
+
with open(stdout_path, encoding="utf-8", errors="replace") as handle:
|
|
450
|
+
text = handle.read()
|
|
451
|
+
except OSError:
|
|
452
|
+
return None
|
|
453
|
+
if not text.strip():
|
|
454
|
+
return None
|
|
455
|
+
for record in reversed(_parse_json_records(text)):
|
|
456
|
+
message = _record_final_message(record)
|
|
457
|
+
if message is not None:
|
|
458
|
+
return message
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
413
462
|
def build_code_review_report(run_id, final_payload):
|
|
414
463
|
stdout = read_text_file(final_payload.get("stdout_path") if final_payload else None)
|
|
415
464
|
parsed = None
|
package/src/session_status.py
CHANGED
|
@@ -338,6 +338,25 @@ def _fetch_codex_status(codex_status_fetcher, custom_codex_status_fetcher, defau
|
|
|
338
338
|
timeout = timeout_seconds or default_status_timeout_seconds
|
|
339
339
|
return codex_status_fetcher(session, timeout=timeout)
|
|
340
340
|
|
|
341
|
+
def session_paths(session):
|
|
342
|
+
"""Where a session's files live, so a caller never rebuilds cdx's layout.
|
|
343
|
+
|
|
344
|
+
`profile_dir` is the session's own directory and `transcripts_dir` the
|
|
345
|
+
provider's conversation store inside it (None for a provider cdx does not
|
|
346
|
+
read transcripts from). The credential home itself is deliberately not
|
|
347
|
+
named, the same rule `cdx status` rows follow.
|
|
348
|
+
"""
|
|
349
|
+
auth_home = session.get("authHome")
|
|
350
|
+
transcripts = {
|
|
351
|
+
PROVIDER_CLAUDE: (".claude", "projects"),
|
|
352
|
+
PROVIDER_CODEX: ("sessions",),
|
|
353
|
+
}.get(session.get("provider"))
|
|
354
|
+
return {
|
|
355
|
+
"profile_dir": session.get("sessionRoot") or auth_home,
|
|
356
|
+
"transcripts_dir": os.path.join(auth_home, *transcripts) if auth_home and transcripts else None,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
341
360
|
def _status_row_from_session(store, s):
|
|
342
361
|
status = s.get("lastStatus")
|
|
343
362
|
enabled = s.get("enabled", True) is not False
|