NODEDC_MISSION_CORE/docs/adr/0009-device-plugin-observat...

6.1 KiB

ADR 0009: device-plugin observation runtime and K1 extraction

  • Status: accepted
  • Date: 2026-07-17
  • Extends: ADR 0003, ADR 0004 and ADR 0008

Context

The manifest/action boundary isolated the browser shell, but the host observation service still imported K1 archive discovery, .k1mqtt preparation, MQTT timing metadata and the K1 normalizer. A second sensor could not provide durable replay without adding vendor branches to SessionStore, SessionRecordingMaterializer and app.py.

Decision

Every installed device plugin may contribute an optional observation runtime:

  • one or more ObservationArchiveSource values with a plugin ID, archive ID, confined root, discovery callback and recovery callback;
  • exactly one recording exporter for that plugin ID;
  • generic ObservationSessionCandidate, SessionSource and ObservationArtifactCandidate values returned to the host.

The host owns SQLite lifecycle, path confinement, preparation scheduling, cross-process cache locking, atomic publication, RRD delivery, recorded-media admission and browser APIs. A plugin owns native evidence discovery, transport format validation, recovery semantics, timeline-origin extraction and conversion of its primary artifact into the canonical recorded Rerun artifact.

For new K1 sessions, the plugin also contributes confined clock artifacts. A durable origin is published before camera production, MQTT shutdown creates a transport-scoped provisional envelope, and the acquisition owner publishes a content-addressed session envelope plus an atomic summary-pointer switch after camera and MQTT/runtime shutdown. A provisional envelope cannot advertise combined camera replay. Valid older sessions without this contract keep their first/last-message compatibility fallback.

ReplayCommand is now vendor-neutral. It contains an owning plugin_id, an opaque primary artifact ID, an ordered tuple of confined artifacts and a ready timeline origin. It contains no filename suffix, MQTT topic or sidecar name. The materializer selects the exporter by plugin_id and stages every validated artifact prefix while preserving only its plugin-owned basename.

Derived RRD cache v9 binds the published RRD to:

  • plugin ID and primary artifact ID;
  • ordered artifact IDs and media types;
  • full file identity and replay boundary for every artifact;
  • per-artifact prefix digest;
  • durable clock origin, active-envelope start/completion, selected clock artifact identity and both digests when present;
  • derived recording digest and zero-based timeline bounds whose origin and end are both materialized as real RRD rows.

Cache v8 and older sidecars are rebuilt once. The native evidence is not rewritten.

The K1 exporter also recognizes bounded ModelingReport messages. It writes the reported route distance, speed and scan time as scalar time series below /metrics/device; it does not infer them from pose integration or a UI timer. Live observation consumes the same status through a plugin-injected raw-message observer before the four-message visual preview queue. The generic VisualizationRuntime still has no K1 decoder or topic knowledge.

The concrete implementation is physically located below:

src/k1link/device_plugins/xgrids_k1/
  ble/ mqtt/ protocol/ analyze/ net/ usb/
  viewer/
  facade.py camera.py archive.py observation.py rrd_export.py cli.py

The manifest entrypoint is k1link.device_plugins.xgrids_k1.facade:build_xgrids_k1_plugin. Generic sessions, web/app.py, web/session_api.py and the Rerun consumer do not import this package or contain K1 formats, topics or identifiers.

Plugin SDK v0alpha2 additionally defines immutable RuntimeActionInvocation and RuntimeActionResult. The host dispatcher creates and validates them for every plugin action, including legacy compatibility routes, before returning the path-free output document.

Acceptance gate

The automated gate builds an unrelated synthetic spatial-sensor plugin which:

  1. discovers its own native evidence extension;
  2. reconciles into the unchanged host SQLite catalog;
  3. produces a generic ReplayCommand;
  4. materializes and reuses an RRD through its plugin-keyed exporter.

A dependency test also fails if generic observation hot-path files regain K1, XGRIDS, topic, native-format or concrete-plugin imports. Adding a second device therefore requires a manifest/runtime contribution and a plugin-owned reviewed UI contribution, not edits to the session API, preparation queue, Rerun consumer or main application shell. ADR 0010 defines that frontend boundary.

Additional acceptance tests bind the K1 capture clock into the ordered replay artifacts, reject corrupt or provisional new-schema boundaries, materialize real origin/end rows and retain archived device-metric samples without traversing the lossy live-preview queue.

Consequences

The semantic and physical vendor boundary is complete for the current single-process architecture. Existing K1 raw evidence, camera archives and operator workflows remain compatible. The k1link distribution name and CLI command remain transitional compatibility names; their implementation is now plugin-owned.

The operator project name is normalized and validated by the K1 contribution, stored as display/catalog metadata and never used as a path component. This ADR does not claim that it reaches the scanner: the inert application-control codec has no publisher, and automatic K1 writes remain disabled pending reviewed operator-owned Keychain item provisioning, physical acceptance of the uninstalled transport and durable post-stop save evidence.

ADR 0011 subsequently places the action control plane behind a versioned descriptor/handshake/health transport seam. Observation discovery and export remain host-called plugin contributions and are not yet portable process transport contracts.

This ADR does not claim process isolation, crash containment, a durable operation journal, signed plugin bundles, multi-device concurrency, portable SDK stream envelopes or SDK EvidenceStore persistence. Those require a supervisor and a new physical regression gate; they must not be inferred from module extraction.