6.0 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
ObservationArchiveSourcevalues with a plugin ID, archive ID, confined root, discovery callback and recovery callback; - exactly one recording exporter for that plugin ID;
- generic
ObservationSessionCandidate,SessionSourceandObservationArtifactCandidatevalues 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:
- discovers its own native evidence extension;
- reconciles into the unchanged host SQLite catalog;
- produces a generic
ReplayCommand; - 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, reviewed emission 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.