> This page is for NeMo Relay, version v0.11.0.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.4) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.11.0: https://docs.nvidia.com/nemo/relay/v0.11.0/llms.txt
> - v0.10.4: https://docs.nvidia.com/nemo/relay/v0.10.4/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Native Plugin Diagnostics

> Inspect native plugin lifecycle and callback outcomes from the host.

The Relay host records native plugin outcomes in its diagnostic context. Enable
diagnostics in the host before loading plugins. A native plugin uses that context.
It does not need a separate diagnostic configuration or store.

Use [CLI and Daemon Diagnostics](/nemo/relay/v0.11.0/reference/diagnostics/cli) for a gateway or daemon.
Use [Embedded Diagnostics](/nemo/relay/v0.11.0/reference/diagnostics/bindings) for a language-hosted
application. If your Rust application loads plugins directly, select its context
with `load_native_plugins_with_diagnostics`.

## Host Recording Coverage

The host records outcomes at the operation that owns them:

| Operation                           | Recorded Evidence                               |
| ----------------------------------- | ----------------------------------------------- |
| Policy, artifact, and schema checks | Failures from the shared dynamic plugin checks. |
| Library loading                     | Native load success or failure.                 |
| Plugin validation                   | Native validation success or failure.           |
| Component registration              | Native registration success or failure.         |
| Runtime callbacks                   | Native callback failures.                       |

Native observations use `PluginExecution::NativeAbi`. Their phase identifies
policy, validation, activation, or runtime work. Failure classes come from
structured status values. `Ok`, `StreamEnd`, and `Backpressured` are not failures.
A later success does not erase an earlier failure.

Common component activation also records its own summary. Do not add that summary
to a native operation's failure count as if they were separate failed activations.
Refer to [producer boundaries](/nemo/relay/v0.11.0/reference/diagnostics#plugin-producer-boundaries)
for ownership across the two layers.

## Record Internal Observations

The native SDK exposes canonical diagnostics through `runtime.diagnostics()`.
Obtain the runtime from the `PluginContext` supplied during registration. Retain
its diagnostic component in callbacks that perform the internal operation.

Use the same lookup example as the Rust and Python worker guides. It declares
one failure code and one success code, then records the observed outcome with a
request ID. Registration runs once for that responsibility. Refer to
[Plugin Diagnostics](/nemo/relay/v0.11.0/build-plugins/diagnostics) for the shared recording pattern
and the differences between host acceptance and queue admission.

```rust title={"Native lookup diagnostics"} showLineNumbers={false}
use std::num::NonZeroU64;

use nemo_relay_plugin::PluginRuntime;
use nemo_relay_plugin::diagnostics::{
    ComponentDeclaration, DiagnosticCode, DiagnosticComponent, DiagnosticError, DiagnosticOutcome,
    Diagnostics, FailureClass, Observation, RecordOutcome,
};

/// Register the lookup vocabulary once for this activation.
pub fn register_lookup_diagnostics(
    runtime: &PluginRuntime,
) -> Result<(Diagnostics, DiagnosticComponent), DiagnosticError> {
    let diagnostics = runtime.diagnostics();
    let component = diagnostics.component_lazy(|| ComponentDeclaration {
        name: "example.lookup",
        codes: &[
            DiagnosticCode::Extension {
                name: "lookup_failed",
                description: "The lookup backend was unavailable",
                outcome: DiagnosticOutcome::Failure,
                allowed_classes: &[FailureClass::Unavailable],
            },
            DiagnosticCode::Extension {
                name: "lookup_completed",
                description: "A lookup completed",
                outcome: DiagnosticOutcome::Success,
                allowed_classes: &[],
            },
        ],
    })?;
    Ok((diagnostics, component))
}

/// Record a lookup whose backend is known to be unavailable.
pub fn record_lookup_failure(
    component: &DiagnosticComponent,
    request_id: &str,
) -> Result<RecordOutcome, DiagnosticError> {
    component.try_record_lazy(|| {
        let mut observation = Observation::failure(
            1,
            FailureClass::Unavailable,
            "The lookup backend was unavailable",
            NonZeroU64::MIN,
        );
        observation.correlation.request_id = Some(request_id.into());
        observation
    })
}

/// Record an independently observed successful lookup.
pub fn record_lookup_success(
    component: &DiagnosticComponent,
    request_id: &str,
) -> Result<RecordOutcome, DiagnosticError> {
    component.try_record_lazy(|| {
        let mut observation = Observation::success(2, "A lookup completed", NonZeroU64::MIN);
        observation.correlation.request_id = Some(request_id.into());
        observation
    })
}
```

`component_lazy` registers the complete code list together. Disabled or unsupported
facades skip its factory and return an inactive component. Inactive components
skip observation factories too. A code position is one-based within that list.
Extensions declare their outcome and allowed failure classes. Common plugin
failure codes can also be selected with `DiagnosticCode::Builtin`.

Each successful record returns `RecordOutcome::Recorded`. Its receipt contains
these fields:

* `sequence` identifies the accepted source update.
* `code_total` holds the updated code count when available.
* `evidence` is `Retained`, `Truncated`, or `Dropped`.

`RecordOutcome::Disabled` means no observation or loss count was produced.
A dropped or truncated evidence payload does not undo accepted counts.
Keep diagnostic errors separate from callback results. Do not retry an uncertain
record automatically, since the host might already have counted it.

`diagnostics.delivery_stats()` reports SDK-local dropped, rejected, and uncertain
submission totals. These are separate from host occurrence counts and retention
loss. `diagnostics.availability()` reports collection availability.
`diagnostics.capabilities()` reports the adapter limits and read grant without
exposing diagnostic evidence.

### Correlation and Lifetime

`Observation::failure` and `Observation::success` default to no correlation.
Set request or session IDs through `observation.correlation`. The shared
`DiagnosticCorrelation` type accepts borrowed and owned strings. Correlation
never becomes a counter key and counts toward the input-text budget.

Native recording is synchronous. There is no delivery queue or flush call.
Explicit `component.close()` fences all clones. Last drop also closes the
registration. Activation teardown revokes handles and releases adapter backing.

Closing a registration does not reset context totals or imply recovery.
Closing live components frees their slots, but cumulative extension and declaration
text budgets remain consumed for the session. Register a stable vocabulary once
instead of creating a new component for each request.

### Concurrent Calls and Recursion

Cloned components can record from concurrent callback threads. Runtime and facade
clones share four SDK registration, recording, or read slots per activation.
Admission happens before producer factories and SDK scratch or buffer allocation.
Exhaustion returns `DiagnosticError::Busy` without waiting or automatic retry.
Local authoring refusals increment `delivery_stats().dropped`; read refusals do
not change authoring counters.

Separately, the SDK admits one in-flight read query/output-buffer pair per
activation. A read can overlap recording within the call limit; another read
returns `Busy` before allocating buffers. Returned JSON and decoded pages are
caller-owned and are outside this in-flight limit and the core memory budget.
Host work has its own four process-wide data permits and serializes access to
each activation. The SDK admission limit does not guarantee host admission.

Same-thread recursion into the same activation returns `Busy`. Nested calls into
different activations are allowed, with up to 32 nested activations per thread
tracked by each SDK copy. Guards release admission slots on return, error, or
panic unwind. Component closure does not require a data-call permit.

## Read Host Evidence

The canonical reader can inspect every component in the host's selected context.
Reads require the host's explicit `native_diagnostics_read_context` grant.
The default is false. Recording does not require this grant.

Set the grant in host policy using the manifest plugin ID. This example uses the
checked native example's ID:

```toml title={"Host policy"} showLineNumbers={false}
[plugins.policy.overrides."examples.rust_native_policy"]
native_diagnostics_read_context = true
```

Resolve the setting through the existing host configuration layers. Within policy,
defaults apply first, followed by ordered matching rules and plugin-ID overrides.
An explicit false can override an earlier grant. A plugin manifest or component
configuration cannot grant access. Workers always receive no grant.

`plugins inspect <id>` and `plugins validate <id-or-path>` report the effective
`native_diagnostics_read_context` permission in text and JSON. The value describes
policy, not whether collection is enabled or an activation is live. Configuration
values stay redacted. Apply policy changes through the existing owner-controlled
reload or reactivation path.

This example reads discovery, a snapshot, and a separate memory page:

```rust title={"Authorized native reads"} showLineNumbers={false}
use nemo_relay_plugin::diagnostics::{
    DiagnosticDiscovery, DiagnosticError, DiagnosticMemoryPage, DiagnosticQuery,
    DiagnosticSnapshot, Diagnostics,
};

/// Read separate discovery, snapshot, and memory views after an explicit grant.
pub fn inspect_authorized_context(
    diagnostics: &Diagnostics,
) -> Result<
    (
        DiagnosticDiscovery,
        DiagnosticSnapshot,
        DiagnosticMemoryPage,
    ),
    DiagnosticError,
> {
    let reader = diagnostics.reader()?;
    let discovery = reader.discovery()?;
    let query = DiagnosticQuery {
        max_components: Some(1),
        max_events: Some(1),
        ..Default::default()
    };
    let snapshot = reader.snapshot(&query)?;
    let memory = reader.inspect_memory(&query)?;
    // Keep these caller-owned values inside the authorized inspection boundary.
    Ok((discovery, snapshot, memory))
}
```

The reader exposes these operations:

| Method                        | Result                                                    |
| ----------------------------- | --------------------------------------------------------- |
| `discovery()`                 | Canonical capabilities for the selected host context.     |
| `snapshot(&query)`            | An independently owned `DiagnosticSnapshot`.              |
| `snapshot_json(&query)`       | Canonical snapshot JSON with exact decimal-string counts. |
| `inspect_memory(&query)`      | An independently owned `DiagnosticMemoryPage`.            |
| `inspect_memory_json(&query)` | Canonical memory-page JSON.                               |

Every read checks live activation and read authority. Without a grant, `reader()`
returns `permission_denied`. After teardown, handles return `closed`. An authorized
read can still fail if collection or queries are disabled, a cursor is invalid,
or the page cannot fit. Use `DiagnosticError::reason()` for stable error labels.

Follow the [Diagnostics API](/nemo/relay/v0.11.0/reference/diagnostics/api) for query fields, precision,
pagination, and coverage. Snapshot and memory pages have separate cursors and
are captured independently. Decoded pages and SDK buffers belong to the caller
and use memory outside the core budget. A short output buffer never yields a
usable partial page. Reads do not start a service or resource collector.

A read grant exposes other components' restricted detail too. Filters select
results but do not narrow permission. Keep returned data within the authorized
inspection boundary. Snapshot reads never automatically feed logs or exporters.

### Handle SDK Errors

Use stable reason labels to handle adapter errors:

| Reason              | Meaning and Response                                                                                                          |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `unsupported`       | The host has no compatible diagnostics extension. Continue without optional diagnostics.                                      |
| `unavailable`       | Adapter session storage could not be admitted. Keep the operation's original result.                                          |
| `closed`            | The activation or registration is closed. Stop using its handles.                                                             |
| `permission_denied` | Host policy did not grant whole-context reads. Recording can still be available.                                              |
| `busy`              | A call/read slot is unavailable or the call recurses into the same activation. Native recording does not retry automatically. |
| `capacity`          | A declaration or request exceeds an adapter limit. Reduce its size or vocabulary.                                             |
| `invalid`           | An input value or handle is invalid. Check code positions, outcomes, and failure classes.                                     |
| `conflict`          | A local component name is already registered. Reuse its existing handle.                                                      |
| `buffer_too_small`  | A caller-owned output buffer cannot fit the complete response. Ignore the output.                                             |
| `internal`          | Adapter acceptance is uncertain. Do not automatically replay the observation.                                                 |

`DiagnosticError::Read` carries a canonical read reason, such as `disabled`,
`query_disabled`, `invalid_cursor`, or `snapshot_capacity`. Refer to the
[shared read errors](/nemo/relay/v0.11.0/reference/diagnostics/api#handle-read-errors) for their meaning.
Include a fallback when matching the non-exhaustive Rust error enum.

## Admission Limits

Native adapter limits apply in addition to core diagnostic storage limits:

| Resource                   | Limit                                                        |
| -------------------------- | ------------------------------------------------------------ |
| Authoring sessions         | Up to 32 per host process.                                   |
| Live components            | Up to eight per session.                                     |
| Codes                      | Up to 32 per component.                                      |
| Extension declarations     | Up to 16 over the session's lifetime.                        |
| Declaration text           | Up to 16 KiB over the session's lifetime.                    |
| Aggregate input text       | Up to 16 KiB per call, including detail and correlation.     |
| Concurrent host data calls | Up to four per process. Cleanup does not need a data permit. |
| Queries and responses      | The selected context's query and page limits.                |

Extension names allow 32 UTF-8 bytes. Descriptions allow 256 UTF-8 bytes.
Core detail and correlation limits still apply and can truncate accepted evidence.
Capacity errors during registration do not disable existing recorders. Full event
history can evict older evidence while counters keep updating.

Capabilities report adapter layout bounds and the current activation's session
bytes separately from core storage. These values do not measure total process RSS
or allocator overhead. Caller-owned read results have their own memory cost.

## SDK and ABI Boundaries

The optional `NativeDiagnosticsApiV1` table follows the frozen execution ABI-8
prefix. The SDK checks the table's size and independent contract version before
using it. Execution ABI 8, ABI-7 negotiation, and manifest native API 1 remain
unchanged. This adds an optional tail to the native host table.

| Contract                       | Current Version |
| ------------------------------ | --------------- |
| Execution ABI                  | 8               |
| Manifest `compat.native_api`   | `"1"`           |
| Diagnostics extension contract | 1               |

Incompatible diagnostics callback or payload layout changes require versioning
that extension contract. Changes to private SDK synchronization do not change
these ABI layouts or callback signatures.

Core recorders and Rust-owned objects stay inside the host. Registration and
recording cross the ABI as fixed layouts with borrowed UTF-8 spans. The host
retains no plugin input pointers. A compatible host without the extension exposes
an unsupported facade and keeps existing execution behavior.

`PluginRuntime::runtime_diagnostics()` remains the older host-level report from
ABI v4. It is ordered by code and does not identify the plugin that recorded an
entry. It does not return canonical snapshots or fall back to them.
Refer to [Runtime Events and Scopes](/nemo/relay/v0.11.0/build-plugins/native/runtime-events-and-scopes#emit-typed-telemetry-and-read-diagnostics)
for that older API, and the
[native ABI reference](/nemo/relay/v0.11.0/build-plugins/native/native-abi-reference#native-diagnostics-extension)
for table ownership and compatibility details.

A plugin that requires canonical diagnostics must declare a Relay compatibility
range that includes this extension. Do not treat `compat.native_api = "1"` as proof
that every compatible host exposes it. Native code remains trusted and shares
the host process.