cdx-manager 0.25.9 → 0.26.1
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 +79 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_26_0.md +31 -0
- package/changelogs/CHANGELOGS_0_26_1.md +21 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +6 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +12 -1
- package/src/cli_args.py +7 -4
- package/src/cli_commands.py +3 -2
- package/src/cli_helpers.py +1 -0
- package/src/commands/conversation.py +34 -0
- package/src/commands/runs.py +2 -4
- package/src/config.py +15 -0
- package/src/conversation.py +187 -0
- package/src/session_service.py +9 -17
- package/src/session_status.py +1 -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
|
|
|
@@ -42,6 +42,27 @@ cdx work
|
|
|
42
42
|
|
|
43
43
|
CDX checks authentication before interactive launches. Handoff keeps the source transcript in place and asks the receiving agent to verify the conversation and workspace before acting. No account switch or prompt submission happens just because you run `cdx next`.
|
|
44
44
|
|
|
45
|
+
## Automatic mission concurrency per profile
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cdx set work --max-parallel 2
|
|
49
|
+
cdx show work
|
|
50
|
+
cdx unset work --max-parallel
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`--max-parallel` accepts an integer from 0 to 5 and stores `launch.max_parallel`
|
|
54
|
+
next to `priority` and `policy`. Zero excludes the profile from automatic
|
|
55
|
+
missions. An absent value adds no profile-specific ceiling: the scheduling
|
|
56
|
+
client applies its own rule. Existing configurations need no migration.
|
|
57
|
+
`cdx unset work --all` also removes the limit.
|
|
58
|
+
|
|
59
|
+
The scheduling client is responsible for enforcing this metadata. It is
|
|
60
|
+
separate from the existing per-command `cdx run --max-parallel` limit.
|
|
61
|
+
`cdx --json` (also `cdx ls --json`) includes the optional field in each
|
|
62
|
+
session's `launch` object; `cdx status --json` exposes `max_parallel` on each
|
|
63
|
+
row (`null` when absent). `cdx show` is an alias for `cdx config` and displays
|
|
64
|
+
`Max parallel` as `default` when unset.
|
|
65
|
+
|
|
45
66
|
## Cache efficiency and fix rounds
|
|
46
67
|
|
|
47
68
|
`cdx history --json` records uncached input, cache reads, cache writes, output tokens, and `usage.cache_hit_ratio` beside the existing token totals. The ratio is cache reads divided by all input tokens, excluding output. Missing provider splits stay `null`, and text views show `cache n/d` instead of claiming a zero hit rate.
|
|
@@ -57,6 +78,10 @@ cdx run --continue-run RUN_ID --cwd /path/to/repo --prompt "Fix the remaining te
|
|
|
57
78
|
|
|
58
79
|
`--continue-run` resumes the exact Claude session or Codex thread and pins the previous run's recorded model and effort, even if the profile settings changed. The profile is inferred from the parent; an explicit session or provider must match. An explicit model or effort must also match. Runs and history record `parent_run_id` and `conversation_id`. Detached continuations use the cdx child supervisor with the same pins. Codex continuation usage is measured between exact transcript snapshots before and after the run, since exec resume can report conversation totals. If either boundary is unavailable, per-run usage stays unknown with `usage_attribution: "continuation_usage_unavailable"` in history instead of counting earlier turns again.
|
|
59
80
|
|
|
81
|
+
Headless runs, including detached, remote and continued runs, keep their
|
|
82
|
+
conversation identity in run records and history without replacing the profile's
|
|
83
|
+
interactive conversation.
|
|
84
|
+
|
|
60
85
|
Conservative assumption: a continuation requires a finished Claude or Codex run, a locally available provider transcript, and recorded model and effort settings. Missing runs, profiles, expired sessions, and unavailable settings produce a clear error; the caller can retry without `--continue-run` for a cold run. `--failover` and `--isolate-workspace` cannot be combined with continuation. Capacity recovery and provider model fallback are disabled for continuations so they cannot silently switch model or account. Provider cache retention remains provider-controlled, so resuming does not guarantee a cache hit.
|
|
61
86
|
|
|
62
87
|
## Run costs and budgets
|
|
@@ -218,3 +243,56 @@ The empty-refresh-token behavior was measured with Codex CLI 0.166.0. Real-host
|
|
|
218
243
|
acceptance should verify that the installed matching CLI does not attempt a
|
|
219
244
|
refresh with the temporary home, that the controller token chain stays unchanged
|
|
220
245
|
for a fresh credential, and that cleanup works after disconnect and SIGTERM.
|
|
246
|
+
|
|
247
|
+
### Read an interactive conversation
|
|
248
|
+
|
|
249
|
+
`cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json`
|
|
250
|
+
returns a read-only JSON page for the profile's current conversation. An explicit
|
|
251
|
+
conversation ID uses an exact transcript lookup and never falls back. When the
|
|
252
|
+
profile's recorded ID has no transcript on this host (for example after a remote
|
|
253
|
+
run on another host), or there is no recorded ID, the command uses the existing
|
|
254
|
+
latest local transcript resolver under the profile's auth home. This fallback is
|
|
255
|
+
best effort and does not rewrite the recorded ID. JSON `resolution_method` is
|
|
256
|
+
`conversation_id` for an exact match, `recency` for a local fallback, or `null`
|
|
257
|
+
when no transcript is resolved.
|
|
258
|
+
|
|
259
|
+
The normal cdx JSON envelope includes `status`, `reason`, `provider`, `session`,
|
|
260
|
+
`entries`, `next_cursor`, and `has_more`. Each entry has exactly `role`
|
|
261
|
+
(`user`, `assistant`, or `tool`), `text`, `tool_name`, `tool_summary`, `at`, and
|
|
262
|
+
`id`. Missing values are `null`; tool text is empty. Entries follow file and
|
|
263
|
+
content-block order, with stable IDs for the same file position. Timestamps are
|
|
264
|
+
provider timestamps when valid. Tool calls/results have a name and a short
|
|
265
|
+
summary; arguments, results, attachments, and reasoning blocks are omitted.
|
|
266
|
+
Result names are recovered from up to 100 recent calls, otherwise `tool`.
|
|
267
|
+
Common credential patterns and secret assignments in text are redacted without
|
|
268
|
+
opening credential stores. Review conversation text before sharing: arbitrary
|
|
269
|
+
unlabelled sensitive prose cannot be identified automatically.
|
|
270
|
+
|
|
271
|
+
Pass `next_cursor` unchanged as `--cursor` to fetch the next page or poll for new
|
|
272
|
+
entries. `--limit` bounds entries (default 100, range 1–1000). Reading seeks to
|
|
273
|
+
the saved byte position rather than replaying history, and scans at most 8 MiB
|
|
274
|
+
per call. A page may be empty while still returning a usable cursor. Poll on a
|
|
275
|
+
timer: `has_more` means unread bytes, which can include an incomplete final line
|
|
276
|
+
that will only become readable after the provider appends its newline.
|
|
277
|
+
|
|
278
|
+
Codex response items and Claude-compatible JSONL transcripts (Claude and
|
|
279
|
+
Antigravity) are supported through the existing transcript resolver. Missing or
|
|
280
|
+
unreadable files return `status: "unavailable"` with a reason. Ollama's terminal
|
|
281
|
+
capture does not reliably identify message roles and returns
|
|
282
|
+
`status: "unsupported"`, including when no capture exists. A transcript record
|
|
283
|
+
larger than 1 MiB returns `unavailable`. A replaced/truncated file or invalid
|
|
284
|
+
cursor returns `reset_required`; discard the cursor and reload the conversation.
|
|
285
|
+
Cursors are opaque, versioned, and bound to a provider and file. Treat transcripts
|
|
286
|
+
as append-only; reset the client if a provider rewrites earlier history.
|
|
287
|
+
|
|
288
|
+
Python callers can use the same page contract:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from cdx_manager.conversation import read_conversation
|
|
292
|
+
|
|
293
|
+
page = read_conversation("claude", "/path/to/profile/home", conversation_id="session-id", limit=100)
|
|
294
|
+
next_page = read_conversation(
|
|
295
|
+
"claude", "/path/to/profile/home", conversation_id="session-id",
|
|
296
|
+
cursor=page["next_cursor"], limit=100,
|
|
297
|
+
)
|
|
298
|
+
```
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.26.1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# CDX Manager 0.26.0
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-03
|
|
4
|
+
|
|
5
|
+
This release adds optional profile concurrency settings for automatic missions
|
|
6
|
+
and incremental JSON access to interactive conversations.
|
|
7
|
+
|
|
8
|
+
## Automatic mission concurrency
|
|
9
|
+
|
|
10
|
+
- Add an optional per-profile `launch.max_parallel` setting through `cdx set
|
|
11
|
+
<name> --max-parallel N`, accepting integers from 0 to 5. Zero excludes a
|
|
12
|
+
profile from automatic missions; an unset value leaves the scheduling client's
|
|
13
|
+
default in effect. Expose the setting in list JSON, status JSON, and profile
|
|
14
|
+
settings, and clear it with `cdx unset <name> --max-parallel`. Scheduling
|
|
15
|
+
clients enforce this setting; the existing `cdx run --max-parallel` limit
|
|
16
|
+
remains separate. (#101)
|
|
17
|
+
|
|
18
|
+
## Interactive conversation reading
|
|
19
|
+
|
|
20
|
+
- Add `cdx conversation <name> --json` and the reusable `read_conversation`
|
|
21
|
+
Python function to read Codex, Claude, and Antigravity transcripts
|
|
22
|
+
incrementally with bounded reads and opaque pagination cursors. Return
|
|
23
|
+
normalized messages and tool activity summaries while omitting tool bodies,
|
|
24
|
+
attachments, metadata, and reasoning, and redacting recognized credentials.
|
|
25
|
+
Report missing transcripts as unavailable and Ollama terminal captures as
|
|
26
|
+
unsupported; require a cursor reset after rotation or truncation. (#100)
|
|
27
|
+
|
|
28
|
+
## Maintenance
|
|
29
|
+
|
|
30
|
+
- Record tray companion checksums for v0.25.9. (`6d0a825b9649da9aead3379e6411e3959cd30a77`)
|
|
31
|
+
- Record release archive checksums for v0.25.9. (`e3682ad8aed14d75c3c686a0c6027f53cfe81cd3`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# CDX Manager 0.26.1
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-03
|
|
4
|
+
|
|
5
|
+
## Fixes
|
|
6
|
+
|
|
7
|
+
- Preserve the profile's interactive conversation across local, detached, remote,
|
|
8
|
+
and continued headless runs. Keep headless Claude identities in run records
|
|
9
|
+
and history, and skip interactive Codex identity observation for run history.
|
|
10
|
+
(#103)
|
|
11
|
+
- Let `cdx conversation` use the latest local transcript when the profile's
|
|
12
|
+
recorded conversation ID has no transcript on this host. Explicit
|
|
13
|
+
`--conversation` lookups remain exact. Expose `resolution_method` as
|
|
14
|
+
`conversation_id`, `recency`, or `null`; recency remains best effort. (#103)
|
|
15
|
+
|
|
16
|
+
## Maintenance
|
|
17
|
+
|
|
18
|
+
- Record tray companion checksums for v0.26.0.
|
|
19
|
+
(`c942c9f6fdcfabd1ad0442e46f978da3b548971f`)
|
|
20
|
+
- Record release archive checksums for v0.26.0.
|
|
21
|
+
(`9a43f53d7294ecb254bd65aea214831bcd1486fb`)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
## Preserve interactive conversations across headless runs
|
|
4
|
+
|
|
5
|
+
- Keep fresh Claude run identities in run records and history without changing the profile's interactive conversation. Skip interactive Codex identity observation when recording headless run history, including detached, remote and continued runs.
|
|
6
|
+
- Let `cdx conversation` fall back to the latest local transcript when a profile's recorded ID has no transcript on this host. Explicit `--conversation` lookups remain exact and never fall back. Report `resolution_method` as `conversation_id`, `recency`, or `null`.
|
|
7
|
+
- Cover a remote Claude run followed by local interactive conversation reading, local/continued run identity preservation, missing recorded IDs and missing explicit IDs.
|
|
8
|
+
|
|
3
9
|
## Antigravity quota and run usage
|
|
4
10
|
|
|
5
11
|
- Install a local Antigravity statusLine command in new profiles and on interactive launch, merging settings and relaying the original stdin to any existing command so its output is preserved. Store only the reading time, plan tier and allowlisted quota fields in an atomic mode-600 profile file; never access Google internal endpoints or antigravity-oauth-token.
|
|
@@ -782,6 +782,28 @@
|
|
|
782
782
|
"x86_64-pc-windows-msvc": "9577f9dff7d457094276376f262904dad8c9aff95ed2ca87b834c9c900fb5493",
|
|
783
783
|
"x86_64-unknown-linux-musl": "db7e322037041c2a898043f0c7a691c22cab6a794aab5b2439a06ac91e92fa39"
|
|
784
784
|
}
|
|
785
|
+
},
|
|
786
|
+
"v0.25.9": {
|
|
787
|
+
"github_tarball_sha256": "cd6d881144cefbd82b0929f375f471ed076e2f23be60744895aee54de5aead12",
|
|
788
|
+
"github_zip_sha256": "ccd28db3f526a081ed75cefc016f54d7eadf0adcaab401c8aed77d477cf917d3",
|
|
789
|
+
"tray_assets": {
|
|
790
|
+
"aarch64-apple-darwin": "4f90576e9e8db1ec08ed856a91a02f4049c7f7186e6dccac1908ba43c72df6fd",
|
|
791
|
+
"aarch64-unknown-linux-musl": "dcdf50751982f78805492eca059d83238f5d2190b97455b2d96d91283b764cdc",
|
|
792
|
+
"x86_64-apple-darwin": "289818bd5d692a6ba5035f10886318179bb2baa98e22a05e7abd751ea64d19ed",
|
|
793
|
+
"x86_64-pc-windows-msvc": "32889713859cf23d4d82647638d909d62075b92d8e6571e32118ebfa208d81b2",
|
|
794
|
+
"x86_64-unknown-linux-musl": "9e94b799fe4b01d95aeb172c1eba28dc011be69cc2b39b5743f0d81986eaa80f"
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
"v0.26.0": {
|
|
798
|
+
"github_tarball_sha256": "09903dc1c2bcf3c35ad0d52d1f5af2aa9f65584c07f508986dbf6829737d5eb2",
|
|
799
|
+
"github_zip_sha256": "e1aa5af189e9591f49aa1873c90cfb3365d3b3e86b4993866599498c21a0f979",
|
|
800
|
+
"tray_assets": {
|
|
801
|
+
"aarch64-apple-darwin": "1ee383b3ba75b9a19fee981d39ae8d26228e855933392ceb3d8fa55498da238b",
|
|
802
|
+
"aarch64-unknown-linux-musl": "832aa41132f900e446adaa8e23072094b6ba42d82315dac9ccd5f75ae623f172",
|
|
803
|
+
"x86_64-apple-darwin": "791898c594fcb2a0009653b9b50732698c0c451169cf1847a613a8d3e3ab3300",
|
|
804
|
+
"x86_64-pc-windows-msvc": "24ba2a8ed13de1bdc0401661a51c105c2c7ed186f3b17a871cd721ac8adf4916",
|
|
805
|
+
"x86_64-unknown-linux-musl": "200c8ab9465625784a32ae326d923bca97b5f9616809c43cb5dc73e0e5d9ca17"
|
|
806
|
+
}
|
|
785
807
|
}
|
|
786
808
|
}
|
|
787
809
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -21,6 +21,7 @@ from .cli_commands import (
|
|
|
21
21
|
handle_config,
|
|
22
22
|
handle_configs,
|
|
23
23
|
handle_context,
|
|
24
|
+
handle_conversation,
|
|
24
25
|
handle_copy,
|
|
25
26
|
handle_disable,
|
|
26
27
|
handle_disk,
|
|
@@ -167,6 +168,7 @@ _COMMAND_HANDLERS = {
|
|
|
167
168
|
"export": handle_export,
|
|
168
169
|
"handoff": handle_handoff,
|
|
169
170
|
"history": handle_history,
|
|
171
|
+
"conversation": handle_conversation,
|
|
170
172
|
"import": handle_import,
|
|
171
173
|
"label": handle_label,
|
|
172
174
|
"last": handle_last,
|
|
@@ -201,6 +203,7 @@ _COMMAND_HANDLERS = {
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
_COMMAND_ALIASES = {
|
|
206
|
+
"show": "config",
|
|
204
207
|
"ren": "rename",
|
|
205
208
|
"mv": "rename",
|
|
206
209
|
}
|
|
@@ -237,6 +240,7 @@ def _print_help(use_color=False):
|
|
|
237
240
|
_style("Usage:", "1", use_color),
|
|
238
241
|
f" {_style('cdx', '36', use_color)}",
|
|
239
242
|
f" {_style('cdx --json', '36', use_color)}",
|
|
243
|
+
f" {_style('cdx ls [--json]', '36', use_color)}",
|
|
240
244
|
f" {_style('cdx status [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
241
245
|
f" {_style('cdx status --small|-s [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
242
246
|
f" {_style('cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
|
|
@@ -258,15 +262,17 @@ def _print_help(use_color=False):
|
|
|
258
262
|
f" {_style('cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]', '36', use_color)}",
|
|
259
263
|
f" {_style('cdx disk [profiles] [--candidates] [--json]', '36', use_color)}",
|
|
260
264
|
f" {_style('cdx config <name> [--json]', '36', use_color)}",
|
|
265
|
+
f" {_style('cdx show <name> [--json]', '36', use_color)}",
|
|
261
266
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
262
267
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
263
268
|
f" {_style(SET_USAGE[len('Usage: '):], '36', use_color)}",
|
|
264
|
-
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
|
|
269
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--max-parallel|--all) [--json]', '36', use_color)}",
|
|
265
270
|
f" {_style('cdx history backfill-projects [--mapping FILE] [--dry-run|--apply] [--json]', '36', use_color)}",
|
|
266
271
|
f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
267
272
|
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
|
|
268
273
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
269
274
|
f" {_style('cdx resume <name> [--project PATH] [--conversation <id>] [--json]', '36', use_color)}",
|
|
275
|
+
f" {_style('cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json', '36', use_color)}",
|
|
270
276
|
f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
|
|
271
277
|
f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
|
|
272
278
|
f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
|
|
@@ -580,6 +586,11 @@ def main(argv, options=None):
|
|
|
580
586
|
# directory cannot be resolved (a service or task with a minimal environment).
|
|
581
587
|
service = options.get("service") or create_session_service({"env": env})
|
|
582
588
|
|
|
589
|
+
if argv[:1] == ["ls"]:
|
|
590
|
+
if argv[1:] not in ([], ["--json"]):
|
|
591
|
+
raise CdxError("Usage: cdx ls [--json]")
|
|
592
|
+
argv = argv[1:]
|
|
593
|
+
|
|
583
594
|
if argv == ["--json"]:
|
|
584
595
|
rows = service["format_list_rows"]()
|
|
585
596
|
notices = _get_update_notices(service, env, options)
|
package/src/cli_args.py
CHANGED
|
@@ -16,6 +16,7 @@ from .config import (
|
|
|
16
16
|
PROVIDER_CODEX,
|
|
17
17
|
PROVIDERS,
|
|
18
18
|
REASONING_EFFORT_VALUES,
|
|
19
|
+
normalize_launch_max_parallel,
|
|
19
20
|
normalize_permission,
|
|
20
21
|
parse_on_off,
|
|
21
22
|
)
|
|
@@ -34,8 +35,8 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
|
|
|
34
35
|
MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
|
|
35
36
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
|
|
36
37
|
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
|
|
37
|
-
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--agent-state on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
|
|
38
|
-
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--agent-state|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
|
|
38
|
+
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--agent-state on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--max-parallel 0..5] [--json]"
|
|
39
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--agent-state|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--max-parallel|--all) [--json]"
|
|
39
40
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
40
41
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
41
42
|
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
@@ -286,6 +287,7 @@ def _parse_set_args(args):
|
|
|
286
287
|
"--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
|
|
287
288
|
"--extra-args": {"key": "extra_args", "type": "str", "default": None},
|
|
288
289
|
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
290
|
+
"--max-parallel": {"key": "max_parallel", "type": "str", "default": None, "transform": normalize_launch_max_parallel},
|
|
289
291
|
"--policy": {"key": "policy", "type": "str", "default": None},
|
|
290
292
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
291
293
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
@@ -303,7 +305,7 @@ def _parse_set_args(args):
|
|
|
303
305
|
raise CdxError(SET_USAGE)
|
|
304
306
|
settings = {
|
|
305
307
|
key: parsed[key]
|
|
306
|
-
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy")
|
|
308
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel")
|
|
307
309
|
if parsed[key] is not None
|
|
308
310
|
}
|
|
309
311
|
if not settings:
|
|
@@ -336,6 +338,7 @@ def _parse_unset_args(args):
|
|
|
336
338
|
"--budget": {"key": "budget", "type": "bool", "default": False},
|
|
337
339
|
"--extra-args": {"key": "extra_args", "type": "bool", "default": False},
|
|
338
340
|
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
341
|
+
"--max-parallel": {"key": "max_parallel", "type": "bool", "default": False},
|
|
339
342
|
"--policy": {"key": "policy", "type": "bool", "default": False},
|
|
340
343
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
341
344
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
@@ -354,7 +357,7 @@ def _parse_unset_args(args):
|
|
|
354
357
|
raise CdxError(UNSET_USAGE)
|
|
355
358
|
_UNSET_KEYS = (
|
|
356
359
|
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro",
|
|
357
|
-
"model", "fallback_model", "budget", "extra_args", "priority", "policy",
|
|
360
|
+
"model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
|
|
358
361
|
)
|
|
359
362
|
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
|
360
363
|
if not keys:
|
package/src/cli_commands.py
CHANGED
|
@@ -57,8 +57,6 @@ from .commands.backup import ( # noqa: F401
|
|
|
57
57
|
handle_export,
|
|
58
58
|
handle_import,
|
|
59
59
|
)
|
|
60
|
-
|
|
61
|
-
# Re-exported for cli.py and tests; see src/commands/__init__.py.
|
|
62
60
|
from .commands.context_memory import ( # noqa: F401
|
|
63
61
|
_list_memory_entries,
|
|
64
62
|
_memory_payload,
|
|
@@ -67,6 +65,9 @@ from .commands.context_memory import ( # noqa: F401
|
|
|
67
65
|
handle_context,
|
|
68
66
|
handle_memory,
|
|
69
67
|
)
|
|
68
|
+
|
|
69
|
+
# Re-exported for cli.py and tests; see src/commands/__init__.py.
|
|
70
|
+
from .commands.conversation import handle_conversation as handle_conversation
|
|
70
71
|
from .commands.launch import ( # re-export for cli.py / tests
|
|
71
72
|
_format_resume_capability as _format_resume_capability,
|
|
72
73
|
)
|
package/src/cli_helpers.py
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""JSON conversation pages for interactive sessions."""
|
|
2
|
+
|
|
3
|
+
from ..cli_args import _parse_flag_args
|
|
4
|
+
from ..cli_helpers import _json_success, _write_json
|
|
5
|
+
from ..conversation import read_conversation
|
|
6
|
+
from ..errors import CdxError
|
|
7
|
+
from ..provider_runtime import _get_auth_home
|
|
8
|
+
|
|
9
|
+
CONVERSATION_USAGE = "Usage: cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_conversation(rest, ctx):
|
|
13
|
+
parsed = _parse_flag_args(rest, {
|
|
14
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
15
|
+
"--conversation": {"key": "conversation", "type": "str"},
|
|
16
|
+
"--cursor": {"key": "cursor", "type": "str"},
|
|
17
|
+
"--limit": {"key": "limit", "type": "str", "default": "100"},
|
|
18
|
+
}, CONVERSATION_USAGE, positionals_key="names", max_positionals=1)
|
|
19
|
+
if not parsed["json"] or len(parsed["names"]) != 1:
|
|
20
|
+
raise CdxError(CONVERSATION_USAGE)
|
|
21
|
+
session = ctx["service"]["get_session"](parsed["names"][0])
|
|
22
|
+
if not session:
|
|
23
|
+
raise CdxError(f"Unknown session: {parsed['names'][0]}")
|
|
24
|
+
try:
|
|
25
|
+
limit = int(parsed["limit"])
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise CdxError(CONVERSATION_USAGE) from None
|
|
28
|
+
page = read_conversation(
|
|
29
|
+
session["provider"], _get_auth_home(session),
|
|
30
|
+
conversation_id=parsed["conversation"] or (session.get("conversation") or {}).get("id"),
|
|
31
|
+
cursor=parsed["cursor"], limit=limit, fallback_to_recency=not parsed["conversation"],
|
|
32
|
+
)
|
|
33
|
+
_write_json(ctx, _json_success("conversation", "Conversation page loaded.", session=session["name"], **page))
|
|
34
|
+
return 0
|
package/src/commands/runs.py
CHANGED
|
@@ -763,10 +763,8 @@ def handle_run(rest, ctx):
|
|
|
763
763
|
# since nothing else frees their slot.
|
|
764
764
|
_reconcile_background_runs(ctx)
|
|
765
765
|
_run_registry(ctx).check_capacity(max_parallel)
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
# Claude run left the session with no identity and `cdx resume` fell
|
|
769
|
-
# back to recency for Claude while working by id for Codex.
|
|
766
|
+
# Fresh runs carry their own identity; only interactive launches update
|
|
767
|
+
# the profile's current conversation. Continuations use the parent run.
|
|
770
768
|
if not parsed.get("continue_run"):
|
|
771
769
|
session = ctx["service"]["begin_conversation"](session["name"])
|
|
772
770
|
run_session = {
|
package/src/config.py
CHANGED
|
@@ -78,6 +78,21 @@ def normalize_permission(value):
|
|
|
78
78
|
return text if text in PERMISSION_VALUES else None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def normalize_launch_max_parallel(value):
|
|
82
|
+
"""Validate the optional per-profile automatic mission limit."""
|
|
83
|
+
from .errors import CdxError
|
|
84
|
+
|
|
85
|
+
if isinstance(value, bool) or not isinstance(value, (int, str)):
|
|
86
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.")
|
|
87
|
+
try:
|
|
88
|
+
limit = int(value)
|
|
89
|
+
except ValueError as error:
|
|
90
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.") from error
|
|
91
|
+
if not 0 <= limit <= 5:
|
|
92
|
+
raise CdxError("Max parallel must be an integer from 0 to 5.")
|
|
93
|
+
return limit
|
|
94
|
+
|
|
95
|
+
|
|
81
96
|
def get_cdx_home(env=None):
|
|
82
97
|
if env is None:
|
|
83
98
|
env = os.environ
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Incremental, read-only conversation pages from provider JSONL transcripts."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from .config import PROVIDERS
|
|
11
|
+
from .errors import CdxError
|
|
12
|
+
from .interactive_usage import conversation_transcript, resolve_transcript
|
|
13
|
+
|
|
14
|
+
MAX_LINE_BYTES = 1024 * 1024
|
|
15
|
+
MAX_PAGE_BYTES = 8 * 1024 * 1024
|
|
16
|
+
REDACTED = "[REDACTED]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _redact(text):
|
|
20
|
+
# There is no shared transcript redactor; launch argument redaction only
|
|
21
|
+
# hides prompt argv. Do not read credential stores to discover secrets.
|
|
22
|
+
text = re.sub(r"-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)", REDACTED, text, flags=re.S)
|
|
23
|
+
text = re.sub(r"(?i)\b(?:[\w.-]*(?:password|passwd|secret|api[_-]?key|token)|authorization)\b[\"']?\s*[:=]\s*(?:\"[^\"]*\"|'[^']*'|[^\s,;}]+)", REDACTED, text)
|
|
24
|
+
text = re.sub(r"(?i)\b(?:bearer|basic)\s+[a-z0-9._~+/=-]+", REDACTED, text)
|
|
25
|
+
text = re.sub(r"\b(?:sk-[a-zA-Z0-9_-]+|gh[pousr]_[a-zA-Z0-9]+|github_pat_[a-zA-Z0-9_]+|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|xox[baprs]-[a-zA-Z0-9-]+|AIza[a-zA-Z0-9_-]+)\b", REDACTED, text)
|
|
26
|
+
text = re.sub(r"\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b", REDACTED, text)
|
|
27
|
+
text = re.sub(r"https?://(?:hooks\.slack\.com/services|(?:discord(?:app)?\.com)/api/webhooks)/[^\s\"'<>]+", REDACTED, text)
|
|
28
|
+
return re.sub(r"(https?://)[^\s/@]+:[^\s/@]+@", r"\1[REDACTED]@", text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _entry(role, text="", name=None, summary=None, at=None):
|
|
32
|
+
return {"role": role, "text": _redact(text), "tool_name": _redact(name) if name else None,
|
|
33
|
+
"tool_summary": summary, "at": at, "id": None}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _tool_name(message, names, result=False):
|
|
37
|
+
identifier = message.get("call_id") or message.get("tool_use_id") or message.get("id")
|
|
38
|
+
key = hashlib.sha256(identifier.encode()).hexdigest() if isinstance(identifier, str) else None
|
|
39
|
+
name = message.get("name") or (names.get(key) if result else None)
|
|
40
|
+
name = _redact(name) if isinstance(name, str) else "tool"
|
|
41
|
+
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_.:/-]{0,99}", name):
|
|
42
|
+
name = "tool"
|
|
43
|
+
if key and not result:
|
|
44
|
+
if len(names) >= 100:
|
|
45
|
+
names.pop(next(iter(names)))
|
|
46
|
+
names[key] = name
|
|
47
|
+
return name
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _entries(provider, record, names):
|
|
51
|
+
if not isinstance(record, dict):
|
|
52
|
+
return []
|
|
53
|
+
stamp = record.get("timestamp")
|
|
54
|
+
try:
|
|
55
|
+
datetime.fromisoformat(stamp.replace("Z", "+00:00"))
|
|
56
|
+
except (AttributeError, TypeError, ValueError):
|
|
57
|
+
stamp = None
|
|
58
|
+
if provider == "codex":
|
|
59
|
+
# event_msg repeats response_item messages; consume only canonical items.
|
|
60
|
+
if record.get("type") != "response_item":
|
|
61
|
+
return []
|
|
62
|
+
message = record.get("payload")
|
|
63
|
+
else:
|
|
64
|
+
message = record.get("message")
|
|
65
|
+
if not isinstance(message, dict):
|
|
66
|
+
return []
|
|
67
|
+
kind = message.get("type")
|
|
68
|
+
if isinstance(kind, str) and (kind.endswith("_call") or kind.endswith("_call_output")):
|
|
69
|
+
result = kind.endswith("output")
|
|
70
|
+
tool = message
|
|
71
|
+
if not message.get("name") and kind not in ("function_call", "custom_tool_call", "function_call_output", "custom_tool_call_output"):
|
|
72
|
+
tool = {**message, "name": kind.removesuffix("_output").removesuffix("_call")}
|
|
73
|
+
name = _tool_name(tool, names, result)
|
|
74
|
+
return [_entry("tool", name=name, summary="Result recorded" if result else "Call recorded", at=stamp)]
|
|
75
|
+
role = message.get("role") or record.get("type")
|
|
76
|
+
if role not in ("user", "assistant", "tool"):
|
|
77
|
+
return []
|
|
78
|
+
if role == "tool":
|
|
79
|
+
return [_entry("tool", name=_tool_name(message, names, True), summary="Result recorded", at=stamp)]
|
|
80
|
+
content = message.get("content")
|
|
81
|
+
if isinstance(content, str):
|
|
82
|
+
return [_entry(role, content, at=stamp)]
|
|
83
|
+
if not isinstance(content, list):
|
|
84
|
+
return []
|
|
85
|
+
entries = []
|
|
86
|
+
for block in content:
|
|
87
|
+
if not isinstance(block, dict):
|
|
88
|
+
continue
|
|
89
|
+
kind = block.get("type")
|
|
90
|
+
if kind in ("text", "input_text", "output_text") and isinstance(block.get("text"), str):
|
|
91
|
+
entries.append(_entry(role, block["text"], at=stamp))
|
|
92
|
+
elif kind in ("tool_use", "tool_result"):
|
|
93
|
+
name = _tool_name(block, names, kind == "tool_result")
|
|
94
|
+
entries.append(_entry("tool", name=name, summary="Call recorded" if kind == "tool_use" else "Result recorded", at=stamp))
|
|
95
|
+
return entries
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _anchor(handle, offset):
|
|
99
|
+
handle.seek(max(0, offset - 128))
|
|
100
|
+
return hashlib.sha256(handle.read(min(offset, 128))).hexdigest()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def read_conversation(provider, auth_home=None, *, conversation_id=None, started_at=None,
|
|
104
|
+
terminal_transcript=None, cursor=None, limit=100, fallback_to_recency=False):
|
|
105
|
+
"""Return a JSON-serializable page; pass next_cursor back unchanged to poll.
|
|
106
|
+
|
|
107
|
+
Entries follow file/block order. Cursors bind a byte offset and block index
|
|
108
|
+
to the resolved file; reset_required means discard the old cursor and page
|
|
109
|
+
again from the beginning. Incomplete trailing lines are retried next time.
|
|
110
|
+
Only implicit session identities may opt into fallback_to_recency.
|
|
111
|
+
Tool bodies and reasoning blocks are never returned. Ollama PTY captures
|
|
112
|
+
cannot reliably distinguish roles and are explicitly unsupported.
|
|
113
|
+
"""
|
|
114
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= 1000:
|
|
115
|
+
raise CdxError("Conversation limit must be an integer between 1 and 1000.")
|
|
116
|
+
page = {"status": "ok", "reason": None, "provider": provider, "entries": [], "next_cursor": None, "has_more": False,
|
|
117
|
+
"resolution_method": None}
|
|
118
|
+
if provider not in PROVIDERS or provider == "ollama":
|
|
119
|
+
return {**page, "status": "unsupported", "reason": "no_structured_transcript_reader"}
|
|
120
|
+
# An explicit conversation must never fall back to an unrelated recent file.
|
|
121
|
+
if conversation_id:
|
|
122
|
+
path = conversation_transcript(provider, auth_home, conversation_id)
|
|
123
|
+
if path:
|
|
124
|
+
page["resolution_method"] = "conversation_id"
|
|
125
|
+
elif fallback_to_recency:
|
|
126
|
+
path, page["resolution_method"] = resolve_transcript(provider, auth_home)
|
|
127
|
+
else:
|
|
128
|
+
path, page["resolution_method"] = resolve_transcript(provider, auth_home, started_at, terminal_transcript=terminal_transcript)
|
|
129
|
+
if not path:
|
|
130
|
+
return {**page, "status": "unavailable", "reason": "transcript_not_found"}
|
|
131
|
+
try:
|
|
132
|
+
with open(path, "rb") as handle:
|
|
133
|
+
stat = os.fstat(handle.fileno())
|
|
134
|
+
identity = hashlib.sha256(f"{provider}:{os.path.realpath(path)}:{stat.st_dev}:{stat.st_ino}".encode()).hexdigest()
|
|
135
|
+
offset, index, names = 0, 0, {}
|
|
136
|
+
if cursor is not None:
|
|
137
|
+
try:
|
|
138
|
+
if not isinstance(cursor, str) or len(cursor) > 65536:
|
|
139
|
+
raise ValueError
|
|
140
|
+
state = json.loads(base64.urlsafe_b64decode(cursor.encode("ascii")))
|
|
141
|
+
offset, index = state["offset"], state["index"]
|
|
142
|
+
names = state["names"]
|
|
143
|
+
if (not isinstance(names, dict) or len(names) > 100
|
|
144
|
+
or any(not isinstance(k, str) or not re.fullmatch(r"[0-9a-f]{64}", k)
|
|
145
|
+
or not isinstance(v, str) or not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_.:/-]{0,99}", v)
|
|
146
|
+
or _redact(v) != v for k, v in names.items())):
|
|
147
|
+
raise ValueError
|
|
148
|
+
if (state["v"] != 1 or state["file"] != identity
|
|
149
|
+
or type(offset) is not int or type(index) is not int
|
|
150
|
+
or not 0 <= offset <= stat.st_size or not 0 <= index <= MAX_LINE_BYTES
|
|
151
|
+
or state["anchor"] != _anchor(handle, offset)):
|
|
152
|
+
raise ValueError
|
|
153
|
+
except (ValueError, TypeError, KeyError, AttributeError, UnicodeError):
|
|
154
|
+
return {**page, "status": "reset_required", "reason": "invalid_or_stale_cursor"}
|
|
155
|
+
handle.seek(offset)
|
|
156
|
+
scanned = 0
|
|
157
|
+
while len(page["entries"]) < limit and scanned < MAX_PAGE_BYTES:
|
|
158
|
+
start = handle.tell()
|
|
159
|
+
line = handle.readline(MAX_LINE_BYTES + 1)
|
|
160
|
+
if len(line) > MAX_LINE_BYTES:
|
|
161
|
+
return {**page, "status": "unavailable", "reason": "record_too_large", "entries": []}
|
|
162
|
+
if not line or not line.endswith(b"\n"):
|
|
163
|
+
break
|
|
164
|
+
scanned += len(line)
|
|
165
|
+
try:
|
|
166
|
+
entries = _entries(provider, json.loads(line), names)
|
|
167
|
+
except (ValueError, UnicodeError):
|
|
168
|
+
entries = []
|
|
169
|
+
for block_index in range(index, len(entries)):
|
|
170
|
+
entry = entries[block_index]
|
|
171
|
+
entry["id"] = f"{identity[:16]}:{start}:{block_index}"
|
|
172
|
+
page["entries"].append(entry)
|
|
173
|
+
if len(page["entries"]) == limit:
|
|
174
|
+
index = block_index + 1
|
|
175
|
+
break
|
|
176
|
+
else:
|
|
177
|
+
index = len(entries)
|
|
178
|
+
if index < len(entries):
|
|
179
|
+
offset = start
|
|
180
|
+
break
|
|
181
|
+
offset, index = handle.tell(), 0
|
|
182
|
+
page["has_more"] = offset < stat.st_size
|
|
183
|
+
state = {"v": 1, "file": identity, "offset": offset, "index": index, "anchor": _anchor(handle, offset), "names": names}
|
|
184
|
+
page["next_cursor"] = base64.urlsafe_b64encode(json.dumps(state, separators=(",", ":")).encode()).decode()
|
|
185
|
+
return page
|
|
186
|
+
except OSError:
|
|
187
|
+
return {**page, "status": "unavailable", "reason": "transcript_unreadable", "entries": []}
|
package/src/session_service.py
CHANGED
|
@@ -45,6 +45,7 @@ from .config import (
|
|
|
45
45
|
PROVIDER_CODEX,
|
|
46
46
|
REASONING_EFFORT_VALUES,
|
|
47
47
|
get_cdx_home,
|
|
48
|
+
normalize_launch_max_parallel,
|
|
48
49
|
normalize_permission,
|
|
49
50
|
split_extra_args,
|
|
50
51
|
)
|
|
@@ -236,6 +237,8 @@ def _normalize_launch_settings(settings, mark_fast_service_tier=True):
|
|
|
236
237
|
if priority < MIN_LAUNCH_PRIORITY or priority > MAX_LAUNCH_PRIORITY:
|
|
237
238
|
raise CdxError(f"Unsupported priority: {settings['priority']}")
|
|
238
239
|
normalized["priority"] = priority
|
|
240
|
+
if "max_parallel" in settings and settings["max_parallel"] is not None:
|
|
241
|
+
normalized["max_parallel"] = normalize_launch_max_parallel(settings["max_parallel"])
|
|
239
242
|
if "policy" in settings and settings["policy"] is not None:
|
|
240
243
|
if settings["policy"] not in ACCOUNT_POLICIES:
|
|
241
244
|
raise CdxError(f"Unsupported policy: {settings['policy']} (use|ask-first|never)")
|
|
@@ -324,7 +327,7 @@ def unset_launch_settings(store, name, keys):
|
|
|
324
327
|
raise CdxError("At least one launch setting is required.")
|
|
325
328
|
allowed = {
|
|
326
329
|
"power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview",
|
|
327
|
-
"scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy",
|
|
330
|
+
"scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
|
|
328
331
|
}
|
|
329
332
|
unknown = [key for key in keys if key not in allowed]
|
|
330
333
|
if unknown:
|
|
@@ -686,12 +689,13 @@ def record_launch_history(store, name, payload):
|
|
|
686
689
|
**payload,
|
|
687
690
|
}
|
|
688
691
|
store["append_launch_history"](entry)
|
|
689
|
-
|
|
692
|
+
if entry.get("action") != "run":
|
|
693
|
+
_observe_codex_conversation(store, session)
|
|
690
694
|
return entry
|
|
691
695
|
|
|
692
696
|
|
|
693
697
|
def _observe_codex_conversation(store, session):
|
|
694
|
-
"""Read Codex's conversation id back after
|
|
698
|
+
"""Read Codex's conversation id back after an interactive launch.
|
|
695
699
|
|
|
696
700
|
Codex mints the id itself, so unlike Claude this can only be observed once
|
|
697
701
|
the rollout exists. Failing to find one is normal and leaves the session on
|
|
@@ -935,26 +939,14 @@ def _conversation_identity_for_launch(session):
|
|
|
935
939
|
|
|
936
940
|
|
|
937
941
|
def begin_conversation(store, name):
|
|
938
|
-
"""
|
|
939
|
-
|
|
940
|
-
`launch_session` does this for interactive launches, but a headless run
|
|
941
|
-
never goes through it, so `cdx run` used to leave Claude sessions with no
|
|
942
|
-
identity at all - `cdx resume` then fell back to recency for Claude while
|
|
943
|
-
working by id for Codex, which records its own after the fact. Same shape
|
|
944
|
-
for both providers now: whatever ran last is what resume names.
|
|
945
|
-
|
|
946
|
-
Returns the updated session, or the unchanged one for providers that mint
|
|
947
|
-
nothing here.
|
|
948
|
-
"""
|
|
942
|
+
"""Return a run-local identity without changing the interactive session."""
|
|
949
943
|
session = store["get_session"](name)
|
|
950
944
|
if not session:
|
|
951
945
|
raise CdxError(f"Unknown session: {name}")
|
|
952
946
|
conversation = _conversation_identity_for_launch(session)
|
|
953
947
|
if not conversation:
|
|
954
948
|
return session
|
|
955
|
-
return
|
|
956
|
-
**s, "conversation": conversation, "updatedAt": _local_now_iso(),
|
|
957
|
-
})
|
|
949
|
+
return {**session, "conversation": conversation}
|
|
958
950
|
|
|
959
951
|
|
|
960
952
|
def mark_conversation_rescued(store, name, conversation_id):
|
package/src/session_status.py
CHANGED
|
@@ -380,6 +380,7 @@ def _status_row_from_session(store, s):
|
|
|
380
380
|
# ignored `--priority` entirely.
|
|
381
381
|
"priority": _row_priority(s),
|
|
382
382
|
"policy": (s.get("launch") or {}).get("policy") or "use",
|
|
383
|
+
"max_parallel": (s.get("launch") or {}).get("max_parallel"),
|
|
383
384
|
"reasoning_effort": _row_reasoning_effort(s),
|
|
384
385
|
"active": bool(runtime) if enabled else False,
|
|
385
386
|
"cwd": runtime.get("cwd") if runtime else None,
|