chore(release): 0.17.1 — op_id mint-fresh + error-code map closure - #103
Merged
Merged
Conversation
…SH-MISMATCH) Pre-fix, NullRunRuntime._check_workflow_budget_impl at runtime.py:1947-1976 read _operation_id_var; if None (first wire call in scope), minted once and stashed. Subsequent /check calls within the same scope REUSED the first call's op_id. The backend's IDEM-01 dedup (compute_gate_semantic_hash, backend/src/redis/idempotency_store.rs:125-140) keys on op_id but verifies an 11-field semantic hash (operation_id, tools, tool, mode, check_type, model, estimated_tokens, input, business_impact, workflow_id, organization_id). A second /check with a different tools / model / input on the SAME op_id therefore 409 IDEMPOTENCY_KEY_MISMATCH, surfaced as NR-B004 in the SDK. Canonical repro: nullrun_openai_approval_demo.py fires a tools=None /gate (LangGraph NullRunCallback.on_llm_start) BEFORE the @sensitive(refund_customer) /gate (tools=['refund_customer']). Both /check calls share the same op_id; the second's semantic hash diverges from the first's, server rejects with 409, SDK reports NR-B004 "You've reached the usage limit for this conversation". Fix: mint-fresh-per-call. /check always reads-and-discards the contextvar (value unused) then unconditionally mints a new UUID v4 and stashes it. /execute at runtime.py:3048-3051 reads the freshly-stashed value within the same logical action (synchronous /check -> /execute chain), so the P0-27 within-action binding (one op_id across /check + /execute) is preserved. The read-then-overwrite pattern also keeps the P0-27 source-pin test "test_check_workflow_budget_reads_contextvar" green. Verified: - 8/8 P0-27 source-pin tests pass (test_audit_p0_27_operation_id_hoist.py) - 1807 pytest pass / 4 skipped / 0 fail (full SDK suite) - Live probe (probe_full.py): 4 distinct operation_ids across 3 /check + 1 /execute - /execute fallback mint pattern unchanged (read contextvar first, mint only if None) - _GATE_CACHE invariant unaffected (cache key doesn't include op_id) - Backend IDEM-01 logic unchanged (compute_gate_semantic_hash unaffected)
…DKT-004 fix-wave-2) DEF-SDKT-004 fix-wave-2 (2026-09-13): backend split `From<JsonRejection> for ApiError` onto three distinct wire codes: - JsonDataError -> 422 + INVALID_FIELD (validation_error slug) - JsonSyntaxError -> 400 + INVALID_JSON (invalid_json slug) - MissingJsonContentType -> 415 + INVALID_INPUT (bad_request slug) The two NEW codes (INVALID_FIELD, INVALID_JSON) are emitted on /gate, /execute, and /track. Pre-fix the SDK's `_V3_ERROR_CODE_MAP` had no entries for them, so they fell through to the generic `NullRunBackendError` (transport.py:2961 fallback). Cookbook recipes that branch on `error_code` lost diagnostic class for parse-level vs schema-level rejections. Map both to `NullRunBackendError` -- siblings to EXECUTION_ID_MALFORMED, EXECUTION_ID_REQUIRED, INVALID_EXECUTION_ID, and IDEMPOTENCY_REDIS_UNAVAILABLE which already follow the same pattern for wire-shape parsing failures. This mirrors the backend's intent: wire-level parsing failures are infrastructure-side issues and the SDK round-trips them through the generic catch-all. The new wire codes are exercised in: - /gate POST body rejection (gate.rs) - /execute POST body rejection (execute.rs) - /track POST body rejection (handlers.rs) - TC-SDKG-006 (truncated JSON) expects 400 + INVALID_JSON NR-007a (new) at `backend/tests/nr007_sdk_error_code_parity.rs` pins the required SDK mappings and will fail CI on future drift (e.g., if someone reverts INVALID_JSON from this map). Tests: pytest passing (existing suite continues to green).
Patch release closing two SDK-side gaps on the 0.17.0 baseline: (1) `/check` mints a fresh `operation_id` per call instead of reusing the first call's op_id within the same scope — closing a silent collision with the backend's `IDEM-01` 11-field semantic-hash dedup that surfaced as a misleading `NR-B004` "usage limit" error whenever a second `/check` diverged in `tools` / `model` / `input`, and (2) `_V3_ERROR_CODE_MAP` now covers the two new wire codes from backend `fix-wave-2` (`INVALID_JSON` 400 + `INVALID_FIELD` 422) — closing a diagnostic-class gap where cookbook recipes lost the ability to branch on `error_code` for parse-level vs schema-level rejections. Both fixes are wire-format-compatible and SDK_MIN_VERSION-unchanged. Cookbook code that already handles `NullRunBackendError` sees no behaviour change. **Fixed** - **DEF-OPID-REUSE-HASH-MISMATCH** — `/check` mints a fresh `operation_id` per call instead of reusing the first call's op_id within the same scope (`src/nullrun/runtime.py`, `a05726e`, +38/-7). Closes the silent collision with backend `IDEM-01` (`compute_gate_semantic_hash` in `backend/src/redis/idempotency_store.rs:125-140` keys on op_id but verifies an 11-field semantic hash — `operation_id`, `tools`, `tool`, `mode`, `check_type`, `model`, `estimated_tokens`, `input`, `business_impact`, `workflow_id`, `organization_id` — so a second `/check` with a different `tools` / `model` / `input` on the same op_id 409s with `IDEMPOTENCY_KEY_MISMATCH` and surfaces to the SDK as the misleading `NR-B004` "You've reached the usage limit for this conversation"). `/execute` continues to read the freshly-stashed op_id within the same logical action so the **P0-27 within-action binding** (one op_id across `/check` + `/execute`) is preserved — pinned by the 8 P0-27 source-pin tests at `tests/test_audit_p0_27_operation_id_hoist.py`. - **DEF-SDKT-004 fix-wave-2** — `_V3_ERROR_CODE_MAP` now contains entries for `INVALID_JSON` (400, `invalid_json` slug, `JsonSyntaxError`) and `INVALID_FIELD` (422, `validation_error` slug, `JsonDataError`) (`src/nullrun/transport.py`, `f5aca80`, +24/-0). Backend `fix-wave-2` (2026-09-13) split `From<JsonRejection> for ApiError` onto three distinct wire codes (also adding `INVALID_INPUT` 415 for `MissingJsonContentType`, which already fell through to the legacy `BAD_REQUEST` arm). Pre-fix the SDK's map missed both new codes so they fell through to the generic `NullRunBackendError` fallback at `transport.py:2961`; cookbook recipes that branch on `error_code` lost diagnostic class. Map both to `NullRunBackendError` — the same exception class used for the legacy wire-shape parsing failures (`EXECUTION_ID_MALFORMED`, `EXECUTION_ID_REQUIRED`, `INVALID_EXECUTION_ID`, `IDEMPOTENCY_REDIS_UNAVAILABLE`), matching the backend's intent that wire-level parsing failures are infrastructure-side issues. **NR-007a** (new) at `backend/tests/nr007_sdk_error_code_parity.rs` pins the required SDK mappings and fails CI on future drift. **Verification** | Check | Result | |---|---| | `ruff check src tests` | All checks passed | | `mypy src/nullrun` | Success: no issues found in 37 source files | | `pytest -q` | **1807 passed, 4 skipped** in 108.84s (vs baseline 1807 at 0.17.0 — no new tests; both fixes fold into existing coverage) | | Scratch diff | clean (no `dist_local/`, no `*.defect*`) | | `nullrun.__version__` | `0.17.1` | | Wire-format compatibility | unchanged from 0.17.0 | **Commits included** - `a05726e` — `fix(gate): mint fresh operation_id per /check call (DEF-OPID-REUSE-HASH-MISMATCH)` (+38/-7 in `src/nullrun/runtime.py`) - `f5aca80` — `fix(sdk): add INVALID_JSON/INVALID_FIELD to _V3_ERROR_CODE_MAP (DEF-SDKT-004 fix-wave-2)` (+24/-0 in `src/nullrun/transport.py`) - version bump commit (folded into this release commit) — `chore: bump 0.17.1` (+27/-3 across `pyproject.toml`, `src/nullrun/__version__.py`, `CHANGELOG.md`, `uv.lock`)
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Patch release closing two SDK-side gaps on the 0.17.0 baseline: (1)
/checkmints a freshoperation_idper call instead of reusing the first call's op_id within the same scope — closing a silent collision with the backend'sIDEM-0111-field semantic-hash dedup that surfaced as a misleadingNR-B004"usage limit" error whenever a second/checkdiverged intools/model/input, and (2)_V3_ERROR_CODE_MAPnow covers the two new wire codes from backendfix-wave-2(INVALID_JSON400 +INVALID_FIELD422) — closing a diagnostic-class gap where cookbook recipes lost the ability to branch onerror_codefor parse-level vs schema-level rejections. Both fixes are wire-format-compatible and SDK_MIN_VERSION-unchanged. Cookbook code that already handlesNullRunBackendErrorsees no behaviour change.Fixed
DEF-OPID-REUSE-HASH-MISMATCH —
/checkmints a freshoperation_idper call instead of reusing the first call's op_id within the same scope (src/nullrun/runtime.py,a05726e, +38/-7). Closes the silent collision with backendIDEM-01(compute_gate_semantic_hashinbackend/src/redis/idempotency_store.rs:125-140keys on op_id but verifies an 11-field semantic hash —operation_id,tools,tool,mode,check_type,model,estimated_tokens,input,business_impact,workflow_id,organization_id— so a second/checkwith a differenttools/model/inputon the same op_id 409s withIDEMPOTENCY_KEY_MISMATCHand surfaces to the SDK as the misleadingNR-B004"You've reached the usage limit for this conversation")./executecontinues to read the freshly-stashed op_id within the same logical action so the P0-27 within-action binding (one op_id across/check+/execute) is preserved — pinned by the 8 P0-27 source-pin tests attests/test_audit_p0_27_operation_id_hoist.py.DEF-SDKT-004 fix-wave-2 —
_V3_ERROR_CODE_MAPnow contains entries forINVALID_JSON(400,invalid_jsonslug,JsonSyntaxError) andINVALID_FIELD(422,validation_errorslug,JsonDataError) (src/nullrun/transport.py,f5aca80, +24/-0). Backendfix-wave-2(2026-09-13) splitFrom<JsonRejection> for ApiErroronto three distinct wire codes (also addingINVALID_INPUT415 forMissingJsonContentType, which already fell through to the legacyBAD_REQUESTarm). Pre-fix the SDK's map missed both new codes so they fell through to the genericNullRunBackendErrorfallback attransport.py:2961; cookbook recipes that branch onerror_codelost diagnostic class. Map both toNullRunBackendError— the same exception class used for the legacy wire-shape parsing failures (EXECUTION_ID_MALFORMED,EXECUTION_ID_REQUIRED,INVALID_EXECUTION_ID,IDEMPOTENCY_REDIS_UNAVAILABLE), matching the backend's intent that wire-level parsing failures are infrastructure-side issues. NR-007a (new) atbackend/tests/nr007_sdk_error_code_parity.rspins the required SDK mappings and fails CI on future drift.Verification
ruff check src testsmypy src/nullrunpytest -qdist_local/, no*.defect*)nullrun.__version__0.17.1Commits included
a05726e—fix(gate): mint fresh operation_id per /check call (DEF-OPID-REUSE-HASH-MISMATCH)(+38/-7 insrc/nullrun/runtime.py)f5aca80—fix(sdk): add INVALID_JSON/INVALID_FIELD to _V3_ERROR_CODE_MAP (DEF-SDKT-004 fix-wave-2)(+24/-0 insrc/nullrun/transport.py)a622f88—chore(release): 0.17.1 — op_id mint-fresh + error-code map closure(+27/-3 acrosspyproject.toml,src/nullrun/__version__.py,CHANGELOG.md,uv.lock)The substantive-fix commits (
a05726e,f5aca80) were authored earlier and pre-existed on the localmasterahead oforigin/master; the release commit folds the version bump (pyproject.toml,src/nullrun/__version__.py), theCHANGELOG.mdexpansion, and theuv.lockstamp (0.17.0→0.17.1) into a single squash-merge target.