Skip to content

chore(release): 0.22.1 — public exception surface + WS sync callback + doc alignment - #117

Merged
maltsev-dev merged 1 commit into
masterfrom
release/0.22.1
Oct 15, 2026
Merged

maltsev-dev merged 1 commit into
masterfrom
release/0.22.1

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Patch release, no protocol change. The SDK's public exception surface is
restored after QA pass u3h (2026-10-15) reported 24 classes the SDK RAISES
were not importable from the top-level nullrun namespace. The classes
existed, _V3_ERROR_CODE_MAP constructed them, and enforcement was never
affected — only the documented public name was missing. The defect survived
because except NullRunError: kept matching the instance; a narrower
except NullRunApprovalDeniedError: only fails when someone goes looking
for the name.

The second behavioural fix closes a discarded-coroutine class of bug on
the WS dispatch path: on_policy_invalidated was wired as async def
while the dispatcher calls it without await, so the callback was created
and silently discarded. The logger.info and the user-supplied callback
never fired — only a stderr RuntimeWarning that nobody reads. Same bug
class as the approval-resolved fix from 2026-07-24, missed on the sibling
callback. The third change aligns stale docstrings with current behaviour
and pins the real kill-vs-on_error-hook asymmetry between sync and async
`@protect` so the comments cannot drift the way the old ones did.

Fixed

  • DEF-PUBEX-01 — nullrun.__init__._LAZY_EXPORTS now exposes 24
    exception classes the SDK RAISES (NullRunApprovalDeniedError,
    NullRunDeniedError, RateLimitError,
    NullRunUnclassifiedRefusalError, …). The same names added to
    __all__ so tab-completion and from nullrun import * reach them.
    Grouped by tier (root / markers, infrastructure, transport, decision,
    approval) so a reader can see the hierarchy without reading
    exceptions.py. Regression test
    tests/test_public_exception_surface.py reads the exception modules
    via AST and asserts both directions of the bug: every raisable class
    is reachable AND every exported class is raisable. (032eccb,
    +385/-4)
  • DEF-WS-SYNC-01 — Transport.connect_websocket wraps
    on_policy_invalidated as a plain def, not async def. The WS
    dispatch path calls it without await, and an un-awaited coroutine
    raises nothing the dispatcher's try/except could catch. The body
    never ran — only a stderr warning that nobody read. Same shape as the
    approval fix from 2026-07-24, missed on the sibling callback.
    wrapped_key_rotated stays async def (it awaits
    _refetch_credentials() and the dispatcher awaits it) — the test
    pins both halves. (564c39e, +170/-11)

Cleanup

  • C:UsersAnatolii (216 bytes — contents are a one-line fatal: ambiguous argument from a previous shell that mis-decoded an
    absolute Windows path and wrote the error output into the repo root)
    accidentally created in working tree and removed in 8e658f4.
    .gitignore extended with "/C:UsersAnatolii*" so the shape cannot
    recur; the broader class ("Windows path decoded without its
    separator") produces single-byte replacement-char filenames that
    don't pattern-match cleanly, so the entry is narrow on purpose.
    (+7)

Documentation

  • DEF-DRIFT-01 — Aligned `@protect` / on_error /
    check_workflow_budget / messages.NR-B007 docstring blocks with
    current behaviour. The BaseException explanation for the hook
    bypass was wrong (WorkflowKilledInterrupt -> NullRunError -> BreakerError -> Exception, verified); the real split is the
    unify_block flag in _protect_body. Sync `@protect`
    re-wraps as NullRunBlockedException (error code NR-W002), the
    hook fires; async `@protect` lets the raw
    WorkflowKilledInterrupt propagate, the hook does NOT fire.
    decision == "throttle" does not exist — the only producer is an
    uncalled phase-2 schema stub. Fail-OPEN is narrower than "any
    transport error": a 5xx WITH a refusal body is HONOURED
    (decision_source=GATEWAY, blocks); only unreachable /
    synthetic-source paths fail open. NR-B007 was removed 2026-09-10
    (it only ever fired on the never-sent throttle decision);
    WorkflowPausedException is NOT zombie and must not be lumped in
    with it. Regression test tests/test_guard_kill_semantics.py pins
    the real behaviour. (2de9a7e, +199/-25)

Verification

Check Result
ruff check src tests All checks passed
mypy src/nullrun Success: no issues found in 39 source files
pytest -q 1849 passed, 12 skipped in 124.76s (vs baseline 1828 / 12 — +21 new tests from test_public_exception_surface, test_policy_invalidated_ws_sync_callback, test_guard_kill_semantics)
Scratch diff clean (dist_local/, *.defect* absent)
nullrun.__version__ 0.22.1
Wire-format compatibility unchanged from 0.22.0

Commits included

8e658f4 chore: remove accidentally-created C:UsersAnatolii scratch artefact
032eccb fix(public-surface): expose all raisable exception classes from top-level (DEF-PUBEX-01)
564c39e fix(transport): make on_policy_invalidated a sync wrapper (DEF-WS-SYNC-01)
2de9a7e chore(docs): align decorator/messages/runtime docstrings with current behaviour (DEF-DRIFT-01)
3555e5a fix errors

…+ doc alignment

## Summary

Patch release, no protocol change. The SDK's public exception surface is
restored after QA pass u3h (2026-10-15) reported 24 classes the SDK RAISES
were not importable from the top-level `nullrun` namespace. The classes
existed, `_V3_ERROR_CODE_MAP` constructed them, and enforcement was never
affected — only the documented public name was missing. The defect survived
because `except NullRunError:` kept matching the instance; a narrower
`except NullRunApprovalDeniedError:` only fails when someone goes looking
for the name.

The second behavioural fix closes a discarded-coroutine class of bug on
the WS dispatch path: `on_policy_invalidated` was wired as `async def`
while the dispatcher calls it without `await`, so the callback was created
and silently discarded. The `logger.info` and the user-supplied callback
never fired — only a stderr `RuntimeWarning` that nobody reads. Same bug
class as the approval-resolved fix from 2026-07-24, missed on the sibling
callback.

The third change aligns stale docstrings with current behaviour: the
kill-vs-on_error-hook asymmetry between sync and async `@protect` was
documented as a `BaseException` bypass that never existed, and chasing the
truth exposed an actual behavioural split governed by `unify_block`, not
by base-class membership. A new regression test pins the real behaviour
so the comments cannot drift the way the old ones did.

### Fixed

- **DEF-PUBEX-01** — `nullrun.__init__._LAZY_EXPORTS` now exposes 24
  exception classes the SDK RAISES (`NullRunApprovalDeniedError`,
  `NullRunDeniedError`, `RateLimitError`,
  `NullRunUnclassifiedRefusalError`, …). The same names added to
  `__all__` so tab-completion and `from nullrun import *` reach them.
  Grouped by tier (root / markers, infrastructure, transport, decision,
  approval) so a reader can see the hierarchy without reading
  `exceptions.py`. Regression test
  `tests/test_public_exception_surface.py` reads the exception modules
  via AST and asserts both directions of the bug: every raisable class
  is reachable AND every exported class is raisable. (032eccb, +385/-4)

- **DEF-WS-SYNC-01** — `Transport.connect_websocket` wraps
  `on_policy_invalidated` as a plain `def`, not `async def`. The WS
  dispatch path calls it without `await`, and an un-awaited coroutine
  raises nothing the dispatcher's `try/except` could catch. The body
  never ran — only a stderr warning that nobody read. Same shape as the
  approval fix from 2026-07-24, missed on the sibling callback.
  `wrapped_key_rotated` stays `async def` (it awaits
  `_refetch_credentials()` and the dispatcher awaits it) — the test
  pins both halves. (564c39e, +170/-11)

### Documentation

- **DEF-DRIFT-01** — Aligned `@protect` / `on_error` /
  `check_workflow_budget` / `messages.NR-B007` docstring blocks with
  current behaviour. The `BaseException` explanation for the hook
  bypass was wrong (`WorkflowKilledInterrupt -> NullRunError ->
  BreakerError -> Exception`, verified); the real split is the
  `unify_block` flag in `_protect_body`. Sync `@protect` re-wraps as
  `NullRunBlockedException` (error code `NR-W002`), the hook fires;
  async `@protect` lets the raw `WorkflowKilledInterrupt` propagate,
  the hook does NOT fire. `decision == "throttle"` does not exist —
  the only producer is an uncalled phase-2 schema stub. Fail-OPEN is
  narrower than "any transport error": a 5xx WITH a refusal body is
  HONOURED (`decision_source=GATEWAY`, blocks); only unreachable /
  synthetic-source paths fail open. `NR-B007` was removed 2026-09-10
  (it only ever fired on the never-sent `throttle` decision);
  `WorkflowPausedException` is NOT zombie and must not be lumped in
  with it. Regression test `tests/test_guard_kill_semantics.py` pins
  the real behaviour. (2de9a7e, +199/-25)

### Cleanup

- Dropped `C:UsersAnatolii` (216 bytes — contents are a one-line
  `fatal: ambiguous argument` from a previous shell that mis-decoded
  an absolute Windows path and wrote the error output into the repo
  root). `.gitignore` extended with `"/C:UsersAnatolii*"` so the shape
  cannot recur; the broader class ("Windows path decoded without its
  separator") produces single-byte replacement-char filenames that
  don't pattern-match cleanly, so the entry is narrow on purpose.
  (8e658f4, +7)

### Verification

| Check | Result |
|---|---|
| `ruff check src tests` | All checks passed |
| `mypy src/nullrun` | Success: no issues found in 39 source files |
| `pytest -q` | **1849 passed, 12 skipped** in 124.76s (vs baseline 1828 / 12 — **+21 new tests** from `test_public_exception_surface`, `test_policy_invalidated_ws_sync_callback`, `test_guard_kill_semantics`) |
| Scratch diff | clean (`dist_local/`, `*.defect*` absent) |
| `nullrun.__version__` | `0.22.1` |
| Wire-format compatibility | unchanged from 0.22.0 |

### Commits included

```
8e658f4 chore: remove accidentally-created C:UsersAnatolii scratch artefact
032eccb fix(public-surface): expose all raisable exception classes from top-level (DEF-PUBEX-01)
564c39e fix(transport): make on_policy_invalidated a sync wrapper (DEF-WS-SYNC-01)
2de9a7e chore(docs): align decorator/messages/runtime docstrings with current behaviour (DEF-DRIFT-01)
3555e5a fix errors
```
@codecov

codecov Bot commented Oct 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 87cd7e8 into master Oct 15, 2026
6 checks passed
@maltsev-dev
maltsev-dev deleted the release/0.22.1 branch October 15, 2026 13:53
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