Skip to content

fix(langchain): close finished scopes from the top of the stack down - #755

Merged
rapids-bot[bot] merged 2 commits into
NVIDIA:release/0.7from
SandyChapman:fix/langchain-callback-scope-leak-0.7
Aug 12, 2026
Merged

fix(langchain): close finished scopes from the top of the stack down#755
rapids-bot[bot] merged 2 commits into
NVIDIA:release/0.7from
SandyChapman:fix/langchain-callback-scope-leak-0.7

Conversation

@SandyChapman

@SandyChapman SandyChapman commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Overview

Targets release/0.7 so this can go into a 0.7.3 cut for Fabric 0.2

What. When a LangChain chain run ends, the handler records it as completed and closes finished scopes from the top of the stack down, instead of attempting a close that the stack may reject and then abandoning the scope.

Why. Relay closes scopes strictly LIFO, but LangGraph schedules sibling chain runs as concurrent asyncio tasks that share one scope stack, so two siblings can finish in an order the stack rejects: A starts, B starts, A ends, B ends. _pop_scope removed the handle from _scope_handles before attempting the pop and then swallowed the rejection, so the scope stayed live on the stack with nothing left able to close it. It remained current for everything that followed, and the caller's own enclosing scope raised on exit:

RuntimeError: invalid argument: scope handle is not at the top of the stack

Downstream this reported fully successful agent work as failed (NVBug 6562846). The enclosing scope belonged to a NeMo Fabric Deep Agents invocation, whose adapter turned a teardown error into an invocation failure (since contained separately in NVIDIA/NeMo-Fabric#191) — 31 otherwise-successful agent-eval trials were scored as adapter failures in one regression run, including at parallelism 1.

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.
  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Details

Each run's handle is tracked from on_chain_start. When a run ends, its prepared output, metadata and end timestamp are recorded as completed state, and finished scopes are closed from the top of the active stack down: read scope.get_handle(), close it when its uuid belongs to a completed run of ours, stop as soon as the top is a run still going or a scope this handler does not own.

Only the current top is ever closed, so the stack is never asked to accept a close it would reject. There is no speculative pop, hence no failure to classify and no dependency on the runtime's error text.

On "drain only the owning stack". Implemented, then removed, with evidence. A stack-identity check cannot be written against the public API: set_thread_scope_stack genuinely shares the stack (a push in the thread is visible outside it) but returns a different Python wrapper, and ScopeStack exposes no id and only identity equality. A handler completing a run on a worker thread would therefore park every completion and strand every scope — the original bug, reintroduced by the guard. Dropping it also removes a strong reference to a ScopeStack per retained completion. Covered by test_a_run_completed_on_a_worker_thread_still_closes.

Cross-stack safety is therefore structural, not an ownership guard: a completion is matched on the top scope's uuid and name. uuid alone is not sufficient — a stack rebuilt from a PropagationContext re-creates a scope carrying an existing uuid, so a uuid-only match pops the stand-in, discards the completion and strands the real scope on its own stack (test_a_propagated_stack_does_not_consume_another_stacks_completion). The two are distinguishable because the runtime names rebuilt scopes propagated-parent and propagated-root (scope_stack.rs), which test_propagated_stand_ins_keep_their_reserved_names pins so a rename fails loudly rather than silently closing the wrong scope. Residual: a chain run named exactly one of those two reserved names would still collide.

@AjayThorve's findings. Output is prepared when the callback fires, so later mutation cannot change emitted telemetry. Cross-stack loss is structural rather than guarded, per above.

@willkill07's second review. Output is prepared before the run is dropped from the handler's map and degrades to None on failure — serializing walks caller data and a cyclic output raises RecursionError; a run removed with no completion recorded is a scope nothing can close. A completion is deleted only once the stack accepts the close, since the runtime can refuse before mutating the stack. Handler state transitions are serialized under a lock.

Reading the current handle is guarded by scope_stack_active(), which does not create a stack, so completing a run in a context without one no longer materialises an empty stack there.

Where should the reviewer start?

_close_completed_scopes_locked in python/nemo_relay/integrations/langchain/callbacks.py — the loop that reads the top and stops as soon as it is not a completed run of ours. Note the locked/unlocked split: _pop_scope holds the lock and calls the locked form, so the lock is never re-acquired on the normal path. That was a real fragility — an earlier revision nested the acquisition and only worked because the lock was reentrant.

Then the tests, which use a real scope stack. test_callbacks.py mocks nemo_relay, and its mock accepted any pop in any order, so it could not observe the LIFO rule this fix turns on; it now models the stack and rejects closing anything but the top.

The LangGraph tests drive real compiled graphs with no stubbing: a two-branch fan-out, a nested three-branch fan-out with a subgraph, and a failing node completing through on_chain_error.

Testing

  • python/tests/integrations — 64 passed, 2 skipped.
  • python/tests (excluding plugin/ and test_dynamic_plugin_host.py, which need just build-test-plugin-fixtures) — 545 passed, 2 skipped.
  • Pre-commit on the changed files: copyright, ruff, ruff-format, ty all pass.
  • Mutation checks, each caught by a named test: closing the top regardless of ownership (13 tests fail); a non-reentrant lock; serializing output at close time; letting output serialization escape; deleting a completion before the pop succeeds; dropping the scope_stack_active guard; and a handler that pushes no scopes at all.
  • Every new test was run against the unpatched upstream handler: 19 of 21 fail, and the 2 that pass are the ones that should — the harness control and the topology test, neither of which depends on this change. Of the 19, nine fail behaviourally (the scope-stack error, or its absence); the rest reference internals that do not exist upstream, so they guard against regressions in this implementation rather than detecting the original defect.
  • End to end against the original symptom: built this branch as a wheel and installed it into a NeMo Fabric checkout. Fabric's own tests that assert the leak exists flip to failing, and its Deep Agents mitigation for this bug never engages because there is nothing left to mitigate. A wheel built from unpatched release/0.7 was used as the control, and confirms the flip is caused by this change rather than by version skew.

Remaining limitations

  • A scope closed out of band can never return to the top, so its completion is retained for the life of the handler. That is the trade for never guessing at error text; the entry is inert and everything around it still closes. Documented on the type and in a test.
  • A sibling that never ends at all — a cancelled task with no on_chain_error — still leaves its parent waiting. Unchanged by this PR, and better than an abandoned scope since the state is visible.
  • Untested: runs that never end retaining their _scope_handles entry, sync (non-async) LangGraph execution, and very wide fan-out as a performance question.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • Relates to internal NVBug

Summary by CodeRabbit

Bug Fixes

  • Improved reliability when concurrent workflow steps complete out of order.
  • Preserved outputs, metadata, and completion timestamps during deferred processing.
  • Prevented lingering workflow state after concurrent or nested executions.
  • Improved handling of failed workflow steps and branch cleanup.
  • Increased reliability for concurrent LangGraph branch execution.

Tests

  • Added coverage for nested, overlapping, and out-of-order workflow lifecycles.
  • Added regression coverage for concurrent branches, shared handlers, worker-thread completion, and cleanup behavior.

@SandyChapman
SandyChapman requested a review from a team as a code owner August 11, 2026 17:09
@github-actions github-actions Bot added size:M PR is medium Bug issue describes bug; PR fixes bug lang:python PR changes/introduces Python code labels Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 20cb9640-f989-4358-a27f-3cf96d3b2ed3

📥 Commits

Reviewing files that changed from the base of the PR and between b0281d9 and d3dd594.

📒 Files selected for processing (1)
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (14)
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

**/*: Use release tags in raw Rust-compatible SemVer without a leading v; tags such as v0.1.0 are prohibited.
Use branch prefixes feat/, fix/, docs/, test/, or refactor/ according to the change purpose.
Every commit in a pull request must include a DCO Signed-off-by: sign-off.
Before submitting a pull request, ensure pre-commit hooks, relevant tests, target-specific builds, documentation updates, and a rebase on the latest main are complete.
Use commit messages in the form type: short description, with a valid type and a first line under 72 characters.

Run the prescribed plugin validation commands, including fixture building, focused Rust and Python package tests, integration tests, documentation checks, and the broader validate-change matrix for broad runtime or public API changes.

**/*: Keep observability changes scoped, surface assumptions, and define focused validation before editing.
Run affected Rust tests and just test-rust for event-field changes; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes; update docs and examples in the same branch.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: Keep FFI and Python, Go, and Node.js binding configuration objects and subscriber/exporter methods aligned with the core observability configuration and lifecycle semantics.
Preserve complete sanitized LLM request input and annotations when enable_full_payloads is enabled, while retaining credential removal and sanitizers.
Use each exporter's documented flush and deregister order before shutdown in observability examples and implementations.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,jsx,ts,tsx,go,c,h,cc,cpp,md,toml,yml,yaml,sh}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, documentation, scripts, and configuration files; the project is Apache-2.0.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case naming in Rust and Python.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,mjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve the existing Tokio-based asynchronous model and callback/future lifetimes; do not unexpectedly block or hide async work in bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain test coverage for Python binding and wrapper changes with the Python test suite.

python/tests/**/*.py: Use pytest to run Python tests.
Do not add @pytest.mark.asyncio to tests; async tests are automatically detected by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, use unittest.mock.MagicMock or unittest.mock.AsyncMock, using spec when necessary, rather than defining a new class.
Prefix mocked class names with mock, not fake.
Prefer pytest fixtures over helper methods.
If a fixture is needed in multiple test files, define it in a conftest.py file instead of repeating it.
Define fixtures using @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and a <fixture_name>_fixture function; specify scope only when it is not function.
Prefer pytest.mark.parametrize over separate tests for different input types.
Run focused pytest tests first when the affected area is known, and run the full suite with just test-python before review.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: Lint Python with Ruff using rule sets E, F, W, and I.
Format Python with the Ruff formatter, using a 120-character line length and double quotes.
Run ty for Python type checking.
Use Python snake_case naming conventions.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,py,go,js,jsx,ts,tsx,c,h}: Run tests for every language affected by a change; changes to the core Rust crate require tests across all bindings.
Use SONAR_IGNORE_START / SONAR_IGNORE_END only for documented false positives, keep ignored blocks minimal, explain them with a comment, and obtain reviewer sign-off.
Preserve the layered architecture in which Rust provides the core runtime and C FFI, PyO3, and NAPI provide bindings that mirror the full API surface.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/{test,tests}/**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

When adding functionality, include tests in the appropriate test files for each affected language binding.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h,html,md,mdx,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include the appropriate SPDX copyright and Apache-2.0 license header in every source file.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{toml,md,rs,py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Treat plugin Relay compatibility as normal SemVer; use >=0.5,<1.0 in examples unless a plugin intentionally declares a narrower range.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/**/*.py

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Python SDK tests belong under python/tests, not under source directories.

python/**/*.py: Format changed Python wrapper and test files with uv run ruff format python python/plugin.
Run uv run ruff format python python/plugin after changing Python wrapper or test files.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
🧠 Learnings (19)
📓 Common learnings
Learnt from: AjayThorve
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/nemo_relay/integrations/langchain/callbacks.py:137-154
Timestamp: 2026-08-11T17:33:57.356Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, `NemoRelayCallbackHandler` can be shared by concurrent async LangGraph invocations that each use a different `ScopeStack`. A deferred `_PendingPop` must retain its originating scope-stack context, and `_drain_pending_pops()` must retry only entries owned by the active scope stack. A handler-wide unpartitioned pending queue can otherwise retry a close on another context's stack, receive `NotFound`, remove it as terminal, and leave the owning scope open. Synchronization alone does not prevent this ownership failure.
Learnt from: SandyChapman
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py:285-314
Timestamp: 2026-08-11T19:49:02.958Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, a deferred `_CompletedScope` cannot be safely classified as orphaned because `ScopeStack` and `ScopeHandle` do not provide weak-reference support or an API to determine whether a handle remains on its owning stack. Eviction or a finite retention cap can discard a valid deferred completion and recreate the LangChain scope leak. A runtime-side stack-membership or liveness accessor is required before safe orphan-completion eviction can be implemented.
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 462
File: python/nemo_relay/_context.py:20-28
Timestamp: 2026-07-17T17:36:18.512Z
Learning: In the Python SDK, `python/nemo_relay/_context.py::ensure_scope_stack()` may use `nemo_relay._native_scope_stack_active()` when `nemo_relay._scope_stack_var` is unset: `crates/core/src/api/runtime/scope_stack.rs::sync_thread_scope_stack()` deliberately updates only the thread-local handle and does not set the explicit-active flag. Thus this branch preserves explicitly bound worker-thread stacks without allowing a stack synchronized by another asyncio task to be treated as task ownership.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to crates/core/src/api/runtime/state.rs : Add chain-execution helpers to `NemoRelayContextState`, following existing helpers such as `tool_sanitize_request_chain` or `tool_request_intercepts_chain`.
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-07-28T21:11:43.864Z
Learning: In `crates/core/src/api/runtime/state.rs`, async tool and LLM middleware snapshot chains must isolate callback panics with `AssertUnwindSafe(...).catch_unwind()`: sanitizer failures/panics fail open by preserving the latest valid payload, while conditional-execution guardrails and request intercepts convert panics into `FlowError::Internal`.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-04T01:50:26.470Z
Learning: Scope stacks must remain hierarchical, always include a root scope, preserve parent-child event relationships and scope-local visibility, provide cleanup boundaries, and isolate concurrent requests.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to **/*.{rs,py,js,ts,tsx,go,java,kt,swift} : Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.
📚 Learning: 2026-08-11T19:49:02.958Z
Learnt from: SandyChapman
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py:285-314
Timestamp: 2026-08-11T19:49:02.958Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, a deferred `_CompletedScope` cannot be safely classified as orphaned because `ScopeStack` and `ScopeHandle` do not provide weak-reference support or an API to determine whether a handle remains on its owning stack. Eviction or a finite retention cap can discard a valid deferred completion and recreate the LangChain scope leak. A runtime-side stack-membership or liveness accessor is required before safe orphan-completion eviction can be implemented.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-11T17:33:57.356Z
Learnt from: AjayThorve
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/nemo_relay/integrations/langchain/callbacks.py:137-154
Timestamp: 2026-08-11T17:33:57.356Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, `NemoRelayCallbackHandler` can be shared by concurrent async LangGraph invocations that each use a different `ScopeStack`. A deferred `_PendingPop` must retain its originating scope-stack context, and `_drain_pending_pops()` must retry only entries owned by the active scope stack. A handler-wide unpartitioned pending queue can otherwise retry a close on another context's stack, receive `NotFound`, remove it as terminal, and leave the owning scope open. Synchronization alone does not prevent this ownership failure.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-17T17:36:18.512Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 462
File: python/nemo_relay/_context.py:20-28
Timestamp: 2026-07-17T17:36:18.512Z
Learning: In the Python SDK, `python/nemo_relay/_context.py::ensure_scope_stack()` may use `nemo_relay._native_scope_stack_active()` when `nemo_relay._scope_stack_var` is unset: `crates/core/src/api/runtime/scope_stack.rs::sync_thread_scope_stack()` deliberately updates only the thread-local handle and does not set the explicit-active flag. Thus this branch preserves explicitly bound worker-thread stacks without allowing a stack synchronized by another asyncio task to be treated as task ownership.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-17T02:50:34.317Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to **/*.{rs,py,js,ts,tsx,go,java,kt,swift} : Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Define fixtures using `pytest.fixture(name="<fixture_name>"[, scope="<scope>"])` and a `<fixture_name>_fixture` function; specify `scope` only when it is not `function`.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Prefer pytest fixtures over helper methods.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : If a fixture is needed in multiple test files, define it in a `conftest.py` file instead of repeating it.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Prefix mocked class names with `mock`, not `fake`.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-27T20:27:55.340Z
Learnt from: fede-kamel
Repo: NVIDIA/NeMo-Relay PR: 554
File: crates/core/tests/integration/pipeline_tests.rs:1442-1515
Timestamp: 2026-07-27T20:27:55.340Z
Learning: In `crates/core/tests/integration/pipeline_tests.rs`, integration tests are serialized with `TEST_MUTEX` and begin with `reset_global()`, which replaces the whole `NemoRelayContextState`. Therefore, the file’s established trailing `deregister_subscriber(...).unwrap()` cleanup pattern does not leak subscribers between tests if a prior test panics; do not request a per-test RAII cleanup guard unless proposing a deliberate file-wide cleanup-pattern refactor.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T21:12:11.017Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 571
File: python/nemo_relay/_native.pyi:1716-1731
Timestamp: 2026-07-28T21:12:11.017Z
Learning: In `python/nemo_relay/_native.pyi`, the standalone middleware helpers `tool_request_intercepts`, `tool_conditional_execution`, `llm_request_intercepts`, and `llm_conditional_execution` intentionally return direct results when called outside an asyncio event loop and awaitables when called from an async caller. Their `T | Awaitable[T]` (or `None | Awaitable[None]`) annotations accurately represent this compatibility contract because Python typing cannot overload return types based on event-loop presence.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-04T01:50:26.470Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-04T01:50:26.470Z
Learning: Scope stacks must remain hierarchical, always include a root scope, preserve parent-child event relationships and scope-local visibility, provide cleanup boundaries, and isolate concurrent requests.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-05-21T22:49:35.949Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/prepare-pr/SKILL.md:0-0
Timestamp: 2026-05-21T22:49:35.949Z
Learning: Branch scope should be coherent and reviewable

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T03:31:11.365Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 564
File: crates/core/src/api/llm.rs:859-875
Timestamp: 2026-07-28T03:31:11.365Z
Learning: For NeMo Relay asynchronous middleware under RELAY-509, no implicit middleware timeout is permitted. FIFO publication barriers intentionally wait for delayed sanitizer work; pending completion-based callbacks must resolve, reject, or observe cancellation. This is documented behavior, not an availability-timeout defect.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T20:07:33.786Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 571
File: crates/core/src/api/runtime/state.rs:996-1020
Timestamp: 2026-07-28T20:07:33.786Z
Learning: In NeMo Relay, RELAY-509 defines sanitizer callback failures as intentional fail-open behavior: sanitizer chains retain and publish the last valid event or payload snapshot, while logging the failure with callback context. This policy applies to event, tool request/response, and LLM request/response sanitizer chains and is documented in the public documentation and migration guide.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-06-26T15:15:34.329Z
Learnt from: teerthsharma
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-06-26T15:15:34.329Z
Learning: In NeMo-Relay adaptive topology-aware ACG, the intended convergence behavior is to detect stability from a reusable agent workflow's stable prompt prefix under the same learning key, while allowing variable task-specific suffixes; learning must reopen when the prompt topology changes. This clarification is relevant to reviews of `crates/adaptive/src/acg_learner.rs` and related convergence tests.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T03:31:28.749Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 564
File: crates/core/src/api/runtime/subscriber_dispatcher.rs:134-141
Timestamp: 2026-07-28T03:31:28.749Z
Learning: For RELAY-509 asynchronous middleware, `crates/core/src/api/runtime/subscriber_dispatcher.rs` intentionally uses FIFO publication barriers without an implicit timeout. `flush_subscribers` must wait for pending asynchronous publication work; pending completions are expected to resolve, reject, or observe cancellation rather than being released by a timeout.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T13:13:11.790Z
Learnt from: afourniernv
Repo: NVIDIA/NeMo-Relay PR: 702
File: docs/about-nemo-relay/concepts/scopes.mdx:103-108
Timestamp: 2026-08-07T13:13:11.790Z
Learning: In the Rust runtime, `crates/core/src/api/tool.rs::tool_call_execute` and `crates/core/src/api/llm.rs::llm_call_execute` do not push `Tool` or `Llm` scopes. They create parent-linked lifecycle events by using `resolve_parent_uuid` and `with_active_event_uuid`; therefore, a managed tool or LLM call under an active `Agent` scope leaves the explicit scope stack unchanged.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T21:11:43.864Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-07-28T21:11:43.864Z
Learning: Queued event-publication assertions in NeMo Relay tests must call `flush_subscribers()` after the triggering lifecycle operation, including after scope-pop cleanup checks, because sanitizer and subscriber delivery occurs asynchronously on the dispatcher.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
🪛 Ruff (0.16.1)
python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py

[warning] 185-185: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 560-560: Dynamically typed expressions (typing.Any) are disallowed in **kwargs

(ANN401)


[warning] 563-563: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (4)
python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py (4)

185-185: Ruff UP017 still flags this line. Use datetime.UTC.


453-458: threading.Thread.join(timeout=...) returns silently on timeout. Both worker-thread tests still lack a liveness assertion after the join.

Also applies to: 482-486


49-61: LGTM!

Also applies to: 64-111, 114-128, 208-230, 233-254, 257-283, 286-315, 318-346, 349-385, 388-422, 489-522, 525-539, 542-577, 580-605, 608-633, 636-686


131-134: 📐 Maintainability & Code Quality

No fixture change is needed. python/tests/conftest.py defines subscribed_events_fixture with the required decorator and flushes and deregisters the subscriber during teardown.

			> Likely an incorrect or invalid review comment.

Walkthrough

The LangChain callback handler now defers out-of-order scope closures, preserves completion data and timestamps, and drains completed scopes in stack order. Tests cover concurrent sibling runs, stack ownership, cleanup, output snapshots, and failure handling.

Changes

LangChain scope closure handling

Layer / File(s) Summary
Deferred closure state and processing
python/nemo_relay/integrations/langchain/callbacks.py
The handler tracks scope ownership, captures output, metadata, and UTC completion timestamps, defers blocked closures, and drains eligible scopes in stack order.
Callback stack model and timestamp assertions
python/tests/integrations/langchain_tests/test_callbacks.py
The mock models LIFO scope behavior. Lifecycle assertions require timestamps for successful, failed, and command-output completions.
Concurrent scope-stack regression coverage
python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
Tests cover nested and concurrent runs, queue draining, stack ownership, output snapshots, worker-thread completion, handler ownership, terminal failures, and reentrant locking.
Concurrent LangGraph fan-out validation
python/tests/integrations/langgraph_tests/test_langgraph_integration.py
Integration tests cover concurrent and nested fan-out branches, additive branch results, scope restoration, lifecycle events, and cleanup after a failing branch.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LangGraph
  participant NemoRelayCallbackHandler
  participant ScopeStack
  LangGraph->>NemoRelayCallbackHandler: start concurrent branches
  NemoRelayCallbackHandler->>ScopeStack: open parent and sibling scopes
  LangGraph->>NemoRelayCallbackHandler: complete fast branch
  NemoRelayCallbackHandler->>ScopeStack: defer non-top sibling closure
  LangGraph->>NemoRelayCallbackHandler: complete slow branch
  NemoRelayCallbackHandler->>ScopeStack: close top sibling scope
  NemoRelayCallbackHandler->>ScopeStack: drain deferred sibling scope
  NemoRelayCallbackHandler->>ScopeStack: close parent scope
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title follows Conventional Commits format, uses an allowed type and scope, states the change clearly, and is 68 characters long.
Description check ✅ Passed The description includes all template sections, checkboxes, implementation details, reviewer guidance, testing results, and related issue context.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/nemo_relay/integrations/langchain/callbacks.py`:
- Line 147: Replace datetime.timezone.utc with datetime.UTC at
python/nemo_relay/integrations/langchain/callbacks.py lines 147-147 in the
_PendingPop construction and
python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py lines
184-184 for a_ended_by, preserving the existing UTC timestamp behavior.
- Around line 156-169: The _drain_pending_pops loop currently rescans the entire
pending queue after each removal, causing quadratic close attempts for wide
fan-out. Update _drain_pending_pops to retry only the tail pending entry whose
handle can become closable after a removal, while preserving deferred entries
and removing terminal outcomes.
- Around line 171-189: Update _close_scope to use _logger.exception for the
non-deferred scope.pop failure while leaving the debug logging unchanged.
Preserve the existing _OUT_OF_ORDER_CLOSE message classification and its
documented test coverage, and track a follow-up to replace this
integration-level message coupling with a public typed error or ordering-check
API from core.
- Around line 137-154: Synchronize shared handler state transitions in the
callback close flow around _scope_handles, _pending_pops, _close_scope, and
_drain_pending_pops so concurrent worker threads cannot race. Preserve each
run’s owning scope-stack context when deferring or replaying a close, ensuring
queued closes are executed against that stack rather than another thread’s
context and are not discarded as FAILED. Add a regression test using one shared
handler with worker-thread callbacks that verifies all scopes close correctly.

In `@python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py`:
- Around line 143-146: Replace both RuntimeError-swallowing try/except blocks
around nemo_relay.scope.pop(request) with contextlib.suppress(RuntimeError),
adding the required import. Preserve the existing explanatory comment above the
with statement near the second occurrence.
- Around line 258-260: Remove the unused handler parameter from
test_relay_still_reports_out_of_order_closes_the_way_the_handler_detects; retain
the test’s direct nemo_relay.scope behavior and autouse isolated_scope_stack
fixture unchanged.
- Around line 47-52: Rename the fixture function isolated_scope_stack to
isolated_scope_stack_fixture and configure the decorator with
name="isolated_scope_stack", preserving its autouse behavior and existing
scope-stack setup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 581eff4c-e336-4c51-a3ef-fff08d78679c

📥 Commits

Reviewing files that changed from the base of the PR and between a6a8572 and 9c01b49.

📒 Files selected for processing (3)
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (19)
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

**/*: Use release tags in raw Rust-compatible SemVer without a leading v; tags such as v0.1.0 are prohibited.
Use branch prefixes feat/, fix/, docs/, test/, or refactor/ according to the change purpose.
Every commit in a pull request must include a DCO Signed-off-by: sign-off.
Before submitting a pull request, ensure pre-commit hooks, relevant tests, target-specific builds, documentation updates, and a rebase on the latest main are complete.
Use commit messages in the form type: short description, with a valid type and a first line under 72 characters.

Run the prescribed plugin validation commands, including fixture building, focused Rust and Python package tests, integration tests, documentation checks, and the broader validate-change matrix for broad runtime or public API changes.

**/*: Keep observability changes scoped, surface assumptions, and define focused validation before editing.
Run affected Rust tests and just test-rust for event-field changes; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes; update docs and examples in the same branch.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: Keep FFI and Python, Go, and Node.js binding configuration objects and subscriber/exporter methods aligned with the core observability configuration and lifecycle semantics.
Preserve complete sanitized LLM request input and annotations when enable_full_payloads is enabled, while retaining credential removal and sanitizers.
Use each exporter's documented flush and deregister order before shutdown in observability examples and implementations.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,jsx,ts,tsx,go,c,h,cc,cpp,md,toml,yml,yaml,sh}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, documentation, scripts, and configuration files; the project is Apache-2.0.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case naming in Rust and Python.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,mjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve the existing Tokio-based asynchronous model and callback/future lifetimes; do not unexpectedly block or hide async work in bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain test coverage for Python binding and wrapper changes with the Python test suite.

python/tests/**/*.py: Use pytest to run Python tests.
Do not add @pytest.mark.asyncio to tests; async tests are automatically detected by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, use unittest.mock.MagicMock or unittest.mock.AsyncMock, using spec when necessary, rather than defining a new class.
Prefix mocked class names with mock, not fake.
Prefer pytest fixtures over helper methods.
If a fixture is needed in multiple test files, define it in a conftest.py file instead of repeating it.
Define fixtures using @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and a <fixture_name>_fixture function; specify scope only when it is not function.
Prefer pytest.mark.parametrize over separate tests for different input types.
Run focused pytest tests first when the affected area is known, and run the full suite with just test-python before review.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: Lint Python with Ruff using rule sets E, F, W, and I.
Format Python with the Ruff formatter, using a 120-character line length and double quotes.
Run ty for Python type checking.
Use Python snake_case naming conventions.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,py,go,js,jsx,ts,tsx,c,h}: Run tests for every language affected by a change; changes to the core Rust crate require tests across all bindings.
Use SONAR_IGNORE_START / SONAR_IGNORE_END only for documented false positives, keep ignored blocks minimal, explain them with a comment, and obtain reviewer sign-off.
Preserve the layered architecture in which Rust provides the core runtime and C FFI, PyO3, and NAPI provide bindings that mirror the full API surface.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/{test,tests}/**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

When adding functionality, include tests in the appropriate test files for each affected language binding.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h,html,md,mdx,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include the appropriate SPDX copyright and Apache-2.0 license header in every source file.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{toml,md,rs,py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Treat plugin Relay compatibility as normal SemVer; use >=0.5,<1.0 in examples unless a plugin intentionally declares a narrower range.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/**/*.py

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Python SDK tests belong under python/tests, not under source directories.

python/**/*.py: Format changed Python wrapper and test files with uv run ruff format python python/plugin.
Run uv run ruff format python python/plugin after changing Python wrapper or test files.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep Python wrapper modules under python/nemo_relay/; the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/integrations/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python integrations must use public framework or plugin APIs.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
🪛 Ruff (0.16.1)
python/nemo_relay/integrations/langchain/callbacks.py

[warning] 147-147: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 187-187: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py

[warning] 143-146: Use contextlib.suppress(RuntimeError) instead of try-except-pass

Replace try-except-pass with with contextlib.suppress(RuntimeError): ...

(SIM105)


[warning] 184-184: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 246-251: Use contextlib.suppress(RuntimeError) instead of try-except-pass

(SIM105)


[warning] 259-259: Unused function argument: handler

(ARG001)

🔇 Additional comments (4)
python/nemo_relay/integrations/langchain/callbacks.py (2)

23-48: LGTM!


59-59: LGTM!

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py (1)

335-357: 📐 Maintainability & Code Quality

No fixture change is needed. subscribed_events is defined once in python/tests/conftest.py and shared through pytest fixture discovery.

			> Likely an incorrect or invalid review comment.
python/tests/integrations/langchain_tests/test_callbacks.py (1)

116-116: 🗄️ Data Integrity & Integration

No signature change is needed. scope.pop, _native.pyi, and the PyO3 binding all accept timestamp. The callback passes a timezone-aware datetime.

			> Likely an incorrect or invalid review comment.

Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py Outdated
Comment thread python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py Outdated
Comment thread python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py Outdated
@github-actions

Copy link
Copy Markdown

@AjayThorve AjayThorve left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported single-stack failure is fixed. One additional telemetry-fidelity issue remains inline.

Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated

@willkill07 willkill07 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative to retrying queued handles and classifying exceptions, could we consider an owner-stack-aware, top-driven state machine?

Store:

  • each run’s handle
  • owning ScopeStack
  • completion payload
  • metadata
  • timestamp

When a run ends, mark it complete and drain only its owning stack:

  • inspect scope.get_handle()
  • close it when the top UUID belongs to a completed run
  • repeat until the top is still running or is not owned by the handler

This would preserve LIFO ordering, prevent cross-stack queue loss, and avoid depending on exception-message matching.

I would also add a regression test using one shared handler across two isolated stacks, plus Ajay’s callback-time output snapshot test.

@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from 9c01b49 to fb379ea Compare August 11, 2026 19:13
@github-actions github-actions Bot added size:L PR is large and removed size:M PR is medium labels Aug 11, 2026
@SandyChapman SandyChapman changed the title fix(langchain): replay an out-of-order scope close instead of abandoning it fix(langchain): close finished scopes from the top of the stack down Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/nemo_relay/integrations/langchain/callbacks.py (1)

137-157: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_pop_scope can now raise into LangChain.

on_chain_start wraps its whole body in try/except and logs. on_chain_end and on_chain_error call _pop_scope unguarded. _prepare_lc_payloads(output) on Line 153 runs user-supplied output through serialization and can raise, and getattr/dict writes run outside any guard. Before this change, the failure surface at end time was the scope.pop call, which the old code caught. Now a serialization failure propagates out of the callback and can fail the chain run.

Wrap the body so telemetry failures stay non-fatal, consistent with on_chain_start.

🛡️ Proposed fix
     def _pop_scope(
         self, run_id: UUID, *, output: dict[str, typing.Any] | None = None, metadata: nemo_relay.Json | None = None
     ) -> None:
-        handle = self._scope_handles.pop(run_id, None)
-        stack = self._scope_stacks.pop(run_id, None)
-        if handle is None:
-            return
-
-        scope_uuid = getattr(handle, "uuid", None)
-        if scope_uuid is None:
-            _logger.error("NeMo Relay: scope handle carries no uuid; cannot close it")
-            return
-
-        self._completed[scope_uuid] = _CompletedScope(
-            handle=handle,
-            stack=stack,
-            output=_prepare_lc_payloads(output) if output is not None else None,
-            metadata=metadata,
-            ended_at=datetime.datetime.now(datetime.UTC),
-        )
-        self._close_completed_scopes(stack)
+        try:
+            handle = self._scope_handles.pop(run_id, None)
+            stack = self._scope_stacks.pop(run_id, None)
+            if handle is None:
+                return
+
+            scope_uuid = getattr(handle, "uuid", None)
+            if scope_uuid is None:
+                _logger.error("NeMo Relay: scope handle carries no uuid; cannot close it")
+                return
+
+            self._completed[scope_uuid] = _CompletedScope(
+                handle=handle,
+                stack=stack,
+                output=_prepare_lc_payloads(output) if output is not None else None,
+                metadata=metadata,
+                ended_at=datetime.datetime.now(datetime.UTC),
+            )
+            self._close_completed_scopes(stack)
+        except Exception:
+            _logger.exception("NeMo Relay: recording a completed scope failed")

Add a test that makes _prepare_lc_payloads raise and asserts the callback returns normally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/nemo_relay/integrations/langchain/callbacks.py` around lines 137 -
157, Wrap the full body of _pop_scope, including handle lookup, UUID access,
completed-scope assignment, and _prepare_lc_payloads, in exception handling so
telemetry failures are logged and never propagate to LangChain, matching
on_chain_start behavior. Add a test that forces _prepare_lc_payloads to raise
and verifies _pop_scope returns normally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py`:
- Around line 285-311: Bound or explicitly account for orphaned completions
retained by the handler’s _completed collection. Update the scope-completion
logic exercised by test_a_scope_closed_out_of_band_does_not_block_other_closes
to evict entries when their owning stack is gone or enforce a finite retention
cap, while preserving correct closure behavior; alternatively, document the
expected upper bound and operational monitoring requirement in the test and
related implementation.

In `@python/tests/integrations/langchain_tests/test_callbacks.py`:
- Around line 19-52: Update the local pop function in _make_mock_nemo_relay to
assert that the supplied handle is the current top-of-stack object before
removing it, while preserving the root-scope guard. Use identity comparison with
stack[-1] so wrong-handle closes fail the mock and handle is no longer unused.

In `@python/tests/integrations/langgraph_tests/test_langgraph_integration.py`:
- Around line 9-10: Rename the unused state parameters in the graph node
functions around the reducer and two-branch topology (including the functions at
the referenced lines) to indicate they are intentionally unused, such as using
the project’s established underscore convention, while preserving the graph
behavior and signatures required for invocation.
- Around line 244-263: Strengthen
test_parallel_fan_out_leaves_the_enclosing_scope_closable by asserting that the
callback handler actually opened the expected branch scopes during
graph.ainvoke. Use the existing subscribed_events fixture and assert lifecycle
scope events for the branch nodes, rather than relying only on the returned
branches and restored handle; do not use empty dictionaries as evidence of scope
creation.

---

Outside diff comments:
In `@python/nemo_relay/integrations/langchain/callbacks.py`:
- Around line 137-157: Wrap the full body of _pop_scope, including handle
lookup, UUID access, completed-scope assignment, and _prepare_lc_payloads, in
exception handling so telemetry failures are logged and never propagate to
LangChain, matching on_chain_start behavior. Add a test that forces
_prepare_lc_payloads to raise and verifies _pop_scope returns normally.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: a143baef-a633-41aa-9f38-3c9c5aa0e5a4

📥 Commits

Reviewing files that changed from the base of the PR and between 9c01b49 and fb379ea.

📒 Files selected for processing (4)
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (19)
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

**/*: Use release tags in raw Rust-compatible SemVer without a leading v; tags such as v0.1.0 are prohibited.
Use branch prefixes feat/, fix/, docs/, test/, or refactor/ according to the change purpose.
Every commit in a pull request must include a DCO Signed-off-by: sign-off.
Before submitting a pull request, ensure pre-commit hooks, relevant tests, target-specific builds, documentation updates, and a rebase on the latest main are complete.
Use commit messages in the form type: short description, with a valid type and a first line under 72 characters.

Run the prescribed plugin validation commands, including fixture building, focused Rust and Python package tests, integration tests, documentation checks, and the broader validate-change matrix for broad runtime or public API changes.

**/*: Keep observability changes scoped, surface assumptions, and define focused validation before editing.
Run affected Rust tests and just test-rust for event-field changes; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes; update docs and examples in the same branch.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: Keep FFI and Python, Go, and Node.js binding configuration objects and subscriber/exporter methods aligned with the core observability configuration and lifecycle semantics.
Preserve complete sanitized LLM request input and annotations when enable_full_payloads is enabled, while retaining credential removal and sanitizers.
Use each exporter's documented flush and deregister order before shutdown in observability examples and implementations.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,jsx,ts,tsx,go,c,h,cc,cpp,md,toml,yml,yaml,sh}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, documentation, scripts, and configuration files; the project is Apache-2.0.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case naming in Rust and Python.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,mjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve the existing Tokio-based asynchronous model and callback/future lifetimes; do not unexpectedly block or hide async work in bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain test coverage for Python binding and wrapper changes with the Python test suite.

python/tests/**/*.py: Use pytest to run Python tests.
Do not add @pytest.mark.asyncio to tests; async tests are automatically detected by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, use unittest.mock.MagicMock or unittest.mock.AsyncMock, using spec when necessary, rather than defining a new class.
Prefix mocked class names with mock, not fake.
Prefer pytest fixtures over helper methods.
If a fixture is needed in multiple test files, define it in a conftest.py file instead of repeating it.
Define fixtures using @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and a <fixture_name>_fixture function; specify scope only when it is not function.
Prefer pytest.mark.parametrize over separate tests for different input types.
Run focused pytest tests first when the affected area is known, and run the full suite with just test-python before review.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: Lint Python with Ruff using rule sets E, F, W, and I.
Format Python with the Ruff formatter, using a 120-character line length and double quotes.
Run ty for Python type checking.
Use Python snake_case naming conventions.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,py,go,js,jsx,ts,tsx,c,h}: Run tests for every language affected by a change; changes to the core Rust crate require tests across all bindings.
Use SONAR_IGNORE_START / SONAR_IGNORE_END only for documented false positives, keep ignored blocks minimal, explain them with a comment, and obtain reviewer sign-off.
Preserve the layered architecture in which Rust provides the core runtime and C FFI, PyO3, and NAPI provide bindings that mirror the full API surface.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/{test,tests}/**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

When adding functionality, include tests in the appropriate test files for each affected language binding.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h,html,md,mdx,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include the appropriate SPDX copyright and Apache-2.0 license header in every source file.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{toml,md,rs,py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Treat plugin Relay compatibility as normal SemVer; use >=0.5,<1.0 in examples unless a plugin intentionally declares a narrower range.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/**/*.py

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Python SDK tests belong under python/tests, not under source directories.

python/**/*.py: Format changed Python wrapper and test files with uv run ruff format python python/plugin.
Run uv run ruff format python python/plugin after changing Python wrapper or test files.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep Python wrapper modules under python/nemo_relay/; the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/integrations/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python integrations must use public framework or plugin APIs.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
🪛 Ruff (0.16.1)
python/tests/integrations/langchain_tests/test_callbacks.py

[warning] 31-31: Missing return type annotation for private function push

(ANN202)


[warning] 31-31: Missing type annotation for **kwargs

(ANN003)


[warning] 41-41: Missing return type annotation for private function pop

Add return type annotation: None

(ANN202)


[warning] 41-41: Unused function argument: handle

(ARG001)


[warning] 41-41: Missing type annotation for **kwargs

(ANN003)


[warning] 41-41: Unused function argument: kwargs

(ARG001)

python/tests/integrations/langgraph_tests/test_langgraph_integration.py

[warning] 226-226: Unused function argument: state

(ARG001)


[warning] 230-230: Unused function argument: state

(ARG001)

python/nemo_relay/integrations/langchain/callbacks.py

[warning] 46-46: Dynamically typed expressions (typing.Any) are disallowed in _current_scope_stack

(ANN401)


[warning] 155-155: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 159-159: Dynamically typed expressions (typing.Any) are disallowed in stack

(ANN401)


[warning] 189-189: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)


[warning] 192-192: Dynamically typed expressions (typing.Any) are disallowed in _current_scope_handle

(ANN401)

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py

[warning] 184-184: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 504-504: Unused lambda argument: args

(ARG005)


[warning] 504-504: Unused lambda argument: kwargs

(ARG005)

🔇 Additional comments (8)
python/nemo_relay/integrations/langchain/callbacks.py (3)

155-155: Ruff UP017 still applies here. Use datetime.UTC instead of datetime.timezone.utc. The same change is needed at python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py Line 184.


159-190: Ownership fix looks correct; the G201 lint is still open.

The drain now keys completions by scope UUID, checks stack identity before reading the active context, and closes only the current top. That removes the cross-stack discard reported earlier, and the loop is now O(n) rather than a rescan.

Line 189 still triggers Ruff G201: use _logger.exception("NeMo Relay: scope.pop failed").


23-53: LGTM!

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py (4)

184-184: Ruff UP017: use datetime.UTC here. Same change as python/nemo_relay/integrations/langchain/callbacks.py Line 155.


514-564: 🩺 Stability & Availability | ⚡ Quick win

Ownership coverage is good; worker-thread coverage is still missing.

test_two_concurrent_invocations_share_one_handler covers the reported asyncio failure and reproduces the interleaving deterministically. It does not cover the second half of the earlier concern: run_inline=True does not bind one handler to one thread, and _scope_handles, _scope_stacks, and _completed are mutated without synchronization. Add a test that drives one shared handler from a ThreadPoolExecutor, each thread on its own scope stack, and asserts every request scope closes.


1-110: LGTM!


113-283: LGTM!

Also applies to: 314-511

python/tests/integrations/langchain_tests/test_callbacks.py (1)

10-10: LGTM!

Also applies to: 133-133, 155-155, 214-214

Comment thread python/tests/integrations/langchain_tests/test_callbacks.py
@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from fb379ea to 1dbd05d Compare August 11, 2026 19:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/nemo_relay/integrations/langchain/callbacks.py (1)

153-168: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_pop_scope can now raise into the LangChain callback.

on_chain_end and on_chain_error call _pop_scope without a guard. Inside _pop_scope, _prepare_lc_payloads(output) runs outside any try. The previous implementation performed serialization inside the guarded pop path, so a serialization failure was logged and swallowed. Now it propagates out of the callback and can fail the user's chain run.

on_chain_start still swallows its exceptions, so the two paths are inconsistent.

Wrap the completion-recording body in the same guard style used by on_chain_start.

🛡️ Proposed fix
         handle = self._scope_handles.pop(run_id, None)
         stack = self._scope_stacks.pop(run_id, None)
         if handle is None:
             return
 
-        self._completed[handle.uuid] = _CompletedScope(
-            handle=handle,
-            stack=stack,
-            output=_prepare_lc_payloads(output) if output is not None else None,
-            metadata=metadata,
-            ended_at=datetime.datetime.now(datetime.UTC),
-        )
+        try:
+            prepared_output = _prepare_lc_payloads(output) if output is not None else None
+        except Exception:
+            _logger.exception("NeMo Relay: preparing chain output failed")
+            prepared_output = None
+
+        self._completed[handle.uuid] = _CompletedScope(
+            handle=handle,
+            stack=stack,
+            output=prepared_output,
+            metadata=metadata,
+            ended_at=datetime.datetime.now(datetime.UTC),
+        )
         self._close_completed_scopes(stack)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/nemo_relay/integrations/langchain/callbacks.py` around lines 153 -
168, Update _pop_scope to wrap completion-recording operations, including
_prepare_lc_payloads(output), _CompletedScope creation, and
_close_completed_scopes(stack), in the same exception-swallowing and logging
guard used by on_chain_start. Ensure serialization or close failures do not
propagate through on_chain_end or on_chain_error, while preserving handle and
stack removal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@python/nemo_relay/integrations/langchain/callbacks.py`:
- Around line 153-168: Update _pop_scope to wrap completion-recording
operations, including _prepare_lc_payloads(output), _CompletedScope creation,
and _close_completed_scopes(stack), in the same exception-swallowing and logging
guard used by on_chain_start. Ensure serialization or close failures do not
propagate through on_chain_end or on_chain_error, while preserving handle and
stack removal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 894f28bf-8759-402f-a9fd-7d97b9ad6ca2

📥 Commits

Reviewing files that changed from the base of the PR and between fb379ea and 1dbd05d.

📒 Files selected for processing (4)
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (19)
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

**/*: Use release tags in raw Rust-compatible SemVer without a leading v; tags such as v0.1.0 are prohibited.
Use branch prefixes feat/, fix/, docs/, test/, or refactor/ according to the change purpose.
Every commit in a pull request must include a DCO Signed-off-by: sign-off.
Before submitting a pull request, ensure pre-commit hooks, relevant tests, target-specific builds, documentation updates, and a rebase on the latest main are complete.
Use commit messages in the form type: short description, with a valid type and a first line under 72 characters.

Run the prescribed plugin validation commands, including fixture building, focused Rust and Python package tests, integration tests, documentation checks, and the broader validate-change matrix for broad runtime or public API changes.

**/*: Keep observability changes scoped, surface assumptions, and define focused validation before editing.
Run affected Rust tests and just test-rust for event-field changes; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes; update docs and examples in the same branch.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: Keep FFI and Python, Go, and Node.js binding configuration objects and subscriber/exporter methods aligned with the core observability configuration and lifecycle semantics.
Preserve complete sanitized LLM request input and annotations when enable_full_payloads is enabled, while retaining credential removal and sanitizers.
Use each exporter's documented flush and deregister order before shutdown in observability examples and implementations.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,jsx,ts,tsx,go,c,h,cc,cpp,md,toml,yml,yaml,sh}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, documentation, scripts, and configuration files; the project is Apache-2.0.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case naming in Rust and Python.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,mjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve the existing Tokio-based asynchronous model and callback/future lifetimes; do not unexpectedly block or hide async work in bindings.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain test coverage for Python binding and wrapper changes with the Python test suite.

python/tests/**/*.py: Use pytest to run Python tests.
Do not add @pytest.mark.asyncio to tests; async tests are automatically detected by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, use unittest.mock.MagicMock or unittest.mock.AsyncMock, using spec when necessary, rather than defining a new class.
Prefix mocked class names with mock, not fake.
Prefer pytest fixtures over helper methods.
If a fixture is needed in multiple test files, define it in a conftest.py file instead of repeating it.
Define fixtures using @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and a <fixture_name>_fixture function; specify scope only when it is not function.
Prefer pytest.mark.parametrize over separate tests for different input types.
Run focused pytest tests first when the affected area is known, and run the full suite with just test-python before review.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: Lint Python with Ruff using rule sets E, F, W, and I.
Format Python with the Ruff formatter, using a 120-character line length and double quotes.
Run ty for Python type checking.
Use Python snake_case naming conventions.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,py,go,js,jsx,ts,tsx,c,h}: Run tests for every language affected by a change; changes to the core Rust crate require tests across all bindings.
Use SONAR_IGNORE_START / SONAR_IGNORE_END only for documented false positives, keep ignored blocks minimal, explain them with a comment, and obtain reviewer sign-off.
Preserve the layered architecture in which Rust provides the core runtime and C FFI, PyO3, and NAPI provide bindings that mirror the full API surface.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/{test,tests}/**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

When adding functionality, include tests in the appropriate test files for each affected language binding.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h,html,md,mdx,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include the appropriate SPDX copyright and Apache-2.0 license header in every source file.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{toml,md,rs,py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Treat plugin Relay compatibility as normal SemVer; use >=0.5,<1.0 in examples unless a plugin intentionally declares a narrower range.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/**/*.py

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Python SDK tests belong under python/tests, not under source directories.

python/**/*.py: Format changed Python wrapper and test files with uv run ruff format python python/plugin.
Run uv run ruff format python python/plugin after changing Python wrapper or test files.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • python/tests/integrations/langchain_tests/test_callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep Python wrapper modules under python/nemo_relay/; the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/integrations/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python integrations must use public framework or plugin APIs.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
🪛 Ruff (0.16.1)
python/tests/integrations/langchain_tests/test_callbacks.py

[warning] 31-31: Missing return type annotation for private function push

(ANN202)


[warning] 31-31: Missing type annotation for **kwargs

(ANN003)


[warning] 41-41: Missing return type annotation for private function pop

Add return type annotation: None

(ANN202)


[warning] 41-41: Missing type annotation for **kwargs

(ANN003)


[warning] 41-41: Unused function argument: kwargs

(ARG001)

python/tests/integrations/langgraph_tests/test_langgraph_integration.py

[warning] 226-226: Unused function argument: state

(ARG001)


[warning] 230-230: Unused function argument: state

(ARG001)

python/nemo_relay/integrations/langchain/callbacks.py

[warning] 166-166: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 200-200: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py

[warning] 184-184: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 507-507: Unused lambda argument: args

(ARG005)


[warning] 507-507: Unused lambda argument: kwargs

(ARG005)

🔇 Additional comments (10)
python/nemo_relay/integrations/langchain/callbacks.py (4)

166-166: Ruff UP017: use datetime.UTC. Still reported at Line 166 here and at Line 184 in python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py.


170-200: Ruff G201: use _logger.exception at Line 200. The drain logic itself is correct: the entry is removed from _completed before the pop attempt, so a failure is not retried and the loop always terminates.


23-50: LGTM!


53-68: LGTM!

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py (3)

184-184: Ruff UP017: use datetime.UTC here as well. Same rule violation as python/nemo_relay/integrations/langchain/callbacks.py Line 166.


285-314: The retained-completion behavior is still unbounded. _completed has no eviction path for a scope closed out of band. The docstring claims the retention is bounded, but a long-lived shared handler accumulates one _CompletedScope per orphaned scope, each holding serialized output.


63-110: LGTM!

Also applies to: 113-158, 207-253, 256-282, 317-346, 349-385, 388-422, 425-458, 461-486, 489-514, 517-567

python/tests/integrations/langgraph_tests/test_langgraph_integration.py (2)

226-232: Ruff ARG001: the state parameters in slow and fast are still unused. Rename them to _state.


9-10: LGTM!

Also applies to: 213-214, 244-272

python/tests/integrations/langchain_tests/test_callbacks.py (1)

10-10: LGTM!

Also applies to: 20-53, 135-135, 157-157, 216-216

Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/nemo_relay/integrations/langchain/callbacks.py Outdated
Comment thread python/nemo_relay/integrations/langchain/callbacks.py
@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from 1dbd05d to b0281d9 Compare August 12, 2026 16:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/nemo_relay/integrations/langchain/callbacks.py`:
- Line 177: Update the timestamp construction near ended_at to use datetime.UTC
instead of datetime.timezone.utc, and change the error logging statements in the
relevant callback handlers around _logger at the two reported locations to use
_logger.exception(...) while preserving their existing messages and exception
context.

In `@python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py`:
- Line 185: Update the datetime construction in the test to use datetime.UTC
instead of datetime.timezone.utc, preserving the existing timezone-aware
timestamp behavior and resolving Ruff UP017.
- Around line 453-458: Both worker-thread tests ignore join timeouts. In
python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py lines
453-458 and 482-486, add a worker.is_alive() liveness assertion immediately
after each worker.join(timeout=10), before the existing failures/_completed and
created assertions, using the message “the worker thread did not finish”.

In `@python/tests/integrations/langgraph_tests/test_langgraph_integration.py`:
- Around line 338-373: Strengthen test_a_failing_node_still_closes_its_siblings
by subscribing the callback handler to lifecycle events, flushing queued
subscriber events after the scope cleanup, and asserting scope.start and
scope.end were emitted for both boom and survivor. Keep the existing failure and
scope-restoration assertions, and verify the assertions use the flushed events
so both branch lifecycles are proven.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 7b1bf521-40af-4d67-bc14-12a537c55818

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbd05d and b0281d9.

📒 Files selected for processing (3)
  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (19)
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

**/*: Use release tags in raw Rust-compatible SemVer without a leading v; tags such as v0.1.0 are prohibited.
Use branch prefixes feat/, fix/, docs/, test/, or refactor/ according to the change purpose.
Every commit in a pull request must include a DCO Signed-off-by: sign-off.
Before submitting a pull request, ensure pre-commit hooks, relevant tests, target-specific builds, documentation updates, and a rebase on the latest main are complete.
Use commit messages in the form type: short description, with a valid type and a first line under 72 characters.

Run the prescribed plugin validation commands, including fixture building, focused Rust and Python package tests, integration tests, documentation checks, and the broader validate-change matrix for broad runtime or public API changes.

**/*: Keep observability changes scoped, surface assumptions, and define focused validation before editing.
Run affected Rust tests and just test-rust for event-field changes; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes; update docs and examples in the same branch.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: Keep FFI and Python, Go, and Node.js binding configuration objects and subscriber/exporter methods aligned with the core observability configuration and lifecycle semantics.
Preserve complete sanitized LLM request input and annotations when enable_full_payloads is enabled, while retaining credential removal and sanitizers.
Use each exporter's documented flush and deregister order before shutdown in observability examples and implementations.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,jsx,ts,tsx,go,c,h,cc,cpp,md,toml,yml,yaml,sh}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, documentation, scripts, and configuration files; the project is Apache-2.0.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Use snake_case naming in Rust and Python.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,js,mjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve the existing Tokio-based asynchronous model and callback/future lifetimes; do not unexpectedly block or hide async work in bindings.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep Python wrapper modules under python/nemo_relay/; the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/nemo_relay/integrations/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python integrations must use public framework or plugin APIs.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: Lint Python with Ruff using rule sets E, F, W, and I.
Format Python with the Ruff formatter, using a 120-character line length and double quotes.
Run ty for Python type checking.
Use Python snake_case naming conventions.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,py,go,js,jsx,ts,tsx,c,h}: Run tests for every language affected by a change; changes to the core Rust crate require tests across all bindings.
Use SONAR_IGNORE_START / SONAR_IGNORE_END only for documented false positives, keep ignored blocks minimal, explain them with a comment, and obtain reviewer sign-off.
Preserve the layered architecture in which Rust provides the core runtime and C FFI, PyO3, and NAPI provide bindings that mirror the full API surface.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{rs,py,go,js,jsx,ts,tsx,c,h,html,md,mdx,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include the appropriate SPDX copyright and Apache-2.0 license header in every source file.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/*.{toml,md,rs,py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Treat plugin Relay compatibility as normal SemVer; use >=0.5,<1.0 in examples unless a plugin intentionally declares a narrower range.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/**/*.py

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Python SDK tests belong under python/tests, not under source directories.

python/**/*.py: Format changed Python wrapper and test files with uv run ruff format python python/plugin.
Run uv run ruff format python python/plugin after changing Python wrapper or test files.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/integrations/langchain/callbacks.py
python/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain test coverage for Python binding and wrapper changes with the Python test suite.

python/tests/**/*.py: Use pytest to run Python tests.
Do not add @pytest.mark.asyncio to tests; async tests are automatically detected by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, use unittest.mock.MagicMock or unittest.mock.AsyncMock, using spec when necessary, rather than defining a new class.
Prefix mocked class names with mock, not fake.
Prefer pytest fixtures over helper methods.
If a fixture is needed in multiple test files, define it in a conftest.py file instead of repeating it.
Define fixtures using @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and a <fixture_name>_fixture function; specify scope only when it is not function.
Prefer pytest.mark.parametrize over separate tests for different input types.
Run focused pytest tests first when the affected area is known, and run the full suite with just test-python before review.

Files:

  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
**/{test,tests}/**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

When adding functionality, include tests in the appropriate test files for each affected language binding.

Files:

  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
🧠 Learnings (22)
📓 Common learnings
Learnt from: AjayThorve
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/nemo_relay/integrations/langchain/callbacks.py:137-154
Timestamp: 2026-08-11T17:33:57.356Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, `NemoRelayCallbackHandler` can be shared by concurrent async LangGraph invocations that each use a different `ScopeStack`. A deferred `_PendingPop` must retain its originating scope-stack context, and `_drain_pending_pops()` must retry only entries owned by the active scope stack. A handler-wide unpartitioned pending queue can otherwise retry a close on another context's stack, receive `NotFound`, remove it as terminal, and leave the owning scope open. Synchronization alone does not prevent this ownership failure.
Learnt from: SandyChapman
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py:285-314
Timestamp: 2026-08-11T19:49:02.958Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, a deferred `_CompletedScope` cannot be safely classified as orphaned because `ScopeStack` and `ScopeHandle` do not provide weak-reference support or an API to determine whether a handle remains on its owning stack. Eviction or a finite retention cap can discard a valid deferred completion and recreate the LangChain scope leak. A runtime-side stack-membership or liveness accessor is required before safe orphan-completion eviction can be implemented.
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 462
File: python/nemo_relay/_context.py:20-28
Timestamp: 2026-07-17T17:36:18.512Z
Learning: In the Python SDK, `python/nemo_relay/_context.py::ensure_scope_stack()` may use `nemo_relay._native_scope_stack_active()` when `nemo_relay._scope_stack_var` is unset: `crates/core/src/api/runtime/scope_stack.rs::sync_thread_scope_stack()` deliberately updates only the thread-local handle and does not set the explicit-active flag. Thus this branch preserves explicitly bound worker-thread stacks without allowing a stack synchronized by another asyncio task to be treated as task ownership.
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-07-28T21:11:43.864Z
Learning: In `crates/core/src/api/runtime/state.rs`, async tool and LLM middleware snapshot chains must isolate callback panics with `AssertUnwindSafe(...).catch_unwind()`: sanitizer failures/panics fail open by preserving the latest valid payload, while conditional-execution guardrails and request intercepts convert panics into `FlowError::Internal`.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to crates/core/src/api/runtime/state.rs : Add chain-execution helpers to `NemoRelayContextState`, following existing helpers such as `tool_sanitize_request_chain` or `tool_request_intercepts_chain`.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-04T01:50:26.470Z
Learning: Scope stacks must remain hierarchical, always include a root scope, preserve parent-child event relationships and scope-local visibility, provide cleanup boundaries, and isolate concurrent requests.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/maintain-dynamic-plugins/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:18.141Z
Learning: Applies to crates/**/*.rs : Runtime helpers must cover marks, scopes, continuations, and isolated scope stacks.
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to **/*.{rs,py,js,ts,tsx,go,java,kt,swift} : Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.
📚 Learning: 2026-08-11T19:49:02.958Z
Learnt from: SandyChapman
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py:285-314
Timestamp: 2026-08-11T19:49:02.958Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, a deferred `_CompletedScope` cannot be safely classified as orphaned because `ScopeStack` and `ScopeHandle` do not provide weak-reference support or an API to determine whether a handle remains on its owning stack. Eviction or a finite retention cap can discard a valid deferred completion and recreate the LangChain scope leak. A runtime-side stack-membership or liveness accessor is required before safe orphan-completion eviction can be implemented.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-11T17:33:57.356Z
Learnt from: AjayThorve
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/nemo_relay/integrations/langchain/callbacks.py:137-154
Timestamp: 2026-08-11T17:33:57.356Z
Learning: In `python/nemo_relay/integrations/langchain/callbacks.py`, `NemoRelayCallbackHandler` can be shared by concurrent async LangGraph invocations that each use a different `ScopeStack`. A deferred `_PendingPop` must retain its originating scope-stack context, and `_drain_pending_pops()` must retry only entries owned by the active scope stack. A handler-wide unpartitioned pending queue can otherwise retry a close on another context's stack, receive `NotFound`, remove it as terminal, and leave the owning scope open. Synchronization alone does not prevent this ownership failure.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-17T17:36:18.512Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 462
File: python/nemo_relay/_context.py:20-28
Timestamp: 2026-07-17T17:36:18.512Z
Learning: In the Python SDK, `python/nemo_relay/_context.py::ensure_scope_stack()` may use `nemo_relay._native_scope_stack_active()` when `nemo_relay._scope_stack_var` is unset: `crates/core/src/api/runtime/scope_stack.rs::sync_thread_scope_stack()` deliberately updates only the thread-local handle and does not set the explicit-active flag. Thus this branch preserves explicitly bound worker-thread stacks without allowing a stack synchronized by another asyncio task to be treated as task ownership.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-08T21:17:00.650Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-binding-feature/SKILL.md:0-0
Timestamp: 2026-07-08T21:17:00.650Z
Learning: Applies to {python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go} : Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-17T02:50:34.317Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to crates/core/src/api/runtime/state.rs : Add chain-execution helpers to `NemoRelayContextState`, following existing helpers such as `tool_sanitize_request_chain` or `tool_request_intercepts_chain`.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
📚 Learning: 2026-07-27T20:27:55.340Z
Learnt from: fede-kamel
Repo: NVIDIA/NeMo-Relay PR: 554
File: crates/core/tests/integration/pipeline_tests.rs:1442-1515
Timestamp: 2026-07-27T20:27:55.340Z
Learning: In `crates/core/tests/integration/pipeline_tests.rs`, integration tests are serialized with `TEST_MUTEX` and begin with `reset_global()`, which replaces the whole `NemoRelayContextState`. Therefore, the file’s established trailing `deregister_subscriber(...).unwrap()` cleanup pattern does not leak subscribers between tests if a prior test panics; do not request a per-test RAII cleanup guard unless proposing a deliberate file-wide cleanup-pattern refactor.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-04T01:50:26.470Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-04T01:50:26.470Z
Learning: Scope stacks must remain hierarchical, always include a root scope, preserve parent-child event relationships and scope-local visibility, provide cleanup boundaries, and isolate concurrent requests.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T21:11:43.864Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-07-28T21:11:43.864Z
Learning: In `crates/core/src/api/runtime/state.rs`, async tool and LLM middleware snapshot chains must isolate callback panics with `AssertUnwindSafe(...).catch_unwind()`: sanitizer failures/panics fail open by preserving the latest valid payload, while conditional-execution guardrails and request intercepts convert panics into `FlowError::Internal`.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
📚 Learning: 2026-06-26T15:15:34.329Z
Learnt from: teerthsharma
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-06-26T15:15:34.329Z
Learning: In NeMo-Relay adaptive topology-aware ACG, the intended convergence behavior is to detect stability from a reusable agent workflow's stable prompt prefix under the same learning key, while allowing variable task-specific suffixes; learning must reopen when the prompt topology changes. This clarification is relevant to reviews of `crates/adaptive/src/acg_learner.rs` and related convergence tests.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T21:11:43.864Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 0
File: :0-0
Timestamp: 2026-07-28T21:11:43.864Z
Learning: Queued event-publication assertions in NeMo Relay tests must call `flush_subscribers()` after the triggering lifecycle operation, including after scope-pop cleanup checks, because sanitizer and subscriber delivery occurs asynchronously on the dispatcher.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T13:13:11.790Z
Learnt from: afourniernv
Repo: NVIDIA/NeMo-Relay PR: 702
File: docs/about-nemo-relay/concepts/scopes.mdx:103-108
Timestamp: 2026-08-07T13:13:11.790Z
Learning: In the Rust runtime, `crates/core/src/api/tool.rs::tool_call_execute` and `crates/core/src/api/llm.rs::llm_call_execute` do not push `Tool` or `Llm` scopes. They create parent-linked lifecycle events by using `resolve_parent_uuid` and `with_active_event_uuid`; therefore, a managed tool or LLM call under an active `Agent` scope leaves the explicit scope stack unchanged.

Applied to files:

  • python/nemo_relay/integrations/langchain/callbacks.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-11T19:44:42.536Z
Learnt from: SandyChapman
Repo: NVIDIA/NeMo-Relay PR: 755
File: python/tests/integrations/langgraph_tests/test_langgraph_integration.py:9-10
Timestamp: 2026-08-11T19:44:42.536Z
Learning: In `python/tests/integrations/langgraph_tests/test_langgraph_integration.py`, LangGraph node functions passed to `StateGraph.add_node` must retain their `state` parameter name because renaming it to `_state` causes `ty` to reject the node signature. The repository Ruff configuration enables only `E`, `F`, `W`, and `I`, so `ARG001` unused-argument findings do not apply.

Applied to files:

  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
📚 Learning: 2026-07-17T02:50:34.317Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/add-middleware/SKILL.md:0-0
Timestamp: 2026-07-17T02:50:34.317Z
Learning: Applies to **/*.{rs,py,js,ts,tsx,go,java,kt,swift} : Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Applied to files:

  • python/tests/integrations/langgraph_tests/test_langgraph_integration.py
  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Define fixtures using `pytest.fixture(name="<fixture_name>"[, scope="<scope>"])` and a `<fixture_name>_fixture` function; specify `scope` only when it is not `function`.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Prefer pytest fixtures over helper methods.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : If a fixture is needed in multiple test files, define it in a `conftest.py` file instead of repeating it.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-08-07T22:42:38.991Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/test-python-binding/SKILL.md:0-0
Timestamp: 2026-08-07T22:42:38.991Z
Learning: Applies to python/tests/**/*.py : Prefix mocked class names with `mock`, not `fake`.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-05-21T22:49:35.949Z
Learnt from: CR
Repo: NVIDIA/NeMo-Relay PR: 0
File: .agents/skills/prepare-pr/SKILL.md:0-0
Timestamp: 2026-05-21T22:49:35.949Z
Learning: Branch scope should be coherent and reviewable

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T03:31:11.365Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 564
File: crates/core/src/api/llm.rs:859-875
Timestamp: 2026-07-28T03:31:11.365Z
Learning: For NeMo Relay asynchronous middleware under RELAY-509, no implicit middleware timeout is permitted. FIFO publication barriers intentionally wait for delayed sanitizer work; pending completion-based callbacks must resolve, reject, or observe cancellation. This is documented behavior, not an availability-timeout defect.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T20:07:33.786Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 571
File: crates/core/src/api/runtime/state.rs:996-1020
Timestamp: 2026-07-28T20:07:33.786Z
Learning: In NeMo Relay, RELAY-509 defines sanitizer callback failures as intentional fail-open behavior: sanitizer chains retain and publish the last valid event or payload snapshot, while logging the failure with callback context. This policy applies to event, tool request/response, and LLM request/response sanitizer chains and is documented in the public documentation and migration guide.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
📚 Learning: 2026-07-28T03:31:28.749Z
Learnt from: willkill07
Repo: NVIDIA/NeMo-Relay PR: 564
File: crates/core/src/api/runtime/subscriber_dispatcher.rs:134-141
Timestamp: 2026-07-28T03:31:28.749Z
Learning: For RELAY-509 asynchronous middleware, `crates/core/src/api/runtime/subscriber_dispatcher.rs` intentionally uses FIFO publication barriers without an implicit timeout. `flush_subscribers` must wait for pending asynchronous publication work; pending completions are expected to resolve, reject, or observe cancellation rather than being released by a timeout.

Applied to files:

  • python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py
🪛 Ruff (0.16.1)
python/nemo_relay/integrations/langchain/callbacks.py

[warning] 177-177: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 218-218: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)


[warning] 236-236: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)

python/tests/integrations/langgraph_tests/test_langgraph_integration.py

[warning] 226-226: Unused function argument: state

(ARG001)


[warning] 230-230: Unused function argument: state

(ARG001)


[warning] 284-284: Unused function argument: state

(ARG001)


[warning] 288-288: Unused function argument: state

(ARG001)


[warning] 292-292: Unused function argument: state

(ARG001)


[warning] 348-348: Unused function argument: state

(ARG001)


[warning] 350-350: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 352-352: Unused function argument: state

(ARG001)


[warning] 367-368: Use a single with statement with multiple contexts instead of nested with statements

(SIM117)

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py

[warning] 185-185: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)

🔇 Additional comments (9)
python/tests/integrations/langgraph_tests/test_langgraph_integration.py (4)

9-10: LGTM!


213-241: LGTM!


244-272: LGTM!


275-336: LGTM!

python/nemo_relay/integrations/langchain/callbacks.py (3)

8-10: LGTM!

Also applies to: 24-71


80-87: LGTM!

Also applies to: 128-129, 159-179


181-237: LGTM!

python/tests/integrations/langchain_tests/test_callbacks_scope_stack.py (2)

1-46: LGTM!

Also applies to: 49-112, 114-159, 162-206, 208-254, 257-283, 286-315, 318-346, 349-385, 388-422, 489-522, 525-539


133-133: 📐 Maintainability & Code Quality

No change needed. subscribed_events is a function-scoped fixture in python/tests/conftest.py and registers before each test.

			> Likely an incorrect or invalid review comment.

Comment thread python/nemo_relay/integrations/langchain/callbacks.py
@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from b0281d9 to 4f8cd00 Compare August 12, 2026 16:24
@github-actions github-actions Bot added size:XL PR is extra large and removed size:L PR is large labels Aug 12, 2026
@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from 4f8cd00 to d3dd594 Compare August 12, 2026 16:25
Relay closes scopes strictly LIFO, but LangGraph schedules sibling chain runs as
concurrent asyncio tasks that share one scope stack, so two siblings can finish
in an order the stack rejects: A starts, B starts, A ends, B ends.

When that happened the handler lost the scope. `_pop_scope` removed the handle
from `_scope_handles` before attempting the pop, then swallowed the rejection, so
the scope stayed live on the stack with nothing left able to close it. It then
stayed current for everything that followed, and the caller's own enclosing scope
raised on exit:

    RuntimeError: invalid argument: scope handle is not at the top of the stack

Downstream that reported fully successful agent work as failed (NVBug 6562846):
the enclosing scope belonged to a NeMo Fabric Deep Agents invocation, whose
adapter turned the teardown error into an invocation failure.

Track each run's handle. When a run ends, record its prepared output, metadata and
end timestamp as completed state, then close finished scopes from the top of the
active stack down: read the current handle, close it when its uuid belongs to a
completed run of ours, and stop as soon as the top is a run still going or a scope
this handler does not own.

Because only the current top is ever closed, the stack is never asked to accept a
close it would reject. There is no speculative pop, so there is no failure to
classify and no dependency on the runtime's error text. Ownership needs no separate
check either: a scope sits on exactly one stack, so its uuid can only be the current
top there. Comparing ScopeStack objects would not work in any case -- propagating a
stack to a worker thread yields a different Python wrapper for the same stack, with
no way to correlate the two -- and a handler completing a run on a worker thread has
to keep working.

Output is prepared before the run is dropped from the handler's map, and degrades to
None if it cannot be serialized. Serializing walks caller-supplied data and can fail
on its own -- a cyclic output raises RecursionError -- and a run removed with no
completion recorded is a scope nothing can ever close. Losing a payload costs one
scope's detail; letting the failure escape would cost the scope itself. End
timestamps are captured at callback time so a delayed close still reports when the
run ended.

A completion is deleted only once the stack has accepted the close. The runtime can
refuse before it mutates the stack, and the completion is the only record able to
close that scope.

Handler state is serialized: start, completion, top-check and pop are multi-step
transitions, and a top-check is only meaningful if the pop it authorizes cannot be
overtaken by another thread pushing onto the same stack. Reading the current handle
is guarded by a status check, since it would otherwise create a scope stack in a
context that had none.

Tests use a real scope stack. The existing handler tests mock nemo_relay, and the
mock accepted any pop in any order, so it could not observe the LIFO rule at all; it
now models the stack and rejects closing anything but the top. LangGraph tests drive
real graphs: a two-branch fan-out, a nested three-branch fan-out, and a failing node
completing through on_chain_error.

Reported and reviewed by @AjayThorve and @willkill07.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@SandyChapman
SandyChapman force-pushed the fix/langchain-callback-scope-leak-0.7 branch from d3dd594 to eeef8f7 Compare August 12, 2026 16:32
A scope rebuilt from a PropagationContext carries the uuid of the scope it
stands in for, so matching a queued completion on uuid alone could pop the
stand-in: the pop succeeds, the completion is discarded, and the real scope is
stranded on its own stack. Match on uuid and name together, which separates the
two because the runtime names rebuilt scopes propagated-parent/propagated-root.

Adds a regression test for the cross-stack close and one pinning those reserved
names, so an upstream rename fails here instead of silently closing the wrong
scope.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@willkill07

Copy link
Copy Markdown
Member

/merge

@rapids-bot
rapids-bot Bot merged commit f245a43 into NVIDIA:release/0.7 Aug 12, 2026
63 of 72 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug issue describes bug; PR fixes bug lang:python PR changes/introduces Python code size:XL PR is extra large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants