claude-dev-env 8.48.6 → 8.48.7
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/docs/account-broker.md
CHANGED
|
@@ -27,7 +27,7 @@ The broker stores cached meters, spent marks, and Claude session bindings in one
|
|
|
27
27
|
| `0` | `choose` or `check` found an account, or the job exited successfully. |
|
|
28
28
|
| `3` | No account clears its floor. The decision includes `resets_at`. |
|
|
29
29
|
| `4` | A Claude job stopped with `advisor_blocked`. |
|
|
30
|
-
| `127` | A Codex job timed out,
|
|
30
|
+
| `127` | A Codex job timed out, its command could not start while no Codex roster is configured, or the command could not start on any account in the roster. |
|
|
31
31
|
| Other command code | `run` returns the command's exit code after a served response. |
|
|
32
32
|
|
|
33
33
|
`run` writes a JSON report with `product`, `command`, `events`, and `final_decision`. A wait report has `final_decision.action` equal to `wait` and a UTC `final_decision.resets_at`.
|
|
@@ -31,6 +31,7 @@ def test_should_move_the_spawn_to_opus(requested_model: object) -> None:
|
|
|
31
31
|
if requested_model is not None:
|
|
32
32
|
tool_input["model"] = requested_model
|
|
33
33
|
decision = json.loads(_run_hook(tool_input))["hookSpecificOutput"]
|
|
34
|
+
assert decision["hookEventName"] == "PreToolUse"
|
|
34
35
|
assert "permissionDecision" not in decision
|
|
35
36
|
assert decision["updatedInput"] == {**SPAWN_INPUT, "model": "opus"}
|
|
36
37
|
|
package/package.json
CHANGED
|
@@ -234,6 +234,9 @@ def _prepare_run(
|
|
|
234
234
|
|
|
235
235
|
def _wait_outcome(context: _RunContext, decision: Decision) -> JobOutcome:
|
|
236
236
|
context.report.final_decision = decision
|
|
237
|
+
all_attempt_statuses = {each_status for _, each_status in context.all_attempts}
|
|
238
|
+
if all_attempt_statuses == {"start_failed"}:
|
|
239
|
+
return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", "", None, tuple(context.all_attempts), "start_failed", None, None)
|
|
237
240
|
status = "exhausted" if context.all_attempts else "wait"
|
|
238
241
|
return JobOutcome(WAIT_EXIT_CODE, "", "", None, tuple(context.all_attempts), status, None, decision.resets_at)
|
|
239
242
|
|
|
@@ -418,6 +418,50 @@ def test_should_leave_later_choices_open_after_start_failure(
|
|
|
418
418
|
assert json.loads(capsys.readouterr().out)["decision"]["account"] == "first"
|
|
419
419
|
|
|
420
420
|
|
|
421
|
+
@pytest.mark.parametrize("product", (Product.CLAUDE, Product.CODEX))
|
|
422
|
+
def test_should_exit_127_when_every_account_fails_to_start(
|
|
423
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, product: Product
|
|
424
|
+
) -> None:
|
|
425
|
+
accounts = (_account("first", product), _account("second", product))
|
|
426
|
+
monkeypatch.setitem(account_broker.all_product_adapters, product, _adapter(accounts, {
|
|
427
|
+
"first": _meters(80, 80), "second": _meters(70, 70)
|
|
428
|
+
}))
|
|
429
|
+
monkeypatch.setattr(account_broker.sys, "stdin", io.TextIOWrapper(io.BytesIO(b""), encoding="utf-8"))
|
|
430
|
+
|
|
431
|
+
def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
|
|
432
|
+
raise OSError("missing command")
|
|
433
|
+
|
|
434
|
+
with account_broker.override_subprocess_runner(runner):
|
|
435
|
+
outcome, _ = account_broker._execute(product, ("job",), now=datetime.now(timezone.utc))
|
|
436
|
+
code = account_broker.main(("run", "--product", product.value, "--report", str(tmp_path / "report.json"), "--", "job"))
|
|
437
|
+
|
|
438
|
+
assert outcome.status == "start_failed"
|
|
439
|
+
assert outcome.returncode == 127
|
|
440
|
+
assert outcome.wait_reset_at is None
|
|
441
|
+
assert code == 127
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_should_report_exhausted_when_start_failure_meets_usage_limit(
|
|
445
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
446
|
+
) -> None:
|
|
447
|
+
accounts = (_account("first"), _account("second"))
|
|
448
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter(accounts, {
|
|
449
|
+
"first": _meters(80, 80), "second": _meters(70, 70)
|
|
450
|
+
}))
|
|
451
|
+
|
|
452
|
+
def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
|
|
453
|
+
if Path(options["env"]["CODEX_HOME"]).name == "first":
|
|
454
|
+
raise OSError("missing command")
|
|
455
|
+
return subprocess.CompletedProcess(argv, 1, "", "rate limit")
|
|
456
|
+
|
|
457
|
+
with account_broker.override_subprocess_runner(runner):
|
|
458
|
+
outcome, _ = account_broker._execute(Product.CODEX, ("job",), now=datetime.now(timezone.utc))
|
|
459
|
+
|
|
460
|
+
assert outcome.attempts == (("first", "start_failed"), ("second", "usage_limited"))
|
|
461
|
+
assert outcome.status == "exhausted"
|
|
462
|
+
assert outcome.returncode == 3
|
|
463
|
+
|
|
464
|
+
|
|
421
465
|
def test_should_stop_after_timeout_without_running_job_again(
|
|
422
466
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
423
467
|
) -> None:
|
|
@@ -707,8 +751,8 @@ def test_should_record_start_failure_without_launching_a_command_missing_from_pa
|
|
|
707
751
|
outcome, _ = account_broker._execute(Product.CLAUDE, ("claude", "-p"), now=NOW)
|
|
708
752
|
|
|
709
753
|
assert outcome.attempts == (("extra", "start_failed"),)
|
|
710
|
-
assert outcome.status == "
|
|
711
|
-
assert outcome.returncode ==
|
|
754
|
+
assert outcome.status == "start_failed"
|
|
755
|
+
assert outcome.returncode == 127
|
|
712
756
|
|
|
713
757
|
|
|
714
758
|
@pytest.mark.parametrize("marked_name", ["retired", "first"])
|