@@ -7,6 +7,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
77
88---
99
10+
1011## [ 0.9.1] - 2026-06-29
1112
1213Patch on top of 0.9.0. Unifies the LLM-call fingerprint scheme so the
@@ -63,6 +64,200 @@ httpx transport and the LangChain callback for the same real call.
6364No public-API break. No behavior change for callers whose
6465instrumentation already populates ` model ` correctly.
6566
67+ ## [ 0.11.0] - 2026-07-02
68+
69+ Wire-protocol v3 alignment with the backend's Sprint 6 v1 cut
70+ (CLAUDE.md v3.4). The previous SDK shipped pre-v3 endpoints
71+ (` /api/v1/gate ` , ` /api/v1/execute ` , ` /api/v1/track/batch ` ) without
72+ the ` X-NULLRUN-PROTOCOL ` header that the v3 backend requires as a
73+ fail-CLOSED pre-check — every signed POST was rejected with HTTP 400
74+ ` PROTOCOL_HEADER_REQUIRED ` . This release aligns the SDK with the v3
75+ wire contract and adds the missing soft-mode / chain / heartbeat /
76+ cancel / budget-estimate surface.
77+
78+ ### BREAKING (wire-contract)
79+
80+ - ** ` X-NULLRUN-PROTOCOL: 3 ` is now mandatory on every signed POST.**
81+ The backend's ` proxy/http/gate/protocol.rs ` middleware rejects
82+ requests without the header with HTTP 400 + error_code
83+ ` PROTOCOL_HEADER_REQUIRED ` BEFORE the gate pipeline runs. Pre-v3
84+ SDKs that don't send it will get 400 on every request, including
85+ ` /auth/verify ` (which is unsigned but goes through the same
86+ protocol guard via the ` _post_auth_with_retry ` path).
87+ - Routed through the new centralised helper in
88+ ` nullrun.transport._protocol_header_value() ` so a future bump
89+ is a one-line change.
90+ - The header is set in ` _build_signed_headers() ` (covers
91+ ` /gate ` , ` /execute ` , ` /track/batch ` , ` _refetch_credentials ` )
92+ AND inlined in the four call sites that build their own
93+ headers dict (track/batch, gate, execute, WS handshake,
94+ auth/verify refresh). The ` runtime._auth_headers() ` helper was
95+ extended to include the header for the three direct
96+ ` self._client.get/post ` call sites (` _post_auth_with_retry ` ,
97+ ` _fetch_remote_state ` , ` get_org_status ` ).
98+
99+ ### Added
100+
101+ - ** ` Transport.check_v3(request) ` — POST /api/v1/check.** The v3
102+ replacement for ` /gate ` . Adds three optional wire fields
103+ (CLAUDE.md §16):
104+ - ` chain_id ` (UUID v4) — pairs with ` chain_op ` for soft-mode
105+ budget enforcement (CLAUDE.md §5, §6).
106+ - ` chain_op ` (` "start" ` / ` "continue" ` / ` "end" ` / ` "auto" ` )
107+ — state-machine transitions; absent defaults to auto-register.
108+ - ` idempotency_key ` — replays return the original decision.
109+ - ` stream: bool ` — hints the backend whether streaming is
110+ expected (no wire-enforced behaviour change yet).
111+ - The response carries a server-minted ` execution_id ` (§24);
112+ callers MUST NOT treat the request's ` execution_id ` as
113+ authoritative.
114+
115+ - ** ` Transport.track_single(request) ` — POST /api/v1/track.**
116+ Single-event consume path with the CONSUME_SCRIPT invariant
117+ (` actual_cost <= reserved_cents + epsilon_cents ` , CLAUDE.md §25).
118+ Returns 422 CONSUME_OVERBUDGET when the call's actual cost
119+ exceeds the reservation by more than epsilon. The reservation is
120+ NOT silently re-reserved (ADR-005).
121+
122+ - ** ` Transport.cancel(execution_id, reason=None) ` — POST
123+ /api/v1/cancel.** Idempotent via ` cancel:{execution_id} ` SETNX
124+ (CLAUDE.md §23). Repeated calls return 200 OK without side
125+ effects. Surfaced as ` NullRunRuntime.cancel_execution() ` for the
126+ ergonomic wrapper.
127+
128+ - ** ` Transport.heartbeat(chain_id) ` — POST /api/v1/heartbeat.**
129+ Atomic ` EXPIRE chain:{org}:{chain_id} 300 ` with SETNX-based
130+ dedup via ` heartbeat:{chain_id}:{ts_floor_30s} ` (CLAUDE.md §26).
131+ Cadence: wall-clock 30s (configurable 10-120s). Skew tolerance
132+ ±5s.
133+
134+ - ** ` Transport.chain_end(chain_id) ` — POST /api/v1/chain/end.**
135+ Explicit chain close (CLAUDE.md §6). Idempotent — unknown
136+ chain_id is a no-op 200. Surfaced as
137+ ` NullRunRuntime.chain_end() ` .
138+
139+ - ** ` Transport.approximate_budget(organization_id=None) ` — GET
140+ /api/v1/budget/approximate.** UI-only budget estimation
141+ (CLAUDE.md §17). Returns 503 ` BUDGET_DATA_UNAVAILABLE ` when
142+ ALL sources fail — NEVER returns 0 (the dashboard must not
143+ display "≈ $0 spent" when data is missing). Surfaced as
144+ ` NullRunRuntime.approximate_budget() ` .
145+
146+ - ** ` Transport._parse_v3_error_envelope(response, endpoint) ` **
147+ — ACTIVE error envelope parser. Maps the backend's
148+ ` error_code ` field to typed SDK exception subclasses
149+ (PROTOCOL_TOO_OLD → ` NullRunProtocolError ` , CONSUME_OVERBUDGET
150+ → ` NullRunConsumeOverbudgetError ` , CHAIN_CROSS_ORG →
151+ ` NullRunChainError ` , WORKFLOW_INACTIVE →
152+ ` NullRunWorkflowInactiveError ` , etc.). Coexists with the
153+ frozen ` _parse_error_envelope ` from 0.6.0 — the frozen
154+ helper remains for the audit/contract test surface.
155+
156+ - ** Chain context (` nullrun.context ` ).** New contextvars
157+ ` _chain_id_var ` + ` _chain_op_var ` plus the public API:
158+ - ` chain(chain_id, op="start") ` — contextmanager (mirrors
159+ ` workflow() ` ).
160+ - ` get_chain_id() ` / ` set_chain_id() ` — manual setters.
161+ - ` get_chain_op() ` / ` set_chain_op() ` — chain-op enum setter.
162+ - Reachable from the top-level ` nullrun ` namespace via
163+ ` _LAZY_EXPORTS ` (consistent with ` workflow ` /
164+ ` set_call_context ` ).
165+
166+ - ** ` NullRunRuntime.ping_chain(chain_id, interval=30.0) ` —
167+ time-based heartbeat scheduler (CLAUDE.md §26).** Returns a
168+ ` stop() ` callable. The daemon thread emits POST /heartbeat on
169+ a wall-clock schedule (` time.monotonic ` ), not on chunk-count.
170+ Pre-fix chunk-based heuristic (every 50 chunks) had two
171+ pathological cases — slow chunk rates left chains idle,
172+ bursty traffic wasted heartbeat budget on a fresh chain.
173+ Cadence clamped to the 10-120s policy range per §26.
174+
175+ - ** ` NullRunRuntime.cancel_execution(execution_id, reason=None) `
176+ + ` chain_end(chain_id) ` + ` approximate_budget() ` ** — ergonomic
177+ wrappers around the new ` Transport ` methods.
178+
179+ ### Added (exceptions)
180+
181+ - ` NullRunProtocolError ` (NR-P001) — PROTOCOL_TOO_OLD /
182+ PROTOCOL_TOO_NEW.
183+ - ` NullRunChainError ` (NR-CH001) — CHAIN_MAX_DURATION_EXCEEDED /
184+ CHAIN_CROSS_ORG / CHAIN_ORG_MISMATCH / CHAIN_NOT_FOUND /
185+ CHAIN_EXPIRED. Carries ` chain_id ` and ` backend_code ` for
186+ diagnostic clarity.
187+ - ` NullRunConsumeOverbudgetError ` (NR-O001) — CONSUME_OVERBUDGET.
188+ Carries ` reserved_cents ` , ` max_allowed_cents ` , ` actual_cost_cents ` ,
189+ ` epsilon_cents ` so callers can reconcile manually without
190+ re-parsing the message string.
191+ - ` NullRunWorkflowInactiveError ` (NR-W004) — WORKFLOW_INACTIVE
192+ (CLAUDE.md §4 fail-CLOSED on soft-deleted workflow + active key,
193+ wired in Sprint 6 v1 12.2).
194+ - ` NullRunRateLimitRedisError ` (NR-R002) —
195+ RATE_LIMIT_REDIS_UNAVAILABLE. Fail-CLOSED per §4 enforcement
196+ table (aggregate rate limit = authoritative gate).
197+
198+ All five are subclasses of either ` NullRunInfrastructureError `
199+ (protocol / rate-limit-redis) or ` NullRunDecision ` (chain /
200+ overbudget / workflow-inactive) so existing `except
201+ NullRunError:` clauses keep matching.
202+
203+ ### Changed
204+
205+ - ** ` check_workflow_budget() ` forwards chain context.** When the
206+ caller has wrapped the gate in ` with chain(chain_id, op="start") ` ,
207+ the SDK now includes ` chain_id ` + ` chain_op ` + ` idempotency_key `
208+ in the /gate (or /check) payload so the backend's Lua
209+ RESERVE_SCRIPT can run the soft-mode branch (CLAUDE.md §5).
210+ Absent chain context, behaviour is identical to 0.10.0 (single-
211+ shot Hard). Wire-shape is additive — legacy callers see no
212+ payload change.
213+ - ** ` Transport.check() ` (legacy /gate) forwards chain_id /
214+ chain_op / idempotency_key / stream when present.** Same
215+ additive contract — missing keys are omitted, not nulled.
216+ - ** ` _auth_headers() ` includes ` X-NULLRUN-PROTOCOL ` .** Affects
217+ ` _post_auth_with_retry ` , ` _fetch_remote_state ` , ` get_org_status ` .
218+ - ** ` runtime._post_auth_with_retry ` now passes headers.** Pre-fix
219+ the helper did ` self._client.post(url, json=json_body) ` with no
220+ headers — the wire had no ` X-API-Key ` , no Authorization, and no
221+ protocol header, which the backend's protocol + CSRF middlewares
222+ reject. Now it passes ` self._auth_headers() ` .
223+
224+ ### Backwards compatibility
225+
226+ - All five new ` Transport ` methods are additive. Existing
227+ ` check() ` / ` execute() ` / batch ` _send_batch_with_retry_info `
228+ paths keep their previous signatures.
229+ - The five new exception classes are subclasses of the existing
230+ public hierarchy (` NullRunError ` ← ` NullRunDecision ` /
231+ ` NullRunInfrastructureError ` ); existing ` except NullRunError: `
232+ clauses keep matching.
233+ - The wire-protocol header is mandatory ONLY when connecting to
234+ a v3-or-later backend. Older pre-v3 backends ignore the header
235+ — no payload-level break.
236+
237+ ### Notes
238+
239+ - The v3 ` gate_reserve_v3 ` Lua script (CLAUDE.md §33) is on
240+ blue-green deployment per §19 — the SDK must work against
241+ BOTH the legacy ` cost/reservation.rs::reserve_budget_atomic `
242+ (v1/v2 default) AND the v3 Lua path. The new ` check_v3 ` /
243+ ` track_single ` helpers are the v3 path; the legacy ` check ` /
244+ batch ` track ` continue to hit the v1/v2 default. Operators
245+ flip the backend flag ` NULLRUN_RESERVE_V3_ENABLED=1 ` to
246+ migrate; SDKs on 0.11.0 work in both modes.
247+ - Soft-mode budget enforcement requires the backend's
248+ ` NULLRUN_SOFT_LIMIT_ENABLED=1 ` flag (CLAUDE.md §0 G3). Without
249+ it, chain_id is forwarded but the backend still treats soft
250+ passes as hard blocks. This is the controlled migration
251+ state noted in §0.
252+
253+ ---
254+
255+ ## [ 0.10.0] - 2026-06-29
256+
257+ (Unreleased — work-in-progress; will be backfilled once 0.11.0
258+ ships.)
259+
260+
66261---
67262
68263## [ 0.9.0] - 2026-06-29
0 commit comments