Skip to content

chore(release): 0.14.2 - three runtime/transport hotfixes#79

Merged
maltsev-dev merged 5 commits into
masterfrom
archive/cleanup-attempted-1c1e326
Jul 25, 2026
Merged

chore(release): 0.14.2 - three runtime/transport hotfixes#79
maltsev-dev merged 5 commits into
masterfrom
archive/cleanup-attempted-1c1e326

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

0.14.2 patch release

Three independent fixes that fell out of the 0.14.1 demo run, plus a stub refresh on the two _RecordingRuntime mocks whose shape the new @protect emit broke.

Fixed

  • @protect decorator now emits a tools/track_tool event - decorators.py:470 and decorators.py:521 (sync + async wrappers) call runtime.track_tool(fn.__name__, metadata={"arguments": _safe_kwargs(kwargs)}) after the wrapped body returns. Pre-0.14.2 the protected decorator only fired the gate check and skipped the bookkeeping emit, so the dashboard never saw a protect execution. The new emit goes through the same sink as llm_call events so it picks up the dedup LRU at runtime.track() for free.
  • track_tool event carries tokens: 0 and a fresh uuidv7 execution_id - runtime.py:3077 stamps both fields onto every tool_call event. The backend's SdkTrackRequest requires tokens: u64 (non-Optional) and a threadable execution_id; pre-0.14.2 the event dict only carried type / tool_name / is_retry and the deserializer rejected it. Span lifecycle events (span_start / span_end) get the same tokens: 0 default via runtime.py:2161.
  • Approval-resolved WS callback is now a plain sync function - transport.py:1757 wrapped_approval_resolved was previously declared async def to be awaitable, but the WebSocket dispatch path invokes it as a plain function (the dispatch signature is dict[str, Any] -> None, not awaitable). The async-decorated coroutine was silently dropped, so the sync threading.Event inside runtime._wait_for_approval_resolution never got set on the first approval round-trip - the demo's first approval hung forever. Caught 2026-07-24.
  • WebSocket cancellation is treated as a clean shutdown - runtime.py:1160 now catches asyncio.CancelledError before the generic except Exception block. WebSocketConnection.close() cancels the receive task to unblock this waiter during normal shutdown; on Python 3.11+ CancelledError derives from BaseException (not Exception), so the old code re-raised it and produced a noisy WS receive loop ended: <never logged> debug line on every clean shutdown. The new branch is silent and the path stays contained.

Tests

  • tests/test_approval_ws_sync_callback.py - 103 lines of new coverage for the WS approval-resolved dispatch path: the callback is invoked as a sync function, the threading.Event is set, the wait returns within the timeout, and the previous async-decorated shape is asserted-not-present.
  • tests/test_runtime_branches.py - 36 lines of new coverage for the await conn._receive_task cancellation path: CancelledError is no longer re-raised out of the block as a WS receive loop ended: ... debug line, and the finally cleanup still runs.
  • tests/test_protect.py and tests/test_preflight_fail_policy.py - _RecordingRuntime stubs refreshed with a track_tool mock. The previous shape only mocked track_event, which is why the new @protect emit silently failed under the new decorator wiring.

Compatibility

  • Backward-compatible bug fix. No SDK_MIN_VERSION bump. No public API change.
  • The new tokens: 0 / execution_id fields on track_tool events are forwarded exactly as minted; the backend's SdkTrackRequest already accepts them (the 0.14.0 envelope contract).
  • The approval-resolved callback is the same public contract (def on_approval_resolved(payload: dict) -> None); only the in-transport wrapper changed from async def to def.
  • The WS cancellation handler is silent in the same way the previous except Exception was silent; the only user-visible delta is a removed debug log line on clean shutdown.

Verification

  • pytest -n auto --cov=src/nullrun --cov-branch --cov-report=xml --cov-fail-under=0 -> 1369 passed, 7 skipped, 29 warnings in 36.77s, cov 81.49%.
  • ruff check src/ tests/ -> All checks passed.
  • mypy src/nullrun --strict -> Success: no issues found in 36 source files.

Out of scope

  • The format-only reformat of the 65 unrelated files flagged by ruff format --check is intentionally deferred to a separate PR. This release only formats the three source files touched by the fix series (decorators / runtime / transport).

Three independent fixes that fell out of the 0.14.1 demo run,
plus a stub refresh on the two _RecordingRuntime mocks whose
shape the new @Protect emit broke.

* fix(decorators): @Protect now emits a tools/track_tool event
  after the wrapped body returns. Pre-0.14.2 the protected
  decorator only fired the gate check and skipped the
  bookkeeping emit, so the dashboard never saw a protect
  execution. The emit goes through the same sink as llm_call
  events so it picks up the dedup LRU at runtime.track() for
  free.

* fix(runtime): track_tool event carries tokens: 0 and a fresh
  uuidv7 execution_id. The backend's SdkTrackRequest requires
  both fields as non-Optional u64 / string; pre-0.14.2 the
  event dict only carried type / tool_name / is_retry and the
  deserializer rejected it. Span lifecycle events get the same
  tokens: 0 default via _enrich_event.

* fix(transport): approval-resolved WS callback is now a plain
  sync function. The WebSocket dispatch path invokes it as a
  dict -> None callable; the previous async-decorated
  coroutine was silently dropped, so the sync threading.Event
  inside _wait_for_approval_resolution never got set on the
  first approval round-trip - the demo's first approval hung
  forever. Caught 2026-07-24.

* fix(runtime): treat websocket cancellation as a clean
  shutdown signal. WebSocketConnection.close() cancels the
  receive task to unblock the waiter during normal end of
  session; on Python 3.11+ CancelledError derives from
  BaseException, so the old except Exception branch re-raised
  it and produced a noisy debug line on every clean exit.
  The new except CancelledError branch is silent and the
  finally cleanup still runs.

* test: refresh _RecordingRuntime in tests/test_protect.py
  and tests/test_preflight_fail_policy.py with a track_tool
  stub. The previous shape only mocked track_event, which
  is why the @Protect emit silently failed under the new
  decorator wiring.

* chore: ruff format on the three source files touched by
  this release (decorators / runtime / transport). The
  format-only reformat of the 65 unrelated files is
  intentionally deferred to a separate PR.

* docs: 0.14.2 changelog entry describing the four fixes
  end-to-end.

No SDK_MIN_VERSION bump. No public API change. No on-wire
breaking change. Backends on 1.0.0 keep working unchanged.

Verification:
  - pytest -n auto -> 1369 passed, 7 skipped, 29 warnings.
  - ruff check src/ tests/ -> All checks passed.
  - mypy src/nullrun --strict -> Success: no issues found
    in 36 source files.
@maltsev-dev maltsev-dev added the bug Something isn't working label Jul 25, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.78788% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/nullrun/runtime.py 70.00% 6 Missing ⚠️
src/nullrun/transport.py 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit 14c673c into master Jul 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant