cdx-manager 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,11 +33,14 @@ 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
40
42
  from .backup_bundle import read_bundle_meta
43
+ from .run_usage import empty_usage, extract_run_usage
41
44
  from .status_view import _format_status_detail, _format_status_rows
42
45
  from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
43
46
  from .update_manager import build_update_plan, format_update_failure, run_update_plan
@@ -51,12 +54,14 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
51
54
  IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
52
55
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
53
56
  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]"
57
+ 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]"
58
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--model|--priority|--all) [--json]"
56
59
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
57
60
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
58
61
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
59
62
  LAST_USAGE = "Usage: cdx last [--json]"
63
+ SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
64
+ 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
65
  API_SCHEMA_VERSION = 1
61
66
  HANDOFF_TRANSCRIPT_CHARS = 120000
62
67
  HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
@@ -78,6 +83,23 @@ def _json_success(action, message, warnings=None, **extra):
78
83
  return payload
79
84
 
80
85
 
86
+ def _json_failure(action, code, message, source="cdx", exit_code=1, warnings=None, **extra):
87
+ payload = {
88
+ "schema_version": API_SCHEMA_VERSION,
89
+ "ok": False,
90
+ "action": action,
91
+ "warnings": warnings or [],
92
+ "error": {
93
+ "source": source,
94
+ "code": code,
95
+ "message": message,
96
+ "exit_code": exit_code,
97
+ },
98
+ }
99
+ payload.update(extra)
100
+ return payload
101
+
102
+
81
103
  def _write_json(ctx, payload):
82
104
  ctx["out"](f"{json.dumps(payload, indent=2)}\n")
83
105
 
@@ -467,12 +489,23 @@ def _parse_fast_value(value):
467
489
  raise CdxError(SET_USAGE)
468
490
 
469
491
 
492
+ def _parse_priority_value(value):
493
+ try:
494
+ priority = int(value)
495
+ except (TypeError, ValueError) as error:
496
+ raise CdxError(SET_USAGE) from error
497
+ if priority < 0 or priority > 100:
498
+ raise CdxError(SET_USAGE)
499
+ return priority
500
+
501
+
470
502
  def _parse_set_args(args):
471
503
  parsed = _parse_flag_args(args, {
472
504
  "--power": {"key": "power", "type": "str", "default": None},
473
505
  "--permission": {"key": "permission", "type": "str", "default": None},
474
506
  "--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
475
507
  "--model": {"key": "model", "type": "str", "default": None},
508
+ "--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
476
509
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
477
510
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
478
511
  "--json": {"key": "json", "type": "bool", "default": False},
@@ -487,7 +520,7 @@ def _parse_set_args(args):
487
520
  raise CdxError(SET_USAGE)
488
521
  settings = {
489
522
  key: parsed[key]
490
- for key in ("power", "permission", "fast", "model")
523
+ for key in ("power", "permission", "fast", "model", "priority")
491
524
  if parsed[key] is not None
492
525
  }
493
526
  if not settings:
@@ -507,6 +540,7 @@ def _parse_unset_args(args):
507
540
  "--permission": {"key": "permission", "type": "bool", "default": False},
508
541
  "--fast": {"key": "fast", "type": "bool", "default": False},
509
542
  "--model": {"key": "model", "type": "bool", "default": False},
543
+ "--priority": {"key": "priority", "type": "bool", "default": False},
510
544
  "--all": {"key": "all", "type": "bool", "default": False},
511
545
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
512
546
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
@@ -520,8 +554,8 @@ def _parse_unset_args(args):
520
554
  raise CdxError(UNSET_USAGE)
521
555
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
522
556
  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]
557
+ keys = ["power", "permission", "fast", "model", "priority"] if parsed["all"] else [
558
+ key for key in ("power", "permission", "fast", "model", "priority") if parsed[key]
525
559
  ]
526
560
  if not keys:
527
561
  raise CdxError(UNSET_USAGE)
@@ -543,6 +577,99 @@ def _parse_config_args(args):
543
577
  return {"name": parsed["names"][0], "json": parsed["json"]}
544
578
 
545
579
 
580
+ def _parse_select_args(args):
581
+ parsed = _parse_flag_args(args, {
582
+ "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
583
+ "--min-reasoning-effort": {"key": "min_reasoning_effort", "type": "str", "default": None},
584
+ "--min-power": {"key": "min_power", "type": "str", "default": None},
585
+ "--require-ready": {"key": "require_ready", "type": "bool", "default": False},
586
+ "--refresh": {"key": "refresh", "type": "bool", "default": False},
587
+ "--json": {"key": "json", "type": "bool", "default": False},
588
+ }, SELECT_USAGE)
589
+ if not parsed["provider"] or not parsed["json"]:
590
+ raise CdxError(SELECT_USAGE)
591
+ effort = _normalize_reasoning_effort(
592
+ reasoning_effort=parsed["min_reasoning_effort"],
593
+ power=parsed["min_power"],
594
+ usage=SELECT_USAGE,
595
+ ).get("reasoning_effort")
596
+ return {
597
+ "provider": parsed["provider"],
598
+ "min_reasoning_effort": effort,
599
+ "require_ready": parsed["require_ready"],
600
+ "refresh": parsed["refresh"],
601
+ "json": parsed["json"],
602
+ }
603
+
604
+
605
+ def _parse_timeout_seconds(value):
606
+ try:
607
+ parsed = float(value)
608
+ except (TypeError, ValueError) as error:
609
+ raise CdxError(RUN_USAGE) from error
610
+ if parsed <= 0:
611
+ raise CdxError(RUN_USAGE)
612
+ return parsed
613
+
614
+
615
+ def _normalize_run_permission(value):
616
+ if value is None:
617
+ return None
618
+ text = str(value).strip().lower()
619
+ aliases = {
620
+ "workspace-write": "default",
621
+ "read-only": "review",
622
+ "danger-full-access": "full",
623
+ }
624
+ text = aliases.get(text, text)
625
+ if text not in ("review", "default", "auto", "full"):
626
+ raise CdxError(RUN_USAGE)
627
+ return text
628
+
629
+
630
+ def _parse_run_args(args):
631
+ parsed = _parse_flag_args(args, {
632
+ "--cwd": {"key": "cwd", "type": "str", "default": None},
633
+ "--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
634
+ "--prompt": {"key": "prompt", "type": "str", "default": None},
635
+ "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, RUN_USAGE)},
636
+ "--model": {"key": "model", "type": "str", "default": None},
637
+ "--reasoning-effort": {"key": "reasoning_effort", "type": "str", "default": None},
638
+ "--power": {"key": "power", "type": "str", "default": None},
639
+ "--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
640
+ "--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_timeout_seconds},
641
+ "--json": {"key": "json", "type": "bool", "default": False},
642
+ }, RUN_USAGE, positionals_key="names", max_positionals=1)
643
+ if not parsed["json"]:
644
+ raise CdxError(RUN_USAGE)
645
+ name = parsed["names"][0] if parsed["names"] else None
646
+ if name and parsed["provider"]:
647
+ raise CdxError(RUN_USAGE)
648
+ if not name and not parsed["provider"]:
649
+ raise CdxError(RUN_USAGE)
650
+ if not parsed["cwd"]:
651
+ raise CdxError(RUN_USAGE)
652
+ if bool(parsed["prompt_file"]) == bool(parsed["prompt"]):
653
+ raise CdxError(RUN_USAGE)
654
+ effort = _normalize_reasoning_effort(
655
+ reasoning_effort=parsed["reasoning_effort"],
656
+ power=parsed["power"],
657
+ usage=RUN_USAGE,
658
+ )
659
+ return {
660
+ "name": name,
661
+ "provider": parsed["provider"],
662
+ "cwd": parsed["cwd"],
663
+ "prompt_file": parsed["prompt_file"],
664
+ "prompt": parsed["prompt"],
665
+ "model": parsed["model"],
666
+ "permission": parsed["permission"],
667
+ "timeout_seconds": parsed["timeout_seconds"],
668
+ "reasoning_effort": effort.get("reasoning_effort"),
669
+ "power": effort.get("power"),
670
+ }
671
+
672
+
546
673
  def _parse_positive_int(value):
547
674
  try:
548
675
  parsed = int(value)
@@ -1025,6 +1152,221 @@ def handle_enable(rest, ctx):
1025
1152
  return 0
1026
1153
 
1027
1154
 
1155
+ def handle_select(rest, ctx):
1156
+ parsed = _parse_select_args(rest)
1157
+ selected = _select_headless_session(
1158
+ ctx,
1159
+ parsed["provider"],
1160
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1161
+ require_ready=parsed["require_ready"],
1162
+ force_refresh=parsed["refresh"],
1163
+ )
1164
+ policy = "ready_then_cooldown_then_health_then_priority_then_name"
1165
+ if not selected:
1166
+ message = "No suitable session found."
1167
+ _write_json(ctx, _json_failure(
1168
+ "select",
1169
+ "no_suitable_session",
1170
+ message,
1171
+ provider=parsed["provider"],
1172
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1173
+ require_ready=parsed["require_ready"],
1174
+ selection_policy=policy,
1175
+ ))
1176
+ return 1
1177
+ session = selected["session"]
1178
+ row = selected.get("row") or {}
1179
+ reason = "highest availability suitable session"
1180
+ _write_json(ctx, _json_success(
1181
+ "select",
1182
+ f"Selected session {session['name']}",
1183
+ session=session["name"],
1184
+ provider=session["provider"],
1185
+ reason=reason,
1186
+ selection_policy=policy,
1187
+ min_reasoning_effort=parsed["min_reasoning_effort"],
1188
+ reasoning_effort=_session_reasoning_effort(session),
1189
+ available_pct=row.get("available_pct"),
1190
+ auth_status=row.get("auth_status"),
1191
+ ))
1192
+ return 0
1193
+
1194
+
1195
+ def _read_run_prompt(parsed):
1196
+ if parsed.get("prompt") is not None:
1197
+ return parsed["prompt"]
1198
+ try:
1199
+ with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
1200
+ return handle.read()
1201
+ except OSError as error:
1202
+ raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
1203
+
1204
+
1205
+ def _run_cdx_error_code(error):
1206
+ message = str(error)
1207
+ if message.startswith("Usage:"):
1208
+ return "invalid_request"
1209
+ if message.startswith("Invalid cwd:"):
1210
+ return "invalid_cwd"
1211
+ if message.startswith("Session is disabled:"):
1212
+ return "session_disabled"
1213
+ if "CLI not found on PATH" in message:
1214
+ return "provider_cli_not_found"
1215
+ if message.startswith("Failed to start "):
1216
+ return "provider_start_failed"
1217
+ if (
1218
+ message.startswith("Unsupported reasoning effort:")
1219
+ or message.startswith("Unsupported power:")
1220
+ or "--reasoning-effort and --power must match" in message
1221
+ ):
1222
+ return "invalid_reasoning_effort"
1223
+ return "cdx_error"
1224
+
1225
+
1226
+ def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
1227
+ run_info = run_info or {}
1228
+ usage = (
1229
+ extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
1230
+ if ok and session else
1231
+ empty_usage()
1232
+ )
1233
+ return {
1234
+ "schema_version": API_SCHEMA_VERSION,
1235
+ "ok": bool(ok),
1236
+ "action": "run",
1237
+ "launcher": "cdx",
1238
+ "session": session.get("name") if session else None,
1239
+ "provider": session.get("provider") if session else parsed.get("provider"),
1240
+ "model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
1241
+ "reasoning_effort": parsed.get("reasoning_effort") or ((session.get("launch") or {}).get("reasoning_effort") if session else None),
1242
+ "power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
1243
+ "cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
1244
+ "run_id": run_info.get("run_id"),
1245
+ "exit_code": run_info.get("returncode"),
1246
+ "duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
1247
+ "transcript_path": run_info.get("transcript_path"),
1248
+ "stdout_path": run_info.get("stdout_path"),
1249
+ "stderr_path": run_info.get("stderr_path"),
1250
+ "usage": usage,
1251
+ "warnings": [],
1252
+ "error": None if ok else {
1253
+ "source": error_source or "cdx",
1254
+ "code": error_code or "cdx_error",
1255
+ "message": str(error) if error else "Run failed.",
1256
+ "provider_code": run_info.get("returncode") if error_source == "provider" else None,
1257
+ },
1258
+ }
1259
+
1260
+
1261
+ def handle_run(rest, ctx):
1262
+ try:
1263
+ parsed = _parse_run_args(rest)
1264
+ session = None
1265
+ if parsed["name"]:
1266
+ session = ctx["service"]["get_session"](parsed["name"])
1267
+ if not session:
1268
+ raise CdxError(f"Unknown session: {parsed['name']}")
1269
+ if session.get("enabled", True) is False:
1270
+ raise CdxError(f"Session is disabled: {parsed['name']}")
1271
+ else:
1272
+ selected = _select_headless_session(
1273
+ ctx,
1274
+ parsed["provider"],
1275
+ min_reasoning_effort=parsed["reasoning_effort"],
1276
+ require_ready=True,
1277
+ force_refresh=False,
1278
+ )
1279
+ if not selected:
1280
+ _write_json(ctx, _json_failure(
1281
+ "run",
1282
+ "no_suitable_session",
1283
+ "No suitable session found.",
1284
+ launcher="cdx",
1285
+ provider=parsed["provider"],
1286
+ ))
1287
+ return 1
1288
+ session = selected["session"]
1289
+
1290
+ cwd = os.path.abspath(parsed["cwd"])
1291
+ if not os.path.isdir(cwd):
1292
+ raise CdxError(f"Invalid cwd: {parsed['cwd']}")
1293
+ prompt = _read_run_prompt(parsed)
1294
+ launch_updates = {}
1295
+ if parsed.get("model"):
1296
+ launch_updates["model"] = parsed["model"]
1297
+ if parsed.get("permission"):
1298
+ launch_updates["permission"] = parsed["permission"]
1299
+ if parsed.get("reasoning_effort"):
1300
+ launch_updates["reasoning_effort"] = parsed["reasoning_effort"]
1301
+ launch_updates["power"] = parsed["reasoning_effort"]
1302
+ run_session = {
1303
+ **session,
1304
+ "launch": {
1305
+ **(session.get("launch") or {}),
1306
+ **launch_updates,
1307
+ },
1308
+ }
1309
+ _ensure_session_authentication(
1310
+ run_session,
1311
+ ctx["service"],
1312
+ spawn=ctx.get("spawn"),
1313
+ spawn_sync=ctx.get("spawn_sync"),
1314
+ env_override=ctx.get("env"),
1315
+ stdin_is_tty=False,
1316
+ behavior="launch",
1317
+ signal_emitter=ctx.get("signal_emitter"),
1318
+ )
1319
+ run_info = _run_headless_provider_command(
1320
+ run_session,
1321
+ cwd=cwd,
1322
+ env_override=ctx.get("env"),
1323
+ initial_prompt=prompt,
1324
+ timeout_seconds=parsed.get("timeout_seconds"),
1325
+ spawn=ctx.get("spawn_headless") or ctx.get("spawn"),
1326
+ )
1327
+ ok = run_info.get("returncode") == 0
1328
+ if ok:
1329
+ ctx["service"]["record_launch_history"](session["name"], {
1330
+ "status": "success",
1331
+ "cwd": cwd,
1332
+ "exit_code": 0,
1333
+ **run_info,
1334
+ })
1335
+ _write_json(ctx, _run_result_payload(True, parsed, run_session, run_info=run_info))
1336
+ return 0
1337
+ message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
1338
+ error = CdxError(message, run_info.get("returncode") or 1)
1339
+ ctx["service"]["record_launch_history"](session["name"], {
1340
+ "status": "failed",
1341
+ "cwd": cwd,
1342
+ "error": str(error),
1343
+ "exit_code": error.exit_code,
1344
+ **run_info,
1345
+ })
1346
+ _write_json(ctx, _run_result_payload(
1347
+ False,
1348
+ parsed,
1349
+ run_session,
1350
+ run_info=run_info,
1351
+ error=error,
1352
+ error_source="provider",
1353
+ error_code="provider_timeout" if run_info.get("timed_out") else "provider_failed",
1354
+ ))
1355
+ return error.exit_code or 1
1356
+ except CdxError as error:
1357
+ run_info = getattr(error, "run_info", None)
1358
+ _write_json(ctx, _run_result_payload(
1359
+ False,
1360
+ locals().get("parsed", {}) or {},
1361
+ locals().get("session"),
1362
+ run_info=run_info,
1363
+ error=error,
1364
+ error_source="cdx",
1365
+ error_code=_run_cdx_error_code(error),
1366
+ ))
1367
+ return error.exit_code
1368
+
1369
+
1028
1370
  def _format_launch_config(session):
1029
1371
  launch = session.get("launch") or {}
1030
1372
  return "\n".join([
@@ -1033,6 +1375,7 @@ def _format_launch_config(session):
1033
1375
  f"permission: {launch.get('permission') or 'default'}",
1034
1376
  f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
1035
1377
  f"model: {launch.get('model') or 'default'}",
1378
+ f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
1036
1379
  ])
1037
1380
 
1038
1381
 
@@ -1062,6 +1405,87 @@ def _resolve_bulk_launch_targets(parsed, service):
1062
1405
  return targets
1063
1406
 
1064
1407
 
1408
+ def _reasoning_rank(value):
1409
+ order = {"low": 0, "medium": 1, "high": 2, "xhigh": 2, "max": 2}
1410
+ return order.get(str(value or "low").lower(), 0)
1411
+
1412
+
1413
+ def _session_reasoning_effort(session):
1414
+ launch = session.get("launch") or {}
1415
+ return (
1416
+ launch.get("reasoning_effort")
1417
+ or launch.get("reasoningEffort")
1418
+ or launch.get("power")
1419
+ or ("low" if launch.get("fast") is True else None)
1420
+ or "low"
1421
+ )
1422
+
1423
+
1424
+ def _session_selection_priority(session):
1425
+ launch = session.get("launch") or {}
1426
+ try:
1427
+ return int(launch.get("priority") or 0)
1428
+ except (TypeError, ValueError):
1429
+ return 0
1430
+
1431
+
1432
+ def _row_blocks_ready(row):
1433
+ if row.get("provider") in (PROVIDER_ANTIGRAVITY, PROVIDER_OLLAMA):
1434
+ auth = "n/a"
1435
+ else:
1436
+ auth = row.get("auth_status") or "unknown"
1437
+ if auth not in ("authenticated", "n/a"):
1438
+ return True
1439
+ available = row.get("available_pct")
1440
+ if available is not None:
1441
+ try:
1442
+ if float(available) <= 0:
1443
+ return True
1444
+ except (TypeError, ValueError):
1445
+ return True
1446
+ return False
1447
+
1448
+
1449
+ def _select_headless_session(ctx, provider, min_reasoning_effort=None, require_ready=False, force_refresh=False):
1450
+ sessions = [
1451
+ session for session in ctx["service"]["list_sessions"]()
1452
+ if session.get("provider") == provider and session.get("enabled", True) is not False
1453
+ ]
1454
+ minimum = _reasoning_rank(min_reasoning_effort)
1455
+ sessions = [
1456
+ session for session in sessions
1457
+ if _reasoning_rank(_session_reasoning_effort(session)) >= minimum
1458
+ ]
1459
+ rows = ctx["service"]["get_status_rows"](force_refresh=force_refresh)
1460
+ row_by_name = {row.get("session_name"): row for row in rows}
1461
+ candidates = []
1462
+ for session in sessions:
1463
+ row = row_by_name.get(session["name"], {})
1464
+ if require_ready and _row_blocks_ready(row):
1465
+ continue
1466
+ available = row.get("available_pct")
1467
+ try:
1468
+ available_sort = float(available) if available is not None else -1.0
1469
+ except (TypeError, ValueError):
1470
+ available_sort = -1.0
1471
+ cooldown_sort = 1 if available_sort > 0 else 0
1472
+ candidates.append({
1473
+ "session": session,
1474
+ "row": row,
1475
+ "sort_key": (
1476
+ -cooldown_sort,
1477
+ -available_sort,
1478
+ -_session_selection_priority(session),
1479
+ _reasoning_rank(_session_reasoning_effort(session)),
1480
+ session["name"],
1481
+ ),
1482
+ })
1483
+ if not candidates:
1484
+ return None
1485
+ candidates.sort(key=lambda item: item["sort_key"])
1486
+ return candidates[0]
1487
+
1488
+
1065
1489
  def _format_bulk_launch_summary(sessions):
1066
1490
  names = [session["name"] for session in sessions]
1067
1491
  if len(names) <= 8:
package/src/cli_render.py CHANGED
@@ -75,10 +75,12 @@ def _style_pct(value, use_color=False):
75
75
  text = _format_pct(value)
76
76
  if value is None:
77
77
  return _style(text, "2", use_color)
78
- if value == 0:
78
+ if value <= 0:
79
79
  return _style(text, "31", use_color)
80
80
  if value <= 10:
81
81
  return _style(text, "33", use_color)
82
+ if value > 80:
83
+ return _style(text, "96", use_color)
82
84
  return _style(text, "32", use_color)
83
85
 
84
86