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/src/cli_args.py
CHANGED
|
@@ -22,6 +22,7 @@ from .config import (
|
|
|
22
22
|
)
|
|
23
23
|
from .errors import CdxArgumentError, CdxError
|
|
24
24
|
from .provider_runtime import _normalize_reasoning_effort
|
|
25
|
+
from .session_service import _normalize_launch_settings
|
|
25
26
|
|
|
26
27
|
STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | cdx status --small|-s [--refresh|--cached] [--timeout SECONDS] | cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]"
|
|
27
28
|
DOCTOR_USAGE = "Usage: cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [--check-provider-flags] [--json]"
|
|
@@ -35,8 +36,8 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
|
|
|
35
36
|
MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
|
|
36
37
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
|
|
37
38
|
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--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
|
+
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] [--status-line 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]"
|
|
40
|
+
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--agent-state|--status-line|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--max-parallel|--all) [--json]"
|
|
40
41
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
41
42
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
42
43
|
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
@@ -49,7 +50,7 @@ CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
|
|
|
49
50
|
SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
50
51
|
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"
|
|
51
52
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
52
|
-
RUN_USAGE = "Usage: cdx run [session] [--remote SSH_TARGET] [--remote-prefix COMMAND] (--cwd|--dir) PATH [--project 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] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
|
|
53
|
+
RUN_USAGE = "Usage: cdx run [session] [--remote SSH_TARGET] [--remote-prefix COMMAND] (--cwd|--dir) PATH [--project 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] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--extra-args 'ARGS'] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
|
|
53
54
|
RUN_USAGE += "\nRemote Ollama generates text only; it is not an autonomous coding agent and does not edit the repository. Requires an installed worker model; no authentication."
|
|
54
55
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
55
56
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
@@ -278,6 +279,7 @@ def _parse_set_args(args):
|
|
|
278
279
|
"--logics": {"key": "logics", "type": "str", "default": None, "transform": _on_off("logics")},
|
|
279
280
|
"--notify": {"key": "notify", "type": "str", "default": None, "transform": _on_off("notify")},
|
|
280
281
|
"--agent-state": {"key": "agent_state", "type": "str", "default": None, "transform": _on_off("agent state")},
|
|
282
|
+
"--status-line": {"key": "status_line", "type": "str", "default": None, "transform": _on_off("status line")},
|
|
281
283
|
"--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _on_off("notify preview")},
|
|
282
284
|
"--scrub-env": {"key": "scrub_env", "type": "str", "default": None, "transform": _on_off("scrub env")},
|
|
283
285
|
"--shared-memory": {"key": "shared_memory", "type": "str", "default": None, "transform": _on_off("shared memory")},
|
|
@@ -305,7 +307,7 @@ def _parse_set_args(args):
|
|
|
305
307
|
raise CdxError(SET_USAGE)
|
|
306
308
|
settings = {
|
|
307
309
|
key: parsed[key]
|
|
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")
|
|
310
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "agent_state", "status_line", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel")
|
|
309
311
|
if parsed[key] is not None
|
|
310
312
|
}
|
|
311
313
|
if not settings:
|
|
@@ -329,6 +331,7 @@ def _parse_unset_args(args):
|
|
|
329
331
|
"--logics": {"key": "logics", "type": "bool", "default": False},
|
|
330
332
|
"--notify": {"key": "notify", "type": "bool", "default": False},
|
|
331
333
|
"--agent-state": {"key": "agent_state", "type": "bool", "default": False},
|
|
334
|
+
"--status-line": {"key": "status_line", "type": "bool", "default": False},
|
|
332
335
|
"--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
|
|
333
336
|
"--scrub-env": {"key": "scrub_env", "type": "bool", "default": False},
|
|
334
337
|
"--shared-memory": {"key": "shared_memory", "type": "bool", "default": False},
|
|
@@ -356,7 +359,7 @@ def _parse_unset_args(args):
|
|
|
356
359
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
357
360
|
raise CdxError(UNSET_USAGE)
|
|
358
361
|
_UNSET_KEYS = (
|
|
359
|
-
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro",
|
|
362
|
+
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "agent_state", "status_line", "notify_preview", "scrub_env", "shared_memory", "retro",
|
|
360
363
|
"model", "fallback_model", "budget", "extra_args", "priority", "policy", "max_parallel",
|
|
361
364
|
)
|
|
362
365
|
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
|
@@ -503,13 +506,13 @@ def cdx_schema():
|
|
|
503
506
|
"kind": {"accepted": list(RUN_KIND_VALUES)},
|
|
504
507
|
"provider": {"accepted": list(PROVIDERS)},
|
|
505
508
|
},
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
# validates it, so publishing it here would claim a guarantee that does
|
|
509
|
-
# not exist. A caller must treat it as opaque.
|
|
509
|
+
# Provider flag semantics remain opaque; only the argument string's
|
|
510
|
+
# syntax and size are checked by cdx.
|
|
510
511
|
"unvalidated": {
|
|
511
512
|
"extra_args": {
|
|
512
|
-
"
|
|
513
|
+
"commands": ["set", "run"],
|
|
514
|
+
"option": "--extra-args",
|
|
515
|
+
"reason": "Split into literal argv entries; cdx does not verify provider flag semantics.",
|
|
513
516
|
"not_checked_by": ["cdx schema", "cdx doctor --check-provider-flags"],
|
|
514
517
|
},
|
|
515
518
|
},
|
|
@@ -695,6 +698,7 @@ def _parse_run_args(args):
|
|
|
695
698
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": _parse_run_provider},
|
|
696
699
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
697
700
|
"--continue-run": {"key": "continue_run", "type": "str", "default": None},
|
|
701
|
+
"--extra-args": {"key": "extra_args", "type": "str", "default": None},
|
|
698
702
|
"--kind": {"key": "kind", "type": "str", "default": "assistant"},
|
|
699
703
|
"--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
|
|
700
704
|
"--power": {"key": "power", "type": "str", "default": None},
|
|
@@ -753,6 +757,7 @@ def _parse_run_args(args):
|
|
|
753
757
|
raise CdxArgumentError("cdx run: --remote-prefix requires --remote and a nonempty command.",
|
|
754
758
|
code=ARG_CODE_INVALID_VALUE, arguments=["--remote-prefix"])
|
|
755
759
|
effort = _parse_run_effort(parsed["reasoning_effort"], parsed["power"])
|
|
760
|
+
extra = _normalize_launch_settings({"extra_args": parsed["extra_args"]}).get("extra_args")
|
|
756
761
|
return {
|
|
757
762
|
"name": name,
|
|
758
763
|
"remote": parsed["remote"],
|
|
@@ -763,6 +768,7 @@ def _parse_run_args(args):
|
|
|
763
768
|
"prompt_file": parsed["prompt_file"],
|
|
764
769
|
"prompt": parsed["prompt"],
|
|
765
770
|
"continue_run": parsed["continue_run"],
|
|
771
|
+
"extra_args": extra,
|
|
766
772
|
"kind": parsed["kind"],
|
|
767
773
|
"model": parsed["model"],
|
|
768
774
|
"permission": parsed["permission"],
|
package/src/cli_helpers.py
CHANGED
|
@@ -313,6 +313,7 @@ def _format_launch_config(session, use_color=False):
|
|
|
313
313
|
("logics", "Logics"),
|
|
314
314
|
("notify", "Agent alerts"),
|
|
315
315
|
("agent_state", "Agent state"),
|
|
316
|
+
("status_line", "Claude quota status line"),
|
|
316
317
|
("notify_preview", "Response preview"),
|
|
317
318
|
("scrub_env", "Scrub agent env"),
|
|
318
319
|
("shared_memory", "Shared memory"),
|
|
@@ -325,6 +326,8 @@ def _format_launch_config(session, use_color=False):
|
|
|
325
326
|
("policy", "Policy"),
|
|
326
327
|
("max_parallel", "Max parallel"),
|
|
327
328
|
]:
|
|
329
|
+
if key == "status_line" and session["provider"] != "claude":
|
|
330
|
+
continue
|
|
328
331
|
rows.append([
|
|
329
332
|
_dim(label, use_color),
|
|
330
333
|
_format_launch_setting_value(launch, key, use_color=use_color),
|
|
@@ -345,14 +348,16 @@ def _format_launch_settings_hint(name="<name>"):
|
|
|
345
348
|
)
|
|
346
349
|
|
|
347
350
|
def _format_launch_setting_value(launch, key, use_color=False):
|
|
348
|
-
if key in ("fast", "rtk", "logics", "notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
351
|
+
if key in ("fast", "rtk", "logics", "notify", "agent_state", "status_line", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
349
352
|
if launch.get(key) is True:
|
|
350
353
|
return _style("on", "32", use_color)
|
|
351
354
|
if launch.get(key) is False:
|
|
352
355
|
return _style("off", "2", use_color)
|
|
356
|
+
if key == "status_line":
|
|
357
|
+
return _dim("on (Claude)", use_color)
|
|
353
358
|
if key == "logics":
|
|
354
359
|
return _dim("auto", use_color)
|
|
355
|
-
if key in ("notify", "agent_state", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
360
|
+
if key in ("notify", "agent_state", "status_line", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
356
361
|
return _style("off", "2", use_color)
|
|
357
362
|
return _dim("default", use_color)
|
|
358
363
|
value = launch.get(key)
|
package/src/codex_usage.py
CHANGED
|
@@ -7,6 +7,7 @@ import threading
|
|
|
7
7
|
import time
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
|
|
10
|
+
from .quota_provenance import quota_windows
|
|
10
11
|
from .status_source import _is_zero_credit_balance
|
|
11
12
|
|
|
12
13
|
try:
|
|
@@ -139,7 +140,7 @@ def normalize_codex_rate_limit_snapshot(snapshot, reset_credits=None):
|
|
|
139
140
|
})
|
|
140
141
|
|
|
141
142
|
raw_status_text = json.dumps(snapshot, sort_keys=True)
|
|
142
|
-
|
|
143
|
+
status = {
|
|
143
144
|
"remaining_5h_pct": _remaining_from_used_percent(
|
|
144
145
|
five_hour.get("usedPercent", five_hour.get("used_percent"))
|
|
145
146
|
),
|
|
@@ -156,6 +157,8 @@ def normalize_codex_rate_limit_snapshot(snapshot, reset_credits=None):
|
|
|
156
157
|
"raw_status_text": raw_status_text,
|
|
157
158
|
"source_ref": "api:codex-app-server-rate-limits",
|
|
158
159
|
}
|
|
160
|
+
status["quota_windows"] = quota_windows(status, "codex")
|
|
161
|
+
return status
|
|
159
162
|
|
|
160
163
|
|
|
161
164
|
def _reader_thread(stream, output):
|
|
@@ -305,13 +308,18 @@ def _request_codex_app_server(auth_home, method, params, failure_reason, timeout
|
|
|
305
308
|
pass
|
|
306
309
|
|
|
307
310
|
|
|
308
|
-
def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
309
|
-
|
|
311
|
+
def fetch_codex_rate_limits(session, timeout=5, popen_factory=None, diagnostic=False):
|
|
312
|
+
result = fetch_codex_rate_limit_diagnostic(
|
|
310
313
|
session,
|
|
311
314
|
timeout=timeout,
|
|
312
315
|
popen_factory=popen_factory,
|
|
313
316
|
)
|
|
314
|
-
|
|
317
|
+
if result.get("ok"):
|
|
318
|
+
return result.get("status")
|
|
319
|
+
if diagnostic:
|
|
320
|
+
reason = "login_expired" if diagnostic_needs_codex_login(result) else result.get("reason")
|
|
321
|
+
return {"_quota_error": reason or "probe_failed"}
|
|
322
|
+
return None
|
|
315
323
|
|
|
316
324
|
|
|
317
325
|
def consume_codex_rate_limit_reset_credit(session, idempotency_key, credit_id=None, timeout=5, popen_factory=None):
|
package/src/commands/backup.py
CHANGED
|
@@ -103,12 +103,15 @@ def handle_import(rest, ctx):
|
|
|
103
103
|
payload = _json_success(
|
|
104
104
|
"import",
|
|
105
105
|
message,
|
|
106
|
+
warnings=result.get("status_line_warnings", []),
|
|
106
107
|
bundle=result,
|
|
107
108
|
)
|
|
108
109
|
if parsed["json"]:
|
|
109
110
|
_write_json(ctx, payload)
|
|
110
111
|
return 0
|
|
111
112
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
113
|
+
for warning in result.get("status_line_warnings", []):
|
|
114
|
+
ctx["out"](f"{warning}\n")
|
|
112
115
|
retained = result.get("retained_local_credentials") or []
|
|
113
116
|
if retained:
|
|
114
117
|
# Merge keeps local values, so say which sessions kept their own login
|
|
@@ -6,13 +6,15 @@ from ..conversation import read_conversation
|
|
|
6
6
|
from ..errors import CdxError
|
|
7
7
|
from ..provider_runtime import _get_auth_home
|
|
8
8
|
|
|
9
|
-
CONVERSATION_USAGE = "Usage: cdx conversation <name> [--conversation ID] [--cwd PATH] [--since ISO] [--cursor CURSOR | --tail N | --before CURSOR] [--limit N] --json"
|
|
9
|
+
CONVERSATION_USAGE = "Usage: cdx conversation <name> [--conversation ID | --pid PID | --process-identity ID] [--cwd PATH] [--since ISO] [--cursor CURSOR | --tail N | --before CURSOR] [--limit N] --json"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def handle_conversation(rest, ctx):
|
|
13
13
|
parsed = _parse_flag_args(rest, {
|
|
14
14
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
15
15
|
"--conversation": {"key": "conversation", "type": "str"},
|
|
16
|
+
"--pid": {"key": "pid", "type": "str"},
|
|
17
|
+
"--process-identity": {"key": "process_identity", "type": "str"},
|
|
16
18
|
"--cwd": {"key": "cwd", "type": "str"},
|
|
17
19
|
"--since": {"key": "since", "type": "str"},
|
|
18
20
|
"--tail": {"key": "tail", "type": "str"},
|
|
@@ -26,15 +28,20 @@ def handle_conversation(rest, ctx):
|
|
|
26
28
|
if not session:
|
|
27
29
|
raise CdxError(f"Unknown session: {parsed['names'][0]}")
|
|
28
30
|
try:
|
|
31
|
+
pid = int(parsed["pid"]) if parsed["pid"] is not None else None
|
|
29
32
|
limit = int(parsed["limit"])
|
|
30
33
|
tail = int(parsed["tail"]) if parsed["tail"] is not None else None
|
|
31
34
|
except ValueError:
|
|
32
35
|
raise CdxError(CONVERSATION_USAGE) from None
|
|
36
|
+
process_selected = parsed["pid"] is not None or parsed["process_identity"] is not None
|
|
37
|
+
if parsed["pid"] is not None and parsed["process_identity"] is not None:
|
|
38
|
+
raise CdxError(CONVERSATION_USAGE)
|
|
33
39
|
page = read_conversation(
|
|
34
40
|
session["provider"], _get_auth_home(session),
|
|
35
|
-
conversation_id=parsed["conversation"] or (session.get("conversation") or {}).get("id"),
|
|
41
|
+
conversation_id=parsed["conversation"] or (None if process_selected else (session.get("conversation") or {}).get("id")),
|
|
42
|
+
pid=pid, process_identity=parsed["process_identity"],
|
|
36
43
|
cwd=parsed["cwd"], started_at=parsed["since"],
|
|
37
|
-
cursor=parsed["cursor"], limit=limit, tail=tail, before=parsed["before"], fallback_to_recency=not parsed["conversation"],
|
|
44
|
+
cursor=parsed["cursor"], limit=limit, tail=tail, before=parsed["before"], fallback_to_recency=not process_selected and not parsed["conversation"],
|
|
38
45
|
)
|
|
39
46
|
_write_json(ctx, _json_success("conversation", "Conversation page loaded.", session=session["name"], **page))
|
|
40
47
|
return 0
|
package/src/commands/launch.py
CHANGED
|
@@ -308,6 +308,13 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
308
308
|
if not json_flag:
|
|
309
309
|
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
310
310
|
_write_update_notice(ctx)
|
|
311
|
+
from ..claude_status_line import provision as provision_status_line
|
|
312
|
+
|
|
313
|
+
status_line_notice = provision_status_line(session, ctx["service"]["base_dir"])
|
|
314
|
+
if status_line_notice:
|
|
315
|
+
warnings.append(status_line_notice)
|
|
316
|
+
if not json_flag:
|
|
317
|
+
ctx["out"](f"{status_line_notice}\n")
|
|
311
318
|
alerts_allowed = _notification_provisioning_enabled(session, ctx, json_flag)
|
|
312
319
|
keep_state = agent_state_enabled(session)
|
|
313
320
|
installed = provision(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from ..claude_status_line import provision as provision_status_line
|
|
6
7
|
from ..cli_args import (
|
|
7
8
|
_parse_add_args,
|
|
8
9
|
_parse_copy_args,
|
|
@@ -70,6 +71,7 @@ def handle_copy(rest, ctx):
|
|
|
70
71
|
json_flag, args = _parse_json_flag(rest)
|
|
71
72
|
parsed = _parse_copy_args(args)
|
|
72
73
|
result = ctx["service"]["copy_session"](parsed["source"], parsed["dest"])
|
|
74
|
+
warning = provision_status_line(result["session"], ctx["service"]["base_dir"])
|
|
73
75
|
overwritten = " (overwritten)" if result["overwritten"] else ""
|
|
74
76
|
message = f"Copied session {parsed['source']} to {parsed['dest']}{overwritten}"
|
|
75
77
|
if json_flag:
|
|
@@ -78,20 +80,26 @@ def handle_copy(rest, ctx):
|
|
|
78
80
|
message,
|
|
79
81
|
session=result["session"],
|
|
80
82
|
overwritten=result["overwritten"],
|
|
83
|
+
warnings=[warning] if warning else [],
|
|
81
84
|
))
|
|
82
85
|
return 0
|
|
83
86
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
87
|
+
if warning:
|
|
88
|
+
ctx["out"](f"{warning}\n")
|
|
84
89
|
return 0
|
|
85
90
|
|
|
86
91
|
def handle_rename(rest, ctx):
|
|
87
92
|
json_flag, args = _parse_json_flag(rest)
|
|
88
93
|
parsed = _parse_rename_args(args)
|
|
89
94
|
session = ctx["service"]["rename_session"](parsed["source"], parsed["dest"])
|
|
95
|
+
warning = provision_status_line(session, ctx["service"]["base_dir"])
|
|
90
96
|
message = f"Renamed session {parsed['source']} to {parsed['dest']}"
|
|
91
97
|
if json_flag:
|
|
92
|
-
_write_json(ctx, _json_success("rename", message, session=session))
|
|
98
|
+
_write_json(ctx, _json_success("rename", message, session=session, warnings=[warning] if warning else []))
|
|
93
99
|
return 0
|
|
94
100
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
101
|
+
if warning:
|
|
102
|
+
ctx["out"](f"{warning}\n")
|
|
95
103
|
return 0
|
|
96
104
|
|
|
97
105
|
def handle_label(rest, ctx):
|
package/src/commands/runs.py
CHANGED
|
@@ -209,6 +209,7 @@ def _detached_child_argv(parsed, prompt_path, session_name, cwd):
|
|
|
209
209
|
("--project", "project"),
|
|
210
210
|
("--budget", "budget"),
|
|
211
211
|
("--continue-run", "continue_run"),
|
|
212
|
+
("--extra-args", "extra_args"),
|
|
212
213
|
("--model", "model"),
|
|
213
214
|
("--kind", "kind"),
|
|
214
215
|
("--permission", "permission"),
|
|
@@ -216,7 +217,8 @@ def _detached_child_argv(parsed, prompt_path, session_name, cwd):
|
|
|
216
217
|
):
|
|
217
218
|
value = parsed.get(key)
|
|
218
219
|
if value:
|
|
219
|
-
|
|
220
|
+
# A literal provider flag may also be a cdx flag, so bind the value.
|
|
221
|
+
argv += [f"{flag}={value}"] if key == "extra_args" else [flag, str(value)]
|
|
220
222
|
# reasoning_effort and power are normalized to the same value upstream;
|
|
221
223
|
# passing one back is enough and avoids re-tripping the must-match check.
|
|
222
224
|
if parsed.get("reasoning_effort"):
|
|
@@ -612,6 +614,14 @@ def _budget_observation(session, paths, baseline):
|
|
|
612
614
|
return observation
|
|
613
615
|
|
|
614
616
|
|
|
617
|
+
def _run_session(session, launch_updates, extra_args):
|
|
618
|
+
"""Apply run-only settings without mutating the profile or its successors."""
|
|
619
|
+
launch = {**(session.get("launch") or {}), **launch_updates}
|
|
620
|
+
if extra_args is not None:
|
|
621
|
+
launch["extra_args"] = " ".join(value for value in (launch.get("extra_args"), extra_args) if value)
|
|
622
|
+
return {**session, "launch": launch}
|
|
623
|
+
|
|
624
|
+
|
|
615
625
|
def _continue_run_session(parsed, ctx):
|
|
616
626
|
"""Resolve only the parent's profile and exact persisted conversation."""
|
|
617
627
|
previous = _run_registry(ctx).get(parsed["continue_run"])
|
|
@@ -651,7 +661,7 @@ def _continue_run_session(parsed, ctx):
|
|
|
651
661
|
pinned_options = {"--model", "--effort", "--fallback-model", "--resume", "--continue",
|
|
652
662
|
"--session-id", "--fork-session", "--resume-session-at", "--from-pr",
|
|
653
663
|
"--settings", "--agent", "--agents"}
|
|
654
|
-
for arg in split_extra_args((session.get("
|
|
664
|
+
for arg in split_extra_args(_run_session(session, {}, parsed.get("extra_args"))["launch"].get("extra_args")) or []:
|
|
655
665
|
if arg.split("=", 1)[0] in pinned_options or arg.startswith(("-r", "-c")):
|
|
656
666
|
raise CdxError("--continue-run cannot use Claude extra_args that override model, effort, or conversation.",
|
|
657
667
|
code="continue_run_settings_mismatch")
|
|
@@ -767,13 +777,7 @@ def handle_run(rest, ctx):
|
|
|
767
777
|
# the profile's current conversation. Continuations use the parent run.
|
|
768
778
|
if not parsed.get("continue_run"):
|
|
769
779
|
session = ctx["service"]["begin_conversation"](session["name"])
|
|
770
|
-
run_session =
|
|
771
|
-
**session,
|
|
772
|
-
"launch": {
|
|
773
|
-
**(session.get("launch") or {}),
|
|
774
|
-
**launch_updates,
|
|
775
|
-
},
|
|
776
|
-
}
|
|
780
|
+
run_session = _run_session(session, launch_updates, parsed.get("extra_args"))
|
|
777
781
|
if not parsed.get("remote"):
|
|
778
782
|
_ensure_session_authentication(
|
|
779
783
|
run_session,
|
|
@@ -970,7 +974,7 @@ def handle_run(rest, ctx):
|
|
|
970
974
|
else:
|
|
971
975
|
selection_row = successor.get("row") or {}
|
|
972
976
|
next_session = successor["session"]
|
|
973
|
-
run_session =
|
|
977
|
+
run_session = _run_session(next_session, launch_updates, parsed.get("extra_args"))
|
|
974
978
|
# Each new profile starts with the requested model, or its own default.
|
|
975
979
|
models = capacity_models(run_session)
|
|
976
980
|
model_index = capacity_retries = 0
|
|
@@ -1025,10 +1029,7 @@ def handle_run(rest, ctx):
|
|
|
1025
1029
|
failover_error_code = "failover_exhausted"
|
|
1026
1030
|
break
|
|
1027
1031
|
selection_row = successor.get("row") or {}
|
|
1028
|
-
run_session =
|
|
1029
|
-
**successor["session"],
|
|
1030
|
-
"launch": {**(successor["session"].get("launch") or {}), **launch_updates},
|
|
1031
|
-
}
|
|
1032
|
+
run_session = _run_session(successor["session"], launch_updates, parsed.get("extra_args"))
|
|
1032
1033
|
attempted.append(run_session["name"])
|
|
1033
1034
|
models = capacity_models(run_session)
|
|
1034
1035
|
model_index = capacity_retries = 0
|
package/src/commands/settings.py
CHANGED
|
@@ -4,6 +4,7 @@ Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from ..agent_notify import supports_agent_alerts
|
|
7
|
+
from ..claude_status_line import provision as provision_status_line
|
|
7
8
|
from ..cli_args import _parse_launch_setting_alias_args, _parse_set_args, _parse_unset_args
|
|
8
9
|
from ..cli_helpers import _format_launch_config, _json_success, _write_json
|
|
9
10
|
from ..cli_render import _success
|
|
@@ -70,6 +71,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
|
|
|
70
71
|
ctx["service"]["set_launch_settings"](session["name"], parsed["settings"])
|
|
71
72
|
for session in targets
|
|
72
73
|
]
|
|
74
|
+
warnings = [notice for session in sessions if (notice := provision_status_line(session, ctx["service"]["base_dir"]))]
|
|
73
75
|
if len(sessions) == 1:
|
|
74
76
|
session = sessions[0]
|
|
75
77
|
message = f"Updated launch settings for {session['name']}"
|
|
@@ -80,6 +82,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
|
|
|
80
82
|
action,
|
|
81
83
|
message,
|
|
82
84
|
updated_count=len(sessions),
|
|
85
|
+
warnings=warnings,
|
|
83
86
|
session=sessions[0] if len(sessions) == 1 else None,
|
|
84
87
|
sessions=sessions,
|
|
85
88
|
launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
|
|
@@ -87,6 +90,8 @@ def _apply_launch_settings(parsed, ctx, action="set"):
|
|
|
87
90
|
_write_json(ctx, payload)
|
|
88
91
|
return 0
|
|
89
92
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
93
|
+
for warning in warnings:
|
|
94
|
+
ctx["out"](f"{warning}\n")
|
|
90
95
|
notice = _notification_setting_notice(parsed["settings"], sessions, action)
|
|
91
96
|
if notice:
|
|
92
97
|
ctx["out"](f"{notice}\n")
|
|
@@ -100,6 +105,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
|
100
105
|
ctx["service"]["unset_launch_settings"](session["name"], parsed["keys"])
|
|
101
106
|
for session in targets
|
|
102
107
|
]
|
|
108
|
+
warnings = [notice for session in sessions if (notice := provision_status_line(session, ctx["service"]["base_dir"]))]
|
|
103
109
|
if len(sessions) == 1:
|
|
104
110
|
session = sessions[0]
|
|
105
111
|
message = f"Cleared launch settings for {session['name']}"
|
|
@@ -110,6 +116,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
|
110
116
|
action,
|
|
111
117
|
message,
|
|
112
118
|
updated_count=len(sessions),
|
|
119
|
+
warnings=warnings,
|
|
113
120
|
session=sessions[0] if len(sessions) == 1 else None,
|
|
114
121
|
sessions=sessions,
|
|
115
122
|
launch=sessions[0].get("launch") or {} if len(sessions) == 1 else None,
|
|
@@ -117,6 +124,8 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
|
117
124
|
_write_json(ctx, payload)
|
|
118
125
|
return 0
|
|
119
126
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
127
|
+
for warning in warnings:
|
|
128
|
+
ctx["out"](f"{warning}\n")
|
|
120
129
|
notice = _notification_setting_notice({key: False for key in parsed["keys"]}, sessions, action)
|
|
121
130
|
if notice:
|
|
122
131
|
ctx["out"](f"{notice}\n")
|
package/src/commands/status.py
CHANGED
|
@@ -51,6 +51,7 @@ from ..provider_runtime import (
|
|
|
51
51
|
_has_local_claude_auth,
|
|
52
52
|
_probe_provider_auth_status,
|
|
53
53
|
)
|
|
54
|
+
from ..quota_provenance import mark_quota_unavailable
|
|
54
55
|
from ..rtk_savings import rtk_saved
|
|
55
56
|
from ..run_usage import (
|
|
56
57
|
TOKEN_USAGE_KEYS,
|
|
@@ -856,6 +857,8 @@ def handle_status(rest, ctx):
|
|
|
856
857
|
}
|
|
857
858
|
for item in refresh_result.get("errors", [])
|
|
858
859
|
]
|
|
860
|
+
quota_errors = {item["session"]: item.get("reason") or "quota_read_failed" for item in refresh_result.get("errors", [])}
|
|
861
|
+
quota_errors.update({item["session"]: "auth_probe_failed" for item in auth_refresh.get("errors", [])})
|
|
859
862
|
status_progress = None if parsed["json"] else _make_status_progress(ctx)
|
|
860
863
|
if len(args) == 1:
|
|
861
864
|
row = ctx["service"]["get_status_row"](
|
|
@@ -865,6 +868,8 @@ def handle_status(rest, ctx):
|
|
|
865
868
|
cache_only=parsed["cached"],
|
|
866
869
|
status_timeout_seconds=parsed["timeout"],
|
|
867
870
|
)
|
|
871
|
+
if row.get("session_name") in quota_errors:
|
|
872
|
+
mark_quota_unavailable(row, quota_errors[row["session_name"]])
|
|
868
873
|
output_warnings = [
|
|
869
874
|
*warnings,
|
|
870
875
|
*_refresh_warning_payloads(refresh_errors, [row]),
|
|
@@ -885,6 +890,9 @@ def handle_status(rest, ctx):
|
|
|
885
890
|
cache_only=parsed["cached"],
|
|
886
891
|
status_timeout_seconds=parsed["timeout"],
|
|
887
892
|
)
|
|
893
|
+
for row in rows:
|
|
894
|
+
if row.get("session_name") in quota_errors:
|
|
895
|
+
mark_quota_unavailable(row, quota_errors[row["session_name"]])
|
|
888
896
|
output_warnings = [
|
|
889
897
|
*warnings,
|
|
890
898
|
*_refresh_warning_payloads(refresh_errors, rows),
|
package/src/conversation.py
CHANGED
|
@@ -379,7 +379,47 @@ def _read_tail(handle, stat, identity, provider, page, count, before):
|
|
|
379
379
|
return page
|
|
380
380
|
|
|
381
381
|
|
|
382
|
+
def _validate_page_options(limit, tail, cursor, before):
|
|
383
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= 1000:
|
|
384
|
+
raise CdxError("Conversation limit must be an integer between 1 and 1000.")
|
|
385
|
+
if tail is not None and (type(tail) is not int or not 1 <= tail <= 1000):
|
|
386
|
+
raise CdxError("Conversation tail must be an integer between 1 and 1000.")
|
|
387
|
+
if cursor is not None and (tail is not None or before is not None):
|
|
388
|
+
raise CdxError("Conversation --cursor cannot be combined with --tail or --before.")
|
|
389
|
+
|
|
390
|
+
|
|
382
391
|
def read_conversation(provider, auth_home=None, *, conversation_id=None, started_at=None,
|
|
392
|
+
terminal_transcript=None, cursor=None, limit=100, fallback_to_recency=False,
|
|
393
|
+
tail=None, before=None, cwd=None, pid=None, process_identity=None):
|
|
394
|
+
"""Read a conversation page, optionally bound to a live native process.
|
|
395
|
+
|
|
396
|
+
Process selection never uses saved session IDs or recency. Keep the returned
|
|
397
|
+
opaque process_identity for later polls to detect PID reuse. Unknown evidence
|
|
398
|
+
returns no entries. See docs/cli.md for platform and snapshot limitations.
|
|
399
|
+
"""
|
|
400
|
+
_validate_page_options(limit, tail, cursor, before)
|
|
401
|
+
options = dict(conversation_id=conversation_id, started_at=started_at,
|
|
402
|
+
terminal_transcript=terminal_transcript, cursor=cursor, limit=limit,
|
|
403
|
+
fallback_to_recency=fallback_to_recency, tail=tail, before=before, cwd=cwd)
|
|
404
|
+
if pid is None and process_identity is None:
|
|
405
|
+
return _read_conversation_page(provider, auth_home, **options)
|
|
406
|
+
if conversation_id is not None or cwd is not None or started_at is not None or terminal_transcript is not None:
|
|
407
|
+
raise CdxError("Process selection cannot be combined with conversation, cwd, since or terminal transcript selection.")
|
|
408
|
+
from .process_conversation import resolve_process_conversation
|
|
409
|
+
|
|
410
|
+
resolution = resolve_process_conversation(provider, auth_home, pid=pid, process_identity=process_identity)
|
|
411
|
+
page = {"status": "unavailable", "provider": provider, "entries": [], "next_cursor": None,
|
|
412
|
+
"has_more": False, "resolution_scope": None, **resolution}
|
|
413
|
+
if tail is not None or before is not None:
|
|
414
|
+
page["before_cursor"] = None
|
|
415
|
+
# No process-bound cursor is issued until native selected-thread evidence
|
|
416
|
+
# exists. Never replay an explicit-ID or recency cursor through this mode.
|
|
417
|
+
if cursor is not None or before is not None:
|
|
418
|
+
page["status"] = "reset_required"
|
|
419
|
+
return page
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _read_conversation_page(provider, auth_home=None, *, conversation_id=None, started_at=None,
|
|
383
423
|
terminal_transcript=None, cursor=None, limit=100, fallback_to_recency=False, tail=None, before=None, cwd=None):
|
|
384
424
|
"""Return a JSON-serializable page; pass next_cursor back unchanged to poll.
|
|
385
425
|
|
|
@@ -394,12 +434,7 @@ def read_conversation(provider, auth_home=None, *, conversation_id=None, started
|
|
|
394
434
|
Tool bodies and reasoning blocks are never returned. Ollama PTY captures
|
|
395
435
|
cannot reliably distinguish roles and are explicitly unsupported.
|
|
396
436
|
"""
|
|
397
|
-
|
|
398
|
-
raise CdxError("Conversation limit must be an integer between 1 and 1000.")
|
|
399
|
-
if tail is not None and (type(tail) is not int or not 1 <= tail <= 1000):
|
|
400
|
-
raise CdxError("Conversation tail must be an integer between 1 and 1000.")
|
|
401
|
-
if cursor is not None and (tail is not None or before is not None):
|
|
402
|
-
raise CdxError("Conversation --cursor cannot be combined with --tail or --before.")
|
|
437
|
+
_validate_page_options(limit, tail, cursor, before)
|
|
403
438
|
if started_at is not None:
|
|
404
439
|
try:
|
|
405
440
|
datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Bounded Linux process identity, without guessing a selected conversation.
|
|
2
|
+
|
|
3
|
+
No argv, environment, credential stores, transcript searches or parent/child
|
|
4
|
+
heuristics participate in identity resolution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PROC_ROOT = Path("/proc")
|
|
14
|
+
_UUID = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
15
|
+
_IDENTITY = re.compile(r"linux:(\d+):[0-9a-f]{64}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Unknown(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bounded(path, limit=8192):
|
|
23
|
+
with open(path, "rb") as handle:
|
|
24
|
+
value = handle.read(limit + 1)
|
|
25
|
+
if len(value) > limit:
|
|
26
|
+
raise _Unknown("evidence_limit")
|
|
27
|
+
return value.decode("utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _process(pid):
|
|
31
|
+
root = PROC_ROOT / str(pid)
|
|
32
|
+
fields = _bounded(root / "stat").rsplit(")", 1)[1].split()
|
|
33
|
+
if fields[0] in ("Z", "X", "x"):
|
|
34
|
+
raise _Unknown("process_not_running")
|
|
35
|
+
uids = next(line.split()[1:] for line in _bounded(root / "status").splitlines() if line.startswith("Uid:"))
|
|
36
|
+
if len(uids) != 4 or any(int(uid) != os.getuid() for uid in uids):
|
|
37
|
+
raise _Unknown("unrelated_user")
|
|
38
|
+
executable = os.readlink(root / "exe")
|
|
39
|
+
if Path(executable).name != "codex":
|
|
40
|
+
raise _Unknown("unrelated_process")
|
|
41
|
+
exe = os.stat(root / "exe")
|
|
42
|
+
boot = _bounded(PROC_ROOT / "sys/kernel/random/boot_id").strip()
|
|
43
|
+
if not re.fullmatch(_UUID, boot):
|
|
44
|
+
raise _Unknown("invalid_evidence")
|
|
45
|
+
digest = hashlib.sha256(f"{boot}:{pid}:{fields[19]}:{uids}:{exe.st_dev}:{exe.st_ino}".encode()).hexdigest()
|
|
46
|
+
return f"linux:{pid}:{digest}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_process_conversation(provider, auth_home, *, pid=None, process_identity=None):
|
|
50
|
+
"""Return explicit unknown when the native selected-thread link is absent.
|
|
51
|
+
|
|
52
|
+
A bare PID means the process alive *now*. Persist process_identity to reject
|
|
53
|
+
reuse on subsequent calls. The identity is an opaque guard, not a secret or
|
|
54
|
+
authorization grant. Only Linux /proc and interactive Codex are supported.
|
|
55
|
+
"""
|
|
56
|
+
result = {"resolution_status": "unknown", "resolution_method": None,
|
|
57
|
+
"conversation_id": None, "process_identity": None, "reason": None}
|
|
58
|
+
if provider != "codex" or sys.platform != "linux":
|
|
59
|
+
return {**result, "reason": "unsupported_process_resolution"}
|
|
60
|
+
if process_identity is not None:
|
|
61
|
+
match = _IDENTITY.fullmatch(process_identity) if isinstance(process_identity, str) and len(process_identity) <= 100 else None
|
|
62
|
+
if not match or pid is not None:
|
|
63
|
+
return {**result, "reason": "invalid_process_identity"}
|
|
64
|
+
pid = int(match[1])
|
|
65
|
+
if type(pid) is not int or not 0 < pid < 2**31 or not auth_home:
|
|
66
|
+
return {**result, "reason": "invalid_process_identity"}
|
|
67
|
+
try:
|
|
68
|
+
identity = _process(pid)
|
|
69
|
+
if process_identity is not None and identity != process_identity:
|
|
70
|
+
raise _Unknown("process_identity_changed")
|
|
71
|
+
result["process_identity"] = identity
|
|
72
|
+
if _process(pid) != identity:
|
|
73
|
+
raise _Unknown("evidence_changed")
|
|
74
|
+
# Even a sole writable rollout may belong to a retained thread while
|
|
75
|
+
# another thread is selected but has not created its rollout. Neither
|
|
76
|
+
# that descriptor nor resume argv is a native selected-thread event.
|
|
77
|
+
return {**result, "reason": "native_conversation_identity_unavailable"}
|
|
78
|
+
except _Unknown as exc:
|
|
79
|
+
return {**result, "reason": str(exc)}
|
|
80
|
+
except (OSError, ValueError, KeyError, IndexError, TypeError, StopIteration, UnicodeError, RecursionError):
|
|
81
|
+
return {**result, "reason": "native_evidence_unavailable"}
|