Skip to content

fix(node): make callback failures catchable#523

Merged
willkill07 merged 3 commits into
NVIDIA:release/0.6from
willkill07:wkk_fix-relay-544-node-callback-failures
Jul 21, 2026
Merged

fix(node): make callback failures catchable#523
willkill07 merged 3 commits into
NVIDIA:release/0.6from
willkill07:wkk_fix-relay-544-node-callback-failures

Conversation

@willkill07

@willkill07 willkill07 commented Jul 21, 2026

Copy link
Copy Markdown
Member

Overview

Make Node tool and LLM callback failures reject their returned promises instead of terminating the Node process.

  • 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

  • Wrap synchronous tool and LLM execution callbacks on the JavaScript thread so thrown exceptions cross N-API as error envelopes.
  • Normalize top-level undefined to null and copy callback results through recursive JSON validation before NAPI-RS conversion.
  • Reject BigInt, sparse arrays, circular values, non-finite numbers, and other unsupported JSON values without aborting Node.
  • Apply the same validation to promise-aware tool and LLM callbacks while preserving original rejection messages.
  • Add regressions for sync throws, sync and async BigInt results, stateful getters, custom array iterators, process survival, and supported undefined behavior.
  • Keep the public Node callback declarations and sync-only execution semantics unchanged.
  • Breaking changes: none.

Validation:

  • Focused Node callback, tool, and LLM tests: 94 passed.
  • Full Node test suite: 283 passed.
  • just build-node
  • just test-node
  • cargo fmt --all
  • just test-rust
  • cargo clippy --workspace --all-targets -- -D warnings
  • uv run pre-commit run --all-files
  • Targeted pre-commit checks for the final staged files.

Where should the reviewer start?

Start with safe_execution_callback in crates/node/src/callable.rs, then compare the promise-aware validation in crates/node/src/promise_call.rs and the process-survival regressions in crates/node/tests/tools_tests.mjs.

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

  • Closes RELAY-544

Summary by CodeRabbit

  • New Features

    • Tool and LLM execution now accept regular JavaScript callback functions directly, with safer cross-boundary execution.
  • Bug Fixes

    • More consistent rejection for invalid or unsupported callback results (e.g., circular references, non-finite numbers, BigInt/sparse arrays), avoiding runtime instability.
    • Completion argument validation is stricter: invalid inputs now fail explicitly instead of defaulting.
  • Tests

    • Added/expanded tool and LLM execution cases covering thrown callbacks, invalid JSON-like outputs, getter/iterator edge cases, and continued operation after failures.

Closes RELAY-544

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 requested a review from a team as a code owner July 21, 2026 19:09
@github-actions github-actions Bot added size:M PR is medium Bug issue describes bug; PR fixes bug lang:js PR changes/introduces Javascript/Typescript code lang:rust PR changes/introduces Rust code labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Node tool and LLM execution APIs now accept JavaScript callbacks directly, validate synchronous and promise results as JSON-safe values, unwrap callback error envelopes, and preserve process usability after callback failures.

Changes

Node callback safety

Layer / File(s) Summary
JSON safety wrappers
crates/node/src/callback_factory.rs, crates/node/src/lib.rs
Cached execution and promise factories convert supported callback results to JSON-safe values and report unsupported values, non-finite numbers, circular structures, and callback exceptions.
Execution API callback wiring
crates/node/src/api/mod.rs, crates/node/src/callable.rs
Tool and LLM execution functions accept JsFunction, invoke it through safe execution, and unwrap { ok, value/error } results into JSON or FlowError.
Promise callback wiring and error propagation
crates/node/src/promise_call.rs
Promise-aware execution uses the shared wrapper and propagates JSON conversion failures as internal FlowError values instead of defaulting to null.
Callback safety validation
crates/node/tests/llm_tests.mjs, crates/node/tests/tools_tests.mjs
Tests cover thrown callbacks, invalid results, recovery after failures, single getter evaluation, and index-based array serialization.

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

Sequence Diagram(s)

sequenceDiagram
  participant NodeApi
  participant JsCallback
  participant callback_factory
  participant ExecutionApi
  NodeApi->>callback_factory: wrap JsFunction
  callback_factory->>JsCallback: invoke callback
  JsCallback-->>callback_factory: value or exception
  callback_factory-->>ExecutionApi: JSON result envelope
  ExecutionApi-->>NodeApi: unwrapped JSON value or FlowError
Loading

Possibly related PRs

  • NVIDIA/NeMo-Relay#478: Uses the same JavaScript callback envelope and unwrapping pattern for other Node callback paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits correctly and summarizes the Node callback failure handling change.
Description check ✅ Passed The description includes the required Overview, Details, reviewer start, and Related Issues sections.
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.

@willkill07 willkill07 self-assigned this Jul 21, 2026
@willkill07 willkill07 added this to the 0.6 milestone Jul 21, 2026
@github-actions

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
crates/node/src/promise_call.rs (1)

185-236: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same jsonValue duplication and per-call script compilation as callable.rs.

create_promise_wrapper embeds the identical recursive jsonValue script as safe_execution_callback in crates/node/src/callable.rs (lines 122-182), and also recompiles the whole factory string via env.run_script each time a promise-aware wrapper is created.

See consolidated comment for the shared fix across both files.

🤖 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 `@crates/node/src/promise_call.rs` around lines 185 - 236, Refactor
create_promise_wrapper and safe_execution_callback to share one reusable
JSON-conversion helper and one cached wrapper factory instead of embedding
duplicate jsonValue implementations and calling env.run_script for every wrapper
creation. Preserve the existing conversion validation and promise callback
behavior while reusing the shared initialized factory.
🤖 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 `@crates/node/src/callable.rs`:
- Around line 122-182: Extract the shared recursive jsonValue validator,
including its finite-number checks, circular detection, array/object traversal,
and error messages, into one reusable JavaScript source constant or builder.
Update both safe_execution_callback and create_promise_wrapper to use that
shared definition, removing their duplicated implementations while preserving
current behavior.
- Around line 127-182: Update safe_execution_callback so the JavaScript wrapper
factory is compiled once per Env and reused across invocations, using an
appropriate Env-keyed cache or lazy initialization of the resulting
JsFunction/Ref. Keep the existing wrapper behavior unchanged, and have each call
invoke the cached factory with func instead of calling env.run_script on the
full source.

---

Outside diff comments:
In `@crates/node/src/promise_call.rs`:
- Around line 185-236: Refactor create_promise_wrapper and
safe_execution_callback to share one reusable JSON-conversion helper and one
cached wrapper factory instead of embedding duplicate jsonValue implementations
and calling env.run_script for every wrapper creation. Preserve the existing
conversion validation and promise callback behavior while reusing the shared
initialized factory.
🪄 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: 1bd4d79c-1d21-445f-acfa-f13ff7d77fc1

📥 Commits

Reviewing files that changed from the base of the PR and between b908e4f and e460332.

📒 Files selected for processing (5)
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
  • crates/node/src/promise_call.rs
  • crates/node/tests/llm_tests.mjs
  • crates/node/tests/tools_tests.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (13)
crates/node/**/*.{js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase for Node.js public APIs.

Files:

  • crates/node/tests/llm_tests.mjs
  • crates/node/tests/tools_tests.mjs
**/*.{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:

  • crates/node/tests/llm_tests.mjs
  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callable.rs
**/*

📄 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:

  • crates/node/tests/llm_tests.mjs
  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callable.rs
crates/{python,ffi,node}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: Treat binding changes as public API changes. Check for parity with the other language bindings, FFI ownership/lifetime safety,
callback error propagation, stable type conversion, and consistent async/stream semantics.
Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere.

Files:

  • crates/node/tests/llm_tests.mjs
  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callable.rs
{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:

  • crates/node/tests/llm_tests.mjs
  • crates/node/tests/tools_tests.mjs
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.rs: If any Rust code changed, always run just test-rust.
If any Rust code changed, also run cargo fmt --all.
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
**/*.{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:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
{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:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
**/*.{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:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
**/*.{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:

  • crates/node/src/promise_call.rs
  • crates/node/src/api/mod.rs
  • crates/node/src/callable.rs
{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:

  • crates/node/src/api/mod.rs
🔇 Additional comments (5)
crates/node/src/callable.rs (1)

379-380: LGTM!

Also applies to: 573-574

crates/node/src/promise_call.rs (1)

159-164: LGTM!

crates/node/tests/llm_tests.mjs (1)

187-222: LGTM!

crates/node/tests/tools_tests.mjs (1)

241-316: LGTM!

crates/node/src/api/mod.rs (1)

1923-1964: 🗄️ Data Integrity & Integration

No Python/Go follow-up needed The Python and Go tool_call_execute/llm_call_execute bindings already surface callback failures as catchable errors or take json.RawMessage directly, so this Node-only crash fix does not need matching changes elsewhere.

			> Likely an incorrect or invalid review comment.

Comment thread crates/node/src/callable.rs
Comment thread crates/node/src/callable.rs
Signed-off-by: Will Killian <wkillian@nvidia.com>

@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: 1

🤖 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 `@crates/node/src/callback_factory.rs`:
- Around line 11-44: Update jsonValue to accept only plain objects in the
generic object branch, while preserving the existing Array handling. Detect and
reject non-plain-object built-ins such as Date, Map, Set, RegExp, Promise, boxed
primitives, and Error with a TypeError instead of passing them to Object.keys
and returning {}. Ensure circular-value detection and cleanup remain correct for
accepted objects.
🪄 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: 01fc1bb8-f78d-4117-add9-1aaec7abaf8e

📥 Commits

Reviewing files that changed from the base of the PR and between e460332 and befaf63.

📒 Files selected for processing (4)
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/lib.rs
  • crates/node/src/promise_call.rs
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (10)
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.rs: If any Rust code changed, always run just test-rust.
If any Rust code changed, also run cargo fmt --all.
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
{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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*

📄 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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
**/*.{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:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
crates/{python,ffi,node}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: Treat binding changes as public API changes. Check for parity with the other language bindings, FFI ownership/lifetime safety,
callback error propagation, stable type conversion, and consistent async/stream semantics.
Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere.

Files:

  • crates/node/src/lib.rs
  • crates/node/src/callable.rs
  • crates/node/src/callback_factory.rs
  • crates/node/src/promise_call.rs
🔇 Additional comments (6)
crates/node/src/callback_factory.rs (1)

46-74: 🩺 Stability & Availability

Confirm parity with Python/FFI binding callback-safety handling.

This introduces the canonical JSON-safety/callback-failure-isolation mechanism for Node callbacks (thrown exceptions, BigInt, circular refs, non-finite numbers no longer abort the process). Per path instructions for crates/{python,ffi,node}/**/*, binding changes should be checked for parity across the other language bindings. Do Python/FFI callback paths have an equivalent guard against callback exceptions or non-serializable results aborting the process, or is that tracked as separate follow-up work?

As per path instructions, "Treat binding changes as public API changes. Check for parity with the other language bindings... Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere."

Source: Path instructions

crates/node/src/lib.rs (1)

19-19: LGTM!

crates/node/src/callable.rs (3)

128-130: Delegation resolves prior duplication/recompilation concerns.

safe_execution_callback now delegates to callback_factory::wrap_execution_callback, resolving the previously flagged duplicated jsonValue logic and per-call env.run_script recompilation.


327-328: Correctly unwraps the new {ok, value/error} envelope for both tool and LLM execution.

Both wrap_js_tool_exec_fn (Line 327-328) and wrap_js_llm_exec_fn (Line 521-522) now pass the raw callback result through unwrap_middleware_result, consistent with the envelope shape produced by callback_factory's execution wrapper.

Also applies to: 521-522


103-121: 🩺 Stability & Availability

safe_middleware_callback doesn't get the new JSON-safety validation.

Unlike safe_execution_callback/the promise wrapper (now backed by callback_factory's jsonValue validation), this middleware wrapper only normalizes top-level undefined and otherwise passes value through untouched. If a JS middleware callback returns a BigInt, circular structure, or non-finite number, is that still safely rejected downstream (via the Json/serde_json::Value conversion), or can it still crash/abort the process the way execution/promise callbacks used to? Worth confirming whether this wrapper needs the same treatment as part of this effort or a tracked follow-up.

crates/node/src/promise_call.rs (1)

161-166: Conversion failures now surface as errors instead of silently defaulting to null, and the wrapper is shared via callback_factory.

Line 161-166 correctly maps a failed ctx.get::<Json>(0) conversion into an explicit FlowError::Internal sent through the completion channel instead of swallowing it as null. Line 197-198 delegates wrapper construction to the shared callback_factory::wrap_promise_callback, resolving the prior duplicated jsonValue implementation.

Also applies to: 197-198

Comment thread crates/node/src/callback_factory.rs
Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 merged commit 5403179 into NVIDIA:release/0.6 Jul 21, 2026
23 of 29 checks passed

@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: 1

🤖 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 `@crates/node/src/callback_factory.rs`:
- Around line 38-42: Update the callback result validation around the prototype
check to accept JSON-safe plain objects from other realms, such as objects
created by node:vm, using a realm-independent plain-object test instead of
direct Object.prototype identity checks. Add a regression test covering a
cross-realm plain object while preserving rejection of unsupported object
values.
🪄 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: b35b8028-2fff-470f-a795-d5996424b70a

📥 Commits

Reviewing files that changed from the base of the PR and between befaf63 and 663d2d0.

📒 Files selected for processing (2)
  • crates/node/src/callback_factory.rs
  • crates/node/tests/tools_tests.mjs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
crates/node/**/*.{js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase for Node.js public APIs.

Files:

  • crates/node/tests/tools_tests.mjs
**/*.{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:

  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callback_factory.rs
**/*

📄 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:

  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callback_factory.rs
crates/{python,ffi,node}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: Treat binding changes as public API changes. Check for parity with the other language bindings, FFI ownership/lifetime safety,
callback error propagation, stable type conversion, and consistent async/stream semantics.
Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere.

Files:

  • crates/node/tests/tools_tests.mjs
  • crates/node/src/callback_factory.rs
{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:

  • crates/node/tests/tools_tests.mjs
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.rs: If any Rust code changed, always run just test-rust.
If any Rust code changed, also run cargo fmt --all.
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/node/src/callback_factory.rs
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • crates/node/src/callback_factory.rs
**/*.{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:

  • crates/node/src/callback_factory.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/node/src/callback_factory.rs
{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:

  • crates/node/src/callback_factory.rs
**/*.{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:

  • crates/node/src/callback_factory.rs
**/*.{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:

  • crates/node/src/callback_factory.rs
🔇 Additional comments (1)
crates/node/tests/tools_tests.mjs (1)

262-263: LGTM!

Comment on lines +38 to +42
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
seen.delete(value);
throw new TypeError('JavaScript callback returned an unsupported object value that cannot be converted to JSON');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

node - <<'NODE'
const vm = require('node:vm');
const value = vm.runInNewContext('({ value: 1 })');

console.log(Object.getPrototypeOf(value) === Object.prototype); // false
console.log(JSON.stringify(value)); // {"value":1}
NODE

Repository: NVIDIA/NeMo-Relay

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby context.
wc -l crates/node/src/callback_factory.rs
sed -n '1,140p' crates/node/src/callback_factory.rs

# Search for related object-conversion logic and tests.
rg -n "unsupported object value|Object.getPrototypeOf|callback returned|node:vm|cross-realm|realm" crates/node -S

Repository: NVIDIA/NeMo-Relay

Length of output: 5462


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Node tests that mention vm and any docs or code that mention realm/restrictions.
sed -n '1,220p' crates/node/tests/index_loader_tests.mjs

printf '\n--- search docs/tests ---\n'
rg -n "same-realm|cross-realm|vm\\.|node:vm|Object.getPrototypeOf\\(|unsupported object value|JSON-safe|JSON safe|callback results" \
  crates/node crates -S

Repository: NVIDIA/NeMo-Relay

Length of output: 7834


Accept cross-realm plain objects or document the restriction

This prototype identity check rejects plain objects created in node:vm even though they are JSON-safe. If callback results may come from other realms, switch to a realm-independent plain-object check and add a regression test; otherwise document the same-realm requirement.

🤖 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 `@crates/node/src/callback_factory.rs` around lines 38 - 42, Update the
callback result validation around the prototype check to accept JSON-safe plain
objects from other realms, such as objects created by node:vm, using a
realm-independent plain-object test instead of direct Object.prototype identity
checks. Add a regression test covering a cross-realm plain object while
preserving rejection of unsupported object values.

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:js PR changes/introduces Javascript/Typescript code lang:rust PR changes/introduces Rust code size:M PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants