Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/claude_agent_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import math
import os
import uuid
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Mapping
Expand Down Expand Up @@ -60,26 +61,47 @@
_MAX_RUN_END_CEILING_MS = 2**31 - 1


def _parse_ceiling_ms(raw: Any) -> int | None:
"""Parse one millisecond spelling the CLI accepts, or ``None`` if invalid.

A plain integer is read exactly, so a very large value keeps the precision
a float round-trip would cost it. Anything else goes through ``float``, so
the scientific notation the CLI also reads (``1e6``) is honored. A value
that is not finite or not integral is rejected rather than truncated,
which keeps ``0.5`` from collapsing into the ``0`` that means no limit.
"""
try:
return int(raw)
except (TypeError, ValueError):
pass
try:
value = float(raw)
except (TypeError, ValueError):
return None
if not math.isfinite(value) or value != int(value):
return None
return int(value)


def run_end_ceiling_ms(options_env: Mapping[str, str]) -> int:
"""Read ``CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`` as the CLI will see it.

``options_env`` (``ClaudeAgentOptions.env``) overrides the inherited
environment, as it does for the CLI subprocess. ``0`` means no limit;
anything that is not a plain non-negative integer falls back to the CLI's
default of 10 minutes (the CLI itself also reads spellings such as
``1e6``).
a non-negative integer is taken as written, in plain decimal or in the
integral scientific notation the CLI also reads (``1e6``). Anything else
falls back to the CLI's default of 10 minutes.
"""
if _RUN_END_CEILING_ENV in options_env:
raw: Any = options_env[_RUN_END_CEILING_ENV]
else:
raw = os.environ.get(_RUN_END_CEILING_ENV)
if raw is None:
return DEFAULT_RUN_END_CEILING_MS
try:
value = int(raw)
except (TypeError, ValueError):
value = _parse_ceiling_ms(raw)
if value is None or value < 0:
return DEFAULT_RUN_END_CEILING_MS
return value if value >= 0 else DEFAULT_RUN_END_CEILING_MS
return value


def _error_result_text(message: dict[str, Any]) -> str:
Expand Down
20 changes: 20 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,16 @@ class TestRunEndCeilingFromEnv:
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "soon"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "-1"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": ""}, "1234", 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "1e6"}, None, 1_000_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "1e2"}, None, 100),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": " 1E3 "}, None, 1_000),
({}, "1e6", 1_000_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "-1e6"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "1.5"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "0.5"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "inf"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "nan"}, None, 600_000),
({"CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS": "9" * 25}, None, int("9" * 25)),
],
ids=[
"default",
Expand All @@ -1367,6 +1377,16 @@ class TestRunEndCeilingFromEnv:
"not_a_number",
"negative",
"empty_option_wins",
"scientific",
"scientific_small",
"scientific_uppercase_and_whitespace",
"scientific_from_ambient",
"scientific_negative",
"fractional",
"fractional_below_one",
"infinite",
"nan",
"huge_integer_stays_exact",
],
)
def test_parse(self, monkeypatch, options_env, ambient, expected):
Expand Down