Skip to content

chore(release): 0.17.1 — op_id mint-fresh + error-code map closure - #103

Merged
maltsev-dev merged 3 commits into
masterfrom
release/0.17.1
Sep 15, 2026
Merged

maltsev-dev merged 3 commits into
masterfrom
release/0.17.1

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

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 117.11s (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

  • a05726efix(gate): mint fresh operation_id per /check call (DEF-OPID-REUSE-HASH-MISMATCH) (+38/-7 in src/nullrun/runtime.py)
  • f5aca80fix(sdk): add INVALID_JSON/INVALID_FIELD to _V3_ERROR_CODE_MAP (DEF-SDKT-004 fix-wave-2) (+24/-0 in src/nullrun/transport.py)
  • a622f88chore(release): 0.17.1 — op_id mint-fresh + error-code map closure (+27/-3 across pyproject.toml, src/nullrun/__version__.py, CHANGELOG.md, uv.lock)

The substantive-fix commits (a05726e, f5aca80) were authored earlier and pre-existed on the local master ahead of origin/master; the release commit folds the version bump (pyproject.toml, src/nullrun/__version__.py), the CHANGELOG.md expansion, and the uv.lock stamp (0.17.00.17.1) into a single squash-merge target.

…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

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit 78dacfc into master Sep 15, 2026
5 checks passed
@maltsev-dev
maltsev-dev deleted the release/0.17.1 branch September 15, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant