Migration Guides
Use this page to upgrade from NeMo Relay 0.8.x to 0.9. Each section covers one part of the upgrade. This guide reflects the current 0.9 release branch.
If you skip releases, read each release’s migration guide and release notes in order.
Upgrade to NeMo Relay 0.9
Refresh Relay-Managed Coding-Agent Integrations
After upgrading the CLI, refresh personal Relay-managed integrations before starting new sessions:
To repair only Codex, use nemo-relay install codex --force. It can repair
Relay-owned marketplace entries that point to missing files. These commands
apply to personal integrations.
For daemon bundles managed by an administrator, follow the daemon upgrade workflow. Close sessions and let workers finish pending requests. Then stop the service and replace the binary at the same path. Keep the saved identity state and existing bundle files.
Pass Tool Execution Context to Intercepts
Tool callbacks use a context object because Relay now provides the optional provider tool-call ID with the tool name and arguments. Existing callbacks no longer match the public API and must be updated.
Tool execution intercepts now receive ToolExecutionContext and a
continuation. The context holds the tool name, arguments, and optional
provider-issued tool-call ID. The continuation calls the next intercept or
the tool itself.
Change Rust, Python, and Go callbacks from (tool_name, args, next) to
(context, next). For Node.js and public C callbacks, replace the argument
payload with the context. Pass the context’s arguments to the continuation.
Read the name and call ID from the context. Node.js uses toolName, args,
and toolCallId. Public C callbacks receive tool_name, args, and
tool_call_id in context_json.
Native and worker plugins that register this callback must exclude Relay 0.8
from their supported version range. Set compat.relay = ">=0.9,<1.0" or
another range that meets this rule. Native plugins use version 5 of the
application binary interface (ABI) for this callback. For binding details and examples, refer to
Tool Execution Context.
Retain the Plugin Host Activation
NeMo Relay 0.9 uses one plugin host to control when plugins start and stop. Global clear and report functions and explicit dynamic activation-spec APIs are removed. Update startup, report handling, and shutdown together in each application that hosts plugins.
initialize now returns a PluginHostActivation handle for static and dynamic
plugins. This handle keeps the plugins active. Keep it for as long as your
application needs them.
A process can have only one active handle. Close it before starting another. If close fails, keep the handle and try closing it again.
Use the following APIs to manage the handle in each binding:
Replace global clear calls with the handle’s close operation. This includes
Python clear() and clear_async(), Node.js clear(), Rust
clear_plugin_configuration(), and the matching Go and C calls. Read reports
from the handle instead of a global report function.
Apply the following cleanup rules for your binding:
- In Python, use
async with plugin.activate(config, path)to close the handle when the block ends. - In Node.js, call
await relay.flushSubscribers()before closing the handle. This lets queued subscribers finish. For existing dynamic-plugin handles, replace theactiveproperty withisActive. - In C, inspect the report after closing the handle. Then release the handle
with
nemo_relay_plugin_host_activation_free.
Replace explicit dynamic-plugin setup with file-based setup:
- Move
DynamicPluginActivationSpecentries to[[plugins.dynamic]]records inplugins.toml. - Replace
initialize_with_dynamic_pluginsorinitializeWithDynamicPluginswithinitialize, and pass the file as its optional path. - Remove calls to Python
load_dynamic_plugin_activation_specs. The host now reads those records from the file.
In Rust, replace initialize_plugins with
nemo_relay::plugin::dynamic::initialize.
For complete examples, refer to Configure and Initialize Plugins. For dynamic-plugin records, refer to Configure Discoverable Plugins.
Validate the Effective Plugin Configuration
validate now checks the same settings, dynamic manifests, version rules, and
trust policy as initialize. It does not load plugin code.
If you pass a plugins.toml path, Relay uses it instead of searching for a
user file. Relay then merges the system file and applies settings from your
application code. Check config_paths and the redacted resolved_config in
the report to confirm which settings Relay used.
Use exact validation to check only a complete static document in memory. It does not read files. Choose the API for your binding:
Both forms return PluginHostReport. Read diagnostics and
runtime_diagnostics under config instead of at the top level.
Check dynamic_plugins separately for errors in manifests, version rules,
file integrity, environments, signatures, and policy. Exact validation does
not check dynamic plugins.
Review Layered Dynamic Plugin Declarations
When the explicit-or-user and system plugins.toml files declare dynamic
records with the same manifest plugin ID, the higher-precedence record now
replaces the lower-precedence record. Relay no longer fails with a duplicate-ID
error, and it does not merge the lower record’s config into the effective
plugin. This allows layered configuration, but a needed lower-precedence
setting can no longer take effect by itself.
If a system deployment repeats a user’s dynamic-plugin ID, copy every setting
needed at runtime into the system record. The system manifest reference and its
lifecycle state also become authoritative. This applies to CLI gateway,
wrapper, persistent gateway, daemon broker, and every binding that uses the
normal initialize(config, path) API.
Python-Specific Errors
Python plugin APIs use more specific error types so applications can tell an
invalid setting from a missing file. Code that only catches RuntimeError can
miss these failures after the upgrade.
nemo_relay.plugin.initialize() and nemo_relay.plugin.activate() now raise
ValueError for invalid static settings. They raise FileNotFoundError when
a referenced configuration or resource is unavailable. Earlier releases
reported these failures as RuntimeError.
Update your exception handlers for these errors. Keep RuntimeError handling
for failures to acquire the plugin host or register a plugin. To inspect
static settings without starting plugins, use validate or validate_exact.
Verify Dynamic-Plugin Trust
Dynamic plugins now go through trust checks in every embedding API. By default, Relay requires a signature, so an unsigned plugin that started on 0.8 can fail to start on 0.9. Choose and validate a trust policy before deployment.
The core host now checks dynamic-plugin attestation through every embedding API. Attestation checks the plugin artifact against the configured trust policy. In 0.8, only CLI lifecycle commands enforced this policy.
If [plugins.policy.defaults] omits the policy fields, Relay uses
startup = "required" and attestation = "signature_required".
An unsigned plugin that started on 0.8 can therefore fail to start on 0.9.
Choose a trust policy before you deploy:
- Sign the artifact and configure
trusted_public_keysto use the default signature requirement. - If your trust policy permits it, explicitly set
attestationto"integrity_only"or"signature_if_present"under[plugins.policy.defaults].
Use nemo-relay plugins validate <plugin-id> and your embedding’s validate
API to check the chosen policy and artifact before deployment.
Embedding validate calls return a report for every lifecycle-selected plugin,
including a plugin with a trust failure. They do not raise for that failure.
Check the matching dynamic_plugins entry’s status.integrity,
status.authenticity, and failure before deployment. A failed trust report
has selected = false because Relay does not activate that plugin. With
startup = "required", initialize rejects the same failure before it loads
plugin code.
integrity_only skips signature checks. It still requires a valid
source.artifact and a matching integrity.sha256 digest.
Choose Trace Boundaries Explicitly
Propagation now keeps the Relay root by default so related work stays in one
trace. This changes which events share a trace and changes the meaning of the
exported ATIF session_id.
Default propagation capture and scope-stack forks now keep the Relay root when one is available. This root identifies the trace shared by related work. When you import a context with a root, work continues in the parent’s OpenTelemetry trace. Without a propagated root, the first local Agent scope creates one.
To start a separate trace in the receiver, use the rootless capture API for your binding:
A rootless context still links Relay events to their immediate parent. The receiver’s first local Agent scope creates a new root. Use explicit root capture when your application supplies a stable run UUID. Authenticate the source of an incoming context before you trust and import it. For details, refer to Scope Concepts.
In ATIF, use trajectory_id as the unique ID for each exported trajectory.
The exported session_id now uses the propagated root when one is present.
Several trajectories can share this ID. Update data joins or rules that
assume session_id and trajectory_id are the same.
Filenames, remote storage keys, and remote session headers still use the
trajectory scope UUID. The historical {session_id} filename placeholder
also uses that UUID. For details, refer to
Run-Scoped Session IDs.
Secure Trace Export and Review Tool Content
All full, gen_ai, and openinference trace endpoints require HTTPS for
remote collectors, with either HTTP or gRPC transport. Plain HTTP works only
with localhost and loopback IP addresses. OTLP/HTTP trace exporters no longer
follow redirects, even between HTTPS endpoints. This keeps trace data from
being sent to an insecure or unexpected destination.
Replace remote http:// trace endpoints with the collector’s final https://
URL, or use a loopback collector. Do not use an endpoint that requires a
redirect. These rules apply to plugin-managed endpoints and direct
subscribers, even when you redact the data.
Log and metric exporters keep their existing transport rules. The redirect change does not apply to gRPC.
The gen_ai output now includes sanitized tool arguments, successful tool
results, and tool definitions. This gives collectors more useful trace data,
but it can also send tool content that they did not receive before. Review
collector access and redaction settings before you upgrade.
The gen_ai output format adds the following fields after sanitization:
Review who can access your collector and how you redact data before upgrading.
Setting enable_full_payloads = false does not disable tool-content export.
It controls how much LLM request history is kept. Use event sanitizers or
the PII trajectory_context preset to remove opaque tool payloads. These are
payloads whose structure Relay cannot safely interpret. For details, refer to
GenAI Projection.
Direct subscribers can now read header values from environment variables
through header_env, or headerEnv in Node.js. Set each variable before
creating the subscriber. Creation fails if a referenced variable is unset,
empty, or contains only whitespace. Values with leading or trailing whitespace
also cause failure. Create a new subscriber to use a changed value.
Do not define the same header in both headers and header_env. Relay ignores
ASCII case when it compares header names. For details, refer to
Direct Subscribers.
Configure Rotating Exporter Headers
Rust observability configuration structs now include header_file. Rust code
that uses a struct literal must set this field, usually to an empty map. Go code
that uses an unkeyed composite literal must use keyed fields instead. Also,
remote exporters with any header source must use HTTPS or WSS, except on
localhost or a loopback IP address. These changes keep rotated credentials from
being sent to an insecure or redirected destination.
Use header_file when a separate process writes a complete credential to a
file, such as a projected token file. Relay reads it for every HTTP delivery.
For ATOF, HTTP POST reads it for each event, WebSocket reads it when it connects
or reconnects, and NDJSON reads it when the sink starts. Use it for ATOF
streams, ATIF HTTP storage, and OpenTelemetry trace, log, and metric exporters.
For Rust struct literals, add an empty HashMap when you do not use file-based
headers. For Go, change an unkeyed literal to a keyed literal and omit
HeaderFile unless you need it. Keep each header name in only one of
headers, header_env, and header_file.
For a remote endpoint with configured headers, replace http:// with the
collector’s final https:// URL. Use wss:// for a remote ATOF WebSocket
destination. Relay does not follow redirects when headers are set. For details,
refer to OpenTelemetry Exporter Headers,
ATIF HTTP Storage, and
ATOF Streaming.
Review Trajectory Redaction and Metric Dimensions
The trajectory_context preset now removes more opaque data and metric
attributes to limit data exposure. Dashboards or data jobs that use those
fields need to change.
The PII trajectory_context preset now removes opaque payloads and unknown
fields. It builds minimal provider payloads from recognized, sanitized
annotations in Relay’s standard format. Runtime and opaque codecs produce
empty objects.
Application IDs are replaced with the same redaction marker. Use Relay lifecycle IDs to link related events.
Typed metrics keep their required measurements. Their attributes are dropped
unless an exact string name and value match metric_string_attribute_allowlist.
The preset also drops numeric and boolean attributes. Add only fixed values
or values from a limited list that your dashboards need. A string array is
kept only if every value is allowed.
Set builtin.preset = "trajectory_context" before using
metric_string_attribute_allowlist or custom_mark_payload_policy.
Do not combine builtin.preset with action, detector, pattern,
target_paths, target_path_globs, mask_char, unmasked_prefix, or
unmasked_suffix in the same builtin settings.
Review code that reads opaque custom-mark payloads, routing metadata, or detailed records of how prices were determined. The stricter preset removes these fields by default. For details, refer to Trajectory Context Preset.
Update Rust Cache Configuration
The Rust response-cache API now uses an enum instead of a string so cache key choices are checked at compile time. Existing Rust source that assigns a string or uses the removed constant no longer builds.
ResponseCacheConfig.key_strategy is now a ResponseCacheKeyStrategy enum.
Replace string assignments and the removed KEY_STRATEGY_EXACT_REQUEST
constant with ResponseCacheKeyStrategy::ExactRequest or
ResponseCacheKeyStrategy::Logical. Import the enum from nemo_relay_adaptive.
Existing TOML and JSON "exact_request" values remain valid. The "logical"
strategy uses a separate set of cache keys. It ignores tool descriptions and
tool order. Use it only if you want requests with those differences to share
cached results.
Align Coding-Agent Versions and Deployment Modes
Upgrade OpenClaw to 2026.9.3 before installing the 0.9 Relay OpenClaw plugin. The plugin’s minimum gateway version is now 2026.9.3.
For new Pi integrations, use Pi 0.84.x and install one Relay extension copy. Relay accepts later minor versions but marks them as unverified. Check that hooks still work after an upgrade.
Personal extension installs and daemon bundles managed by an administrator use different setup steps. Follow Pi or Daemon for your deployment mode.
Managed daemons require system-owned settings and a route credential for each
user on each computer. They also need persistent WebSocket control connections.
For remote deployments, use HTTPS and ensure that the daemon can reach client
workers. Set reverse proxies to pass WebSocket upgrades and ping/pong traffic
under /_nemo-relay/control/.
Confirm that worker plugins enforce policy or produce exporter output. If a worker fails to start, an authenticated route can enter pass-through mode and forward requests without those plugins. For details, refer to Daemon Operations and Reverse Proxy Configuration.
Related Release Information
For release highlights, compatibility updates, and current known issues, refer to the Release Notes. Use GitHub Releases for the complete release history and notes for a specific tag.