cdx-manager 0.26.6 → 0.26.8
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 +82 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_26_7.md +20 -0
- package/changelogs/CHANGELOGS_0_26_8.md +19 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +18 -0
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +73 -1
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/src/claude_refresh.py +17 -4
- package/src/claude_status_line.py +139 -0
- package/src/claude_usage.py +10 -7
- package/src/cli.py +2 -2
- package/src/cli_args.py +16 -10
- package/src/cli_helpers.py +7 -2
- package/src/codex_usage.py +12 -4
- package/src/commands/backup.py +3 -0
- package/src/commands/conversation.py +10 -3
- package/src/commands/launch.py +7 -0
- package/src/commands/lifecycle.py +9 -1
- package/src/commands/runs.py +15 -14
- package/src/commands/settings.py +9 -0
- package/src/commands/status.py +8 -0
- package/src/conversation.py +41 -6
- package/src/process_conversation.py +81 -0
- package/src/quota_gauge.txt +2 -0
- package/src/quota_provenance.py +45 -0
- package/src/session_backup.py +4 -0
- package/src/session_service.py +27 -8
- package/src/session_status.py +26 -2
- package/src/status_source.py +7 -8
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,41 @@ 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
|
+
## Quota provenance in status JSON
|
|
46
|
+
|
|
47
|
+
Every profile in `cdx status --json` (including `--cached` and a named profile)
|
|
48
|
+
includes `quota_windows.5h` and `quota_windows.weekly` alongside the existing
|
|
49
|
+
percentage, reset and timestamp fields. Each window contains:
|
|
50
|
+
|
|
51
|
+
- `source`: `codex_rate_limits` for the existing `account/rateLimits/read`
|
|
52
|
+
reader, `claude_probe` for the existing one-token Claude probe,
|
|
53
|
+
`claude_statusline` for explicitly identified Claude statusline evidence,
|
|
54
|
+
`cache` for legacy/local evidence without a known provider origin, or
|
|
55
|
+
`unknown` when the window has no measurement. A cached reading retains its
|
|
56
|
+
known original source; no extra probe is performed for this metadata.
|
|
57
|
+
- `observed_at`: the ISO 8601 measurement time for this profile and window,
|
|
58
|
+
or `null` when absent or unknown. Reading a cache never advances this time.
|
|
59
|
+
- `availability`: `measured` below the freshness threshold, `stale` at or
|
|
60
|
+
beyond it (or when a value has no valid observation time), or `unavailable`
|
|
61
|
+
when no value exists or the current read/authentication attempt failed.
|
|
62
|
+
- `reason`: `null` unless unavailable; otherwise a stable explanation such
|
|
63
|
+
as `missing_window`, `login_required`, `login_expired`, `quota_read_failed`,
|
|
64
|
+
`probe_timeout`, `auth_probe_failed`, `profile_disabled`, or the Codex
|
|
65
|
+
diagnostic reason (`auth_locked`, `initialize_failed`,
|
|
66
|
+
`rate_limits_read_failed`, `missing_rate_limits`, `codex_cli_not_found`,
|
|
67
|
+
`missing_auth_home`, `probe_failed`). Provider error bodies are not copied.
|
|
68
|
+
|
|
69
|
+
Freshness thresholds match the existing cache TTLs: Codex **300 seconds**,
|
|
70
|
+
Claude **600 seconds**, and other providers **60 seconds** for these generic
|
|
71
|
+
window fields. Provider-specific fields such as Antigravity's quota freshness
|
|
72
|
+
retain their existing semantics. Missing windows remain `null`, never an
|
|
73
|
+
assumed 0% or 100%. On a failed refresh, existing cached percentages and reset
|
|
74
|
+
times remain available for compatibility, but the new availability is
|
|
75
|
+
`unavailable` with the failure reason and the original observation time.
|
|
76
|
+
Transient read errors describe the current command; `--cached` evaluates the
|
|
77
|
+
stored evidence and authentication state without making a new read attempt.
|
|
78
|
+
Reset times describe the quota window, not when it was measured.
|
|
79
|
+
|
|
45
80
|
## Automatic mission concurrency per profile
|
|
46
81
|
|
|
47
82
|
```bash
|
|
@@ -85,6 +120,14 @@ cdx run work --cwd /path/to/repo --prompt "Implement the change" --reasoning-eff
|
|
|
85
120
|
cdx run --continue-run RUN_ID --cwd /path/to/repo --prompt "Fix the remaining test failure" --json
|
|
86
121
|
```
|
|
87
122
|
|
|
123
|
+
Pass provider arguments for a single run with `--extra-args 'ARGS'`. They use the session setting's POSIX quoting and validation (nonempty, at most 512 characters, no control characters, balanced quotes), and are appended after the profile's `extra_args`. They do not change the profile or subsequent interactive launches. Provider flag semantics remain unvalidated, as listed under `unvalidated.extra_args` in `cdx schema --json`; existing provider permission and workspace isolation handling still applies. Codex headless launches retain their existing behavior of ignoring session and run extra arguments.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cdx run claude-work --cwd /path/to/repo --permission default --prompt 'Run: touch t' --extra-args '--allowedTools "Bash(touch t)" --strict-mcp-config' --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This allows the requested Claude tool and restricts MCP configuration for this run only. The option also follows detached runs and profile failover. With `--continue-run`, Claude extra arguments that override model, effort or conversation are refused, whether supplied by the session or this run.
|
|
130
|
+
|
|
88
131
|
`--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.
|
|
89
132
|
|
|
90
133
|
Headless runs, including detached, remote and continued runs, keep their
|
|
@@ -358,3 +401,41 @@ next_page = read_conversation(
|
|
|
358
401
|
cursor=page["next_cursor"], limit=100,
|
|
359
402
|
)
|
|
360
403
|
```
|
|
404
|
+
|
|
405
|
+
### Claude quota status line
|
|
406
|
+
|
|
407
|
+
cdx installs a cache-only `statusLine` command in each new Claude profile's
|
|
408
|
+
`.claude/settings.json` and updates it on copy, rename, import, interactive launch/resume or `cdx set` /
|
|
409
|
+
`cdx unset`. Existing profiles pick it up at their next interactive launch.
|
|
410
|
+
The line uses the launcher's eight-cell remaining-capacity gauge, for example:
|
|
411
|
+
|
|
412
|
+
```text
|
|
413
|
+
█▁▁▁▁▁▁▁ 5h 25% · wk 16% (reset Nov 07 09:30) · grif
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The gauge follows the lowest known window. Each window below 20% includes its
|
|
417
|
+
cached reset time when available. `stale` marks observations at least ten minutes
|
|
418
|
+
old, missing an observation timestamp, or dated in the future. Missing, unreadable,
|
|
419
|
+
malformed, or unknown quota data produces only `quota ?`, with no error output.
|
|
420
|
+
The status command reads `sessions.json` directly without taking a store lock,
|
|
421
|
+
refreshing authentication, starting a provider, or making a network call. Refresh
|
|
422
|
+
quota separately with `cdx status --refresh` or the tray's normal refresh cycle.
|
|
423
|
+
It targets less than 100 ms per render; Claude's input JSON is not used.
|
|
424
|
+
|
|
425
|
+
The command pins the cdx store path and resolves the profile from the session's
|
|
426
|
+
`HOME` / `USERPROFILE`, with `CDX_SESSION_NAME` as fallback when no home is set.
|
|
427
|
+
It does not follow the launcher's currently selected account. Operator-defined
|
|
428
|
+
`statusLine` values and unrelated settings are preserved; cdx reports a conflict
|
|
429
|
+
in launch/settings output (also in JSON `warnings`). Invalid settings are skipped.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
cdx set grif --status-line off # Remove only cdx's command and keep it disabled.
|
|
433
|
+
cdx set grif --status-line on # Reinstall without replacing an operator command.
|
|
434
|
+
cdx unset grif --status-line # Return to the default (enabled for Claude).
|
|
435
|
+
cdx set --provider claude --status-line on # Update existing Claude profiles now.
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Codex has a built-in status line configured with `tui.status_line`, an ordered list
|
|
439
|
+
of built-in footer item identifiers, rather than Claude's shell-command setting
|
|
440
|
+
([official configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference)).
|
|
441
|
+
cdx does not change Codex configuration for this feature.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.26.
|
|
1
|
+
0.26.8
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# CDX Manager 0.26.7
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-07
|
|
4
|
+
|
|
5
|
+
## Improvements
|
|
6
|
+
|
|
7
|
+
- Expose per-profile quota provenance and freshness in status JSON (#118,
|
|
8
|
+
`3a7a30b4c9b0dce099027be1a6874c2b87d574b6`). Add `quota_windows.5h` and
|
|
9
|
+
`quota_windows.weekly` with the source, observation time, availability and
|
|
10
|
+
unavailable reason while preserving existing fields and reset times.
|
|
11
|
+
- Reuse existing quota readers without additional requests. Preserve observation
|
|
12
|
+
times for cached evidence and mark old or undated readings stale rather than
|
|
13
|
+
inventing fresh measurements.
|
|
14
|
+
|
|
15
|
+
## Maintenance
|
|
16
|
+
|
|
17
|
+
- Record tray companion checksums for v0.26.6
|
|
18
|
+
(`8e006ca40c942115cac99fd998b6f885358f17f3`).
|
|
19
|
+
- Record release archive checksums for v0.26.6
|
|
20
|
+
(`0f0c53d2991ef84f368e73b60cb293a94ce4af4d`).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CDX Manager 0.26.8
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-07
|
|
4
|
+
|
|
5
|
+
## Improvements
|
|
6
|
+
|
|
7
|
+
- Resolve the conversation of a live Codex process (#125,
|
|
8
|
+
`2150671b`). `cdx conversation <profile> --pid PID` and
|
|
9
|
+
`--process-identity ID` read the exact conversation of a native Codex
|
|
10
|
+
process, without falling back to workspace or recency matching.
|
|
11
|
+
- Add per-run provider arguments to `cdx run` (#120, `92620af`).
|
|
12
|
+
`--extra-args 'ARGS'` is validated like the session setting and appended
|
|
13
|
+
after the profile's arguments, without changing saved settings or
|
|
14
|
+
interactive launches.
|
|
15
|
+
|
|
16
|
+
## Maintenance
|
|
17
|
+
|
|
18
|
+
- Record tray companion checksums for v0.26.7 (`bd53cb9`).
|
|
19
|
+
- Record release archive checksums for v0.26.7 (`145ec42`).
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
## Per-run provider arguments (#120)
|
|
4
|
+
|
|
5
|
+
- Add `cdx run --extra-args 'ARGS'` with the session setting's validation and POSIX splitting, appended after the selected profile's arguments without changing saved settings or interactive launches. Preserve existing provider permission and isolation handling.
|
|
6
|
+
- Forward run arguments to detached supervisors and profile failover; reject Claude model, effort and conversation overrides during continuation. List the option under `cdx schema --json` unvalidated arguments and cover run-only Claude tool allowlists and strict MCP configuration.
|
|
7
|
+
|
|
8
|
+
## Claude profile quota status line
|
|
9
|
+
|
|
10
|
+
- Install a fast cache-only status line for Claude profiles at creation, settings updates and interactive launch/resume. Show the shared launcher gauge, both remaining windows, profile name, low-quota resets and a stale marker; use `quota ?` when data is unavailable.
|
|
11
|
+
- Preserve and report operator-defined status lines. Add `cdx set --status-line on|off` and `cdx unset --status-line` for reversible profile control; leave Codex settings unchanged and document its built-in footer equivalent.
|
|
12
|
+
- Write the standalone status line as UTF-8 when piped, including on Windows, and read repository contract fixtures explicitly as UTF-8.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Quota provenance and profile freshness
|
|
16
|
+
|
|
17
|
+
- Add `quota_windows.5h` and `quota_windows.weekly` to status JSON with `source`, per-profile/window ISO `observed_at`, `availability` (`measured`, `stale`, `unavailable`) and a nullable unavailable `reason`. Keep existing fields and reset times compatible.
|
|
18
|
+
- Reuse the existing Codex rate-limit reader and Claude one-token probe without adding requests; distinguish login/read failures and missing windows without inventing percentages. Preserve older window timestamps during backfill.
|
|
19
|
+
- Mark Codex evidence stale at 300 seconds and Claude evidence at 600 seconds (60 seconds for generic other-provider windows); unknown measurement times are stale. Cached evidence retains known provenance and never receives a new observation time.
|
|
20
|
+
|
|
3
21
|
## Find and resume interactive sessions
|
|
4
22
|
|
|
5
23
|
- Add exact resolved `cdx history --cwd PATH` and `--interactive` filters before the result limit, preserving the JSON envelope and period/summary support.
|
|
@@ -859,6 +859,28 @@
|
|
|
859
859
|
"x86_64-pc-windows-msvc": "1ea355b54e05ea6d78a3a23757a9ad5dd2228aa1d939fb7992082e16da579abb",
|
|
860
860
|
"x86_64-unknown-linux-musl": "770fabb26784efb607d2cce6d6bee51c3d7121283c55cf4cacb9b54ac75e4450"
|
|
861
861
|
}
|
|
862
|
+
},
|
|
863
|
+
"v0.26.6": {
|
|
864
|
+
"github_tarball_sha256": "ac54b4e9cb3ab6eb232d31806fcccb9a56720a3d75ee26cb3b81bd9923dd4ca0",
|
|
865
|
+
"github_zip_sha256": "6a195d19ee29ef568d43b6edbf23980b29c9fc35f295269b82e1f32ce5b7b490",
|
|
866
|
+
"tray_assets": {
|
|
867
|
+
"aarch64-apple-darwin": "4d9be82842365447f4d34ddc577b0571e620c0d900da9f751f50337d3362ff68",
|
|
868
|
+
"aarch64-unknown-linux-musl": "0c13305e28348578fd7437e319dc53aecc0f1e4b3ff21db25e19a70b2fdc6e8b",
|
|
869
|
+
"x86_64-apple-darwin": "b7a8d7ea2b3f0e2b8eba08d845eeb7c41e06b8dbfc1915257c920577e23e01d7",
|
|
870
|
+
"x86_64-pc-windows-msvc": "82fcceda389ba33ef60bc01ccce62249c0a1866479611192761778646ad33880",
|
|
871
|
+
"x86_64-unknown-linux-musl": "a1194dc6e672729e7d6ba079b7c1a393e7098e83edd6ab3b6fd63832a7dad136"
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
"v0.26.7": {
|
|
875
|
+
"github_tarball_sha256": "26e961c60c2473946cb5bfadec467b4d4ca9fd5a07cd313145cf4c4d427b1239",
|
|
876
|
+
"github_zip_sha256": "324c8f938f037e19c31b19e9fc6d4721ca822a7aa9a14d8e0e1af8da52aafaad",
|
|
877
|
+
"tray_assets": {
|
|
878
|
+
"aarch64-apple-darwin": "80088939f95fb6508f98b1e9a29643372dcc5e4cb2f72f06c5016f1708f6f526",
|
|
879
|
+
"aarch64-unknown-linux-musl": "29a044fa1241f38ce2892bd37388258ab1f10d66a74c3beef6462d85a8ab52c2",
|
|
880
|
+
"x86_64-apple-darwin": "d32be4180b361195ec9faef888dc9ff1a996bb74295882008ccb2b07dd16789d",
|
|
881
|
+
"x86_64-pc-windows-msvc": "098c27b535f041b72b6aac89d2c34464a986178be881336b89f99c8dfb5fdb5d",
|
|
882
|
+
"x86_64-unknown-linux-musl": "195737be7f25dc30bab676f53aed2c426612aee55de4cfc95a55a5b2d8ca9b76"
|
|
883
|
+
}
|
|
862
884
|
}
|
|
863
885
|
}
|
|
864
886
|
}
|
package/docs/cli.md
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
| `cdx agents [--max-age-hours N] [--json]` | Each session's last known agent state (working, waiting, done, failed, limit, ended), waiting first; records older than N hours (default 24) are dropped. See [Agent state](sessions-and-notifications.md#agent-state) |
|
|
59
59
|
| `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
|
|
60
60
|
| `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 |
|
|
61
|
-
| `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] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--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 (with `--detach` too: the detached cdx child applies it and the run's `occupancies` record each move), `--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 |
|
|
61
|
+
| `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] [--budget USD] [--extra-args 'ARGS'] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--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 (with `--detach` too: the detached cdx child applies it and the run's `occupancies` record each move), `--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 |
|
|
62
62
|
| `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) |
|
|
63
63
|
| `cdx run-status <run_id> --json` | Status of one run by id |
|
|
64
64
|
| `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 |
|
|
@@ -683,3 +683,75 @@ Interactive launches appear as `status: "running"` as soon as the provider start
|
|
|
683
683
|
History entries expose `conversation_id` and `provider_transcript_match: "conversation_id"` when an exact provider transcript exists. Claude uses its launch session ID; Codex can recover a unique thread from transcript metadata matching the launch directory and time window. Ambiguous discovery remains `provider_transcript_match: "recency"`, an explicitly unverified fallback that must not be treated as a verified resume target. A known ID with no transcript does not borrow another conversation. Existing history is enriched on read without rewriting old rows.
|
|
684
684
|
|
|
685
685
|
`first_user_prompt` contains the first real user text (up to 2,000 characters) from a bounded, redacted transcript read. Provider command wrappers, system reminders, tool results and bare absolute file paths are skipped. Missing, oversized or unavailable evidence leaves the prompt unknown; a supplied initial prompt is used when available.
|
|
686
|
+
|
|
687
|
+
### Resolve the conversation of a live Codex process
|
|
688
|
+
|
|
689
|
+
Consumers such as remote-ops can request process-scoped resolution without
|
|
690
|
+
falling back to workspace/recency:
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
cdx conversation work2 --pid 12345 --tail 20 --json
|
|
694
|
+
cdx conversation work2 --process-identity 'linux:12345:<opaque digest from response>' --json
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Supply the native Codex PID, not the shell, tmux pane PID, Node launcher, or CDX
|
|
698
|
+
supervisor. CDX does not traverse descendants or choose between processes.
|
|
699
|
+
`work2` supplies the provider/profile boundary; its saved conversation is ignored
|
|
700
|
+
in process mode. The selectors are mutually exclusive and cannot be combined with
|
|
701
|
+
`--conversation`, `--cwd`, or `--since`.
|
|
702
|
+
|
|
703
|
+
The Python API is `read_conversation("codex", auth_home, pid=12345, tail=20)` in
|
|
704
|
+
`cdx_manager.conversation` (`src.conversation` in a source checkout). Retain
|
|
705
|
+
`process_identity` and pass it instead of `pid` on later calls to reject PID reuse.
|
|
706
|
+
A bare PID identifies its current occupant, not a previously observed process.
|
|
707
|
+
The opaque identity is a lifetime guard, not an authorization grant.
|
|
708
|
+
|
|
709
|
+
Process-selected pages expose:
|
|
710
|
+
|
|
711
|
+
| Field | Current contract |
|
|
712
|
+
| --- | --- |
|
|
713
|
+
| `resolution_status` | `unknown` until native selected-conversation evidence is available |
|
|
714
|
+
| `resolution_method` | `null`; kernel process identity alone is not a conversation resolution method |
|
|
715
|
+
| `conversation_id` | `null`; no candidate or guessed ID is exposed |
|
|
716
|
+
| `process_identity` | Opaque Linux lifetime guard when process validation succeeds |
|
|
717
|
+
| `reason` | Explicit evidence boundary or process validation failure |
|
|
718
|
+
|
|
719
|
+
**Current measured boundary:** process-to-selected-conversation resolution is not
|
|
720
|
+
available. Even a sole open writable rollout proves only that a process owns a
|
|
721
|
+
thread. During a TUI conversation switch it may belong to a retained thread while
|
|
722
|
+
the selected thread has no rollout yet. Resume argv, session/profile state, cwd,
|
|
723
|
+
mtime, a unique new transcript, and fuzzy text also cannot prove the current
|
|
724
|
+
selection. This implementation deliberately returns
|
|
725
|
+
`native_conversation_identity_unavailable` after validating a live Codex process;
|
|
726
|
+
it does not claim that an open file establishes the selected conversation.
|
|
727
|
+
Remote-ops must treat this as an explicit unknown, never retry via recency.
|
|
728
|
+
|
|
729
|
+
Process mode returns `status: "unavailable"`, empty entries, null conversation ID
|
|
730
|
+
and no cursors. With `--cursor` or `--before`, it returns `reset_required` and no
|
|
731
|
+
entries: a cursor from an explicit-ID or recency lookup cannot cross into process
|
|
732
|
+
mode. No process cursor is issued until it can be bound to both a verified process
|
|
733
|
+
lifetime and a native conversation selection/generation. Existing explicit-ID,
|
|
734
|
+
recency, forward, tail and backward pagination contracts remain unchanged.
|
|
735
|
+
|
|
736
|
+
Only Linux `/proc` process identity is currently supported. Each validation reads
|
|
737
|
+
at most 8 KiB each from process `stat`, `status` and kernel `boot_id`, twice, plus
|
|
738
|
+
the executable symlink/stat. The guard includes PID, start ticks, boot ID, all
|
|
739
|
+
four target UIDs and executable device/inode. Wrong user, a non-`codex` executable,
|
|
740
|
+
dead/reused PID, inaccessible/oversized/malformed evidence, or unsupported
|
|
741
|
+
provider/platform fails closed. Other reasons include `unrelated_user`,
|
|
742
|
+
`unrelated_process`, `process_not_running`, `process_identity_changed`,
|
|
743
|
+
`native_evidence_unavailable`, `evidence_limit`, and
|
|
744
|
+
`unsupported_process_resolution`. No argv, environment, credential store,
|
|
745
|
+
transcript file, directory scan or history database is read in process mode.
|
|
746
|
+
A process identity does not prove profile ownership; missing native profile and
|
|
747
|
+
thread association therefore remains unknown even for an existing profile.
|
|
748
|
+
|
|
749
|
+
The smallest required launch-time tracking is a provider-native event containing
|
|
750
|
+
the selected conversation ID on fresh start, resume and every selection change.
|
|
751
|
+
The launcher must bind each event atomically to the child's boot ID/PID/start ticks,
|
|
752
|
+
profile and a monotonic selection generation; invalidate it on exit. A future
|
|
753
|
+
resolver can consume that bounded record and bind cursors to the same generation.
|
|
754
|
+
An original resume ID or launch timestamp alone is insufficient. No launch/resume
|
|
755
|
+
flags or dependencies are changed here; implementing this requires a verified
|
|
756
|
+
provider lifecycle event source, which the inspected interactive interface does
|
|
757
|
+
not supply. See the mission fragment for the local native probe and Tower checks.
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cdx-manager"
|
|
7
|
-
version = "0.26.
|
|
7
|
+
version = "0.26.8"
|
|
8
8
|
description = "Terminal session manager for Codex and Claude accounts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9.2"
|
|
@@ -55,6 +55,9 @@ cdx = "cdx_manager.cli:cli_entry"
|
|
|
55
55
|
# of which ship the source tree wholesale — worked fine.
|
|
56
56
|
packages = ["cdx_manager", "cdx_manager.commands"]
|
|
57
57
|
|
|
58
|
+
[tool.setuptools.package-data]
|
|
59
|
+
cdx_manager = ["quota_gauge.txt"]
|
|
60
|
+
|
|
58
61
|
[tool.setuptools.package-dir]
|
|
59
62
|
cdx_manager = "src"
|
|
60
63
|
"cdx_manager.commands" = "src/commands"
|
package/src/claude_refresh.py
CHANGED
|
@@ -2,7 +2,7 @@ import inspect
|
|
|
2
2
|
import threading
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
|
|
5
|
-
from .claude_usage import refresh_claude_session_status
|
|
5
|
+
from .claude_usage import ClaudeAuthInvalidError, refresh_claude_session_status
|
|
6
6
|
from .config import PROVIDER_CLAUDE
|
|
7
7
|
from .errors import CdxError
|
|
8
8
|
|
|
@@ -48,6 +48,7 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
48
48
|
|
|
49
49
|
errors = []
|
|
50
50
|
results = {}
|
|
51
|
+
completed = set()
|
|
51
52
|
|
|
52
53
|
threads = []
|
|
53
54
|
|
|
@@ -61,15 +62,22 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
61
62
|
# refresh_fn is the real default, since a caller-injected
|
|
62
63
|
# refresh_fn (tests, mocks) may not accept auth_refresher.
|
|
63
64
|
if refresh_fn is refresh_claude_session_status and s["name"] in already_auth_refreshed:
|
|
64
|
-
usage = refresh_fn(s, auth_refresher=lambda _auth_home: None)
|
|
65
|
+
usage = refresh_fn(s, auth_refresher=lambda _auth_home: None, diagnostic=True)
|
|
66
|
+
elif refresh_fn is refresh_claude_session_status:
|
|
67
|
+
usage = refresh_fn(s, diagnostic=True)
|
|
65
68
|
else:
|
|
66
69
|
usage = refresh_fn(s)
|
|
67
70
|
if inspect.isawaitable(usage):
|
|
68
71
|
raise CdxError("Claude refresh function returned an awaitable from a sync callable.")
|
|
69
|
-
if usage:
|
|
72
|
+
if usage and usage.get("_quota_error"):
|
|
73
|
+
errors.append({"session": s["name"], "error": CdxError("Claude quota unavailable"), "reason": usage["_quota_error"]})
|
|
74
|
+
elif usage:
|
|
70
75
|
results[s["name"]] = usage
|
|
71
76
|
except Exception as e:
|
|
72
|
-
errors.append({"session": s["name"], "error": e
|
|
77
|
+
errors.append({"session": s["name"], "error": e,
|
|
78
|
+
"reason": "login_expired" if isinstance(e, ClaudeAuthInvalidError) else "quota_read_failed"})
|
|
79
|
+
finally:
|
|
80
|
+
completed.add(s["name"])
|
|
73
81
|
|
|
74
82
|
for s in claude_sessions:
|
|
75
83
|
t = threading.Thread(target=fetch, args=(s,), daemon=True)
|
|
@@ -83,6 +91,11 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
83
91
|
results = dict(results)
|
|
84
92
|
errors = list(errors)
|
|
85
93
|
|
|
94
|
+
completed = set(completed)
|
|
95
|
+
for session in claude_sessions:
|
|
96
|
+
if session["name"] not in completed:
|
|
97
|
+
errors.append({"session": session["name"], "error": CdxError("Claude quota read timed out"), "reason": "probe_timeout"})
|
|
98
|
+
|
|
86
99
|
recorded = []
|
|
87
100
|
for name, usage in results.items():
|
|
88
101
|
try:
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Fast, cache-only Claude status line; executable without importing the CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
MARKER = "--cdx-claude-status"
|
|
11
|
+
CACHE_TTL_SECONDS = 10 * 60 # Same freshness window as Claude's cached status.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read(path):
|
|
15
|
+
try:
|
|
16
|
+
with open(path, encoding="utf-8") as handle:
|
|
17
|
+
return json.load(handle)
|
|
18
|
+
except (OSError, ValueError):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _pct(value):
|
|
23
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 100 and math.isfinite(value) else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def gauge(value):
|
|
27
|
+
# The tray menu embeds this same eight-cell glyph strip at build time.
|
|
28
|
+
full, empty = Path(__file__).with_name("quota_gauge.txt").read_text(encoding="utf-8").splitlines()
|
|
29
|
+
filled = int(math.floor((value or 0) / 100 * len(full) + 0.5))
|
|
30
|
+
return full[:filled] + empty[filled:]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _clean(value):
|
|
34
|
+
return "".join(c for c in str(value) if c.isprintable())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render(base_dir, env=None, now=None):
|
|
38
|
+
env = os.environ if env is None else env
|
|
39
|
+
data = _read(os.path.join(base_dir, "sessions.json"))
|
|
40
|
+
if not isinstance(data, dict) or not isinstance(data.get("sessions"), list):
|
|
41
|
+
return "quota ?"
|
|
42
|
+
home = env.get("HOME") or env.get("USERPROFILE")
|
|
43
|
+
name = env.get("CDX_SESSION_NAME")
|
|
44
|
+
matches = [s for s in data["sessions"] if isinstance(s, dict) and s.get("provider") == "claude" and (
|
|
45
|
+
os.path.realpath(s.get("authHome") or "") == os.path.realpath(home) if home else s.get("name") == name
|
|
46
|
+
)]
|
|
47
|
+
if len(matches) != 1:
|
|
48
|
+
return "quota ?"
|
|
49
|
+
session = matches[0]
|
|
50
|
+
status = session.get("lastStatus")
|
|
51
|
+
if not isinstance(status, dict):
|
|
52
|
+
return "quota ?"
|
|
53
|
+
values = [_pct(status.get(f"remaining_{window}_pct")) for window in ("5h", "week")]
|
|
54
|
+
known = [v for v in values if v is not None]
|
|
55
|
+
if not known:
|
|
56
|
+
return "quota ?"
|
|
57
|
+
now = now or datetime.now(timezone.utc)
|
|
58
|
+
evidence = status.get("quota_windows") or {}
|
|
59
|
+
stale = False
|
|
60
|
+
for window, value in zip(("5h", "weekly"), values):
|
|
61
|
+
if value is None:
|
|
62
|
+
continue
|
|
63
|
+
reading = evidence.get(window, {}) if isinstance(evidence, dict) else {}
|
|
64
|
+
if not isinstance(reading, dict):
|
|
65
|
+
reading = {}
|
|
66
|
+
timestamp = reading.get("observed_at", status.get("updated_at") or session.get("lastStatusAt"))
|
|
67
|
+
try:
|
|
68
|
+
observed = datetime.fromisoformat(str(timestamp).replace("Z", "+00:00"))
|
|
69
|
+
stale |= observed.tzinfo is None or not 0 <= (now - observed).total_seconds() < CACHE_TTL_SECONDS
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
stale = True
|
|
72
|
+
stale |= reading.get("availability") in ("stale", "unavailable")
|
|
73
|
+
parts = []
|
|
74
|
+
for window, label, value in zip(("5h", "week"), ("5h", "wk"), values):
|
|
75
|
+
text = f"{label} {value:g}%" if value is not None else f"{label} ?"
|
|
76
|
+
reset = status.get(f"reset_{window}_at")
|
|
77
|
+
if value is not None and value < 20 and reset:
|
|
78
|
+
text += f" (reset {_clean(reset)})"
|
|
79
|
+
parts.append(text)
|
|
80
|
+
parts.append(_clean(session.get("name", "?")))
|
|
81
|
+
if stale:
|
|
82
|
+
parts.append("stale")
|
|
83
|
+
return f"{gauge(min(known))} " + " · ".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def provision(session, base_dir):
|
|
87
|
+
"""Return a report on conflicts; never replace an operator's status line."""
|
|
88
|
+
if session.get("provider") != "claude":
|
|
89
|
+
return None
|
|
90
|
+
import shlex
|
|
91
|
+
import subprocess
|
|
92
|
+
|
|
93
|
+
from .agent_notify import _read_json, _write_json
|
|
94
|
+
|
|
95
|
+
path = os.path.join(session["authHome"], ".claude", "settings.json")
|
|
96
|
+
settings = _read_json(path)
|
|
97
|
+
if settings is None:
|
|
98
|
+
return f"{session['name']}: statusLine skipped (unreadable settings.json)"
|
|
99
|
+
current = settings.get("statusLine")
|
|
100
|
+
own = False
|
|
101
|
+
if isinstance(current, dict) and isinstance(current.get("command"), str):
|
|
102
|
+
try:
|
|
103
|
+
parts = shlex.split(current["command"], posix=os.name != "nt")
|
|
104
|
+
own = len(parts) == 4 and os.path.basename(parts[1].strip('"')) == "claude_status_line.py" and parts[2] == MARKER
|
|
105
|
+
except ValueError:
|
|
106
|
+
pass
|
|
107
|
+
if "statusLine" in settings and not own:
|
|
108
|
+
return f"{session['name']}: operator-defined statusLine preserved"
|
|
109
|
+
if (session.get("launch") or {}).get("status_line") is False:
|
|
110
|
+
if not own:
|
|
111
|
+
return None
|
|
112
|
+
del settings["statusLine"]
|
|
113
|
+
else:
|
|
114
|
+
args = [sys.executable, os.path.abspath(__file__), MARKER, os.path.abspath(base_dir)]
|
|
115
|
+
command = subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args)
|
|
116
|
+
desired = {**(current or {}), "type": "command", "command": command}
|
|
117
|
+
if current == desired:
|
|
118
|
+
return None
|
|
119
|
+
settings["statusLine"] = desired
|
|
120
|
+
if not _write_json(path, settings):
|
|
121
|
+
return f"{session['name']}: statusLine skipped (settings.json is not writable)"
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv=None, stdout=None, env=None):
|
|
126
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
127
|
+
stdout = sys.stdout if stdout is None else stdout
|
|
128
|
+
try:
|
|
129
|
+
text = render(argv[1], env=env) if len(argv) == 2 and argv[0] == MARKER else "quota ?"
|
|
130
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
131
|
+
text = "quota ?"
|
|
132
|
+
stdout.write(text + "\n")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
# Claude reads UTF-8 from a pipe; Windows otherwise uses its ANSI code page.
|
|
138
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
139
|
+
raise SystemExit(main())
|
package/src/claude_usage.py
CHANGED
|
@@ -15,6 +15,7 @@ except ImportError: # pragma: no cover - Windows
|
|
|
15
15
|
|
|
16
16
|
from .claude_credentials import read_keychain_credentials
|
|
17
17
|
from .errors import CdxError
|
|
18
|
+
from .quota_provenance import quota_windows
|
|
18
19
|
|
|
19
20
|
MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
20
21
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
@@ -368,7 +369,7 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
368
369
|
reset_week_at = _format_reset_date(_parse_reset_epoch(reset_7d)) if reset_7d else None
|
|
369
370
|
reset_at = reset_week_at or reset_5h_at
|
|
370
371
|
|
|
371
|
-
|
|
372
|
+
status = {
|
|
372
373
|
"remaining_5h_pct": _remaining_from_utilization(utilization_5h),
|
|
373
374
|
"remaining_week_pct": _remaining_from_utilization(utilization_7d),
|
|
374
375
|
"reset_5h_at": reset_5h_at,
|
|
@@ -378,20 +379,22 @@ def fetch_claude_rate_limit_headers(access_token):
|
|
|
378
379
|
"raw_status_text": None,
|
|
379
380
|
"source_ref": "api:anthropic-ratelimit-headers",
|
|
380
381
|
}
|
|
382
|
+
status["quota_windows"] = quota_windows(status, "claude")
|
|
383
|
+
return status
|
|
381
384
|
|
|
382
385
|
|
|
383
|
-
def refresh_claude_session_status(session, auth_refresher=None):
|
|
386
|
+
def refresh_claude_session_status(session, auth_refresher=None, diagnostic=False):
|
|
384
387
|
auth_home = session.get("authHome", "")
|
|
385
388
|
refresher = auth_refresher or _refresh_claude_cli_credentials
|
|
386
389
|
refresher(auth_home)
|
|
387
390
|
creds = _read_claude_credentials(auth_home)
|
|
388
391
|
if not creds or not creds.get("accessToken"):
|
|
389
|
-
return None
|
|
392
|
+
return {"_quota_error": "login_required"} if diagnostic else None
|
|
390
393
|
if _access_token_expired(creds):
|
|
391
394
|
# ponytail: `claude auth status` doesn't refresh an expired access
|
|
392
395
|
# token (verified no-op) and only a real Claude Code invocation does.
|
|
393
396
|
# Probing anyway just reproduces a 401 we can already predict
|
|
394
|
-
# offline, so skip it
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return
|
|
397
|
+
# offline, so skip it and retain the cached observation time.
|
|
398
|
+
return {"_quota_error": "login_expired"} if diagnostic else None
|
|
399
|
+
status = fetch_claude_rate_limit_headers(creds["accessToken"])
|
|
400
|
+
return status or ({"_quota_error": "quota_read_failed"} if diagnostic else None)
|
package/src/cli.py
CHANGED
|
@@ -266,14 +266,14 @@ def _print_help(use_color=False):
|
|
|
266
266
|
f" {_style('cdx configs [--json]', '36', use_color)}",
|
|
267
267
|
f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
|
|
268
268
|
f" {_style(SET_USAGE[len('Usage: '):], '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)}",
|
|
269
|
+
f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--status-line|--notify-preview|--model|--priority|--max-parallel|--all) [--json]', '36', use_color)}",
|
|
270
270
|
f" {_style('cdx start <name> [--dir PATH] [--resume] [--json]', '36', use_color)}",
|
|
271
271
|
f" {_style('cdx history backfill-projects [--mapping FILE] [--dry-run|--apply] [--json]', '36', use_color)}",
|
|
272
272
|
f" {_style('cdx history [name] [--cwd PATH] [--interactive] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
|
|
273
273
|
f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
|
|
274
274
|
f" {_style('cdx last [--json]', '36', use_color)}",
|
|
275
275
|
f" {_style('cdx resume <name> [--project PATH] [--conversation <id>] [--json]', '36', use_color)}",
|
|
276
|
-
f" {_style('cdx conversation <name> [--conversation ID] [--cwd PATH] [--since ISO] [--cursor CURSOR | --tail N | --before CURSOR] [--limit N] --json', '36', use_color)}",
|
|
276
|
+
f" {_style('cdx conversation <name> [--conversation ID | --pid PID | --process-identity ID] [--cwd PATH] [--since ISO] [--cursor CURSOR | --tail N | --before CURSOR] [--limit N] --json', '36', use_color)}",
|
|
277
277
|
f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
|
|
278
278
|
f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
|
|
279
279
|
f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
|