# Followups
Open work items deferred during /loop iterations. Triaged at the top of
every iteration. New items are appended under `## Open`; resolved items
move to `## Resolved` with a date + commit hash.
## Open
> **Status snapshot (2026-05-06):** Of the 8 entries in this section, only **F3** is genuinely open work. Every other entry's `**Status:**` line documents its closure (resolved with a date + commit pointer, or marked out-of-scope). They stay in this section as load-bearing context for future contributors who hit the same problems — moving them to `## Resolved` would orphan their analysis from the F-numbers other followups reference. New work goes here; status lines are authoritative for whether an entry needs further action.
### F48 — Execute `cargo publish` for the V1 release cut
**Status:** **Out of scope — internal usage only, no crates.io publish planned.** Confirmed 2026-05-06 by maintainer. The workspace stays at `version = "0.0.0"` indefinitely; consumers depend via path or git, not crates.io. F43's dry-run validation (`design/F48-publish-dry-run.md`) is retained as a workspace-hygiene check (each crate's `cargo package --list` produces a clean tarball, no accidental captures/big files), not as release prep.
If this changes (e.g. internal consumer wants registry-style versioning via a private cargo registry), the V1 publish recipe in `design/F48-publish-dry-run.md` describes the steps. For now: no work needed.
### F50 — Run the F46 Suspend/Activate Frida capture live
**Status:** **Resolved 2026-05-06.** Two captures landed under `captures/123-frida-suspend-advised-instrumented/` (suspend-advised scenario) and `captures/124-frida-activate-advised-instrumented/` (activate-advised scenario). Per-byte evidence in `docs/F50-suspend-activate-evidence.md`; R5 in `design/70-risks-and-open-questions.md` moved to settled.
**Verdict:**
- **Suspend** is server-side: emits NMX `PutRequest` with command `0x2D` ~140ms after the LMX-proxy entry, body `2d 01 00 + correlation_id + 22 bytes` (same shape family as `0x1F` AdviseSupervisory).
- **Activate** against a non-suspended item is client-side only — no wire traffic, returns Success synchronously. The harness `activate-advised` scenario doesn't sequence Suspend-then-Activate; if direct evidence for Activate-after-Suspend is needed later, add a new scenario to `MxTraceHarness/Program.cs`.
**Severity:** P3 — residual from F46 (script ready, capture not yet run).
**Source:** F46 closeout (`design/followups.md`) + `analysis/frida/mx-nmx-trace.js` header procedure.
**Scope.** Run the Frida script against a live `MxTraceHarness.exe` exercising the suspend-advised + activate-advised scenarios on `TestChildObject.ScanState`. Save under `captures/NNN-frida-suspend-activate-instrumented/`. If the new `mx.suspend.*` / `mx.activate.*` events accompany NMX traffic in the same time window: document the wire opnum + body shape in `docs/M6-buffered-evidence.md` and `analysis/proxy/nmxsvcps-procedures.tsv`. If no NMX traffic accompanies the hook fires: update `design/70-risks-and-open-questions.md` R5 to "settled — client-side only".
**Definition of done:** R5 is fully settled (either with a documented wire opnum or a "client-side only" verdict backed by capture).
**Resolves when:** the capture lands and R5's status is updated.
### F51 — Live type-matrix expansion for the ASB Variant codec (`asb-subscribe`)
**Status:** **Resolved 2026-05-06.** Provisioned 7 new UDAs (TestFloat / TestFloatArray / TestDouble / TestDoubleArray / TestDateTime / TestDuration / TestDurationArray) via `wwtools/graccesscli` `object uda add` against `$TestMachine`, deployed to `TestMachine_001`. New `crates/mxaccess/examples/asb-type-matrix.rs` reads each tag in a single batch and dumps the live `AsbVariant` bytes to per-tag fixture files when `MX_ASB_DUMP_FIXTURES=
` is set.
Live evidence (one cold-start run; subsequent runs hit the F31 InvalidConnectionId cool-down — wait 60+ seconds with no ASB activity before re-running):
| Tag | type_id | length | payload bytes |
|---|---|---|---|
| TestChangingInt | 4 (Int32) | 4 | 4 |
| TestAlarm001 | 17 (Boolean) | 1 | 1 |
| MachineCode | 10 (String) | 30 | 30 |
| TestFloat | 8 (Float) | 4 | 4 |
| TestDouble | 9 (Double) | 8 | 8 |
| TestDateTime | 11 (DateTime) | 8 | 8 |
| TestDuration | 12 (ElapsedTime) | 8 | 8 |
`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal + type_id / length pin. Per-fixture .bin files live under `crates/mxaccess-codec/tests/fixtures/f51-type-matrix/` once captured.
Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 length=0 payload=0 bytes` because no consumer has written values to them — provisioned but unpopulated. Codec-side array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when value-write seeding lands, regenerate fixtures and add `*_array_round_trip` tests per shape. `docs/galaxy-test-fixtures.md` documents the full provisioning + regeneration recipe.
**Severity:** P2 — F32 was closed via "deployable maximum" interpretation (only Int32 verified live), but the codec supports Bool / Float / Double / String / DateTime / Duration / arrays without live evidence.
**Source:** F32 closeout (`design/followups.md`); `work_remain.md:108-113` documents the proven matrix from .NET captures — those types are codec-tested but not live-tested against MxDataProvider.
**Scope.** Provision sample tags on the local Galaxy for each missing type (Bool, Float, Double, String, DateTime, Duration, plus 1-2 representative array shapes). Extend `examples/asb-subscribe.rs` with a per-type loop that registers + reads + subscribes against each. Capture the wire bytes via `examples/asb-relay.rs` middleman and add round-trip parity tests in `crates/mxaccess-asb/tests/` for each type.
**Definition of done:**
1. Per-type Galaxy fixture documented in `docs/galaxy-test-fixtures.md` (which child object names to provision, expected attribute types).
2. `cargo run -p mxaccess --example asb-subscribe -- --type-matrix` exercises all proven types and reports per-type wire bytes + decoded value.
3. Round-trip test per type in `crates/mxaccess-asb/tests/` pinning the captured wire bytes.
**Resolves when:** every proven type from `work_remain.md:108-113` has a live wire fixture + a passing round-trip test.
### F52 — Codec performance optimisations deferred from F39
**Severity:** P3 — R12 < 5 allocs/write target is already met; these are nice-to-haves.
**Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1.
**Scope.** Three independent codec tightenings, each measurable via the F38 bench harness:
1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying. ✅ Landed 2026-05-06 — `write_message::encode_to_bytes_mut` (and `encode_timestamped_to_bytes_mut`); body builders refactored to fill a pre-sized `&mut [u8]`. Bench delta in `design/M6-bench-baseline.md` § F52.1.
2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names. ✅ Landed 2026-05-06 — thread-local `HashMap` keyed by raw name; bounded at 1024 entries. `MxReferenceHandle::from_names` drops 2 → 0 allocs/op once warm. Bench delta in `design/M6-bench-baseline.md` § F52.2.
3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes. ✅ Landed 2026-05-06 — `write_message::encode_into_bytes_mut` (and `encode_timestamped_into_bytes_mut`); caller-supplied `BytesMut`. Pooled Int32 = 1 alloc/op (was 2); pooled Boolean = 0 allocs/op (was 1). Bench delta in `design/M6-bench-baseline.md` § F52.3.
**Definition of done:**
1. ✅ Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta. (commits `4e76b44` F52.1, `a0fa5be` F52.2, this commit F52.3)
2. ✅ No correctness regressions in the round-trip fixture suite. (267 tests pass)
3. ✅ Default API surface unchanged. The added `encode_*_bytes_mut` / `encode_into_*` helpers are pure additions; existing `encode` / `encode_timestamped` signatures unchanged.
**Resolved 2026-05-06:** all three optimisations landed.
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
| Crate | Missing-docs warnings |
|---|---|
| `mxaccess-asb` | 422 |
| `mxaccess-nmx` | 398 |
| `mxaccess-callback` | 371 |
| `mxaccess-galaxy` | 229 |
| `mxaccess-codec` | 205 |
| `mxaccess-rpc` | 147 |
| `mxaccess-asb-nettcp` | 111 |
| **Total** | **1883** |
Most of those are protocol-internal types (struct fields, enum variants on wire-shape records) whose meaning is already documented at the consumer-facing layer. Filling 1883 one-liners adds noise without consumer value, and turning them into errors (`RUSTDOCFLAGS="-D warnings"`) would block routine `cargo doc` runs. Lint stays off on protocol crates indefinitely; if a future contributor wants per-crate enforcement, they can re-introduce on a per-module basis with `#![allow(missing_docs)]` exemptions for the protocol-internal modules.
**Severity:** P3 — doc-coverage tightening; not a correctness or release blocker.
**Source:** F42 closeout — the missing-docs lint was deferred because enabling it surfaces hundreds of low-priority public-item gaps that are out of scope for that F-number.
**Scope.** Per crate root, add `#![warn(missing_docs)]` (or `#![deny(missing_docs)]` for the consumer-facing `mxaccess` + `mxaccess-compat`). Then walk each warning and add at minimum a one-line doc comment per public item. Strategy: do the consumer-facing crates first (`mxaccess`, `mxaccess-compat`); the protocol crates (`mxaccess-codec`, `mxaccess-rpc`, etc.) can land later since their consumers are the higher-level crates which already document the surfaces they re-export.
**Definition of done:**
1. `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` continues to pass with the lints enabled.
2. Every public item in `mxaccess` + `mxaccess-compat` has at least a one-line doc comment.
3. Protocol crates either get the lint enabled too or have an inline `#[allow(missing_docs)]` with a reason that points at this followup.
**Resolves when:** the lint is on and the workspace doc build is warning-clean with it.
### F56 — `subscribe` / `subscribe_buffered` complete on the wire but never receive `0x33` DataUpdate frames
**Status:** **Resolved 2026-05-06.**
**Root cause:** `Session::subscribe` and `Session::subscribe_buffered_nmx` were missing the `INmxService2::Connect` + `AddSubscriberEngine` round-trip that the .NET reference's `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) issues before the first advise against a given publishing engine. Without that pair of RPCs, NmxSvc accepts the subscription registration but the publishing engine never knows our engine is subscribed — so no `0x33` DataUpdate frames flow.
Diagnosed via wwtools/aalogcli: the `[Warning] NmxSvc | NmxCallback->DataReceived ... failed with error 0x{N}` log lines turned out to be NmxSvc's normal log spam where N is the bufferSize, NOT an actual error — the .NET reference's own probe triggers identical entries while still receiving `0x33` DataUpdate frames successfully. The real issue was that those frames never started being sent in the first place.
Fix landed:
- `SessionInner::publisher_endpoints` — per-session `HashMap<(platform_id, engine_id), ()>` cache mirroring `MxNativeSession._publisherEndpoints`.
- `Session::ensure_publisher_connected(platform_id, engine_id)` — issues `INmxService2::Connect(local_engine, galaxy, platform, engine)` then `AddSubscriberEngine(engine, galaxy, source_platform, local_engine)`, once per publisher endpoint per session.
- `Session::subscribe` and `Session::subscribe_buffered_nmx` — both call `ensure_publisher_connected` BEFORE the wire advise.
- `subscribe_buffered_nmx` — additionally issues `AdviseSupervisory` after `RegisterReference`. The .NET reference's `RegisterBufferedItemAsync` only calls RegisterReference, but on this AVEVA install RegisterReference alone produces the registration result + heartbeat callbacks without ever starting DataUpdate dispatch; AdviseSupervisory unblocks the dispatch. Difference may be version-specific.
Live verification passes for both paths against `TestMachine_001.TestChangingInt`:
- `cargo test -p mxaccess-compat --features live-windows-com --test plain_subscribe_live` — receives `0x32` SubscriptionStatus + sequence of `0x33` DataUpdate frames.
- `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live` — same.
Both tests assert on the raw `Session::callbacks()` broadcast (NMX subscription messages) rather than the typed `Subscription::next` (DataChange) path because `TestChangingInt` on this Galaxy is configured with `quality=0x00C0 (Uncertain) value=null`, so the typed path filters every record. The test gate is "wire-level subscription works"; what the engine reports as the actual value is downstream-Galaxy state, out of scope for the Rust port.
**Codec fixes** that ALSO landed in this session as part of the F56 investigation (independent from the resolution above; would have been needed even after the Connect/AddSubscriberEngine fix to make the inbound path readable):
- `NmxSubscriptionMessage::try_parse_process_data_received_body` — peels the `ProcessDataReceived` envelope before calling `parse_inner`. The router previously called `parse_inner` directly on wire bytes, which would have silently dropped any `0x33` even if one arrived.
- `NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body` + router branch — drops `0x11` registration-result frames cleanly instead of logging "unexpected opcode 0x11".
- `Session::subscribe_buffered_nmx` — split-form (object, attribute) wire body + per-session monotonic `item_handle` counter (mirrors `MxNativeCompatibilityServer.AddBufferedItemAsync`'s `_nextItemHandle++`).
**Severity:** P1 — blocked F49 step 1 (F36 buffered live verification), F49 step 2 (F45 recovery replay), and all consumers relying on subscription data flow on this Galaxy. Now unblocked.
**Source:** F49 step 1 live attempt 2026-05-06. The pre-resolution debugging analysis (initial buffered-only hypothesis ruled out via byte-identical parity test → "plain subscribe also fails too" → revised hypothesis around DCOM sink IID / vtable mismatch and disabled object scanning → final landing on the missing `EnsurePublisherConnected` round-trip) is preserved in this file's git history. Run `git log -p design/followups.md` around 2026-05-06 / 2026-05-07 if the dead-end branches are needed for future archeology.
### F55 — Hand-rolled callback exporter rejected by `RegisterEngine2` on this AVEVA install
**Status:** Resolved 2026-05-06 by Path A (DCOM-managed `INmxSvcCallback` sink in `mxaccess-callback::dcom_sink`, wired into `Session::from_nmx_client` behind the `windows-com` feature). Live test `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` passes end-to-end: RegisterEngine2 succeeds, write round-trips, OnWriteComplete fires with status from the wire. The hand-rolled `CallbackExporter` is retained for unit tests that exercise the exporter against an in-process fake NMX peer.
**Severity:** P1 — blocks F49 live verification of every M6 feature that needs an `Engine` registered (i.e. all of them).
**Source:** Live attempt 2026-05-06 against the local AVEVA install. Both the Rust port and the .NET reference's `--probe-register-managed-callback` (which uses the same hand-rolled-exporter approach as the Rust port) fail `RegisterEngine2` with HRESULT `0x800706BA` (`RPC_S_SERVER_UNAVAILABLE` wrapped as Win32 HRESULT). The .NET reference's `--probe-session-write` SUCCEEDS because it goes through `MxNativeSession.Open` → `CreateRegisteredService` (`MxNativeSession.cs:624`) which does **`ComObjRefProvider.MarshalInterfaceObjRef(callback, INmxSvcCallback, DifferentMachine)`** on a real C# COM object — letting Windows DCOM proxy/stub infrastructure handle the callback dispatch — instead of building a hand-rolled OBJREF + TCP listener.
**The Rust port mirrors the .NET reference's `ManagedCallbackExporter` design exactly.** Both fail. So this isn't a Rust port regression — it's a pre-existing issue in the hand-rolled callback architecture that wasn't previously live-tested end-to-end against this NmxSvc install.
**Diagnostic chain (logged from `mxaccess::Session::from_nmx_client`):**
1. `Session::connect_nmx_auto` → `NmxClient::create` → all 6 steps OK (activate, marshal, ResolveOxid, RemQI, final bind). Endpoint resolved to `[fe80::...]:64311`. The new `IUnknownHolder` (mirrors `_activatedComObject` from `ManagedNmxService2Client.cs:15`) keeps the COM ref alive across the steps.
2. `from_nmx_client` builds the callback OBJREF (162 bytes, byte-structurally identical to .NET's at `ProbeRegisterEngine2ManagedCallback.managed_callback_objref_hex` modulo random fields).
3. `RegisterEngine2(engine_id, engine_name, version=6, callback_obj_ref)` returns `Transport(Fault { status: 0x800706BA })`.
**The OBJREF binding is correct:** `DESKTOP-6JL3KKO[]` with `port` from `tokio::net::TcpListener::bind(0.0.0.0:0)`. Windows Firewall is OFF on all profiles. The hand-rolled exporter accepts connections; NmxSvc just refuses to use it.
**Path C investigation (2026-05-06).** Captured the OBJREF byte structure from both paths via the .NET probe:
| Field | DCOM-marshalled (works) | Hand-rolled (fails) |
|---|---|---|
| Total size | 338 bytes | 162 bytes |
| `std_flags` | `0x0A80` (SORF_OXRES4+OXRES6+OXRES8) | `0x280` (SORF_OXRES4+OXRES6) |
| `std_public_refs` | 5 | 5 |
| `std_oxid` / `std_oid` / `std_ipid` | random per session | random per session |
| ncacn_ip_tcp bindings | 4 (DESKTOP-6JL3KKO, 10.100.0.48, 2x IPv6 link-local) — **no ports** | 1 (DESKTOP-6JL3KKO[]) — **with port** |
| Security bindings | 7 | 7 |
Tried setting `std_flags = 0x0A80` on the hand-rolled OBJREF (matching the DCOM-marshalled flag bits): **RegisterEngine2 still fails with the same 1722.** Reverted.
**Updated diagnosis.** The likely cause is that NmxSvc, on receiving RegisterEngine2 with a callback OBJREF, does its own SCM-side OXID resolution: it calls `IObjectExporter::ResolveOxid` against the local SCM at `127.0.0.1:135` to get the bindings for the OBJREF's OXID, then dials those bindings. Our hand-rolled OXID is **never registered with the local SCM**, so the resolution fails and NmxSvc returns `RPC_S_SERVER_UNAVAILABLE` (1722) — matching the symptom and the sub-second timing (no TCP-dial-back attempt to our listener happens at all).
DCOM marshalling fixes this because `CoMarshalInterface` internally registers the OXID with RPCSS, so NmxSvc's SCM-side ResolveOxid succeeds. The bindings carry no port because RPCSS-side resolution returns the dynamic port from the Windows DCOM stub layer.
This makes Path A the architecturally correct fix: the callback exporter must be a DCOM-managed object (registered with RPCSS) for NmxSvc to accept the callback. The hand-rolled-listener-with-explicit-port-in-OBJREF approach used by both the Rust port and the .NET reference's `ManagedCallbackExporter` doesn't satisfy NmxSvc's callback validation.
**Three resolution paths (each substantial):**
- **Path A — switch to DCOM-marshalled callback.** Refactor `mxaccess-callback` so the callback is a real COM class (`#[implement]` via `windows-rs`) registered with the local DCOM SCM, then marshal it via `CoMarshalInterface` for the OBJREF. Abandons the project's "bypass DCOM proxy/stubs" goal but matches what .NET's working path does. ~1 week of work.
- **Path B — hybrid: register via DCOM, dispatch via hand-rolled.** Use `CoMarshalInterface` only to build the OBJREF (which NmxSvc accepts), but intercept the inbound callback connection at the TCP layer to bypass DCOM stub dispatch. Requires reading the `CoMarshalInterface`-produced OBJREF, extracting the OXID/IPID, and standing up a TCP listener that responds to OXID resolution against itself. Architecturally awkward.
- **Path C — investigate the OBJREF rejection at NmxSvc.** Capture the wire bytes NmxSvc sees from the .NET DCOM-marshalled path vs the hand-rolled path; diff to find what NmxSvc actually validates. May reveal a single field difference (e.g. a flag bit) that, set correctly in the hand-rolled OBJREF, makes it work. Cheapest if it pans out, but unbounded if it doesn't.
**Definition of done:** F49 step 5 (LmxClient OnWriteComplete round-trip) runs end-to-end against the live AVEVA install: `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` passes.
**Resolves when:** one of the three paths above lands.
### F3 — Cross-domain NTLM Type1/2/3 fixture
**Severity:** P2
**Status:** Permanently out-of-scope on the current dev host (no second AD domain). Resolution requires external infrastructure not available here.
**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs`. All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in `design/70-risks-and-open-questions.md` R8 (P1 risk) and the open-evidence-gaps table.
**Concrete next step:** See the full provisioning recipe at [`docs/F3-cross-domain-ntlm-recipe.md`](../docs/F3-cross-domain-ntlm-recipe.md). It documents the lab topology (two forests + bidirectional forest trust + a `LAB-B\probe.user` authenticating against an AVEVA install on `LAB-A`), the DC + DNS + trust + user provisioning steps, the Wireshark + `connect-write-read` capture procedure, the exact fixture layout under `crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/`, the round-trip test skeleton (replay the captured Type 2 bytes → regenerate Type 3 → assert byte-equality), and the redaction checklist. Clears R8 in the risks doc when the fixture lands.
## Resolved
### F54 — Per-operation context correlation + compat `OnWriteComplete` fan-out
**Resolved:** 2026-05-06 (commit ``). Two-crate plumbing.
**Part 1 — `mxaccess` (per-operation correlation).** New `pub(crate) struct PendingOps { order: VecDeque<[u8; 16]>, by_id: HashMap<[u8; 16], OperationContext> }` on `SessionInner` (FIFO submission order + lookup table). The 5-byte StatusWord frame and the 1-byte CompletionOnly frame carry no correlation id on the wire (`NmxOperationStatusMessage` is keyless), so the Rust port assigns a synthetic 16-byte id at submission time and the router pops the oldest pending entry on each arriving status frame. Operations on a single `Mutex` complete in submission order, so FIFO is the right correlation strategy. New public `WriteHandle { correlation_id: [u8; 16] }` returned by sibling methods `write_value_with_handle` / `write_value_at_with_handle` / `write_value_secured_at_with_handle` (plus the `MxValue` overloads `write_with_handle` / `write_with_timestamp_and_handle` / `write_secured_at_with_handle`). The non-handle methods `write_value` / `write_value_at` / etc. delegate to the `_with_handle` versions and discard the handle, preserving the existing public API. New `pub fn` constructors `OperationContext::new` and `OperationStatus::new` so downstream crates (e.g. `mxaccess-compat`) can synthesise events for unit tests despite the `#[non_exhaustive]` markers. `callback_router` gains a `pending_ops: Arc>` parameter and pops the oldest entry when an op-status frame arrives — populating `OperationStatus.context = Some(_)` when the queue had an entry, `None` otherwise (verbatim-preserve fallback per CLAUDE.md). Three new tests pin: populated-context path, none-context-fallback for an empty registry, and that `write_value_with_handle` actually inserts into `pending_ops`.
**Part 2 — `mxaccess-compat` (compat-layer fan-out task).** New `correlation_to_item: Arc>>` on `LmxInner`. `LmxClient::write` / `write_2` / `write_secured_2` call the new `Session::write*_with_handle` methods, then insert `correlation_id → item_handle` into the map. `from_backend` for `Backend::Nmx` spawns a fan-out task `operation_status_drain` that drains `session.operation_status_stream()` and routes each event: `OperationKind::Write | WriteSecured` → `WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recovery }` on `on_write_complete_tx`; any other kind → `OperationCompleteEvent` on `on_operation_complete_tx`; events with `context: None` or with a correlation id missing from the map drop silently (no bogus `item_handle = 0` events). The `JoinHandle` is held in a `std::sync::Mutex