fix(sdk): read approval_timeout_seconds from /gate response (Разрыв 1c SDK sync)#74
Merged
Merged
Conversation
…c SDK sync)
Backend commit 0ad03b9 (Разрыв 1c, gate hot-path trigger) added
`approval_timeout_seconds: Option<i64>` and
`approval_expires_at: Option<String>` to the GateResponse wire
format. Before this SDK fix, the approval wait path used
`NULLRUN_APPROVAL_TIMEOUT_SECONDS` env default (default 300s) as
the ONLY source of wait duration — which is exactly the Разрыв 3
class of bug that the backend sweeper was written to prevent on
the backend side.
Concretely: a backend approval rule configured with
`expires_in_seconds=20` (short-approval use case) would
have the backend's expiry sweeper close the row at 20s, but the
SDK would have timed out the parked gate call at 300s — a silent
desync. The 300s/300s coincidence worked only because no UI-1
yet exists to set non-default expirations, and because the env
default matched the backend default.
This commit closes the SDK side of the Разрыв 1c contract:
## Changes
### `_wait_for_approval_resolution` (runtime.py:1225)
New optional kwarg `timeout_seconds: float | None = None`:
- When set to a positive number, used as the event.wait() timeout
(server-authoritative, takes precedence over the env default).
- When `None` (legacy backend without Разрыв 1c field, or
malformed response), falls back to
`self._approval_timeout_seconds` (the env default) —
pre-Разрыв 1c behaviour preserved.
- When set to a non-positive number (0 or negative), also falls
back to env default; we explicitly reject these because
`event.wait(timeout=0)` deadlocks on the very first call.
The effective timeout is also stored on the pending entry as
`entry["timeout_seconds"]` so callers / tests can inspect which
value actually drove the wait.
When the server value diverges from the env default, a DEBUG
log line is emitted ("approval {id}: using server timeout={X}s
(env default would have been {Y}s)") so an operator inspecting
logs can see which value actually drove the wait — useful for
diagnosing 'why did this approval time out earlier than I
configured' tickets.
### `check_workflow_budget` (runtime.py:1611)
Reads `response["approval_timeout_seconds"]` (server value),
validates the type (must be a number) and sign (must be positive),
and falls back to `None` (which then triggers env-default in
_wait_for_approval_resolution) on any validation failure. The
existing env-default path is preserved as the explicit fallback
contract.
`approval_expires_at` is intentionally not parsed in the SDK:
the field is documented as informational (UI/logs) and isn't
required for the SDK's wait math. If a future backend draft
sends only the ISO8601 string, the SDK will fall through to the
env default — same behaviour as the field being absent.
The approval-required INFO log line now includes which timeout
drove the wait ("env-default" vs the server value) for
diagnostic visibility.
## Tests (6 new in tests/test_approval_timeout_field.py)
- `test_server_timeout_used_when_response_has_valid_value`:
server timeout=15s is stored on the entry, env default 300s
is NOT used.
- `test_env_fallback_when_response_omits_field`: pre-Разрыв 1c
behaviour preserved when server sends no timeout field.
- `test_env_fallback_when_server_value_is_zero`: 0/0.0/-1/-100.0
all fall back to env default (regression guard against the
`event.wait(timeout=0)` deadlock footgun).
- `test_env_fallback_when_server_value_is_non_numeric`: pre-
validated None falls back to env default.
- `test_timeout_sentinel_returned_when_no_ws_push`: timeout
fires at the SERVER timeout, not the env default (0.1s
server, 300s env, 300s timeout would mean a 5-min test run).
- `test_diverging_server_value_logs_at_debug`: caplog captures
the 'using server timeout=Xs (env default would have been Ys)'
DEBUG line when values diverge.
## Verification
```
pytest tests/test_approval_timeout_field.py → 6 passed
pytest -n auto --cov=src/nullrun --cov-branch
--cov-report=xml --cov-fail-under=0
→ 1243 passed, 7 skipped, 28 warnings
in 34.44s (coverage 80.92%)
```
No public API change (new optional kwarg, backward compatible).
No SDK_MIN_VERSION bump. No on-wire change.
## SDK release note (for CHANGELOG.md follow-up)
When the SDK version is bumped, add:
```
### Fixed
- approval wait: SDK now uses server-authoritative
`approval_timeout_seconds` from the /gate response when
available, falling back to `NULLRUN_APPROVAL_TIMEOUT_SECONDS`
env default only on missing/non-positive/non-numeric values
(Разрыв 1c SDK sync; matches backend commit 0ad03b9).
```
(CHANGELOG.md edit deferred to release PR — this commit is
behaviour-only.)
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
maltsev-dev
added a commit
that referenced
this pull request
Jul 21, 2026
Bump SDK 0.13.12 -> 0.13.13. Behaviour change (backward compatible): the approval wait now uses the server-authoritative approval_timeout_seconds from the /gate response when present, falling back to the NULLRUN_APPROVAL_TIMEOUT_SECONDS env default only on missing/non-positive/non-numeric values. Pairs with backend commit 0ad03b9 (Разрыв 1c, gate hot-path trigger). Changes: - pyproject.toml version + comment history bumped to 0.13.13. - src/nullrun/__version__.py docstring adds the v3.27 / 0.13.13 entry with full Разрыв 1c rationale, fix details, tests summary, and verification commands. - CHANGELOG.md adds the ## [0.13.13] - 2026-07-21 section (Fixed / Tests / Compatibility) ahead of 0.13.12. The runtime fix (src/nullrun/runtime.py:_wait_for_approval_resolution, _check_workflow_budget) and the 6-test contract suite landed in PR #74 (commit bde45c3). This release commit is the version-bump + changelog half. Verified: pytest -n auto --cov=src/nullrun --cov-branch --cov-report=xml --cov-fail-under=0 → 1243 passed, 7 skipped, 29 warnings in 33.15s, cov 80.92% ruff check src/ tests/ → All checks passed mypy src/ → Success: no issues found in 34 source files
5 tasks
maltsev-dev
added a commit
that referenced
this pull request
Jul 21, 2026
* fix(sdk): read approval_timeout_seconds from /gate response (Разрыв 1c SDK sync)
Backend commit 0ad03b9 (Разрыв 1c, gate hot-path trigger) added
`approval_timeout_seconds: Option<i64>` and
`approval_expires_at: Option<String>` to the GateResponse wire
format. Before this SDK fix, the approval wait path used
`NULLRUN_APPROVAL_TIMEOUT_SECONDS` env default (default 300s) as
the ONLY source of wait duration — which is exactly the Разрыв 3
class of bug that the backend sweeper was written to prevent on
the backend side.
Concretely: a backend approval rule configured with
`expires_in_seconds=20` (short-approval use case) would
have the backend's expiry sweeper close the row at 20s, but the
SDK would have timed out the parked gate call at 300s — a silent
desync. The 300s/300s coincidence worked only because no UI-1
yet exists to set non-default expirations, and because the env
default matched the backend default.
This commit closes the SDK side of the Разрыв 1c contract:
## Changes
### `_wait_for_approval_resolution` (runtime.py:1225)
New optional kwarg `timeout_seconds: float | None = None`:
- When set to a positive number, used as the event.wait() timeout
(server-authoritative, takes precedence over the env default).
- When `None` (legacy backend without Разрыв 1c field, or
malformed response), falls back to
`self._approval_timeout_seconds` (the env default) —
pre-Разрыв 1c behaviour preserved.
- When set to a non-positive number (0 or negative), also falls
back to env default; we explicitly reject these because
`event.wait(timeout=0)` deadlocks on the very first call.
The effective timeout is also stored on the pending entry as
`entry["timeout_seconds"]` so callers / tests can inspect which
value actually drove the wait.
When the server value diverges from the env default, a DEBUG
log line is emitted ("approval {id}: using server timeout={X}s
(env default would have been {Y}s)") so an operator inspecting
logs can see which value actually drove the wait — useful for
diagnosing 'why did this approval time out earlier than I
configured' tickets.
### `check_workflow_budget` (runtime.py:1611)
Reads `response["approval_timeout_seconds"]` (server value),
validates the type (must be a number) and sign (must be positive),
and falls back to `None` (which then triggers env-default in
_wait_for_approval_resolution) on any validation failure. The
existing env-default path is preserved as the explicit fallback
contract.
`approval_expires_at` is intentionally not parsed in the SDK:
the field is documented as informational (UI/logs) and isn't
required for the SDK's wait math. If a future backend draft
sends only the ISO8601 string, the SDK will fall through to the
env default — same behaviour as the field being absent.
The approval-required INFO log line now includes which timeout
drove the wait ("env-default" vs the server value) for
diagnostic visibility.
## Tests (6 new in tests/test_approval_timeout_field.py)
- `test_server_timeout_used_when_response_has_valid_value`:
server timeout=15s is stored on the entry, env default 300s
is NOT used.
- `test_env_fallback_when_response_omits_field`: pre-Разрыв 1c
behaviour preserved when server sends no timeout field.
- `test_env_fallback_when_server_value_is_zero`: 0/0.0/-1/-100.0
all fall back to env default (regression guard against the
`event.wait(timeout=0)` deadlock footgun).
- `test_env_fallback_when_server_value_is_non_numeric`: pre-
validated None falls back to env default.
- `test_timeout_sentinel_returned_when_no_ws_push`: timeout
fires at the SERVER timeout, not the env default (0.1s
server, 300s env, 300s timeout would mean a 5-min test run).
- `test_diverging_server_value_logs_at_debug`: caplog captures
the 'using server timeout=Xs (env default would have been Ys)'
DEBUG line when values diverge.
## Verification
```
pytest tests/test_approval_timeout_field.py → 6 passed
pytest -n auto --cov=src/nullrun --cov-branch
--cov-report=xml --cov-fail-under=0
→ 1243 passed, 7 skipped, 28 warnings
in 34.44s (coverage 80.92%)
```
No public API change (new optional kwarg, backward compatible).
No SDK_MIN_VERSION bump. No on-wire change.
## SDK release note (for CHANGELOG.md follow-up)
When the SDK version is bumped, add:
```
### Fixed
- approval wait: SDK now uses server-authoritative
`approval_timeout_seconds` from the /gate response when
available, falling back to `NULLRUN_APPROVAL_TIMEOUT_SECONDS`
env default only on missing/non-positive/non-numeric values
(Разрыв 1c SDK sync; matches backend commit 0ad03b9).
```
(CHANGELOG.md edit deferred to release PR — this commit is
behaviour-only.)
* chore(release): 0.13.13 — Разрыв 1c SDK sync
Bump SDK 0.13.12 -> 0.13.13. Behaviour change (backward compatible):
the approval wait now uses the server-authoritative
approval_timeout_seconds from the /gate response when present,
falling back to the NULLRUN_APPROVAL_TIMEOUT_SECONDS env default
only on missing/non-positive/non-numeric values. Pairs with
backend commit 0ad03b9 (Разрыв 1c, gate hot-path trigger).
Changes:
- pyproject.toml version + comment history bumped to 0.13.13.
- src/nullrun/__version__.py docstring adds the v3.27 / 0.13.13
entry with full Разрыв 1c rationale, fix details, tests
summary, and verification commands.
- CHANGELOG.md adds the ## [0.13.13] - 2026-07-21 section
(Fixed / Tests / Compatibility) ahead of 0.13.12.
The runtime fix (src/nullrun/runtime.py:_wait_for_approval_resolution,
_check_workflow_budget) and the 6-test contract suite landed
in PR #74 (commit bde45c3). This release commit is the
version-bump + changelog half.
Verified: pytest -n auto --cov=src/nullrun --cov-branch
--cov-report=xml --cov-fail-under=0
→ 1243 passed, 7 skipped, 29 warnings in 33.15s, cov 80.92%
ruff check src/ tests/ → All checks passed
mypy src/ → Success: no issues found in 34 source files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the SDK side of the Разрыв 1c contract: SDK now reads the server-authoritative
approval_timeout_secondsfrom the/gateresponse when present, falling back to theNULLRUN_APPROVAL_TIMEOUT_SECONDSenv default only on missing/non-positive/non-numeric values.Pairs with backend commit
0ad03b9(Разрыв 1c, gate hot-path trigger) which addedapproval_timeout_seconds: Option<i64>andapproval_expires_at: Option<String>to the GateResponse wire format.Why this matters
Before this fix, the approval wait path used the env default (300s) as the only source of wait duration. A backend approval rule configured with
expires_in_seconds=20(short-approval use case) would have the backend's expiry sweeper close the row at 20s, but the SDK would have timed out the parked gate call at 300s — a silent desync.The 300s/300s coincidence worked only because no UI-1 yet exists to set non-default expirations, and because the env default matched the backend default. Разрыв 3 prevention on the backend side.
Changes
_wait_for_approval_resolution(runtime.py:1225)New optional kwarg
timeout_seconds: float | None = None:None(legacy backend without Разрыв 1c field, or malformed response), falls back toself._approval_timeout_seconds(env default) — pre-Разрыв 1c behaviour preserved.event.wait(timeout=0)deadlocks on the very first call.When the server value diverges from the env default, a DEBUG log line is emitted ("approval {id}: using server timeout={X}s (env default would have been {Y}s)") for diagnostic visibility.
check_workflow_budget(runtime.py:1611)Reads
response["approval_timeout_seconds"](server value), validates the type (must be a number) and sign (must be positive), and falls back toNoneon any validation failure. Existing env-default path preserved as the explicit fallback contract.approval_expires_atis intentionally not parsed in the SDK (informational only; the SDK's wait math doesn't need it).Tests (6 new in tests/test_approval_timeout_field.py)
test_server_timeout_used_when_response_has_valid_valuetest_env_fallback_when_response_omits_fieldtest_env_fallback_when_server_value_is_zero(0 / 0.0 / -1 / -100.0)test_env_fallback_when_server_value_is_non_numerictest_timeout_sentinel_returned_when_no_ws_pushtest_diverging_server_value_logs_at_debugVerification
Compatibility
timeout_secondskwarg defaults toNone, so existing callers are unaffected.CHANGELOG entry (deferred to release PR)
CHANGELOG.md + version bump will land in a follow-up release PR — this commit is behaviour-only.