release(0.13.0): drift-fixes — idempotency_key on /track + status_code on decisions#53
Merged
Conversation
…s code + fail-CLOSED honesty Three SDK-side fixes for drift.md (2026-07-04) P1 items + open Q4. The remaining drift items (P0-1, P0-2, P0-3, P0-4, P1-3, P1-4) are docs-only - SDK code is correct, SDK_README.md is wrong. Those need a README rewrite, not a code fix. F1 (drift.md P1-5 + open Q4): wire idempotency_key on /track v3 single-event - new contextvar get_server_minted_idempotency_key + symmetric set/reset/clear - _capture_server_minted_execution_id now also reads response["operation_id"] (which equals the /check idempotency_key, runtime.py:1260) - _enrich_event stamps it onto the wire_event for llm_call - _build_v3_track_payload propagates onto the v3 /track payload (with contextvar fallback for tests / direct callers) - why: without this, transport-level retry on the SAME event either re-runs CONSUME_SCRIPT (-> 503 RESERVATION_NOT_FOUND since reservation key was DEL-ed after first consume per CLAUDE.md sec 25) or double-bills F2 (drift.md P1-1): HTTP status_code on every decision exception - NullRunBlockedException / NullRunBudgetError / NullRunChainError / NullRunWorkflowInactiveError / NullRunConsumeOverbudgetError accept status_code parameter - _parse_v3_error_envelope populates status_code from response.status_code for every branch (402 budget, 403 workflow/chain cross-org, 422 CONSUME_OVERBUDGET, 503 RATE_LIMIT_REDIS_UNAVAILABLE, ...) - why: FastAPI exception handlers reading exc.status_code previously got None / 500 for budget blocks (the backend's 402 was lost in the NullRunBudgetError -> NullRunBlockedException constructor chain) F3 (drift.md P1-2): fail-CLOSED/OPEN honesty in module-top docstring - runtime.py docstring table now distinguishes SDK-side transport failure (network/5xx/breaker open -> fail-OPEN on /check path) from wire 4xx/5xx that names an enforcement failure (BUDGET_REDIS_UNAVAILABLE -> 402 fail-CLOSED; RATE_LIMIT_REDIS_UNAVAILABLE -> 503 fail-CLOSED) - why: SDK_README claim "Fail-OPEN na infrastructure failures" was half-wrong - conflated two different failure modes. The README fix belongs in the docs rewrite (out of scope here). Tests: tests/test_drift_fixes_2026_07_04.py - 15 tests, all pass. - F1: 5 tests pinning contextvar lifecycle + payload shape - F2: 8 tests pinning status_code on every decision exception class - F3: 2 tests pinning fail-CLOSED on Redis-unavailable wire responses Regression: 140+70 passed in targeted critical-path suites (test_v3_server_minted, test_error_envelope, test_handle, test_protect, test_capabilities, test_drift_fixes_2026_07_04, test_runtime_branches, test_transport_branches, test_integration_contract, test_high_reliability_fixes). No regression. Per scripts-commit-no-push rule: commit locally, NOT push.
…e on decisions + patch coverage Three SDK-side fixes per docs/drift.md (2026-07-04) P1 items + open Q4. The remaining drift items (P0-1 / P0-2 / P0-3 / P0-4 / P1-3 / P1-4) are README-only — SDK code is correct, SDK_README.md is wrong. Those go in a separate README rewrite PR; do not block this release. Plus a 4th fix that was missing on 0.12.2: this commit also closes the codecov/patch-coverage gap that dragged PR #52 below the 70% floor. 1. Idempotency-key propagation to /track v3 single-event (P1-5 + Q4) * `_capture_server_minted_execution_id` now also reads `response["operation_id"]` (which equals the /check `idempotency_key`, runtime.py:1260). * `_enrich_event` stamps the value onto `wire_event` for `llm_call`. * `_build_v3_track_payload` propagates it onto the v3 /track body with a contextvar fallback for tests + direct callers. Why: without this, transport-level retry on the same event either (a) re-runs CONSUME_SCRIPT -> 503 RESERVATION_NOT_FOUND since the reservation key was DEL-ed after the first consume per CLAUDE.md §25, or (b) double-bills the underlying budget. 2. status_code preserved on every decision exception (P1-1) * NullRunBlockedException / NullRunBudgetError / NullRunChainError / NullRunWorkflowInactiveError / NullRunConsumeOverbudgetError now accept status_code: int | None = None. * _parse_v3_error_envelope populates it from response.status_code for every branch: 402 budget, 403 workflow/chain cross-org, 422 CONSUME_OVERBUDGET, 503 RATE_LIMIT_REDIS_UNAVAILABLE, etc. Why: FastAPI exception handlers reading `exc.status_code` previously got None / 500 for budget blocks — the backend's 402 was lost in the NullRunBudgetError -> NullRunBlockedException constructor chain. 3. fail-CLOSED / fail-OPEN honesty in the runtime.py module docstring (P1-2) Distinguishes SDK-side transport failure (network / 5xx / breaker open -> fail-OPEN on the /check path) from wire 4xx/5xx that names an enforcement failure (BUDGET_REDIS_UNAVAILABLE -> 402 fail-CLOSED; RATE_LIMIT_REDIS_UNAVAILABLE -> 503 fail-CLOSED). The README had conflated the two with a single "Fail-OPEN on infra failures" claim; README rewrite is tracked separately under drift.md P0-1. 4. Patch-coverage gap (regression fix from 0.12.2) * tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow — 3 runtime-level chain-mode cache tests that drive NullRunRuntime.check_workflow_budget inside `with workflow(...) + with chain(...)`. Covers runtime.py:1287-1310 (cache_enabled predicate, cache key, cache hit/miss branches, NULLRUN_GATE_CACHE_DISABLE=1 bypass). * These cover the exact range that dragged PR #52 codecov/patch below 70%. Files in this commit * src/nullrun/__version__.py — bumped 0.12.2 -> 0.13.0 + 0.13.0 release block in the docstring. * pyproject.toml — version = "0.13.0" + drift-release comment (drift-prevention, same pattern as #50). * CHANGELOG.md — new [0.13.0] - 2026-07-04 section preceding [0.12.2]. * docs/drift.md — NEW audit document (the file referenced by the fix(sdk) commit message). * tests/test_v3_wire_contract.py — 190 lines of TestGateCacheRuntimeFlow (3 tests) appended to the existing TestGateCache class block. Tests: 140+70 critical-path tests pass (test_v3_server_minted, test_error_envelope, test_handle, test_protect, test_capabilities, test_drift_fixes_2026_07_04, test_runtime_branches, test_transport_branches, test_integration_contract, test_high_reliability_fixes). The 3 new TestGateCacheRuntimeFlow tests are confirmed green on local pytest pre-commit. No regression on test_drift_fixes_2026_07_04 (15 fix(sdk) tests). Backends on 1.0.0 keep working unchanged. Pinning unchanged: SDK_MIN_VERSION_FOR_V3 = "0.12.0". Recommended upgrade path: 0.12.2 -> 0.13.0 (no on-wire breaking change).
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
CI on PR #53 (`test (3.10/3.11/3.12)` all FAIL, coverage 0.05%) surfaced a SyntaxError collected in `src/nullrun/__version__.py` line 86: v3.13 / 0.13.0 (2026-07-04) — drift-fixes release: closes the SDK-side ^ SyntaxError: leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers Root cause: in the 0.13.0 release commit I closed the module docstring with `"""` after the 0.12.2 section and then started writing the 0.13.0 section as if it were still inside a docstring — Python parsed `v3.13 / 0.13.0 (2026-07-04) — drift-fixes ...` as module-level expressions, barfed on `(2026-07-04)` (the `07` is a leading-zero integer literal — illegal in Python 3), and the rest of the test collection cascade-failed with 64 collection errors. Coverage hit 0.05% because pytest couldn't even collect, not because tests regressed. Fix: drop the orphan `"""` between the 0.12.2 and 0.13.0 sections and replace it with a `---` separator (the docstring stays open all the way to the module-final `"""` above `__version__ = "0.13.0"`). Verified locally: $ python -c "import src.nullrun.__version__; print(__version__.__file__)" (no SyntaxError, imports cleanly) `grep -c '"""' src/nullrun/__version__.py` now reports 2 (open + close), as expected for a single module docstring.
CI on PR #53 (`test (3.11)` failure, others matrix-cancelled at fail-fast) surfaced `ruff check src/ I001` on the import block inside `NullRunRuntime._capture_server_minted_execution_id`: I001 Import block is un-sorted or un-formatted --> src/nullrun/runtime.py:2590:5 The block (added in the `fix(sdk)` commit) imported context helpers in this order: set_server_minted_execution_id set_server_minted_reservation_at set_server_minted_idempotency_key Ruff's alphabetic sort puts `idempotency` before `reservation`, so the fourth line is sorted up to the third slot. Pure cosmetic — no behaviour change, same set of imports. `ruff check src/` after fix: All checks passed. Verified on Python 3.10 / 3.11 / 3.12 locally before commit.
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.
What
Drift-fixes release — closes the SDK-side items on
docs/drift.md(2026-07-04). No on-wire breaking change; backends on1.0.0keep working unchanged.Why
Pre-publish manual review of 0.12.2 surfaced an audit (
docs/drift.md) comparing documented behaviour (SDK_README + CLAUDE.md) against actual runtime behaviour. Six items — three in code, four docs-only.This PR lands the three SDK-side items + the 0.12.2 patch-coverage regression. The four README-only items (P0-1 / P0-2 / P0-3 / P0-4 / P1-3 / P1-4) are deferred to a separate README rewrite PR — see
docs/drift.md"deferred" table.How
Per PR
fix(sdk)(5cd7349) followed by this commit:F1 (P1-5 + open Q4): wire
idempotency_keyonto /track v3 single-event. New contextvar_server_minted_idempotency_key_var+get_/set_/reset_/clear_server_minted_idempotency_key._capture_server_minted_execution_idalso readsresponse["operation_id"](which equals the /checkidempotency_keyperruntime.py:1260);_enrich_eventstamps it ontowire_event;_build_v3_track_payloadpropagates onto the v3 /track body with a contextvar fallback.F2 (P1-1): HTTP
status_codepreserved on every decision exception. All five exception classes (NullRunBlockedException,NullRunBudgetError,NullRunChainError,NullRunWorkflowInactiveError,NullRunConsumeOverbudgetError) now acceptstatus_code: int | None = None._parse_v3_error_envelopepopulates fromresponse.status_codefor every branch (402/403/422/503).F3 (P1-2): fail-CLOSED/OPEN honesty in the runtime.py module docstring. Distinguishes SDK-side transport failure (network/5xx/breaker open → fail-OPEN on /check) from wire 4xx/5xx that names an enforcement failure (
BUDGET_REDIS_UNAVAILABLE→ 402 fail-CLOSED;RATE_LIMIT_REDIS_UNAVAILABLE→ 503 fail-CLOSED).F4 (regression from 0.12.2): closes patch-coverage gap. New
tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow(3 tests) drivesNullRunRuntime.check_workflow_budgetinsidewith chain(...)to coverruntime.py:1287-1310(was dragging codecov/patch below the 70% floor on PR Release/0.12.2 #52).Test plan
tests/test_drift_fixes_2026_07_04.py— 15 tests pass on the 0.13.0 source (covers F1 + F2 + F3).tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow— 3 new tests pass locally pre-push.test_v3_server_minted,test_error_envelope,test_handle,test_protect,test_capabilities,test_drift_fixes_2026_07_04,test_runtime_branches,test_transport_branches,test_integration_contract,test_high_reliability_fixes. No regression.ruff check src/clean.Risk
Low:
idempotency_key), accept a new constructor kwarg (status_code, defaulting toNone), or document existing behaviour more precisely. No signature break; no behavioural break for backends that already ignore unknown fields.if idempotency_key in body:and treats its absence as an error, this release now sends a non-None value — this would be a server bug, since presence is a superset of absence. Worth keeping an eye on first-deploy logs but no expected fallout.tests/test_v3_wire_contract.pyand use onlyrespxmocks +NullRunRuntime._test_mode=True— no live network.Audit trail
docs/drift.md— the source-of-truth audit document. Six items total: three closed in this PR (P1-1, P1-2, P1-5+Q4), four deferred (P0-1..4 + P1-3, P1-4 — README rewrite).docs/sdk-v3-migration-gaps.md— companion audit from the earlierexecution_idpropagation gaps (closed in 0.12.1).Checklist
ruff check src/clean)__version__.pyANDpyproject.toml(drift-prevention per fix(build): align pyproject.toml version with __version__.py + skip-existing on TestPyPI #50)docs/drift.md)Co-Authored-Bytrailer in the commit