cdx-manager 0.24.6 → 0.24.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 +9 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_24_7.md +22 -0
- package/changelogs/CHANGELOGS_0_24_8.md +16 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli_args.py +1 -0
- package/src/commands/runs.py +100 -7
- package/src/provider_runtime.py +15 -1
- package/src/run_command.py +2 -1
- package/src/run_failover.py +32 -2
- package/src/run_registry.py +11 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
|
@@ -42,6 +42,14 @@ 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
|
+
## Codex model capacity recovery
|
|
46
|
+
|
|
47
|
+
`cdx run` (including `--detach`) automatically recovers from Codex's "Selected model is at capacity" failure. It retries the same prompt, profile, and model after 1 and 2 seconds, then tries the profile's `fallback_model` list in configured order, removing duplicates. Configure alternatives with `cdx set work --fallback-model MODEL_A,MODEL_B`.
|
|
48
|
+
|
|
49
|
+
After exhausting those models, CDX selects another ready Codex profile using the normal ranking, including profile priority. Disabled, logged-out, `never`, and `ask-first` profiles are excluded from automatic selection. To choose an `ask-first` profile explicitly, name it in a new command (`cdx run NAME ...`). Each successor uses `--model` if supplied, otherwise its own configured/default model, followed by its own fallback list. Each model gets at most three attempts; at most four profile switches are allowed. Settings are not changed. Each attempt retains the command's timeout; the backoff adds at most three seconds per model.
|
|
50
|
+
|
|
51
|
+
Recovery does not require `--failover`, which still controls account quota exhaustion separately. Other provider failures and timeouts do not trigger capacity recovery. Attempts restart the prompt in the same workspace; files from earlier attempts remain there. Each retry, model fallback, profile fallback, and exhausted budget is logged to stderr and persisted in `recovery_events` in the run registry and final JSON (`cdx run-status` / `cdx run-report`). Events include archived attempt output paths. Recovery keeps one run ID and reports the final effective model. If no eligible alternative remains or the profile switch budget is reached, the run fails with `capacity_recovery_exhausted`.
|
|
52
|
+
|
|
45
53
|
## Documentation
|
|
46
54
|
|
|
47
55
|
- [Installation and first steps](docs/getting-started.md) — prerequisites, npm/pipx/uv, Windows, configuration.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.24.
|
|
1
|
+
0.24.8
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# CDX Manager 0.24.7
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-29
|
|
4
|
+
|
|
5
|
+
This patch fixes command execution in isolated Codex workspace runs. (#65)
|
|
6
|
+
|
|
7
|
+
## Workspace runs
|
|
8
|
+
|
|
9
|
+
- Drop Codex's nested sandbox when `--isolate-workspace` is enabled, avoiding the user namespace mapping failure that prevented commands from running. The outer workspace sandbox remains the isolation boundary. (#65)
|
|
10
|
+
- Remove Codex's sandbox mode and network overrides inside the isolated workspace and pass `--dangerously-bypass-approvals-and-sandbox`; the outer sandbox controls network access and keeps review runs read-only through its workspace mount. (#65)
|
|
11
|
+
|
|
12
|
+
## Codex capacity recovery
|
|
13
|
+
|
|
14
|
+
- Retry headless Codex model capacity failures after 1 and 2 seconds before trying configured fallback models, then ready Codex profiles in the existing priority-aware order. This also applies to detached runs and does not require `--failover`.
|
|
15
|
+
- Exclude disabled, logged-out, `never`, and `ask-first` profiles from automatic fallback. Bound recovery to three attempts per model and four profile switches, with a distinct `capacity_recovery_exhausted` outcome.
|
|
16
|
+
- Log each recovery action to stderr and structured run data, preserve failed attempt output, and report the effective model without changing profile settings.
|
|
17
|
+
- Cover capacity detection, retry and profile budgets, fallback order, selection policies, and backoff cancellation with tests.
|
|
18
|
+
|
|
19
|
+
## Maintenance
|
|
20
|
+
|
|
21
|
+
- Record tray companion checksums for v0.24.6. (`87aba5d2cffdfb1a96e386abcdabe54df9ee4808`)
|
|
22
|
+
- Record release archive checksums for v0.24.6. (`6b92094143ea28bf0cc096aef88d1015565aba18`)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CDX Manager 0.24.8
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-30
|
|
4
|
+
|
|
5
|
+
This patch improves recovery from Codex model capacity failures. (#67)
|
|
6
|
+
|
|
7
|
+
## Codex capacity recovery
|
|
8
|
+
|
|
9
|
+
- Retry headless Codex capacity failures after 1 and 2 seconds before trying configured fallback models, then ready Codex profiles in priority-aware order. Recovery also applies to detached runs and does not require `--failover`. (#67)
|
|
10
|
+
- Exclude disabled, logged-out, `never`, and `ask-first` profiles from automatic fallback. Limit recovery to three attempts per model and four profile switches, reporting `capacity_recovery_exhausted` when no eligible alternative remains or the budget is exhausted. (#67)
|
|
11
|
+
- Log recovery actions to stderr and structured run data, preserve failed attempt output, and report the effective model without changing profile settings. (#67)
|
|
12
|
+
|
|
13
|
+
## Maintenance
|
|
14
|
+
|
|
15
|
+
- Record tray companion checksums for v0.24.7. (`16938c0d22de4589f5b4757fd7ff55e848127e7d`)
|
|
16
|
+
- Record release archive checksums for v0.24.7. (`a169c3eee41fcbc97477ca4d74bfc8f77c1330d5`)
|
|
@@ -639,6 +639,28 @@
|
|
|
639
639
|
"x86_64-pc-windows-msvc": "74124b2e40a58a58b55724ef453c4b543a83eefab9a5f92685b239c5d97e7f67",
|
|
640
640
|
"x86_64-unknown-linux-musl": "089e552a4dfae4080a2e76cedb6fe72fc8013a9cd08cf3c8b333f916198da57c"
|
|
641
641
|
}
|
|
642
|
+
},
|
|
643
|
+
"v0.24.6": {
|
|
644
|
+
"github_tarball_sha256": "4b61861ae26fc7904e751571b65d0783c16dcf35d3b485379434469e27511fb4",
|
|
645
|
+
"github_zip_sha256": "6c5cfa2139eb53a20a8eca8624437a06b18392b0df7774d9c7c64a74b8066f59",
|
|
646
|
+
"tray_assets": {
|
|
647
|
+
"aarch64-apple-darwin": "7200f658e3413b2614a7ffa5d03e1dac173fc1a653dc659d92f2bef40f2bf5d4",
|
|
648
|
+
"aarch64-unknown-linux-musl": "b892a9a3bf009e5069dc798ab910b20e77ac9d03b9b575a80cb19af1dc13533a",
|
|
649
|
+
"x86_64-apple-darwin": "d1e1b619feab8eae1e2a25e17942d8d690fc1e6184f294599a15858a1f0c146e",
|
|
650
|
+
"x86_64-pc-windows-msvc": "0d125d8e0f7d9880a2ed9e0a794964f3d69ef3353b90a1d594b17f9392a0a0af",
|
|
651
|
+
"x86_64-unknown-linux-musl": "d6c369ace10a6d373366cd12e4e6cd7f2fd278fbce2fd9a13c5bf51047847441"
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
"v0.24.7": {
|
|
655
|
+
"github_tarball_sha256": "170df61319347dc133442c6750c31812a55a3e8a002fae60bd4822771839750f",
|
|
656
|
+
"github_zip_sha256": "fa3eda26dbfec1af36e25f72a0fcf01a18fa064dc442ffc39b64c7bb2ab7e1ae",
|
|
657
|
+
"tray_assets": {
|
|
658
|
+
"aarch64-apple-darwin": "f775ae9e140f1147368c4658a27e6f0751585a5b001b633fcfccfe252b85721e",
|
|
659
|
+
"aarch64-unknown-linux-musl": "528d61bb9dc5c395f74aad98d35f8cebea958ba2561fd15302846eb8bc036343",
|
|
660
|
+
"x86_64-apple-darwin": "e85cd5d2cb2f5dd1a2dd89ff9b532432cffd86175e709d147ff4595551d4ff50",
|
|
661
|
+
"x86_64-pc-windows-msvc": "41df35daa664802b87c052d6a9f634b240eb4e4a915868f1dd629b6734eab76a",
|
|
662
|
+
"x86_64-unknown-linux-musl": "2492e137e5c883fa16f502843a867e0325744dfc66d7d38be2c00eb362425af6"
|
|
663
|
+
}
|
|
642
664
|
}
|
|
643
665
|
}
|
|
644
666
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli_args.py
CHANGED
package/src/commands/runs.py
CHANGED
|
@@ -5,8 +5,10 @@ Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
|
5
5
|
|
|
6
6
|
import importlib.util
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
11
|
+
import time
|
|
10
12
|
|
|
11
13
|
from ..cli_args import (
|
|
12
14
|
RUN_REPORT_USAGE,
|
|
@@ -39,10 +41,18 @@ from ..provider_runtime import (
|
|
|
39
41
|
_build_launch_spec,
|
|
40
42
|
_ensure_session_authentication,
|
|
41
43
|
_headless_artifact_paths,
|
|
44
|
+
_restore_signal_handlers,
|
|
45
|
+
_route_termination_to_interrupt,
|
|
42
46
|
_run_headless_provider_command,
|
|
43
47
|
)
|
|
44
48
|
from ..run_command import read_run_prompt, run_cdx_error_code, run_launch_payload, run_result_payload
|
|
45
|
-
from ..run_failover import
|
|
49
|
+
from ..run_failover import (
|
|
50
|
+
CAPACITY_RETRY_DELAYS,
|
|
51
|
+
MAX_FAILOVER_TRANSITIONS,
|
|
52
|
+
capacity_models,
|
|
53
|
+
failover_reason,
|
|
54
|
+
looks_model_at_capacity,
|
|
55
|
+
)
|
|
46
56
|
from ..run_registry import RunRegistry, build_code_review_report, final_message
|
|
47
57
|
from ..run_usage import extract_run_observation
|
|
48
58
|
from ..session_ranking import FACTOR_DESCRIPTIONS, rank_sessions, selection_policy
|
|
@@ -676,7 +686,13 @@ def handle_run(rest, ctx):
|
|
|
676
686
|
|
|
677
687
|
attempted = [run_session["name"]]
|
|
678
688
|
failover_error_code = None
|
|
689
|
+
recovery_events = []
|
|
690
|
+
attempt = 0
|
|
691
|
+
models = capacity_models(run_session)
|
|
692
|
+
model_index = 0
|
|
693
|
+
capacity_retries = 0
|
|
679
694
|
while True:
|
|
695
|
+
attempt += 1
|
|
680
696
|
run_info = _run_headless_provider_command(
|
|
681
697
|
run_session,
|
|
682
698
|
cwd=cwd,
|
|
@@ -690,16 +706,70 @@ def handle_run(rest, ctx):
|
|
|
690
706
|
run_session.get("provider"), run_info.get("stdout_path"),
|
|
691
707
|
run_session.get("authHome"),
|
|
692
708
|
)
|
|
693
|
-
run_info = {**run_info, "usage": usage, "usage_model": usage_model
|
|
709
|
+
run_info = {**run_info, "usage": usage, "usage_model": usage_model,
|
|
710
|
+
"effective_model": (run_session.get("launch") or {}).get("model"),
|
|
711
|
+
"recovery_events": recovery_events}
|
|
694
712
|
ctx["service"]["record_launch_history"](run_session["name"], {
|
|
695
713
|
"status": "success" if run_info.get("returncode") == 0 else "failed",
|
|
696
714
|
"action": "run",
|
|
697
715
|
"run_id": run_id,
|
|
698
|
-
"attempt":
|
|
716
|
+
"attempt": attempt,
|
|
699
717
|
"cwd": cwd,
|
|
700
718
|
"exit_code": run_info.get("returncode"),
|
|
701
719
|
**run_info,
|
|
702
720
|
})
|
|
721
|
+
if looks_model_at_capacity(run_session.get("provider"), run_info):
|
|
722
|
+
event = {"reason": "model_at_capacity", "attempt": attempt,
|
|
723
|
+
"session": run_session["name"], "model": models[model_index]}
|
|
724
|
+
if capacity_retries < len(CAPACITY_RETRY_DELAYS):
|
|
725
|
+
delay = CAPACITY_RETRY_DELAYS[capacity_retries]
|
|
726
|
+
capacity_retries += 1
|
|
727
|
+
event.update(action="retry", retry=capacity_retries, delay_seconds=delay)
|
|
728
|
+
elif model_index + 1 < len(models):
|
|
729
|
+
model_index += 1
|
|
730
|
+
capacity_retries = 0
|
|
731
|
+
run_session["launch"]["model"] = models[model_index]
|
|
732
|
+
event.update(action="fallback_model", target_model=models[model_index])
|
|
733
|
+
else:
|
|
734
|
+
successor = None if len(attempted) > MAX_FAILOVER_TRANSITIONS else _select_headless_session(
|
|
735
|
+
ctx, "codex", min_reasoning_effort=parsed["reasoning_effort"],
|
|
736
|
+
require_ready=True, exclude=attempted,
|
|
737
|
+
)
|
|
738
|
+
if not successor:
|
|
739
|
+
failover_error_code = "capacity_recovery_exhausted"
|
|
740
|
+
event.update(action="exhausted")
|
|
741
|
+
else:
|
|
742
|
+
selection_row = successor.get("row") or {}
|
|
743
|
+
next_session = successor["session"]
|
|
744
|
+
run_session = {**next_session, "launch": {**(next_session.get("launch") or {}), **launch_updates}}
|
|
745
|
+
# Each new profile starts with the requested model, or its own default.
|
|
746
|
+
models = capacity_models(run_session)
|
|
747
|
+
model_index = capacity_retries = 0
|
|
748
|
+
attempted.append(run_session["name"])
|
|
749
|
+
event.update(action="fallback_profile", target_session=run_session["name"], target_model=models[0])
|
|
750
|
+
_record_capacity_recovery(ctx, registry, run_id, event, run_info, recovery_events)
|
|
751
|
+
if failover_error_code:
|
|
752
|
+
break
|
|
753
|
+
if event["action"] == "retry":
|
|
754
|
+
previous_handlers = _route_termination_to_interrupt()
|
|
755
|
+
try:
|
|
756
|
+
time.sleep(event["delay_seconds"])
|
|
757
|
+
except KeyboardInterrupt as interrupt:
|
|
758
|
+
error = CdxError("Codex capacity recovery was cancelled.", 130)
|
|
759
|
+
error.cancelled = True
|
|
760
|
+
error.history_recorded = True
|
|
761
|
+
error.run_info = {**run_info, "returncode": 130}
|
|
762
|
+
raise error from interrupt
|
|
763
|
+
finally:
|
|
764
|
+
_restore_signal_handlers(previous_handlers)
|
|
765
|
+
elif event["action"] == "fallback_profile":
|
|
766
|
+
_ensure_session_authentication(
|
|
767
|
+
run_session, ctx["service"], spawn=ctx.get("spawn"), spawn_sync=ctx.get("spawn_sync"),
|
|
768
|
+
env_override=ctx.get("env"), stdin_is_tty=False, behavior="launch",
|
|
769
|
+
signal_emitter=ctx.get("signal_emitter"), trust_local_credentials=False,
|
|
770
|
+
)
|
|
771
|
+
registry.migrate(run_id, session=run_session["name"], provider="codex", reason="model_at_capacity")
|
|
772
|
+
continue
|
|
703
773
|
if run_info.get("returncode") == 0 or not parsed.get("failover"):
|
|
704
774
|
break
|
|
705
775
|
reason = failover_reason(
|
|
@@ -731,6 +801,8 @@ def handle_run(rest, ctx):
|
|
|
731
801
|
"launch": {**(successor["session"].get("launch") or {}), **launch_updates},
|
|
732
802
|
}
|
|
733
803
|
attempted.append(run_session["name"])
|
|
804
|
+
models = capacity_models(run_session)
|
|
805
|
+
model_index = capacity_retries = 0
|
|
734
806
|
# A different account means different credentials; an unauthenticated
|
|
735
807
|
# successor would fail for a reason unrelated to the task.
|
|
736
808
|
_ensure_session_authentication(
|
|
@@ -769,7 +841,9 @@ def handle_run(rest, ctx):
|
|
|
769
841
|
)
|
|
770
842
|
_write_json(ctx, final_payload)
|
|
771
843
|
return 0
|
|
772
|
-
if failover_error_code == "
|
|
844
|
+
if failover_error_code == "capacity_recovery_exhausted":
|
|
845
|
+
message = "Codex model capacity recovery exhausted its retry, model, or profile budget."
|
|
846
|
+
elif failover_error_code == "failover_exhausted":
|
|
773
847
|
message = f"Every eligible session was rate limited: {', '.join(attempted)}."
|
|
774
848
|
elif failover_error_code == "failover_limit_reached":
|
|
775
849
|
message = f"Failover stopped after {MAX_FAILOVER_TRANSITIONS} transitions: {', '.join(attempted)}."
|
|
@@ -806,7 +880,9 @@ def handle_run(rest, ctx):
|
|
|
806
880
|
except CdxError as error:
|
|
807
881
|
run_info = getattr(error, "run_info", None)
|
|
808
882
|
cancelled = bool(getattr(error, "cancelled", False))
|
|
809
|
-
if run_info
|
|
883
|
+
if run_info is not None:
|
|
884
|
+
run_info = {**run_info, "recovery_events": locals().get("recovery_events", [])}
|
|
885
|
+
if run_info and run_id and not getattr(error, "history_recorded", False):
|
|
810
886
|
usage, usage_model = extract_run_observation(
|
|
811
887
|
run_session.get("provider"), run_info.get("stdout_path"),
|
|
812
888
|
run_session.get("authHome"),
|
|
@@ -816,7 +892,7 @@ def handle_run(rest, ctx):
|
|
|
816
892
|
"status": "failed",
|
|
817
893
|
"action": "run",
|
|
818
894
|
"run_id": run_id,
|
|
819
|
-
"attempt":
|
|
895
|
+
"attempt": locals().get("attempt", 1),
|
|
820
896
|
"cwd": cwd,
|
|
821
897
|
"exit_code": error.exit_code,
|
|
822
898
|
**run_info,
|
|
@@ -825,13 +901,14 @@ def handle_run(rest, ctx):
|
|
|
825
901
|
API_SCHEMA_VERSION,
|
|
826
902
|
False,
|
|
827
903
|
locals().get("parsed", {}) or {},
|
|
828
|
-
locals().get("session"),
|
|
904
|
+
locals().get("run_session", locals().get("session")),
|
|
829
905
|
run_info=run_info,
|
|
830
906
|
error=error,
|
|
831
907
|
error_source="provider" if cancelled else "cdx",
|
|
832
908
|
error_code="run_cancelled" if cancelled else run_cdx_error_code(error),
|
|
833
909
|
selection_row=selection_row,
|
|
834
910
|
)
|
|
911
|
+
final_payload["recovery_events"] = locals().get("recovery_events", [])
|
|
835
912
|
if registry and run_id:
|
|
836
913
|
registry.finish(
|
|
837
914
|
run_id,
|
|
@@ -845,6 +922,22 @@ def handle_run(rest, ctx):
|
|
|
845
922
|
_write_json(ctx, final_payload)
|
|
846
923
|
return error.exit_code
|
|
847
924
|
|
|
925
|
+
def _record_capacity_recovery(ctx, registry, run_id, event, run_info, events):
|
|
926
|
+
# Subsequent attempts reuse the run's paths; retain the failed output first.
|
|
927
|
+
artifacts = {}
|
|
928
|
+
for key in ("stdout_path", "stderr_path", "transcript_path"):
|
|
929
|
+
path = run_info.get(key)
|
|
930
|
+
if path and os.path.isfile(path):
|
|
931
|
+
archived = f"{path}.attempt-{event['attempt']}"
|
|
932
|
+
shutil.copyfile(path, archived)
|
|
933
|
+
artifacts[key] = archived
|
|
934
|
+
event["artifacts"] = artifacts
|
|
935
|
+
events.append(event)
|
|
936
|
+
registry.record_recovery(run_id, event)
|
|
937
|
+
details = ", ".join(f"{key}={value}" for key, value in event.items() if key != "artifacts")
|
|
938
|
+
ctx["err"](f"cdx capacity recovery: {details}\n")
|
|
939
|
+
|
|
940
|
+
|
|
848
941
|
def _select_headless_session(
|
|
849
942
|
ctx, provider, min_reasoning_effort=None, require_ready=False, force_refresh=False, exclude=None,
|
|
850
943
|
):
|
package/src/provider_runtime.py
CHANGED
|
@@ -1212,7 +1212,10 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
1212
1212
|
)
|
|
1213
1213
|
launch = session.get("launch") or {}
|
|
1214
1214
|
if launch.get("network_access"):
|
|
1215
|
-
if session.get("provider") == PROVIDER_CODEX:
|
|
1215
|
+
if session.get("provider") == PROVIDER_CODEX and launch.get("isolate_workspace"):
|
|
1216
|
+
# The outer sandbox owns the network; Codex's own sandbox is dropped below.
|
|
1217
|
+
pass
|
|
1218
|
+
elif session.get("provider") == PROVIDER_CODEX:
|
|
1216
1219
|
# Keep the workspace sandbox; append overrides before the prompt separator.
|
|
1217
1220
|
index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
|
|
1218
1221
|
spec["args"][index:index] = ["-c", 'sandbox_workspace_write.network_access=true', '-c', 'sandbox_workspace_write.writable_roots=["/cache", "/home/agent"]']
|
|
@@ -1226,6 +1229,17 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
1226
1229
|
# The outer OS sandbox is the permission boundary for unattended Bash.
|
|
1227
1230
|
index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
|
|
1228
1231
|
spec["args"][index:index] = ["--permission-mode", "bypassPermissions"]
|
|
1232
|
+
elif session.get("provider") == PROVIDER_CODEX:
|
|
1233
|
+
# Codex's bubblewrap sandbox cannot nest inside ours (no uid map), so the
|
|
1234
|
+
# outer box is the boundary: drop `-s <mode>` and bypass the inner one.
|
|
1235
|
+
# A review run stays read-only through the outer mount.
|
|
1236
|
+
args = spec["args"]
|
|
1237
|
+
while "-s" in args:
|
|
1238
|
+
index = args.index("-s")
|
|
1239
|
+
del args[index:index + 2]
|
|
1240
|
+
if "--dangerously-bypass-approvals-and-sandbox" not in args:
|
|
1241
|
+
index = args.index("--") if "--" in args else len(args)
|
|
1242
|
+
args[index:index] = ["--dangerously-bypass-approvals-and-sandbox"]
|
|
1229
1243
|
try:
|
|
1230
1244
|
argv, child_env = sandbox_command(
|
|
1231
1245
|
[spec["command"], *spec["args"]], Path(cwd),
|
package/src/run_command.py
CHANGED
|
@@ -210,7 +210,8 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
|
210
210
|
"launcher": "cdx",
|
|
211
211
|
"session": session.get("name") if session else None,
|
|
212
212
|
"provider": session.get("provider") if session else parsed.get("provider"),
|
|
213
|
-
"model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
|
|
213
|
+
"model": run_info.get("effective_model", parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None)),
|
|
214
|
+
"recovery_events": run_info.get("recovery_events", []),
|
|
214
215
|
"reasoning_effort": run_payload_reasoning_effort(parsed, session),
|
|
215
216
|
"power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
|
|
216
217
|
"cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
|
package/src/run_failover.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Classify finished runs for account exhaustion or model capacity recovery.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
For account exhaustion, two independent signals must agree before cdx moves a task to another
|
|
4
4
|
account, because the two ways of being wrong are not symmetric. Missing a rate
|
|
5
5
|
limit costs nothing new: the run fails exactly as it does today. Inventing one
|
|
6
6
|
migrates a healthy run off a working account, discards its progress, and
|
|
@@ -22,6 +22,7 @@ codebase reads provider output for this purpose.
|
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
24
|
import os
|
|
25
|
+
import re
|
|
25
26
|
|
|
26
27
|
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
|
|
27
28
|
|
|
@@ -235,3 +236,32 @@ def failover_reason(provider, run_info, status_row):
|
|
|
235
236
|
|
|
236
237
|
def should_fail_over(provider, run_info, status_row):
|
|
237
238
|
return failover_reason(provider, run_info, status_row) is not None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
CAPACITY_RETRY_DELAYS = (1, 2)
|
|
242
|
+
_CAPACITY_MESSAGE = re.compile(r"(?:selected model|(?:the )?model(?: [\w.-]+)?) is at capacity\b", re.IGNORECASE)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def looks_model_at_capacity(provider, run_info):
|
|
246
|
+
"""Recognize Codex failures, excluding successful output and assistant prose."""
|
|
247
|
+
if provider != PROVIDER_CODEX or run_info.get("timed_out") or run_info.get("returncode") in (None, 0):
|
|
248
|
+
return False
|
|
249
|
+
for key in ("stdout_path", "stderr_path"):
|
|
250
|
+
text = _read_tail(run_info.get(key))
|
|
251
|
+
for record in _structured_records(provider, text):
|
|
252
|
+
if any(_CAPACITY_MESSAGE.search(message) for message in _error_texts(provider, record)):
|
|
253
|
+
return True
|
|
254
|
+
# Some Codex versions print this diagnostic as a standalone plain line.
|
|
255
|
+
for line in text.splitlines():
|
|
256
|
+
line = line.strip()
|
|
257
|
+
if _CAPACITY_MESSAGE.match(re.sub(r"^error:\s*", "", line, flags=re.IGNORECASE)):
|
|
258
|
+
return True
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def capacity_models(session):
|
|
263
|
+
"""Configured alternatives in order, without retrying the current model."""
|
|
264
|
+
launch = session.get("launch") or {}
|
|
265
|
+
models = [launch.get("model")]
|
|
266
|
+
models.extend(str(launch.get("fallback_model") or "").split(","))
|
|
267
|
+
return list(dict.fromkeys(model.strip() if model else None for model in models if model is None or model.strip()))
|
package/src/run_registry.py
CHANGED
|
@@ -321,6 +321,16 @@ class RunRegistry:
|
|
|
321
321
|
return run
|
|
322
322
|
return None
|
|
323
323
|
|
|
324
|
+
def record_recovery(self, run_id, event):
|
|
325
|
+
"""Publish retries and fallbacks while a supervised run is still active."""
|
|
326
|
+
with _registry_lock(self.path):
|
|
327
|
+
data = _read_registry(self.path)
|
|
328
|
+
for run in data["runs"]:
|
|
329
|
+
if run.get("run_id") == run_id:
|
|
330
|
+
run.setdefault("recovery_events", []).append({"at": utc_now_iso(), **event})
|
|
331
|
+
_write_registry(self.path, data)
|
|
332
|
+
return
|
|
333
|
+
|
|
324
334
|
def finish(self, run_id, *, status, final_payload=None, run_info=None, error=None, task_report=None):
|
|
325
335
|
with _registry_lock(self.path):
|
|
326
336
|
return self._finish_locked(run_id, status=status, final_payload=final_payload, run_info=run_info, error=error, task_report=task_report)
|
|
@@ -359,6 +369,7 @@ class RunRegistry:
|
|
|
359
369
|
# used to store whichever shape the producing reader emitted.
|
|
360
370
|
run["usage"] = coerce_usage(final_payload.get("usage"))
|
|
361
371
|
run["usage_model"] = final_payload.get("usage_model")
|
|
372
|
+
run["model"] = final_payload.get("model")
|
|
362
373
|
run["final_payload"] = final_payload
|
|
363
374
|
if error:
|
|
364
375
|
run["error"] = error
|