Skip to content

fix: restore task-local Python scope stacks#462

Merged
rapids-bot[bot] merged 1 commit into
NVIDIA:release/0.6from
willkill07:fix/relay-497-task-safe-python-scope-sync
Jul 17, 2026
Merged

fix: restore task-local Python scope stacks#462
rapids-bot[bot] merged 1 commit into
NVIDIA:release/0.6from
willkill07:fix/relay-497-task-safe-python-scope-sync

Conversation

@willkill07

@willkill07 willkill07 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Overview

Restore the active task's Python scope stack before every context-sensitive native lifecycle call. This prevents concurrent asyncio tasks from closing scopes or invoking tool/LLM middleware against a sibling task's stack.

  • 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

  • Centralize ContextVar-to-native stack synchronization in a private helper.
  • Apply synchronization to scope, tool, and LLM lifecycle wrappers, including managed and streaming execution.
  • Close context-manager scopes through the synchronized pop() wrapper.
  • Add concurrent asyncio regressions for scope cleanup, middleware isolation, lifecycle helpers, and event ancestry.

Where should the reviewer start?

Start with python/nemo_relay/_context.py, then python/tests/test_context_isolation.py for the concurrent task coverage.

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

  • Fixes RELAY-497

Summary by CodeRabbit

  • Bug Fixes

    • Improved scope and context handling for concurrent operations.
    • Ensured tool and LLM lifecycle events remain associated with the correct task.
    • Improved cleanup when scoped operations complete, including status metadata handling.
  • Tests

    • Added coverage for concurrent tool and LLM operations.
    • Added validation for task-local scope restoration and event ancestry.

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 requested a review from a team as a code owner July 17, 2026 14:08
@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 Jul 17, 2026
@willkill07 willkill07 self-assigned this Jul 17, 2026
@willkill07 willkill07 added this to the 0.6 milestone Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Centralizes scope-stack synchronization in a shared context helper, applies it across LLM and tool lifecycle APIs, routes scope cleanup through the Python pop wrapper, and adds concurrent task-isolation coverage for scope restoration and event ancestry.

Changes

Task-local scope isolation

Layer / File(s) Summary
Centralize scope-stack synchronization
python/nemo_relay/_context.py, python/nemo_relay/scope.py
Adds ensure_scope_stack() and uses it as the shared scope initialization path.
Apply synchronization to lifecycle APIs
python/nemo_relay/llm.py, python/nemo_relay/tools.py, python/nemo_relay/scope.py
Initializes scope state before LLM and tool lifecycle operations, and closes managed scopes through pop().
Validate concurrent task ownership
python/tests/test_context_isolation.py
Tests per-task scope restoration, tool and LLM lifecycle isolation, streamed execution, and event parent UUIDs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant LLMOrTools
  participant ensure_scope_stack
  participant NativeLifecycle
  participant ScopeSubscribers
  Task->>LLMOrTools: invoke lifecycle helper
  LLMOrTools->>ensure_scope_stack: synchronize task scope
  ensure_scope_stack->>NativeLifecycle: update native scope state
  LLMOrTools->>NativeLifecycle: execute lifecycle operation
  NativeLifecycle->>ScopeSubscribers: emit scope events
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits and accurately summarizes the task-local scope stack fix.
Description check ✅ Passed The description matches the template with Overview, Details, review start, checklist items, and related issue reference.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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: 2

🤖 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/_context.py`:
- Around line 20-28: The scope initialization logic around
_native_scope_stack_active must not treat an active thread-local stack as
ownership for a fresh asyncio task; detect an active task and initialize it
through get_scope_stack(), while preserving explicit worker-thread behavior.
Update tests to exercise this through the public API with initially unset
ContextVar values, avoiding direct _scope_stack_var manipulation.

In `@python/tests/test_context_isolation.py`:
- Around line 50-65: Update the test’s run_scope/main coordination to use
asyncio.Event barriers: ensure task B has activated its scope before task A
exits, rather than relying on asyncio.sleep(0) or asserting scheduler order.
Replace the completion-order assertion with an assertion that both tasks
complete, while preserving the scope setup and _scope_stack_var reset behavior.
🪄 Autofix (Beta)

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: 4f07593e-93c8-463e-beed-78e5ebcbaeb7

📥 Commits

Reviewing files that changed from the base of the PR and between 0325ba2 and 5a50370.

📒 Files selected for processing (5)
  • python/nemo_relay/_context.py
  • python/nemo_relay/llm.py
  • python/nemo_relay/scope.py
  • python/nemo_relay/tools.py
  • python/tests/test_context_isolation.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 (15)
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.py
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: Use Json = serde_json::Value in Rust-facing runtime APIs where the existing code expects JSON payloads.
Use Result<T> with FlowError in core runtime paths, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Python wrapper modules live under python/nemo_relay/, and the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/nemo_relay/scope.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: When changing the Python wrapper package, tests, or docs tooling, lint with Ruff (E, F, W, I), format with Ruff formatter (120-character lines, double quotes), and pass ty type checking.
Add the SPDX license header to all Python source files using the # comment form.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.py
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.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/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/nemo_relay/scope.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/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/nemo_relay/scope.py
{crates/**/src/**/*.rs,python/**/*.py}

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

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.py
**/*.{py,go,js,ts}

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

Keep Python, Go, and Node.js config objects and subscriber/exporter methods aligned so all bindings expose the same logical knobs and semantics.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.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.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.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.

Files:

  • python/nemo_relay/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.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/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/tests/test_context_isolation.py
  • python/nemo_relay/scope.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/_context.py
  • python/nemo_relay/tools.py
  • python/nemo_relay/llm.py
  • python/nemo_relay/scope.py
python/tests/**/*.py

📄 CodeRabbit inference engine (.agents/skills/test-python-binding/SKILL.md)

python/tests/**/*.py: Pytest is used to run tests.
Do not add @pytest.mark.asyncio to any test; async tests are automatically detected and run by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, do not define a new class; use unittest.mock.MagicMock or unittest.mock.AsyncMock, with the spec constructor argument when necessary.
Name mocked classes with the mock prefix, not fake.
Prefer pytest fixtures over helper methods.
Do not repeat fixtures; if a fixture is needed in multiple test files, place it in a conftest.py file.
When creating a fixture, use @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and define the fixture function as def <fixture_name>_fixture() -> <return_type>:; only specify scope when it is not function.
Prefer pytest.mark.parametrize over creating individual tests for different input types.

Files:

  • python/tests/test_context_isolation.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/test_context_isolation.py
🪛 OpenGrep (1.25.0)
python/tests/test_context_isolation.py

[ERROR] 98-102: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)


[ERROR] 158-162: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)

🪛 Ruff (0.15.21)
python/tests/test_context_isolation.py

[warning] 52-52: Missing return type annotation for private function run_scope

Add return type annotation: None

(ANN202)


[warning] 61-61: Missing return type annotation for private function main

Add return type annotation: None

(ANN202)


[warning] 71-71: Missing return type annotation for private function run_tool

(ANN202)


[warning] 80-80: Boolean positional value in function call

(FBT003)


[warning] 81-81: Unused lambda argument: name

(ARG005)


[warning] 87-87: Unused lambda argument: name

(ARG005)


[warning] 107-107: Missing return type annotation for private function main

(ANN202)


[warning] 128-128: Missing return type annotation for private function run_llm

(ANN202)


[warning] 134-134: Missing return type annotation for private function intercept

(ANN202)


[warning] 134-134: Unused function argument: name

(ARG001)


[warning] 140-140: Boolean positional value in function call

(FBT003)


[warning] 166-166: Missing return type annotation for private function stream_func

(ANN202)


[warning] 184-184: Missing return type annotation for private function main

(ANN202)

🔇 Additional comments (4)
python/nemo_relay/scope.py (1)

24-24: LGTM!

Also applies to: 260-260

python/nemo_relay/llm.py (1)

36-36: LGTM!

Also applies to: 123-123, 174-174, 262-262, 360-360, 393-393, 412-412

python/nemo_relay/tools.py (1)

25-25: LGTM!

Also applies to: 99-100, 132-133, 185-186, 205-206, 224-225

python/tests/test_context_isolation.py (1)

68-203: LGTM!

Comment thread python/nemo_relay/_context.py
Comment thread python/tests/test_context_isolation.py
@github-actions

Copy link
Copy Markdown

@mnajafian-nv mnajafian-nv 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.

LGTM

@willkill07

Copy link
Copy Markdown
Member Author

/merge

@rapids-bot
rapids-bot Bot merged commit 8c334e7 into NVIDIA:release/0.6 Jul 17, 2026
36 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:M PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants