I/O observations, capacity and retained recovery

IO07, IO09 and IO10 use explicit caller-owned diagnostic objects. Default request, stream, slot and owner fields remain unchanged; the platform creates no global sampler, hidden thread or automatic device-recovery policy. Diagnostics observe actual state; deadlines, errors and diagnostic detach never revoke accepted borrows.

Observing a request through stable identity

Core’s request_observer.h depends only on Core. The caller binds a zeroed nx_request_observer_t to a stable slot and nonzero epoch. Sample first verifies the same live incarnation before accessing the request; it caches no request pointer. After slot release or reuse, the old observer returns STATE. Epochs never wrap for reuse; the existing slot contract rejects further admission at the limit.

The caller excludes concurrent sample versus slot bind/release, request prepare/reuse and observer mutation, and ensures bound storage remains valid. Core adds no IRQ mask, OS lock or multicore synchronization on the caller’s behalf. Normal device executors may still advance requests; final result/count is read only after acquire-observed SETTLED. While borrowed, outputs are result=BUSY and count=0, preventing unpublished results from appearing valid.

Timestamps share the request deadline’s monotonic clock domain. Backward time returns IO, preserving outputs and the last valid time; diagnostic counters saturate. max_gap and transitions describe differences between samples, not hardware completion time, all internal transitions or WCET. Detach removes only observation binding and is permitted during QUARANTINED, but actual device borrows must still be drained afterward.

Outputs must be separate from the complete descriptor, payload and control storage. Public Core knows only the request base size and cannot validate the full derived-descriptor range or detect freed C pointers. Owners usually release their control slot before outer SETTLED; observing that slot then returns STATE. To continue sampling after final-result publication, the caller should retain its own independent stable slot and release it after its readers exit.

Explicit owner progress diagnostics

Optional Nexus::OwnerObserver is called by the sole owner executor. Each call executes exactly one real nx_bus_owner_service and returns its result unchanged. BUSY is counted separately from other errors. Backward observation time sets diagnostic_result without overwriting service_result or preventing continued drain.

It then copies active ticket, request state and idle under the existing short metadata guard, without hardware calls or slot-array scans. Copies retain no request/payload references. quarantines counts observations, not failed operations. Service SUCCESS does not mean the current operation finally succeeded; final results still come through acquire-observed request results. It does not infer nonexistent errors from void device service.

Observer, owner, slots and guard/executor/clock must retain the same binding and lifecycle. Producers keep the original owner guard; diagnostics add no device hot-path vtable layer. After detach, the owner still owns all undrained work. Diagnostic outputs must not overwrite accepted operations or payloads.

Separate read-only consumers from normal progress

IT-R01 composes existing mechanisms without adding a public state API. The unique executor calls nx_owner_observer_service at its normal progress point and receives an nx_owner_observation_t value copy. A monitor receives only that copy and an optional nx_request_observation_t copy, then calls the pure nx_io_diagnose function. Reading the same copy does not service again, read device registers, advance a transfer, read a clock or release a borrow. A copy describes its observation time; it neither guarantees current state nor grants permission to cancel, recover or reuse storage.

A consumer marks owner admission, queue and slot fields as known only when diagnostic_result succeeds. Missing request, stream, lifecycle, service-budget and observer-join facts remain unknown. A closed, idle owner does not prove that UART/SPI hardware has stopped, IRQ publishers have exited or external readers have joined; the real lifecycle owner completes those steps separately. Publishing copies across tasks requires caller-provided synchronization and storage lifetime. Concurrent access to an ordinary C structure is not an atomic snapshot.

This composition changes no default request, slot, owner or provider fields and adds no default queue, counter, worker, vtable or device hot-path call. Unselected observation adds no resident cost. When observation is selected, the caller provides the existing sidecar and copied-value storage; ABI-dependent sizes appear in this page’s storage table. The combined regression in tests/google/io_owner_diagnostic_pipeline_test.cpp uses real Native UART/SPI providers and existing C executors to check active, quarantine, closed/idle and unknown facts, same-kind instance isolation and copies surviving completed drain. It contains four Google cases; internal transport branches are not extra cases. Model execution does not qualify physical shutdown or timing.

Stream snapshots and capacity budgets

nx_stream_observe copies maintained aggregates under one short IRQ guard, with work independent of block count and no slot scans or payload reads. free_blocks is the total free count; next_free says whether the normal stream producer cursor’s slot is available. Out-of-order release can leave free blocks while that cursor remains occupied. A preposted producer has its own cursor and must determine its next target through the corresponding reservation API; next_free is not permission for the next continuous-DMA target.

nx_stream_budget_calculate is a pure cold-path check. The caller declares rate, block units, bytes per unit, maximum FIFO-frontier release delay, hardware-target count and spare count. Its calculation is:

retained = ceil(rate * release_delay_us / (block_units * 1000000))
required_blocks = hardware_targets + spare_blocks + retained
required_payload_bytes = required_blocks * block_units * unit_bytes
block_fill_us = floor(block_units * 1000000 / rate)

Rate and block use the same units. For example, multichannel ADC may use complete scans, with unit_bytes explicitly covering every channel per scan. Both uint64 multiplications and size_t addition/multiplication are overflow-checked. Invalid input or overflow preserves old output and returns INVALID/EXHAUSTED; it never saturates into an apparently valid budget. configured_blocks >= required_blocks meets declared capacity; target-replacement latency must be strictly below the rounded-down fill time. This does not create backpressure or remove FIFO blocking, CT ABA or real clock/IRQ latency. Physical qualification still needs measured rates and worst-case service gaps for the board.

External application quarantine recovery policy

nexus-examples/apps/uart_owner_runtime/quarantine_policy.c retains its old SDK pin and directly composes existing BusOwner APIs. Actual main owner-progress sites use this policy so service errors enter observable fault stages instead of being silently discarded with (void). BUSY is normal waiting.

Real errors close admission, publish FAULT_OBSERVED and establish explicit finite cleanup-time/step budgets. Backward time, deadline-add overflow or budget exhaustion publishes ESCALATED_RETAINED. Later calls still perform actual service; there is no worker kill, reset, automatic replay or undrained-buffer return. Actual idle eventually publishes LOAN_DRAINED, proving only owner loans ended. Producer join, IRQ detach, hardware stop, observer join and notification destruction still follow original lifecycle order.

The fault sink runs outside the guard and must be bounded and non-reentrant. Applications retain stages and fault state and wake the coordinator; product safe outputs, device reset and field handling remain external product-project responsibilities. This example establishes no safety level.

Storage and verification boundaries

The following sizeof fixture uses the actual Cortex-M4F GNU Arm 14.3 -fshort-enums ABI; array symbols are ARM-compiled and read with nm. It measures explicit object layout only, not whole-image code size or physical cycles.

Explicit object

Bytes

request observer / copied observation

48 / 40

owner observer / copied observation

96 / 72

stream copied observation

32

stream budget input / result

48 / 32

Measured default sizes are request 24, request slot 16, stream 40, stream slot 40, BusOwner 84 and owner slot 48 bytes. Default structures gain no fields; no sidecar is created when observers are unselected. Other compilers, enum ABIs and pointer widths require fresh measurement rather than reuse of these values.

New Google suites contain 15 observer/budget and 7 descriptor/provider state-machine cases. Native UART executes 32 sequences, SPI 16 sequences, and ADC 4 consume/stop sequences; these loops are not additional Google cases. I2C public-dispatch sequences mock external asynchronous-provider dependencies to prove public borrow/retry contracts, without claiming Native I2C asynchronous hardware. Six quarantine-policy cases were added within the external example’s 28 actual owner/lifecycle Google cases. First real stub RED and final GREEN logs, fresh XML, commands and return codes are retained in tools/testing/evidence/io-observer-iteration/. ARM32 capacity checks exposed a strict type-limits compilation error; RED/GREEN for the same production ABI and extreme-capacity regressions are retained together.

These are development software evidence. Clean-source SDK, external consumers and the complete candidate matrix bind final sources again through the unified release process. Boards are not connected; physical DMA target switching, error injection, UART timing and safe outputs have not been executed.