cdx-manager 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,9 @@ from .notify import (
33
33
  from .provider_runtime import (
34
34
  _ensure_session_authentication,
35
35
  _list_launch_transcript_paths,
36
+ _normalize_reasoning_effort,
36
37
  _probe_provider_auth,
38
+ _run_headless_provider_command,
37
39
  _run_interactive_provider_command,
38
40
  )
39
41
  from .repair import format_repair_report, repair_health
@@ -51,12 +53,14 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
51
53
  IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
52
54
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
53
55
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
54
- SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--json]"
55
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--all) [--json]"
56
+ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--model MODEL] [--priority 0..100] [--json]"
57
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--priority|--all) [--json]"
56
58
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
57
59
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
58
60
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
59
61
  LAST_USAGE = "Usage: cdx last [--json]"
62
+ SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
63
+ RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
60
64
  API_SCHEMA_VERSION = 1
61
65
  HANDOFF_TRANSCRIPT_CHARS = 120000
62
66
  HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
@@ -78,6 +82,23 @@ def _json_success(action, message, warnings=None, **extra):
78
82
  return payload
79
83
 
80
84
 
85
+ def _json_failure(action, code, message, source="cdx", exit_code=1, warnings=None, **extra):
86
+ payload = {
87
+ "schema_version": API_SCHEMA_VERSION,
88
+ "ok": False,
89
+ "action": action,
90
+ "warnings": warnings or [],
91
+ "error": {
92
+ "source": source,
93
+ "code": code,
94
+ "message": message,
95
+ "exit_code": exit_code,
96
+ },
97
+ }
98
+ payload.update(extra)
99
+ return payload
100
+
101
+
81
102
  def _write_json(ctx, payload):
82
103
  ctx["out"](f"{json.dumps(payload, indent=2)}\n")
83
104
 
@@ -467,12 +488,23 @@ def _parse_fast_value(value):
467
488
  raise CdxError(SET_USAGE)
468
489
 
469
490
 
491
+ def _parse_priority_value(value):
492
+ try:
493
+ priority = int(value)
494
+ except (TypeError, ValueError) as error:
495
+ raise CdxError(SET_USAGE) from error
496
+ if priority < 0 or priority > 100:
497
+ raise CdxError(SET_USAGE)
498
+ return priority
499
+
500
+
470
501
  def _parse_set_args(args):
471
502
  parsed = _parse_flag_args(args, {
472
503
  "--power": {"key": "power", "type": "str", "default": None},
473
504
  "--permission": {"key": "permission", "type": "str", "default": None},
474
505
  "--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
475
506
  "--model": {"key": "model", "type": "str", "default": None},
507
+ "--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
476
508
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
477
509
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
478
510
  "--json": {"key": "json", "type": "bool", "default": False},
@@ -487,7 +519,7 @@ def _parse_set_args(args):
487
519
  raise CdxError(SET_USAGE)
488
520
  settings = {
489
521
  key: parsed[key]
490
- for key in ("power", "permission", "fast", "model")
522
+ for key in ("power", "permission", "fast", "model", "priority")
491
523
  if parsed[key] is not None
492
524
  }
493
525
  if not settings:
@@ -507,6 +539,7 @@ def _parse_unset_args(args):
507
539
  "--permission": {"key": "permission", "type": "bool", "default": False},
508
540
  "--fast": {"key": "fast", "type": "bool", "default": False},
509
541
  "--model": {"key": "model", "type": "bool", "default": False},
542
+ "--priority": {"key": "priority", "type": "bool", "default": False},
510
543
  "--all": {"key": "all", "type": "bool", "default": False},
511
544
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
512
545
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
@@ -520,8 +553,8 @@ def _parse_unset_args(args):
520
553
  raise CdxError(UNSET_USAGE)
521
554
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
522
555
  raise CdxError(UNSET_USAGE)
523
- keys = ["power", "permission", "fast", "model"] if parsed["all"] else [
524
- key for key in ("power", "permission", "fast", "model") if parsed[key]
556
+ keys = ["power", "permission", "fast", "model", "priority"] if parsed["all"] else [
557
+ key for key in ("power", "permission", "fast", "model", "priority") if parsed[key]
525
558
  ]
526
559
  if not keys:
527
560
  raise CdxError(UNSET_USAGE)
@@ -543,6 +576,99 @@ def _parse_config_args(args):
543
576
  return {"name": parsed["names"][0], "json": parsed["json"]}
544
577
 
545
578
 
579
+ def _parse_select_args(args):
580
+ parsed = _parse_flag_args(args, {
581
+ "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
582
+ "--min-reasoning-effort": {"key": "min_reasoning_effort", "type": "str", "default": None},
583
+ "--min-power": {"key": "min_power", "type": "str", "default": None},
584
+ "--require-ready": {"key": "require_ready", "type": "bool", "default": False},
585
+ "--refresh": {"key": "refresh", "type": "bool", "default": False},
586
+ "--json": {"key": "json", "type": "bool", "default": False},
587
+ }, SELECT_USAGE)
588
+ if not parsed["provider"] or not parsed["json"]:
589
+ raise CdxError(SELECT_USAGE)
590
+ effort = _normalize_reasoning_effort(
591
+ reasoning_effort=parsed["min_reasoning_effort"],
592
+ power=parsed["min_power"],
593
+ usage=SELECT_USAGE,
594
+ ).get("reasoning_effort")
595
+ return {
596
+ "provider": parsed["provider"],
597
+ "min_reasoning_effort": effort,
598
+ "require_ready": parsed["require_ready"],
599
+ "refresh": parsed["refresh"],
600
+ "json": parsed["json"],
601
+ }
602
+
603
+
604
+ def _parse_timeout_seconds(value):
605
+ try:
606
+ parsed = float(value)
607
+ except (TypeError, ValueError) as error:
608
+ raise CdxError(RUN_USAGE) from error
609
+ if parsed <= 0:
610
+ raise CdxError(RUN_USAGE)
611
+ return parsed
612
+
613
+
614
+ def _normalize_run_permission(value):
615
+ if value is None:
616
+ return None
617
+ text = str(value).strip().lower()
618
+ aliases = {
619
+ "workspace-write": "default",
620
+ "read-only": "review",
621
+ "danger-full-access": "full",
622
+ }
623
+ text = aliases.get(text, text)
624
+ if text not in ("review", "default", "auto", "full"):
625
+ raise CdxError(RUN_USAGE)
626
+ return text
627
+
628
+
629
+ def _parse_run_args(args):
630
+ parsed = _parse_flag_args(args, {
631
+ "--cwd": {"key": "cwd", "type": "str", "default": None},
632
+ "--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
633
+ "--prompt": {"key": "prompt", "type": "str", "default": None},
634
+ "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
635
+ "--model": {"key": "model", "type": "str", "default": None},
636
+ "--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
637
+ "--power": {"key": "power", "type": "str", "default": None},
638
+ "--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
639
+ "--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_timeout_seconds},
640
+ "--json": {"key": "json", "type": "bool", "default": False},
641
+ }, RUN_USAGE, positionals_key="names", max_positionals=1)
642
+ if not parsed["json"]:
643
+ raise CdxError(RUN_USAGE)
644
+ name = parsed["names"][0] if parsed["names"] else None
645
+ if name and parsed["provider"]:
646
+ raise CdxError(RUN_USAGE)
647
+ if not name and not parsed["provider"]:
648
+ raise CdxError(RUN_USAGE)
649
+ if not parsed["cwd"]:
650
+ raise CdxError(RUN_USAGE)
651
+ if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
652
+ raise CdxError(RUN_USAGE)
653
+ effort = _normalize_reasoning_effort(
654
+ reasoning_effort=parsed["reasoning_effort"],
655
+ power=parsed["power"],
656
+ usage=RUN_USAGE,
657
+ )
658
+ return {
659
+ "name": name,
660
+ "provider": parsed["provider"],
661
+ "cwd": parsed["cwd"],
662
+ "prompt_file": parsed["prompt_file"],
663
+ "prompt": parsed["prompt"],
664
+ "model": parsed["model"],
665
+ "permission": parsed["permission"],
666
+ "timeout_seconds": parsed["timeout_seconds"],
667
+ "reasoning_effort": effort.get("reasoning_effort"),
668
+ "power": effort.get("power"),
669
+ }
670
+
671
+
546
672
  def _parse_positive_int(value):
547
673
  try:
548
674
  parsed = int(value)
@@ -892,19 +1018,18 @@ def _bootstrap_claude_setup_token(session, ctx):
892
1018
  "Claude setup-token completed, but cdx could not capture the token. "
893
1019
  "Run claude setup-token and save the token under credentials/default.json."
894
1020
  )
895
- try:
896
- with open(transcript_path, "r", encoding="utf-8", errors="replace") as handle:
897
- transcript = handle.read()
898
- finally:
899
- try:
900
- os.remove(transcript_path)
901
- except OSError:
902
- pass
1021
+ with open(transcript_path, "r", encoding="utf-8", errors="replace") as handle:
1022
+ transcript = handle.read()
903
1023
  token = _extract_claude_oauth_token(transcript)
904
1024
  if not token:
905
1025
  raise CdxError(
906
- "Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output."
1026
+ "Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output. "
1027
+ f"Transcript kept at: {transcript_path}"
907
1028
  )
1029
+ try:
1030
+ os.remove(transcript_path)
1031
+ except OSError:
1032
+ pass
908
1033
  return _write_claude_oauth_token(session.get("authHome") or "", token)
909
1034
 
910
1035
 
@@ -1026,6 +1151,223 @@ def handle_enable(rest, ctx):
1026
1151
  return 0
1027
1152
 
1028
1153
 
1154
+ def handle_select(rest, ctx):
1155
+ parsed = _parse_select_args(rest)
1156
+ selected = _select_headless_session(
1157
+ ctx,
1158
+ parsed["provider"],
1159
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1160
+ require_ready=parsed["require_ready"],
1161
+ force_refresh=parsed["refresh"],
1162
+ )
1163
+ policy = "ready_then_cooldown_then_health_then_priority_then_name"
1164
+ if not selected:
1165
+ message = "No suitable session found."
1166
+ _write_json(ctx, _json_failure(
1167
+ "select",
1168
+ "no_suitable_session",
1169
+ message,
1170
+ provider=parsed["provider"],
1171
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1172
+ require_ready=parsed["require_ready"],
1173
+ selection_policy=policy,
1174
+ ))
1175
+ return 1
1176
+ session = selected["session"]
1177
+ row = selected.get("row") or {}
1178
+ reason = "highest availability suitable session"
1179
+ _write_json(ctx, _json_success(
1180
+ "select",
1181
+ f"Selected session {session['name']}",
1182
+ session=session["name"],
1183
+ provider=session["provider"],
1184
+ reason=reason,
1185
+ selection_policy=policy,
1186
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1187
+ reasoning_effort=_session_reasoning_effort(session),
1188
+ available_pct=row.get("available_pct"),
1189
+ auth_status=row.get("auth_status"),
1190
+ ))
1191
+ return 0
1192
+
1193
+
1194
+ def _read_run_prompt(parsed):
1195
+ if parsed.get("prompt") is not None:
1196
+ return parsed["prompt"]
1197
+ try:
1198
+ with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
1199
+ return handle.read()
1200
+ except OSError as error:
1201
+ raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
1202
+
1203
+
1204
+ def _run_usage_payload():
1205
+ return {
1206
+ "input_tokens": None,
1207
+ "output_tokens": None,
1208
+ "reasoning_tokens": None,
1209
+ "total_tokens": None,
1210
+ }
1211
+
1212
+
1213
+ def _run_cdx_error_code(error):
1214
+ message = str(error)
1215
+ if message.startswith("Usage:"):
1216
+ return "invalid_request"
1217
+ if message.startswith("Invalid cwd:"):
1218
+ return "invalid_cwd"
1219
+ if message.startswith("Session is disabled:"):
1220
+ return "session_disabled"
1221
+ if "CLI not found on PATH" in message:
1222
+ return "provider_cli_not_found"
1223
+ if message.startswith("Failed to start "):
1224
+ return "provider_start_failed"
1225
+ if (
1226
+ message.startswith("Unsupported reasoning effort:")
1227
+ or message.startswith("Unsupported power:")
1228
+ or "--reasoning-effort and --power must match" in message
1229
+ ):
1230
+ return "invalid_reasoning_effort"
1231
+ return "cdx_error"
1232
+
1233
+
1234
+ def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
1235
+ run_info = run_info or {}
1236
+ return {
1237
+ "schema_version": API_SCHEMA_VERSION,
1238
+ "ok": bool(ok),
1239
+ "action": "run",
1240
+ "session": session.get("name") if session else None,
1241
+ "provider": session.get("provider") if session else parsed.get("provider"),
1242
+ "model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
1243
+ "reasoning_effort": parsed.get("reasoning_effort") or ((session.get("launch") or {}).get("reasoning_effort") if session else None),
1244
+ "power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
1245
+ "cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
1246
+ "run_id": run_info.get("run_id"),
1247
+ "exit_code": run_info.get("returncode"),
1248
+ "duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
1249
+ "transcript_path": run_info.get("transcript_path"),
1250
+ "stdout_path": run_info.get("stdout_path"),
1251
+ "stderr_path": run_info.get("stderr_path"),
1252
+ "usage": _run_usage_payload(),
1253
+ "warnings": [],
1254
+ "error": None if ok else {
1255
+ "source": error_source or "cdx",
1256
+ "code": error_code or "cdx_error",
1257
+ "message": str(error) if error else "Run failed.",
1258
+ "provider_code": run_info.get("returncode") if error_source == "provider" else None,
1259
+ },
1260
+ }
1261
+
1262
+
1263
+ def handle_run(rest, ctx):
1264
+ try:
1265
+ parsed = _parse_run_args(rest)
1266
+ session = None
1267
+ if parsed["name"]:
1268
+ session = ctx["service"]["get_session"](parsed["name"])
1269
+ if not session:
1270
+ raise CdxError(f"Unknown session: {parsed['name']}")
1271
+ if session.get("enabled", True) is False:
1272
+ raise CdxError(f"Session is disabled: {parsed['name']}")
1273
+ else:
1274
+ selected = _select_headless_session(
1275
+ ctx,
1276
+ parsed["provider"],
1277
+ min_reasoning_effort=parsed["reasoning_effort"],
1278
+ require_ready=True,
1279
+ force_refresh=False,
1280
+ )
1281
+ if not selected:
1282
+ _write_json(ctx, _json_failure(
1283
+ "run",
1284
+ "no_suitable_session",
1285
+ "No suitable session found.",
1286
+ provider=parsed["provider"],
1287
+ ))
1288
+ return 1
1289
+ session = selected["session"]
1290
+
1291
+ cwd = os.path.abspath(parsed["cwd"])
1292
+ if not os.path.isdir(cwd):
1293
+ raise CdxError(f"Invalid cwd: {parsed['cwd']}")
1294
+ prompt = _read_run_prompt(parsed)
1295
+ launch_updates = {}
1296
+ if parsed.get("model"):
1297
+ launch_updates["model"] = parsed["model"]
1298
+ if parsed.get("permission"):
1299
+ launch_updates["permission"] = parsed["permission"]
1300
+ if parsed.get("reasoning_effort"):
1301
+ launch_updates["reasoning_effort"] = parsed["reasoning_effort"]
1302
+ launch_updates["power"] = parsed["reasoning_effort"]
1303
+ run_session = {
1304
+ **session,
1305
+ "launch": {
1306
+ **(session.get("launch") or {}),
1307
+ **launch_updates,
1308
+ },
1309
+ }
1310
+ _ensure_session_authentication(
1311
+ run_session,
1312
+ ctx["service"],
1313
+ spawn=ctx.get("spawn"),
1314
+ spawn_sync=ctx.get("spawn_sync"),
1315
+ env_override=ctx.get("env"),
1316
+ stdin_is_tty=False,
1317
+ behavior="launch",
1318
+ signal_emitter=ctx.get("signal_emitter"),
1319
+ )
1320
+ run_info = _run_headless_provider_command(
1321
+ run_session,
1322
+ cwd=cwd,
1323
+ env_override=ctx.get("env"),
1324
+ initial_prompt=prompt,
1325
+ timeout_seconds=parsed.get("timeout_seconds"),
1326
+ spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
1327
+ )
1328
+ ok = run_info.get("returncode") == 0
1329
+ if ok:
1330
+ ctx["service"]["record_launch_history"](session["name"], {
1331
+ "status": "success",
1332
+ "cwd": cwd,
1333
+ "exit_code": 0,
1334
+ **run_info,
1335
+ })
1336
+ _write_json(ctx, _run_result_payload(True, parsed, run_session, run_info=run_info))
1337
+ return 0
1338
+ message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
1339
+ error = CdxError(message, run_info.get("returncode") or 1)
1340
+ ctx["service"]["record_launch_history"](session["name"], {
1341
+ "status": "failed",
1342
+ "cwd": cwd,
1343
+ "error": str(error),
1344
+ "exit_code": error.exit_code,
1345
+ **run_info,
1346
+ })
1347
+ _write_json(ctx, _run_result_payload(
1348
+ False,
1349
+ parsed,
1350
+ run_session,
1351
+ run_info=run_info,
1352
+ error=error,
1353
+ error_source="provider",
1354
+ error_code="provider_timeout" if run_info.get("timed_out") else "provider_failed",
1355
+ ))
1356
+ return error.exit_code or 1
1357
+ except CdxError as error:
1358
+ run_info = getattr(error, "run_info", None)
1359
+ _write_json(ctx, _run_result_payload(
1360
+ False,
1361
+ locals().get("parsed", {}) or {},
1362
+ locals().get("session"),
1363
+ run_info=run_info,
1364
+ error=error,
1365
+ error_source="cdx",
1366
+ error_code=_run_cdx_error_code(error),
1367
+ ))
1368
+ return error.exit_code
1369
+
1370
+
1029
1371
  def _format_launch_config(session):
1030
1372
  launch = session.get("launch") or {}
1031
1373
  return "\n".join([
@@ -1034,6 +1376,7 @@ def _format_launch_config(session):
1034
1376
  f"permission: {launch.get('permission') or 'default'}",
1035
1377
  f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
1036
1378
  f"model: {launch.get('model') or 'default'}",
1379
+ f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
1037
1380
  ])
1038
1381
 
1039
1382
 
@@ -1063,6 +1406,87 @@ def _resolve_bulk_launch_targets(parsed, service):
1063
1406
  return targets
1064
1407
 
1065
1408
 
1409
+ def _reasoning_rank(value):
1410
+ order = {"low": 0, "medium": 1, "high": 2, "xhigh": 2, "max": 2}
1411
+ return order.get(str(value or "low").lower(), 0)
1412
+
1413
+
1414
+ def _session_reasoning_effort(session):
1415
+ launch = session.get("launch") or {}
1416
+ return (
1417
+ launch.get("reasoning_effort")
1418
+ or launch.get("reasoningEffort")
1419
+ or launch.get("power")
1420
+ or ("low" if launch.get("fast") is True else None)
1421
+ or "low"
1422
+ )
1423
+
1424
+
1425
+ def _session_selection_priority(session):
1426
+ launch = session.get("launch") or {}
1427
+ try:
1428
+ return int(launch.get("priority") or 0)
1429
+ except (TypeError, ValueError):
1430
+ return 0
1431
+
1432
+
1433
+ def _row_blocks_ready(row):
1434
+ if row.get("provider") in (PROVIDER_ANTIGRAVITY, PROVIDER_OLLAMA):
1435
+ auth = "n/a"
1436
+ else:
1437
+ auth = row.get("auth_status") or "unknown"
1438
+ if auth not in ("authenticated", "n/a"):
1439
+ return True
1440
+ available = row.get("available_pct")
1441
+ if available is not None:
1442
+ try:
1443
+ if float(available) <= 0:
1444
+ return True
1445
+ except (TypeError, ValueError):
1446
+ return True
1447
+ return False
1448
+
1449
+
1450
+ def _select_headless_session(ctx, provider, min_reasoning_effort=None, require_ready=False, force_refresh=False):
1451
+ sessions = [
1452
+ session for session in ctx["service"]["list_sessions"]()
1453
+ if session.get("provider") == provider and session.get("enabled", True) is not False
1454
+ ]
1455
+ minimum = _reasoning_rank(min_reasoning_effort)
1456
+ sessions = [
1457
+ session for session in sessions
1458
+ if _reasoning_rank(_session_reasoning_effort(session)) >= minimum
1459
+ ]
1460
+ rows = ctx["service"]["get_status_rows"](force_refresh=force_refresh)
1461
+ row_by_name = {row.get("session_name"): row for row in rows}
1462
+ candidates = []
1463
+ for session in sessions:
1464
+ row = row_by_name.get(session["name"], {})
1465
+ if require_ready and _row_blocks_ready(row):
1466
+ continue
1467
+ available = row.get("available_pct")
1468
+ try:
1469
+ available_sort = float(available) if available is not None else -1.0
1470
+ except (TypeError, ValueError):
1471
+ available_sort = -1.0
1472
+ cooldown_sort = 1 if available_sort > 0 else 0
1473
+ candidates.append({
1474
+ "session": session,
1475
+ "row": row,
1476
+ "sort_key": (
1477
+ -cooldown_sort,
1478
+ -available_sort,
1479
+ -_session_selection_priority(session),
1480
+ _reasoning_rank(_session_reasoning_effort(session)),
1481
+ session["name"],
1482
+ ),
1483
+ })
1484
+ if not candidates:
1485
+ return None
1486
+ candidates.sort(key=lambda item: item["sort_key"])
1487
+ return candidates[0]
1488
+
1489
+
1066
1490
  def _format_bulk_launch_summary(sessions):
1067
1491
  names = [session["name"] for session in sessions]
1068
1492
  if len(names) <= 8:
@@ -1599,6 +2023,20 @@ def handle_status(rest, ctx):
1599
2023
  if len(args) == 1 and parsed["small"]:
1600
2024
  raise CdxError(STATUS_USAGE)
1601
2025
 
2026
+ auth_refresh = _refresh_claude_auth_states(
2027
+ ctx["service"],
2028
+ target_names=args if len(args) == 1 else None,
2029
+ spawn_sync=ctx.get("spawn_sync"),
2030
+ env_override=ctx.get("env"),
2031
+ )
2032
+ warnings = [
2033
+ {
2034
+ "code": "claude_auth_probe_failed",
2035
+ "session": item.get("session") or "unknown",
2036
+ "message": item.get("error") or "unknown error",
2037
+ }
2038
+ for item in auth_refresh.get("errors", [])
2039
+ ]
1602
2040
  refresh_result = _refresh_claude_sessions(
1603
2041
  ctx["service"],
1604
2042
  ctx.get("refresh_fn"),
@@ -1612,27 +2050,13 @@ def handle_status(rest, ctx):
1612
2050
  }
1613
2051
  for item in refresh_result.get("errors", [])
1614
2052
  ]
1615
- warnings = [
2053
+ warnings.extend([
1616
2054
  {
1617
2055
  "code": "claude_refresh_failed",
1618
2056
  "session": item.get("session") or "unknown",
1619
2057
  "message": item.get("error") or "unknown error",
1620
2058
  }
1621
2059
  for item in refresh_errors
1622
- ]
1623
- auth_refresh = _refresh_claude_auth_states(
1624
- ctx["service"],
1625
- target_names=args if len(args) == 1 else None,
1626
- spawn_sync=ctx.get("spawn_sync"),
1627
- env_override=ctx.get("env"),
1628
- )
1629
- warnings.extend([
1630
- {
1631
- "code": "claude_auth_probe_failed",
1632
- "session": item.get("session") or "unknown",
1633
- "message": item.get("error") or "unknown error",
1634
- }
1635
- for item in auth_refresh.get("errors", [])
1636
2060
  ])
1637
2061
  update_warning = _update_notice_warning(ctx)
1638
2062
  if update_warning: