Skip to content

chore(ci): fix publish.yml (correct project name, trusted-publishing only) - #1

Merged
maltsev-dev merged 1 commit into
masterfrom
chore/fix-publish-yml
Jun 18, 2026
Merged

chore(ci): fix publish.yml (correct project name, trusted-publishing only)#1
maltsev-dev merged 1 commit into
masterfrom
chore/fix-publish-yml

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

What

  • Project name in the `pypi` environment URL was `nullrun-sdk`; the actual
    PyPI project is `nullrun` (per `pyproject.toml` `name = "nullrun"`).
  • Stale comment referenced the old `maltsev-dev/nullrun-sdk` repo; updated
    to point at `nullrunio/nullrun-sdk-python`.
  • Removed the dead "Variant 2: API token" block — we use Trusted Publishing
    only, no API token should ever be set as a secret.
  • Added a `workflow_dispatch:` trigger for manual emergency re-publishes.

Why

The bootstrap workflow was written when this lived in
`maltsev-dev/nullrun-sdk`. After the repo move to `nullrunio/nullrun-sdk-python`
the project name mismatch means `gh-action-pypi-publish` would publish under
the wrong project URL on the `pypi` environment page, and the stale comment
would mislead the next reader.

Test plan

  • Approve and merge this PR.
  • On pypi.org → Project → Publishing → Add a new pending publisher:
    • Owner: `nullrunio`
    • Repository: `nullrun-sdk-python`
    • Workflow filename: `publish.yml`
    • Environment name: `pypi`
  • Tag a release (`git tag v0.3.0 && git push --tags`) and verify the
    publish job runs end-to-end without errors.

@maltsev-dev
maltsev-dev merged commit da9372f into master Jun 18, 2026
0 of 4 checks passed
@maltsev-dev
maltsev-dev deleted the chore/fix-publish-yml branch June 18, 2026 15:05
maltsev-dev added a commit that referenced this pull request Jul 8, 2026
…ace respx

PR #60 landed the cancellable-sleep fix in Transport._flush_loop and
expected CI wall-clock to drop to 3-5 minutes. The first green run
on PR #60 (PR #60 run #1) actually took 9m 47s — the test step
dominated by a retry storm:

  Request failed (attempt 5/11), retrying in 8.46s: ConnectError
  Request failed (attempt 6/11), retrying in 9.16s: ConnectError
  ...
  Circuit breaker OPEN. Batch of 10 events will be re-queued.

Root cause: `tests/conftest.py:reset_runtime` teardown nulled the
runtime reference WITHOUT calling `runtime.shutdown()`. The
transport flush thread therefore kept running across tests, the
buffer drained through httpx with no respx context active, and the
xdist workers spent the next 9 minutes retry-sending the buffer
against the real (unreachable in CI) backend. `_retry_with_backoff
(max_retries=10, max_delay=10s)` is 65s of pure sleep per failed
batch, and with 4 xdist workers and many buffered batches this
multiplied into 9m 47s — i.e. a CI-noise fix that hid a deeper
lifecycle bug.

Pre-fix CI was already paying this cost (5s shutdown-sleep × 200+
tests ≈ 17 min of teardown per Python leg); the retry storm was
always there but masked by the dominant 5s cost. PR #60's 5s fix
exposed it.

Fix: add `flush: bool = True` to both `Transport.stop()` and
`NullRunRuntime.shutdown()`. When False, the transport thread is
cancelled WITHOUT a final `_do_flush()` / `_persist_to_wal()`.
`tests/conftest.py:reset_runtime` teardown now calls
`inst.shutdown(flush=False)` before nilling the reference. This
makes the conftest teardown a true no-op for the buffer — the test
that wrote the events is responsible for asserting on what it
cared about. The production default (`flush=True`) is preserved,
so the `nullrun.shutdown()` audit contract ("drain in-flight
events") is unchanged.

Pins:

  * `tests/test_transport.py::test_stop_flush_false_skips_final_flush
    ` — buffers an event, calls `stop(flush=False)` with no
    respx active, asserts the call returns in <1s AND the buffer
    is left untouched. Pre-fix this would have hung for 65s+ on
    the first retry.

  * `tests/test_init_contract.py::TestShutdownFlushKwarg::
    test_runtime_shutdown_flush_false_skips_final_flush` — same
    contract at the `NullRunRuntime` level: `shutdown(flush=False
    )` propagates the `flush=False` flag to
    `Transport.stop()`.

Public API additions:

  * `Transport.stop(timeout=10.0, flush: bool = True)` — `flush
    =False` is the new flag.
  * `NullRunRuntime.shutdown(flush: bool = True)` — propagates.
  * `nullrun.shutdown(timeout=2.0, flush: bool = True)` — passes
    `flush` through to the runtime.

No on-wire or production behaviour change. CI step is expected to
drop from ~9m 47s (PR #60 run #1) to ~30-60s on the next run.
maltsev-dev added a commit that referenced this pull request Jul 8, 2026
* perf(ci): cancel flush-thread sleep so shutdown() returns in ms, not 5s

The Transport flush loop used `time.sleep(self.config.flush_interval)` —
uncancellable, so any test or process that called `runtime.shutdown()`
while the thread was mid-sleep blocked on `thread.join()` for the full
default 5s flush_interval. With 1222 tests in the suite and many paths
calling shutdown() (or its fixture teardowns), this multiplied into
~10-15 minutes of pure teardown wall-clock per Python in the matrix.

Replace the bare sleep with `Event.wait`, which returns the instant
`stop()` sets the event. `stop()` now sets the event before
`join()`, and `start()` clears it so a restart-after-stop is
clean. Pin contract in tests/test_transport.py::

    test_stop_interrupts_flush_sleep

…uses a 30s flush_interval; pre-fix this took 30s, post-fix <5s.

CI hygiene in the same commit so the suite can actually use the freed
time:

- ci.yml / publish*.yml: enable pip cache (`cache: pip` +
  `cache-dependency-path: pyproject.toml`) — saves ~60-90s of cold
  install per matrix leg.
- ci.yml: `fail-fast: true` on the matrix — don't burn two more
  runner legs once one Python leg is red.
- ci.yml / coverage / publish*.yml: install `pytest-xdist>=3.6` and
  pass `-n auto` to pytest. `pytest-xdist` is also added to
  `[project.optional-dependencies.dev]` so a local
  `pip install -e .[dev]` brings it in.
- pyproject.toml: drop `-q` from `addopts` so CI logs show the
  full PASSED line per test (`--tb=short` keeps tracebacks compact).
  `-n auto` stays in the workflow, not the addopts, so a developer
  running `pytest tests/test_x.py` gets a single process.

No public API change. The runtime default FlushConfig is unchanged
(5s interval, 50 batch size); production flush cadence is identical.
The fix only shortens the worst-case shutdown latency.

* remove redundant docs

* chore(release): bump version 0.13.4 -> 0.13.5

Pairs with the preceding release/0.13.5 commits:

  * perf(ci): cancel flush-thread sleep (transport.py:816)
  * remove redundant docs (drift.md, sdk-v3-migration-gaps.md)

Wire format unchanged; pure version bump + changelog entry
covering both the perf fix and the CI hygiene so the SDK_MIN_VERSION
floor is up to date.

No on-wire breaking change; backends on 1.0.0 keep working
unchanged. Recommended upgrade path: 0.13.4 -> 0.13.5.

* fix(tests): stop transport flush thread between tests so it doesn't race respx

PR #60 landed the cancellable-sleep fix in Transport._flush_loop and
expected CI wall-clock to drop to 3-5 minutes. The first green run
on PR #60 (PR #60 run #1) actually took 9m 47s — the test step
dominated by a retry storm:

  Request failed (attempt 5/11), retrying in 8.46s: ConnectError
  Request failed (attempt 6/11), retrying in 9.16s: ConnectError
  ...
  Circuit breaker OPEN. Batch of 10 events will be re-queued.

Root cause: `tests/conftest.py:reset_runtime` teardown nulled the
runtime reference WITHOUT calling `runtime.shutdown()`. The
transport flush thread therefore kept running across tests, the
buffer drained through httpx with no respx context active, and the
xdist workers spent the next 9 minutes retry-sending the buffer
against the real (unreachable in CI) backend. `_retry_with_backoff
(max_retries=10, max_delay=10s)` is 65s of pure sleep per failed
batch, and with 4 xdist workers and many buffered batches this
multiplied into 9m 47s — i.e. a CI-noise fix that hid a deeper
lifecycle bug.

Pre-fix CI was already paying this cost (5s shutdown-sleep × 200+
tests ≈ 17 min of teardown per Python leg); the retry storm was
always there but masked by the dominant 5s cost. PR #60's 5s fix
exposed it.

Fix: add `flush: bool = True` to both `Transport.stop()` and
`NullRunRuntime.shutdown()`. When False, the transport thread is
cancelled WITHOUT a final `_do_flush()` / `_persist_to_wal()`.
`tests/conftest.py:reset_runtime` teardown now calls
`inst.shutdown(flush=False)` before nilling the reference. This
makes the conftest teardown a true no-op for the buffer — the test
that wrote the events is responsible for asserting on what it
cared about. The production default (`flush=True`) is preserved,
so the `nullrun.shutdown()` audit contract ("drain in-flight
events") is unchanged.

Pins:

  * `tests/test_transport.py::test_stop_flush_false_skips_final_flush
    ` — buffers an event, calls `stop(flush=False)` with no
    respx active, asserts the call returns in <1s AND the buffer
    is left untouched. Pre-fix this would have hung for 65s+ on
    the first retry.

  * `tests/test_init_contract.py::TestShutdownFlushKwarg::
    test_runtime_shutdown_flush_false_skips_final_flush` — same
    contract at the `NullRunRuntime` level: `shutdown(flush=False
    )` propagates the `flush=False` flag to
    `Transport.stop()`.

Public API additions:

  * `Transport.stop(timeout=10.0, flush: bool = True)` — `flush
    =False` is the new flag.
  * `NullRunRuntime.shutdown(flush: bool = True)` — propagates.
  * `nullrun.shutdown(timeout=2.0, flush: bool = True)` — passes
    `flush` through to the runtime.

No on-wire or production behaviour change. CI step is expected to
drop from ~9m 47s (PR #60 run #1) to ~30-60s on the next run.
maltsev-dev added a commit that referenced this pull request Jul 11, 2026
#61)

* perf(ci): cancel flush-thread sleep so shutdown() returns in ms, not 5s

The Transport flush loop used `time.sleep(self.config.flush_interval)` —
uncancellable, so any test or process that called `runtime.shutdown()`
while the thread was mid-sleep blocked on `thread.join()` for the full
default 5s flush_interval. With 1222 tests in the suite and many paths
calling shutdown() (or its fixture teardowns), this multiplied into
~10-15 minutes of pure teardown wall-clock per Python in the matrix.

Replace the bare sleep with `Event.wait`, which returns the instant
`stop()` sets the event. `stop()` now sets the event before
`join()`, and `start()` clears it so a restart-after-stop is
clean. Pin contract in tests/test_transport.py::

    test_stop_interrupts_flush_sleep

…uses a 30s flush_interval; pre-fix this took 30s, post-fix <5s.

CI hygiene in the same commit so the suite can actually use the freed
time:

- ci.yml / publish*.yml: enable pip cache (`cache: pip` +
  `cache-dependency-path: pyproject.toml`) — saves ~60-90s of cold
  install per matrix leg.
- ci.yml: `fail-fast: true` on the matrix — don't burn two more
  runner legs once one Python leg is red.
- ci.yml / coverage / publish*.yml: install `pytest-xdist>=3.6` and
  pass `-n auto` to pytest. `pytest-xdist` is also added to
  `[project.optional-dependencies.dev]` so a local
  `pip install -e .[dev]` brings it in.
- pyproject.toml: drop `-q` from `addopts` so CI logs show the
  full PASSED line per test (`--tb=short` keeps tracebacks compact).
  `-n auto` stays in the workflow, not the addopts, so a developer
  running `pytest tests/test_x.py` gets a single process.

No public API change. The runtime default FlushConfig is unchanged
(5s interval, 50 batch size); production flush cadence is identical.
The fix only shortens the worst-case shutdown latency.

* remove redundant docs

* chore(release): bump version 0.13.4 -> 0.13.5

Pairs with the preceding release/0.13.5 commits:

  * perf(ci): cancel flush-thread sleep (transport.py:816)
  * remove redundant docs (drift.md, sdk-v3-migration-gaps.md)

Wire format unchanged; pure version bump + changelog entry
covering both the perf fix and the CI hygiene so the SDK_MIN_VERSION
floor is up to date.

No on-wire breaking change; backends on 1.0.0 keep working
unchanged. Recommended upgrade path: 0.13.4 -> 0.13.5.

* fix(tests): stop transport flush thread between tests so it doesn't race respx

PR #60 landed the cancellable-sleep fix in Transport._flush_loop and
expected CI wall-clock to drop to 3-5 minutes. The first green run
on PR #60 (PR #60 run #1) actually took 9m 47s — the test step
dominated by a retry storm:

  Request failed (attempt 5/11), retrying in 8.46s: ConnectError
  Request failed (attempt 6/11), retrying in 9.16s: ConnectError
  ...
  Circuit breaker OPEN. Batch of 10 events will be re-queued.

Root cause: `tests/conftest.py:reset_runtime` teardown nulled the
runtime reference WITHOUT calling `runtime.shutdown()`. The
transport flush thread therefore kept running across tests, the
buffer drained through httpx with no respx context active, and the
xdist workers spent the next 9 minutes retry-sending the buffer
against the real (unreachable in CI) backend. `_retry_with_backoff
(max_retries=10, max_delay=10s)` is 65s of pure sleep per failed
batch, and with 4 xdist workers and many buffered batches this
multiplied into 9m 47s — i.e. a CI-noise fix that hid a deeper
lifecycle bug.

Pre-fix CI was already paying this cost (5s shutdown-sleep × 200+
tests ≈ 17 min of teardown per Python leg); the retry storm was
always there but masked by the dominant 5s cost. PR #60's 5s fix
exposed it.

Fix: add `flush: bool = True` to both `Transport.stop()` and
`NullRunRuntime.shutdown()`. When False, the transport thread is
cancelled WITHOUT a final `_do_flush()` / `_persist_to_wal()`.
`tests/conftest.py:reset_runtime` teardown now calls
`inst.shutdown(flush=False)` before nilling the reference. This
makes the conftest teardown a true no-op for the buffer — the test
that wrote the events is responsible for asserting on what it
cared about. The production default (`flush=True`) is preserved,
so the `nullrun.shutdown()` audit contract ("drain in-flight
events") is unchanged.

Pins:

  * `tests/test_transport.py::test_stop_flush_false_skips_final_flush
    ` — buffers an event, calls `stop(flush=False)` with no
    respx active, asserts the call returns in <1s AND the buffer
    is left untouched. Pre-fix this would have hung for 65s+ on
    the first retry.

  * `tests/test_init_contract.py::TestShutdownFlushKwarg::
    test_runtime_shutdown_flush_false_skips_final_flush` — same
    contract at the `NullRunRuntime` level: `shutdown(flush=False
    )` propagates the `flush=False` flag to
    `Transport.stop()`.

Public API additions:

  * `Transport.stop(timeout=10.0, flush: bool = True)` — `flush
    =False` is the new flag.
  * `NullRunRuntime.shutdown(flush: bool = True)` — propagates.
  * `nullrun.shutdown(timeout=2.0, flush: bool = True)` — passes
    `flush` through to the runtime.

No on-wire or production behaviour change. CI step is expected to
drop from ~9m 47s (PR #60 run #1) to ~30-60s on the next run.

* fix(langgraph): attach LLM spans to parent chain via callback run_id

Sprint 2026-07-12 (multi-agent span attachment). Previously
on_llm_end called runtime.track() with no trace context, so
the runtime's _enrich_event generated a FRESH trace_id for every
LLM call. The downstream effect on multi-agent / reflection
flows was 4/5 empty rows in the workflow detail 'Recent
executions' panel:

  https://nullrun.io/control-center/workflows/<id>

  ┌────────────────────────────────────────────────┐
  │  1cf7f505-…  trace: 1cf7  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  c4be95fe-…  trace: c4be  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  9295df0f-…  trace: 9295  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  019f5060-…  trace: 019f  cost: $0.00013 ✓      │  ← cost_events orphan, by luck
  └────────────────────────────────────────────────┘

The cost_summary LEFT JOIN in db/mod.rs::get_execution_records_*
keyed on cs.join_kind='trace_id' AND cs.join_id=u.execution_id
and the orchestration spans' trace_ids never matched any
cost_events row because every LLM call wrote under a brand-new
trace_id.

Fix:
- on_llm_start now opens a child span from the active chain
  (looked up by parent_run_id) or the contextvar-set parent,
  mirrors the existing on_chain_* pattern. Stores the
  SpanContext under the LangChain run_id key.
- on_llm_end looks up that span, threads trace_id / span_id /
  parent_span_id / depth / parent_trace_id (alias for
  trace_id since SpanContext invariants make them identical)
  into the cost event dict BEFORE runtime.track(). _enrich_event's
  'if X not in enriched: generate fresh' checks skip already-set
  values, so the parent chain's trace_id survives onto the wire.
- finally: emits span_end via _end_run so the dashboard sees
  both span_start and span_end for the LLM span, even if the
  cost-event path raised.

Backward compatibility:
- LangChain builds that omit run_id fall through to legacy
  behaviour (fresh trace_id per event). Tested by
  test_on_llm_without_run_id_is_silent_no_op.
- Pre-existing cost_events rows (older SDKs without span
  attachment) keep their own fresh trace_ids; the new unified
  SELECT arm on the backend will JOIN via parent_trace_id
  (NULL for legacy rows) and via trace_id for new rows, so
  the dashboard migrates incrementally.

Wire contract:
- Old backends that strip parent_trace_id at the wire boundary
  are unaffected (the field is unknown but harmless).
- New backends write it to cost_events.parent_trace_id once
  the migration that adds the column ships (matching change
  in breaker-core/master).

Tests (test_langgraph_callback.py):
- test_on_llm_start_then_end_attaches_parent_chain_trace_id:
  - chain span root depth=0 (parent_run_id chain-1)
  - LLM span child depth>=1, span_kind=llm, parent_span_id
    matches chain span_id
  - cost event trace_id == chain trace_id (the contract)
  - parent_trace_id on cost event == chain trace_id (alias)
  - span_start + span_end both fire around the cost event
- test_on_llm_without_run_id_is_silent_no_op: legacy LangChain
  path doesn't crash, no spans opened, cost event fallback
- test_on_llm_end_emits_span_end_even_if_track_raises: finally
  block guarantees cleanup on backend errors

42/42 langgraph tests pass after the change (was 39 before).

* chore(release): 0.13.6 — multi-agent span attachment (parent_trace_id)

Bump __version__ to 0.13.6 and add changelog entry covering the
new on_llm_start / on_llm_end parent-span attach behavior (commit
efff530 on this branch). No public API change.

Wire format: backward-compatible. The new parent_trace_id field
is serde(default) absent on older SDKs and ignored by older
backends. Operators upgrading from 0.13.5 must upgrade both
sides together (SDK to 0.13.6 + backend with migration 217);
the SDK alone still works on 1.0.0 backends.

Recommended upgrade path: 0.13.5 -> 0.13.6.
SDK_MIN_VERSION_FOR_V3 unchanged (0.12.0).
maltsev-dev added a commit that referenced this pull request Aug 7, 2026
* perf(ci): cancel flush-thread sleep so shutdown() returns in ms, not 5s

The Transport flush loop used `time.sleep(self.config.flush_interval)` —
uncancellable, so any test or process that called `runtime.shutdown()`
while the thread was mid-sleep blocked on `thread.join()` for the full
default 5s flush_interval. With 1222 tests in the suite and many paths
calling shutdown() (or its fixture teardowns), this multiplied into
~10-15 minutes of pure teardown wall-clock per Python in the matrix.

Replace the bare sleep with `Event.wait`, which returns the instant
`stop()` sets the event. `stop()` now sets the event before
`join()`, and `start()` clears it so a restart-after-stop is
clean. Pin contract in tests/test_transport.py::

    test_stop_interrupts_flush_sleep

…uses a 30s flush_interval; pre-fix this took 30s, post-fix <5s.

CI hygiene in the same commit so the suite can actually use the freed
time:

- ci.yml / publish*.yml: enable pip cache (`cache: pip` +
  `cache-dependency-path: pyproject.toml`) — saves ~60-90s of cold
  install per matrix leg.
- ci.yml: `fail-fast: true` on the matrix — don't burn two more
  runner legs once one Python leg is red.
- ci.yml / coverage / publish*.yml: install `pytest-xdist>=3.6` and
  pass `-n auto` to pytest. `pytest-xdist` is also added to
  `[project.optional-dependencies.dev]` so a local
  `pip install -e .[dev]` brings it in.
- pyproject.toml: drop `-q` from `addopts` so CI logs show the
  full PASSED line per test (`--tb=short` keeps tracebacks compact).
  `-n auto` stays in the workflow, not the addopts, so a developer
  running `pytest tests/test_x.py` gets a single process.

No public API change. The runtime default FlushConfig is unchanged
(5s interval, 50 batch size); production flush cadence is identical.
The fix only shortens the worst-case shutdown latency.

* remove redundant docs

* chore(release): bump version 0.13.4 -> 0.13.5

Pairs with the preceding release/0.13.5 commits:

  * perf(ci): cancel flush-thread sleep (transport.py:816)
  * remove redundant docs (drift.md, sdk-v3-migration-gaps.md)

Wire format unchanged; pure version bump + changelog entry
covering both the perf fix and the CI hygiene so the SDK_MIN_VERSION
floor is up to date.

No on-wire breaking change; backends on 1.0.0 keep working
unchanged. Recommended upgrade path: 0.13.4 -> 0.13.5.

* fix(tests): stop transport flush thread between tests so it doesn't race respx

PR #60 landed the cancellable-sleep fix in Transport._flush_loop and
expected CI wall-clock to drop to 3-5 minutes. The first green run
on PR #60 (PR #60 run #1) actually took 9m 47s — the test step
dominated by a retry storm:

  Request failed (attempt 5/11), retrying in 8.46s: ConnectError
  Request failed (attempt 6/11), retrying in 9.16s: ConnectError
  ...
  Circuit breaker OPEN. Batch of 10 events will be re-queued.

Root cause: `tests/conftest.py:reset_runtime` teardown nulled the
runtime reference WITHOUT calling `runtime.shutdown()`. The
transport flush thread therefore kept running across tests, the
buffer drained through httpx with no respx context active, and the
xdist workers spent the next 9 minutes retry-sending the buffer
against the real (unreachable in CI) backend. `_retry_with_backoff
(max_retries=10, max_delay=10s)` is 65s of pure sleep per failed
batch, and with 4 xdist workers and many buffered batches this
multiplied into 9m 47s — i.e. a CI-noise fix that hid a deeper
lifecycle bug.

Pre-fix CI was already paying this cost (5s shutdown-sleep × 200+
tests ≈ 17 min of teardown per Python leg); the retry storm was
always there but masked by the dominant 5s cost. PR #60's 5s fix
exposed it.

Fix: add `flush: bool = True` to both `Transport.stop()` and
`NullRunRuntime.shutdown()`. When False, the transport thread is
cancelled WITHOUT a final `_do_flush()` / `_persist_to_wal()`.
`tests/conftest.py:reset_runtime` teardown now calls
`inst.shutdown(flush=False)` before nilling the reference. This
makes the conftest teardown a true no-op for the buffer — the test
that wrote the events is responsible for asserting on what it
cared about. The production default (`flush=True`) is preserved,
so the `nullrun.shutdown()` audit contract ("drain in-flight
events") is unchanged.

Pins:

  * `tests/test_transport.py::test_stop_flush_false_skips_final_flush
    ` — buffers an event, calls `stop(flush=False)` with no
    respx active, asserts the call returns in <1s AND the buffer
    is left untouched. Pre-fix this would have hung for 65s+ on
    the first retry.

  * `tests/test_init_contract.py::TestShutdownFlushKwarg::
    test_runtime_shutdown_flush_false_skips_final_flush` — same
    contract at the `NullRunRuntime` level: `shutdown(flush=False
    )` propagates the `flush=False` flag to
    `Transport.stop()`.

Public API additions:

  * `Transport.stop(timeout=10.0, flush: bool = True)` — `flush
    =False` is the new flag.
  * `NullRunRuntime.shutdown(flush: bool = True)` — propagates.
  * `nullrun.shutdown(timeout=2.0, flush: bool = True)` — passes
    `flush` through to the runtime.

No on-wire or production behaviour change. CI step is expected to
drop from ~9m 47s (PR #60 run #1) to ~30-60s on the next run.
maltsev-dev added a commit that referenced this pull request Aug 7, 2026
#61)

* perf(ci): cancel flush-thread sleep so shutdown() returns in ms, not 5s

The Transport flush loop used `time.sleep(self.config.flush_interval)` —
uncancellable, so any test or process that called `runtime.shutdown()`
while the thread was mid-sleep blocked on `thread.join()` for the full
default 5s flush_interval. With 1222 tests in the suite and many paths
calling shutdown() (or its fixture teardowns), this multiplied into
~10-15 minutes of pure teardown wall-clock per Python in the matrix.

Replace the bare sleep with `Event.wait`, which returns the instant
`stop()` sets the event. `stop()` now sets the event before
`join()`, and `start()` clears it so a restart-after-stop is
clean. Pin contract in tests/test_transport.py::

    test_stop_interrupts_flush_sleep

…uses a 30s flush_interval; pre-fix this took 30s, post-fix <5s.

CI hygiene in the same commit so the suite can actually use the freed
time:

- ci.yml / publish*.yml: enable pip cache (`cache: pip` +
  `cache-dependency-path: pyproject.toml`) — saves ~60-90s of cold
  install per matrix leg.
- ci.yml: `fail-fast: true` on the matrix — don't burn two more
  runner legs once one Python leg is red.
- ci.yml / coverage / publish*.yml: install `pytest-xdist>=3.6` and
  pass `-n auto` to pytest. `pytest-xdist` is also added to
  `[project.optional-dependencies.dev]` so a local
  `pip install -e .[dev]` brings it in.
- pyproject.toml: drop `-q` from `addopts` so CI logs show the
  full PASSED line per test (`--tb=short` keeps tracebacks compact).
  `-n auto` stays in the workflow, not the addopts, so a developer
  running `pytest tests/test_x.py` gets a single process.

No public API change. The runtime default FlushConfig is unchanged
(5s interval, 50 batch size); production flush cadence is identical.
The fix only shortens the worst-case shutdown latency.

* remove redundant docs

* chore(release): bump version 0.13.4 -> 0.13.5

Pairs with the preceding release/0.13.5 commits:

  * perf(ci): cancel flush-thread sleep (transport.py:816)
  * remove redundant docs (drift.md, sdk-v3-migration-gaps.md)

Wire format unchanged; pure version bump + changelog entry
covering both the perf fix and the CI hygiene so the SDK_MIN_VERSION
floor is up to date.

No on-wire breaking change; backends on 1.0.0 keep working
unchanged. Recommended upgrade path: 0.13.4 -> 0.13.5.

* fix(tests): stop transport flush thread between tests so it doesn't race respx

PR #60 landed the cancellable-sleep fix in Transport._flush_loop and
expected CI wall-clock to drop to 3-5 minutes. The first green run
on PR #60 (PR #60 run #1) actually took 9m 47s — the test step
dominated by a retry storm:

  Request failed (attempt 5/11), retrying in 8.46s: ConnectError
  Request failed (attempt 6/11), retrying in 9.16s: ConnectError
  ...
  Circuit breaker OPEN. Batch of 10 events will be re-queued.

Root cause: `tests/conftest.py:reset_runtime` teardown nulled the
runtime reference WITHOUT calling `runtime.shutdown()`. The
transport flush thread therefore kept running across tests, the
buffer drained through httpx with no respx context active, and the
xdist workers spent the next 9 minutes retry-sending the buffer
against the real (unreachable in CI) backend. `_retry_with_backoff
(max_retries=10, max_delay=10s)` is 65s of pure sleep per failed
batch, and with 4 xdist workers and many buffered batches this
multiplied into 9m 47s — i.e. a CI-noise fix that hid a deeper
lifecycle bug.

Pre-fix CI was already paying this cost (5s shutdown-sleep × 200+
tests ≈ 17 min of teardown per Python leg); the retry storm was
always there but masked by the dominant 5s cost. PR #60's 5s fix
exposed it.

Fix: add `flush: bool = True` to both `Transport.stop()` and
`NullRunRuntime.shutdown()`. When False, the transport thread is
cancelled WITHOUT a final `_do_flush()` / `_persist_to_wal()`.
`tests/conftest.py:reset_runtime` teardown now calls
`inst.shutdown(flush=False)` before nilling the reference. This
makes the conftest teardown a true no-op for the buffer — the test
that wrote the events is responsible for asserting on what it
cared about. The production default (`flush=True`) is preserved,
so the `nullrun.shutdown()` audit contract ("drain in-flight
events") is unchanged.

Pins:

  * `tests/test_transport.py::test_stop_flush_false_skips_final_flush
    ` — buffers an event, calls `stop(flush=False)` with no
    respx active, asserts the call returns in <1s AND the buffer
    is left untouched. Pre-fix this would have hung for 65s+ on
    the first retry.

  * `tests/test_init_contract.py::TestShutdownFlushKwarg::
    test_runtime_shutdown_flush_false_skips_final_flush` — same
    contract at the `NullRunRuntime` level: `shutdown(flush=False
    )` propagates the `flush=False` flag to
    `Transport.stop()`.

Public API additions:

  * `Transport.stop(timeout=10.0, flush: bool = True)` — `flush
    =False` is the new flag.
  * `NullRunRuntime.shutdown(flush: bool = True)` — propagates.
  * `nullrun.shutdown(timeout=2.0, flush: bool = True)` — passes
    `flush` through to the runtime.

No on-wire or production behaviour change. CI step is expected to
drop from ~9m 47s (PR #60 run #1) to ~30-60s on the next run.

* fix(langgraph): attach LLM spans to parent chain via callback run_id

Sprint 2026-07-12 (multi-agent span attachment). Previously
on_llm_end called runtime.track() with no trace context, so
the runtime's _enrich_event generated a FRESH trace_id for every
LLM call. The downstream effect on multi-agent / reflection
flows was 4/5 empty rows in the workflow detail 'Recent
executions' panel:

  https://nullrun.io/control-center/workflows/<id>

  ┌────────────────────────────────────────────────┐
  │  1cf7f505-…  trace: 1cf7  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  c4be95fe-…  trace: c4be  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  9295df0f-…  trace: 9295  cost: /usr/bin/bash.00           │  ← orchestration span only
  │  019f5060-…  trace: 019f  cost: $0.00013 ✓      │  ← cost_events orphan, by luck
  └────────────────────────────────────────────────┘

The cost_summary LEFT JOIN in db/mod.rs::get_execution_records_*
keyed on cs.join_kind='trace_id' AND cs.join_id=u.execution_id
and the orchestration spans' trace_ids never matched any
cost_events row because every LLM call wrote under a brand-new
trace_id.

Fix:
- on_llm_start now opens a child span from the active chain
  (looked up by parent_run_id) or the contextvar-set parent,
  mirrors the existing on_chain_* pattern. Stores the
  SpanContext under the LangChain run_id key.
- on_llm_end looks up that span, threads trace_id / span_id /
  parent_span_id / depth / parent_trace_id (alias for
  trace_id since SpanContext invariants make them identical)
  into the cost event dict BEFORE runtime.track(). _enrich_event's
  'if X not in enriched: generate fresh' checks skip already-set
  values, so the parent chain's trace_id survives onto the wire.
- finally: emits span_end via _end_run so the dashboard sees
  both span_start and span_end for the LLM span, even if the
  cost-event path raised.

Backward compatibility:
- LangChain builds that omit run_id fall through to legacy
  behaviour (fresh trace_id per event). Tested by
  test_on_llm_without_run_id_is_silent_no_op.
- Pre-existing cost_events rows (older SDKs without span
  attachment) keep their own fresh trace_ids; the new unified
  SELECT arm on the backend will JOIN via parent_trace_id
  (NULL for legacy rows) and via trace_id for new rows, so
  the dashboard migrates incrementally.

Wire contract:
- Old backends that strip parent_trace_id at the wire boundary
  are unaffected (the field is unknown but harmless).
- New backends write it to cost_events.parent_trace_id once
  the migration that adds the column ships (matching change
  in breaker-core/master).

Tests (test_langgraph_callback.py):
- test_on_llm_start_then_end_attaches_parent_chain_trace_id:
  - chain span root depth=0 (parent_run_id chain-1)
  - LLM span child depth>=1, span_kind=llm, parent_span_id
    matches chain span_id
  - cost event trace_id == chain trace_id (the contract)
  - parent_trace_id on cost event == chain trace_id (alias)
  - span_start + span_end both fire around the cost event
- test_on_llm_without_run_id_is_silent_no_op: legacy LangChain
  path doesn't crash, no spans opened, cost event fallback
- test_on_llm_end_emits_span_end_even_if_track_raises: finally
  block guarantees cleanup on backend errors

42/42 langgraph tests pass after the change (was 39 before).

* chore(release): 0.13.6 — multi-agent span attachment (parent_trace_id)

Bump __version__ to 0.13.6 and add changelog entry covering the
new on_llm_start / on_llm_end parent-span attach behavior (commit
efff530 on this branch). No public API change.

Wire format: backward-compatible. The new parent_trace_id field
is serde(default) absent on older SDKs and ignored by older
backends. Operators upgrading from 0.13.5 must upgrade both
sides together (SDK to 0.13.6 + backend with migration 217);
the SDK alone still works on 1.0.0 backends.

Recommended upgrade path: 0.13.5 -> 0.13.6.
SDK_MIN_VERSION_FOR_V3 unchanged (0.12.0).
maltsev-dev added a commit that referenced this pull request Aug 12, 2026
…print cleanup (#88)

* cleanup(sprint3): remove dead code, redundant tests, memoir comments, dup CHANGELOG

P1 — dead code:
- Remove deprecated start_recording/stop_recording no-op stubs from runtime.py
  (replaced by direct return-value gates; tests for them removed).
- Delete breaker/__main__.py stub (was a no-op CLI entry point).
- Delete unused import warnings in runtime.py after the deprecated stubs.

P2 — redundant tests:
- Delete one-shot fix-dump tests (test_<fix_name>.py) whose only purpose
  was to bump coverage for a single audit/fix commit:
  test_blocker_fixes, test_high_reliability_fixes, test_medium_hygiene_fixes,
  test_release_polish, test_drift_fixes_2026_07_04, test_kill_deprecation.
- Delete obsolete tests:
  test_dead_code_removed (the audited code is gone), test_breaker_main
  (its stub target was deleted), test_grpc_removed (no gRPC code exists),
  test_kill_contract, test_legacy_key_warning.
- Consolidate test_X_branches.py into test_X.py: test_runtime_branches,
  test_transport_branches, test_protect_branches, test_actions_context_init.
- Consolidate test_v3_server_minted.py and test_v3_38_drift_fixes.py into
  test_v3_wire_contract.py.

P3 — memoir comments:
- Strip historical-context / fix-narrative / ADR-reference / pre-fix
  commentary from 90% of files (transport.py / runtime.py / decorators.py /
  breaker/exceptions.py / observability/__init__.py / test_runtime.py /
  test_protect.py / test_actions.py / test_transport.py / test_v3_wire_contract.py
  / conftest.py). Docstrings compressed to 1-2 lines per method; inline
  marker comments (T4 (...), P0-4, FIX-F3, PR #N, 2026-07-02, ADR-008,
  observed: ..., pre-fix, ...) collapsed to a single short line.
- Replace 'Merged from X.py' section markers with semantic headers.

P4 — CHANGELOG deduplication:
- src/nullrun/__version__.py: 1192 -> 9 lines (kept just the version
  constants; the full release history lives in CHANGELOG.md).
- pyproject.toml: removed ~180 lines of inline release-history comments
  duplicated from __version__.py; only the current version is pinned.

Verification: 1341 passed, 7 skipped, 2 warnings in 77.86s.

* cleanup(sprint4): trim VCS bloat - Dockerfile fix + drop orphans + tighten CHANGELOG

Dockerfile:
- Drop the broken ENTRYPOINT [python, -m, nullrun.breaker]: nullrun.breaker
  is a package with no __main__.py and no console_scripts entry in
  pyproject.toml. The SDK is a library, not a service. Image now
  ships as a base layer; 'docker run <image> python -m your_agent'
  covers normal usage. No CI workflow ever built this image (orphan).

Dockerfile.dev:
- Delete. 404 B, CMD 'tail -f /dev/null' antipattern, no CI consumer.

docs/assets/banner.svg:
- Delete. 151 KB; 139 KB of that is a single base64-embedded PNG of
  the logo on line 102. Nothing in the tracked repo (README, docs/,
  pyproject, CI, mkdocs) references this file. Original is
  recoverable from git history if needed.

CHANGELOG.md:
- Drop 126 KB -> 52 KB (-59%), 2035 -> 865 lines. Three trimming
  passes:
  1. Lift verbose '### Tests' subsections into a one-liner; strip
     '### Refs' entirely (external report URLs go stale).
  2. Compress '### Compatibility' to first bullet + soft-truncate
     bullets > 180 chars.
  3. Cap each release entry to max 35 lines. The 8 most-recent
     releases (0.14.x + 0.13.13/0.13.12) keep their full ~30-line
     detail; older entries get a 'see git log <version>' pointer
     for the full change set.

Total: 4 files changed, 96 insertions(+), 1516 deletions(-).

* cleanup(sprint5): trim long docstrings/memoirs + scrub Cyrillic from comments

CATEGORY 2 (memoirs / dangling comments + Cyrillic scrub):

  - runtime.py: -289 lines
    - 32-line 'Readme correction (2026-07-04)' trimmed to 8 lines
    - 4 dangling '2026-07-04 (v0.12.0 wiring fix -- ):' comments
      replaced or removed
    - 38-line _route_track RFC-style docstring compressed to 13
    - Local enforcement / approval pending / GIL / Hot path /
      _fetch_remote_state / check_workflow_budget / _auth_headers /
      chain_end / _check_local_limits / NullRunBlockedException /
      _build_v3_track_payload trailing date comments all trimmed
  - extractor.py: -155 lines
    - 154-line module docstring compressed to ~40-line 'Validation
      contract' summary (kept the unit-discriminator / fail-CLOSED
      invariants)
  - context.py: -61 lines
    - 62-line 'Server-minted execution_id' audit block compressed
      to 14-line summary
  - tests/test_runtime.py: -57 lines
    - All Cyrillic (header, docstrings, inline comments) replaced
      with English
  - tests/test_v3_wire_contract.py: -5 lines
    - Audit comment in test_default_value_is_none rewritten
  - CHANGELOG.md: -2 lines
    - 'Разрыв 2' -> 'Breakpoint-2', 'Разрыв 1c' -> 'approval field'

No semantic change. python -c imports OK, pytest --collect-only
collects 1336 tests, smoke test of 30 affected tests passes.

Follow-up: dead-code, duplication, CHANGELOG bloat, CI/build, docs.

* cleanup(sprint5): dedupe sync/async wrappers + dead code in src/nullrun

#1 Dead code
- extractor: drop _cached_signature (lru_cache helper, never called)
  and compute_impact_digest (thin alias, no callers); remove unused
  imports (functools, Optional, Union).
- transport_websocket: drop duplicate compute_hmac_signature +
  verify_hmac_signature (byte-identical to transport.py); re-export
  from transport. Update test imports.
- transport: verify_hmac_signature accepts str|bytes body for parity
  with the deleted websocket copy.
- _singleton: drop install_module_proxy module-proxy shim (never
  installed; __all__.append now removed).
- _registry: drop replace_for_test (no callers).
- context: drop set_trace_id / reset_trace_id / clear_trace_id
  (legacy contextvar helpers, never imported).
- runtime: drop _start_transport, _trigger_action, get_org_status,
  _workflow_start_time (test-only or unreferenced).

#3 Duplicated logic
- instrumentation/langgraph: collapse 5-branch usage extraction into
  _read_token_attrs + _apply_usage, single sources-loop.
- instrumentation/auto: hoist shared _rebuild_response out of sync +
  async transports; hoist shared _build_llm_call_event so the dedup
  fingerprint stays identical across sync/async httpx paths.
- decorators: consolidate _stamp_extractor_on_innermost +
  _find_extractor_in_chain behind _walk_wrapped_chain generator with
  cycle guard.
- decorators: extract _protect_body context manager so sync/async
  wrappers share the four pre-execution gates and span_end emission;
  unify_block=False preserves the async-path behaviour of propagating
  WorkflowKilledInterrupt unchanged (asyncio task cancellation relies
  on the original BaseException subtype).

Tests: 1334 pass, 2 skip (pre-existing).

* cleanup(sprint5): CHANGELOG order + Makefile CI parity + error-code docs

#5 CHANGELOG bloat
- Drop WIP [0.10.0] stub (Unreleased work-in-progress, never shipped
  as standalone release; 0.11.0 became the canonical v3.0 cut).
- Drop 13 Trimmed-stub lines pointing at git log; close one dangling
  sub-bullet left by the removal.
- Reorder release blocks in strict descending version order:
  was 0.9.1 -> 0.11.0 -> 0.9.0 (lower: 0.3.1 -> 0.5.2 -> 0.4.0);
  now 0.11.0 -> 0.9.1 -> 0.9.0 (lower: 0.5.2 -> 0.4.0 -> 0.3.1).
  Net: -29 lines, semver -> date sort invariant holds.

#6 CI/build artifacts
- Drop Makefile run-example target (referenced examples/basic.py;
  examples/ was deleted in 0.3.1 alongside the gRPC transport).
  Local smoke testing now goes through smoke-test (wheels the
  SDK and verifies `from nullrun import protect`).
- Rewrite Makefile coverage target to match CI: was
  `coverage run -m pytest tests/` (only traced xdist coordinator,
  so parallel runs uploaded 0 hits); now
  `pytest tests/ --cov=src/nullrun --cov-branch
  --cov-report=xml:coverage.xml --cov-report=term`, matching
  .github/workflows/ci.yml:82.
- clean target now also removes coverage.xml.

#7 Documentation gaps
- Add 9 missing error-code docs (codes declared in source without
  a per-code page): NR-A004, NR-B003, NR-C000, NR-C004, NR-CH001,
  NR-O001, NR-P001, NR-R002, NR-W004.
- Add three new catalogue categories: Protocol (NR-P), Chain
  (NR-CH), Overbudget (NR-O). README.md catalogue now covers all
  23 documented codes. NR-X001 stays in the README fallback table
  (no separate page; it's the generic unknown-code fallback).
  Verified via cross-check: all source-referenced codes are
  documented.

Tests: 23/23 exception hierarchy pass; full suite remains green.

* chore(release): 0.14.10 — Sprint 5 internal cleanup

Bump __version__ 0.14.9 -> 0.14.10 and add the matching CHANGELOG
entry. Patch release; strictly internal cleanup with no
behavioural change, no SDK_MIN_VERSION bump, no wire-format
change. Backward-compatible drop-in for 0.14.9.

This release consolidates the three sprint-5 cleanup commits on
cleanup/p1p2-dead-code-tests:

- #1 Dead code (383 lines, 6 files): extractor cache helpers,
  duplicate HMAC signatures, install_module_proxy, replace_for_test,
  context set/reset/clear_trace_id, runtime._start_transport +
  _trigger_action + get_org_status + _workflow_start_time.
- #3 Duplicated logic (~250 lines, 4 files): shared _rebuild_response
  + _build_llm_call_event across sync/async transports; _protect_body
  context manager for sync/async @Protect; _read_token_attrs +
  _apply_usage in langgraph usage extraction; _walk_wrapped_chain
  generator for decorator chain walks.
- #5 CHANGELOG bloat (-29 lines): dropped WIP [0.10.0] stub + 13
  Trimmed placeholders; fixed descending-version sort order.
- #6 CI/build: dropped Makefile run-example (missing examples/basic.py);
  rewrote coverage target to match CI's pytest --cov pipeline.
- #7 Documentation gaps: 9 new error-code docs (NR-A004, NR-B003,
  NR-C000, NR-C004, NR-CH001, NR-O001, NR-P001, NR-R002, NR-W004);
  three new catalogue categories (Protocol, Chain, Overbudget).

Tests: 1334 pass, 2 skip (pre-existing); 23/23 exception hierarchy
pass. No public API change.

* fix(sdk): route /auth/verify non-200 through canonical envelope parser (DEF-ERRHDL-AUTH-PATH-CODE-PIN-01, RUN_ID 20260811-1)

Pre-fix, /auth/verify raised NullRunAuthenticationError (NR-A001) for
ANY non-200 status, including 5xx (500/502/503/504). The canonical
dispatcher at transport._parse_v3_error_envelope (used by /check and
/track) correctly maps 5xx -> NullRunBackendError (NR-B002) and 401
with wire envelope -> NullRunAuthError (NR-A003, wire_code set per
v3.38). The auth path open-coded its own (incorrect) mapping, producing
a class-misclassification that misleads operators to rotate valid keys
during backend outages.

Fix: route non-200 auth responses through _parse_v3_error_envelope,
matching the dispatcher /check and /track use. Lazy import inside the
else arm keeps runtime.py's top-level import graph stable.

Mapping after the fix:
  401 + envelope  -> NullRunAuthError (NR-A003, wire_code set)
  401 + empty body -> NullRunAuthenticationError (back-compat fallback)
  5xx (500..504)  -> NullRunBackendError (NR-B002, retryable)
  429             -> RateLimitError (NR-R001, retry_after honored)
  other 4xx       -> NullRunBackendError with status_code set

NullRunAuthError is a subclass of NullRunAuthenticationError, so existing
'except NullRunAuthenticationError' clauses still match. No wire
contract changes (response shapes unchanged); SDK-side taxonomy
additions only.

Tests: 5 new regression tests in tests/test_runtime.py pin the
per-status mapping. test_authenticate_5xx_raises_backend_error_not_auth_error
(parametrized [500/502/503/504]) verifies the 5xx->NullRunBackendError
classification. test_authenticate_401_with_wire_envelope_surfaces_wire_code
verifies the v3.38 wire_code contract for /auth/verify.

Verification: pytest tests/test_runtime.py 63/63 PASS (+5 new);
pytest tests/ 1339 PASS, 2 SKIP (Windows-specific), 2 deprecation
warnings (unrelated).

Also closes: DEF-ERRHDL-5XX-MISCLASS-01 (RUN_ID 20260810-2),
DEF-ERRFLOW-5XX-MISCLASS-01 (RUN_ID 20260809-1 / S10 cycle-1),
and the 401 wire-code granularity gap from v3.38 in the auth path.

Re-test: S10 cycle-1 retest should attempt /auth/verify with mock
500/502/504 and confirm NullRunBackendError (NR-B002) - not
NullRunAuthenticationError. Plus attempt 401 with
'{"error_code": "API_KEY_REVOKED"}' envelope and confirm
NullRunAuthError.wire_code == 'API_KEY_REVOKED'.

* Revert "fix(sdk): route /auth/verify non-200 through canonical envelope parser (DEF-ERRHDL-AUTH-PATH-CODE-PIN-01, RUN_ID 20260811-1)"

This reverts commit 370d5f5.

* Revert "cleanup(sprint5): trim long docstrings/memoirs + scrub Cyrillic from comments"

This reverts commit ea77e21.

* fix(sdk): restore branch-coverage tests deleted by sprint3 cleanup (a666624)

Sprint3 cleanup (a666624) consolidated test_*_branches.py files into
their main test_*.py counterparts and removed them. Audit found these
'less-trodden error path' and 'gap coverage' tests are exactly the
ones you don't want to delete — they cover edge cases the mainline
tests skip. Removing them = silent coverage regression.

Files restored (all from master HEAD):
- tests/test_protect_branches.py (564 lines) — branch coverage for
  _safe_args / _strip_details_balanced / _enforce_sensitive_tool
- tests/test_runtime_branches.py (517 lines) — less-trodden error paths
  in runtime.py. Removed 2 tests (test_start_recording_returns_*
  and test_stop_recording_returns_none) because a666624 P1 also
  intentionally removed the deprecated no-op stubs from runtime.py
  (replaced by direct return-value gates per the commit message).
  Restoring the tests without the methods would create dead tests.
- tests/test_transport_branches.py (647 lines) — branch coverage gaps
  in transport.py

Verification: pytest tests/ → 1462 passed, 6 skipped, 0 failed.
The 6 skipped are pre-existing environment markers.

Pairs with commit 700b0af (revert of ea77e21 Cyrillic scrub). Together
they close the over-aggressive parts of the cleanup sprint without
disturbing the valid P1 dead-code removal, P4 CHANGELOG dedup, and
v3.38/server-minted test consolidations.

* chore(release): 0.14.11 — partial revert of sprint-5 cleanup

Bump __version__ 0.14.10 -> 0.14.11 and add the matching CHANGELOG
entry. Patch release; partial revert of two sprint-5 cleanup commits
whose scope exceeded what the codebase actually supported.

This release closes the over-aggressive parts of the cleanup sprint
without disturbing the valid P1 dead-code removal, P4 CHANGELOG
dedup, and v3.38/server-minted test consolidations.

- Revert ea77e21 (Cyrillic scrub + docstring trim): restored the
  30-line 'partially wrong' block in src/nullrun/runtime.py
  (codifies CLAUDE.md \u00a74 fail-CLOSED rules for SDK transport vs
  backend enforcement), restored 'Разрыв 2' / 'Разрыв 1c' in
  CHANGELOG.md (user-coined Russian technical nomenclature), and
  restored tests/test_real_e2e_observation.py (321 lines, the only
  real-socket integration test).
- Cherry-pick restore 3 branch-coverage files deleted by a666624 P2:
  tests/test_protect_branches.py (564), tests/test_runtime_branches.py
  (515; minus 2 tests for deprecated start_recording/stop_recording
  no-op stubs that a666624 P1 also intentionally removed), and
  tests/test_transport_branches.py (647). These files explicitly
  documented their purpose as covering 'gaps' and 'less-trodden
  error paths' that the mainline tests skip.

Verification: pytest tests/ -> 1462 passed, 6 skipped, 0 failed.

Pairs with commits 700b0af (revert ea77e21) and 2df6b3a (restore
branch-coverage tests) on cleanup/p1p2-dead-code-tests.

Compatibility: No SDK_MIN_VERSION bump. No public API change, no
wire-format change, no behavioural change. Drop-in replacement for
0.14.10.

* feat(sdk): ADR-009 P1 governance audit read surface (0.15.0)

nullrun.audit module + runtime.audit proxy + 34 tests.

* chore(release): 0.15.0 — ADR-009 P1 governance audit read surface

* fix(sdk): defer runtime.py annotations to avoid AuditProxy.list shadowing built-in

AuditProxy defines a public method named list() (ADR-009 P1 surface),
which shadowed the built-in list inside the class body. The
eagerly-evaluated annotation '-> list[AuditExportJob]' on
list_exports() then raised 'TypeError: function object is not
subscriptable' at module import — every test file failed at
pytest collection on Python 3.12.

Fix: add 'from __future__ import annotations' to runtime.py so
all annotations become PEP 563 lazy strings. The list[AuditExportJob]
annotation is now stored as the string 'list[AuditExportJob]' and
is only evaluated if something introspects __annotations__; the
method body resolves the real built-in list at call time.

Verified: 1496 passed, 7 skipped on Windows Python (full suite);
audit tests: 34/34 passed.

* fix(sdk): ruff I001 + UP037 cleanup after adding __future__ annotations

Adding 'from __future__ import annotations' to runtime.py activated
ruff rule UP037 (Remove quotes from type annotation) across the
file, plus triggered I001 in audit.py where the future-import was
positioned mid-file.

Auto-fixed via 'ruff check src/ --fix':
- I001 in audit.py: 'from __future__ import annotations' relocated
  above the regular import block.
- UP037 in audit.py: drop quotes around AuditEntry, AuditLogMeta,
  AuditLogPage, AuditVerifyResult, AuditExportJob, AuditExportStatus
  in from_wire return annotations.
- UP037 in runtime.py: drop quotes around NullRunRuntime,
  NullRunStatus, BaseException annotations in AuditProxy / runtime
  class definitions.

Verified: 1496 passed, 7 skipped; ruff clean.

* fix(sdk): mypy valid-type + arg-type cleanups in audit/runtime

Two mypy errors surfaced after the 'from __future__ import
annotations' import landed in runtime.py and ruff auto-fix
normalised audit.py annotations:

1. audit.py AuditVerifyResult.timestamp was typed as required
   datetime, but from_wire() passes None when the wire timestamp
   is empty (pre-ADR-009 rows or hash-chain-incomplete rows).
   Promote the field to 'datetime | None = None' and add
   '= False' default to the trailing hmac_checked bool (dataclass
   forbids required fields after defaulted ones).

2. runtime.py AuditProxy.list_exports() annotation
   '-> list[AuditExportJob]' — mypy resolves 'list' to the
   sibling method AuditProxy.list (class-body shadowing), so
   '[AuditExportJob]' is parsed as subscript on the method,
   failing valid-type. Switch to 'builtins.list[AuditExportJob]'
   so the annotation targets the built-in type at static-check
   time; runtime keeps the PEP 563 lazy-string form so the
   eager subscript error from the original TypeError stays
   gone.

Verified: mypy clean (37 files), ruff clean, pytest 1496 passed.
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