fix(k1): restore low-latency live recovery path

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 16:17:27 +03:00
parent 85035fa07b
commit 1001a31638
12 changed files with 1252 additions and 124 deletions
@@ -0,0 +1,150 @@
# ADR 0042 — K1 run ledger and regression telemetry
Status: proposed for acceptance
Date: 2026-08-22
Named physical references and redacted evidence hashes are retained in
[`docs/lab/010_K1_MISSION_CORE_INTERNAL_LIVE_BASELINE_20260822.redacted.md`](../lab/010_K1_MISSION_CORE_INTERNAL_LIVE_BASELINE_20260822.redacted.md).
That report deliberately distinguishes the fast Mission Core internal
ideal-path reference from the accepted recovery-capable checkpoint and from
manufacturer/LixelGO evidence.
## Context
The K1 integration has repeatedly returned to the same visible failures after
otherwise successful recovery work: delayed first point cloud, a camera that
repeatedly disappears and reopens, browser decoder failures, slow START/STOP,
and connection-mode transitions that disturb an already accepted Bridge path.
The current evidence is individually strong but operationally fragmented. Raw
MQTT capture, camera fMP4 epochs, acquisition checkpoints, browser diagnostics,
redacted manifests and operator timing notes use different identities and must
be correlated manually. A successful run is described in Ops, but is not yet an
executable baseline that can reject a later regression.
The repository already owns an accepted telemetry plane under ADR 0031:
Telegraf and a bounded native JSONL outbox publish through authenticated MQTT to
the Mission Core normalizer and TimescaleDB. Introducing another broker or
database solely for K1 would create a second lifecycle, credential boundary and
recovery problem without improving the local K1-to-Mission-Core evidence path.
## Decision
Every physical or replayed K1 acquisition is one explicit **K1 run**. The run is
identified by the acquisition id and its evidence-session lineage. All derived
events carry the same redacted identity, code revision, UI build id, connection
mode and monotonic clock origin.
The authoritative write path is:
```text
K1 control, MQTT receiver, camera gateway and browser diagnostics
-> local append-only K1 run journal
-> bounded durable outbox
-> authenticated telemetry MQTT topic
-> existing Mission Core telemetry normalizer
-> existing TimescaleDB
-> run comparison and acceptance report
```
The local journal remains the source of truth during network loss. Database
availability never gates START, STOP, raw evidence capture, camera archival or
local recovery. Delivery resumes from the outbox when the telemetry contour is
available.
The initial event contract is `missioncore.k1-run-event/v1`. Each event contains:
- `run_id`, `acquisition_id`, redacted `evidence_session_id` lineage;
- UTC and monotonic timestamps plus a per-run sequence number;
- Mission Core commit, UI build id and K1 compatibility-profile id;
- connection mode, host-path epoch and producer generations without addresses,
credentials, project names or raw device identifiers;
- event code, stage, outcome and bounded numeric facts;
- source component and source schema version.
The required timeline includes at least:
- operator START intent, physical command publication and confirmed SCANNING;
- calibration start/end when the device exposes those facts;
- first authoritative PCL received and first PCL published to Rerun;
- browser Rerun store admitted and first visible point-cloud frame;
- camera producer init, first media, browser first frame and sustained playing;
- camera decoder errors, preview-reader retirements, IDR admission, queue age,
bytes and fragment counts;
- connection gap start, recovery attempts, recovered binding and gap end;
- operator STOP intent, confirmed READY and archive finalization;
- terminal run result and automatically evaluated acceptance checks.
An acceptance report is generated from the journal after each run. It compares
the run with a named accepted baseline and reports exact deltas. The following
conditions are hard failures rather than performance warnings:
- any browser `MEDIA_ERR_DECODE`;
- any decode error in the archived camera stream;
- a missing first point-cloud admission or missing terminal archive;
- loss of acquisition/control lineage;
- a device state asserted by the UI without authoritative device evidence.
Latency and FPS thresholds are versioned baseline checks. They are never
implemented by silently reducing point count, image quality, bitrate or source
FPS. A threshold change requires its own reviewed acceptance run.
The engineering work format changes accordingly:
1. Name the accepted baseline and the single invariant targeted by the change.
2. Make one bounded implementation increment.
3. Run focused synthetic tests before a physical test.
4. Perform one canonical UI run; do not substitute direct device commands.
5. Generate and retain the run comparison automatically.
6. Do not merge a change that improves its target while regressing an already
accepted invariant.
## Rejected alternatives
### Redis
Redis is useful for disposable cache and coordination. It is not the evidence
system of record for offline physical runs and does not replace a durable local
journal or analytical store.
### Kafka
Kafka is not admitted for this local single-producer contour. The existing MQTT
plane already supplies authenticated fan-out and reconnect delivery, while the
local outbox supplies offline durability. Kafka would add a second broker and
operational quorum without repairing the K1, RTSP, browser or local-network
boundaries. It can be reconsidered only when independent high-volume consumers,
cross-site retention or replay requirements exceed the accepted MQTT contour.
### ClickHouse
ClickHouse is not admitted as a second telemetry database. Existing TimescaleDB
already stores run identity, timestamps and JSON payloads and is sufficient for
the current event volume and comparisons. The event schema remains storage
neutral, so a later measured analytical-volume limit can justify a reviewed
ClickHouse migration without changing producers.
## Implementation increments
1. Generate a local K1 run report from the evidence and diagnostic artifacts
already produced today; no runtime transport change.
2. Emit the versioned append-only event contract directly during a run and seal
it with the evidence session.
3. Tail the bounded outbox through the accepted ADR 0031 telemetry plane and
extend the normalizer with K1 run series.
4. Add baseline comparison and a CI/replay gate. Physical acceptance remains a
separate operator-controlled UI test.
No product window, status family or K1 control command is introduced by this
decision.
## Consequences
- A stable K1 run becomes executable regression evidence rather than a prose
memory.
- Camera, point-cloud, control and recovery events can be compared on one clock
and one run identity.
- Wi-Fi or telemetry-plane loss cannot erase the local run or block the device.
- The existing telemetry stack is extended instead of duplicated.
- The first implementation work is an evidence/reporting increment; it does not
authorize broad recomposition of the fragile K1 communication path.
@@ -0,0 +1,213 @@
# K1 ↔ Mission Core internal live baseline — 2026-08-22
Status: accepted internal engineering reference
Scope: one owner-controlled XGRIDS/LixelKity K1, exact firmware 3.0.2
Connection: Mission Core Bridge/direct-LAN path
Operator path: canonical Mission Core UI only
## Classification
This is the fast **Mission Core ↔ K1 internal reference**, not a vendor/LixelGO
reference, not a production SLA and not proof for arbitrary Wi-Fi conditions,
another K1, another firmware, Quick Connect, Direct Connect, LTE, a tunnel or a
vehicle installation.
The reference exists because the same visible regressions repeatedly returned:
slow START/STOP, delayed point-cloud admission, a camera that disappeared and
reopened, browser decoder errors and a discrete trajectory. It records both the
fastest clean internal run and the accepted recovery-capable checkpoint that
followed it. Raw MQTT, video and device evidence remain outside Git; only
redacted summaries, hashes and engineering conclusions are retained here.
No point count, image quality, bitrate, source FPS or K1 protocol setting was
reduced to obtain either result.
## Baseline A — fast ideal-path reference
- Code: `eaad9de` (`fix(k1): stabilize live recovery and media admission`).
- Evidence session: `20260822T105904Z_viewer_live`.
- Local redacted manifest SHA-256:
`990b9433d40965b2b712df155f5f96d38770d53fddde804852c11eca90d96b3e`.
- Local MQTT summary SHA-256:
`9438cc8947c7a868ff8ffcfc163233dfea40a95b7aae34050806bb072b2a7750`.
- Local camera summary SHA-256:
`bc333e1dd1e7800204b242eecd01a83d1c91c388ab2202a238e8d2fc4a43aa14`.
- Final phase: `idle`; stop reason: `external_stop`; rejected MQTT messages: 0.
Operator-observed UI/physical timing:
| Boundary | Observed time |
| --- | ---: |
| START click → K1 calibration onset | < 5 s |
| K1 calibration | 21 s |
| calibration end → visible point cloud | 2 s |
| calibration end → visible right camera | 4 s |
| STOP click → physical stop onset | 1 s |
Recorded Mission Core facts:
| Metric | Value |
| --- | ---: |
| PCL frames / pose frames | 121 / 132 |
| points published | 412,175 |
| point decode errors | 0 |
| preview dropped | 0 |
| MQTT callback → publish p50 / p95 | 23.839 / 41.541 ms |
| camera archive | complete, 70 segments, 5,645,411 bytes |
| camera failure code | none |
| offline H.264 decode errors | 0 |
This run used a fresh, short physical-command history: the terminal ledger was
2,989 bytes at revision 15 with no archive segments. That distinction is
material. The timings are a low-latency reference, not evidence that a mature
recovery ledger can be cryptographically reread several times per UI poll for
free.
## Regression that exposed the shared bottleneck
The recovery-capable working line later produced session
`20260822T124524Z_viewer_live`:
- START click → calibration onset: 26 s;
- calibration end → cloud: about 2 s;
- calibration end → camera: 19 s with repeated disappearance and large lag;
- STOP click → physical stop onset: 12 s;
- preview dropped: 393;
- callback → publish p50/p95: 110.331/309.434 ms;
- the retained fMP4 archive contained real H.264 macroblock/bytestream decode
errors, while the ideal reference did not.
The camera UI resets were therefore a symptom, not the primary cause. Increasing
preview queues or reducing media/point quality would only hide overload.
Profiling found repeated full parsing, validation and archive-hash traversal of
the physical START/STOP ledger inside passive `state.read`, nested public state
projections and the first-PCL camera path. The mature ledger was 18,378 bytes at
revision 1,219 plus five immutable archive segments / 318,027 bytes. One passive
state request took 240680 ms while the UI requested state four times per second.
That contention delayed MQTT publication and FFmpeg stdout consumption together.
## Baseline B — accepted recovery-capable checkpoint
Evidence session: `20260822T130323Z_viewer_live`.
- Local redacted manifest SHA-256:
`f870bedd9036a3f03993344d405eab4ef668d1f0fdae55e16adf8ecdcb51f48a`.
- Local MQTT summary SHA-256:
`2d42d31c14d98d77bcefc8a1b9983e102cace2375ada9e5f89f021a33920a55d`.
- Local camera summary SHA-256:
`1386ea16615a9716b699d6ba35733ef26d367b6a5257d572cde11509e546cdda`.
- Final phase: `idle`; stop reason: `external_stop`; rejected MQTT messages: 0.
Operator-observed UI/physical timing:
| Boundary | Observed time |
| --- | ---: |
| START click → K1 calibration onset | 14 s |
| K1 calibration | 22 s |
| calibration end → visible point cloud | about 1 s |
| calibration end → visible right camera | 8 s |
| STOP click → physical stop onset | 7 s |
Recorded Mission Core facts:
| Metric | Value |
| --- | ---: |
| PCL frames / pose frames | 369 / 400 |
| points published | 1,318,018 |
| point decode errors | 0 |
| preview dropped | 70 |
| callback → publish p50 / p95 | 83.934 / 223.589 ms |
| first-PCL camera admission | 65 ms |
| camera authority / FFmpeg / post-spawn commit | 5 / 16 / 2 ms |
| total backend camera activation | 23 ms |
| backend activation → browser playing | 5.383 s |
| browser camera restarts / slow-consumer retirements | 0 / 0 |
| camera archive | complete, 342 segments, 27,469,046 bytes |
| camera failure code | none |
| offline H.264 decode errors | 0 |
The accepted STOP was emitted once. Mission Core received an exactly correlated
successful K1 application response after about 2.35 s, later observed fresh
`READY` and resolved the durable physical command as
`stop-standby-observed`. No automatic physical retry was introduced.
After the bounded read-path change, passive `state.read` measured 2343 ms on
the same mature ledger instead of 240680 ms.
## What restored the usable path
1. A single verified physical-ledger snapshot is shared across one public state
projection instead of being recursively reloaded by nested projections.
2. Process-local control facts can be read without implicitly traversing the
durable physical ledger again.
3. Passive snapshots reuse an already cryptographically verified ledger only
while the ledger directory, main file, archive directory and every bounded
archive entry retain the exact device/inode/mode/owner/link-count/size/
`mtime_ns`/`ctime_ns` fingerprint. Every mutation and proof still performs a
complete reload. Cross-process atomic writes and same-size archive tampering
invalidate the cache and fail closed.
4. Camera authority is fully proved before FFmpeg `Popen`; after spawn only a
bounded in-memory compare-and-swap may commit the producer. Reader threads
therefore start without a second durable-ledger pause that can back up the
fMP4 pipe.
5. First-authoritative-PCL camera admission no longer recursively rereads the
same physical ledger on the 10 Hz ingress callback.
6. Structured timings were added for first-PCL admission, camera activation and
STOP dispatch. These are diagnostics only; they do not add a K1 command,
status, retry or product window.
## Fragile boundaries that remain open
- Baseline B is usable but does not equal Baseline A: 70 preview drops and
83.934/223.589 ms p50/p95 remain above the clean reference of 0 drops and
23.839/41.541 ms.
- START and physical STOP onset remain materially slower than the fast reference.
The current evidence separates local validation, correlated application
response and later `READY`, but does not yet expose a device-side calibration
onset or physical motor/LED timestamp on the same clock.
- Camera presentation still depends on RTSP startup, the next usable H.264
parameter-set/IDR boundary and browser MSE admission. The accepted archive has
non-monotonic-DTS warnings but no H.264 decode corruption.
- A successful application response is necessary but not sufficient to dismiss
the interface. Mission Core must keep the active scene until authoritative K1
state proves the requested physical transition.
- Wi-Fi loss, controller-process loss, K1 power/battery loss and return must
retain the current no-automatic-START/STOP rule and exact lineage. They need
separate UI acceptance runs; this baseline must not be used to infer them.
- Bridge remains the accepted product path. Quick Connect/Bridge switching must
not reset the whole reactive application or reuse the other mode's reconnect
affordance, and must not perturb the Bridge protocol to improve Quick Connect.
- The original LixelGO packet captures remain manufacturer-app evidence. They
are not interchangeable with this Mission Core internal baseline.
## Regression rule
Future K1 work starts from one named baseline and changes one bounded invariant.
The canonical acceptance path is the Mission Core UI. A run is rejected if it
introduces an archived-camera H.264 decode error, browser `MEDIA_ERR_DECODE`,
missing point-cloud admission, missing terminal archive, lost control/acquisition
lineage, duplicate physical command or a UI state not backed by authoritative K1
evidence. Performance must not be recovered by silently reducing source data.
## Delivery validation
- Canonical UI physical run: accepted as Baseline B above.
- Focused backend regression suite for the touched viewer diagnostics,
acquisition lifecycle, application session, camera gateway and physical
ledger: 800/800 passed.
- Ruff: passed for every changed Python source and test file.
- mypy: passed for all five changed source modules.
- `git diff --check`: passed.
- Offline decode of the accepted 50-second camera window: no H.264 decode
corruption; only non-monotonic-DTS warnings were observed.
- Canonical Mission Core service remained available on `127.0.0.1:8000` after
validation.
The next performance increment should be driven by a generated run comparison
under ADR 0042. Kafka, Redis or ClickHouse are not a substitute for fixing the
local control/media hot path and are not admitted without a measured requirement
beyond the existing MQTT/Timescale telemetry contour.
+60 -24
View File
@@ -661,11 +661,13 @@ class XgridsK1CameraGateway:
target_host: str,
session_dir: Path,
*,
pre_prepare_fence: CameraProducerCommitFence,
commit_fence: CameraProducerCommitFence,
committed_before_start: CameraProducerCommittedObserver | None = None,
) -> dict[str, Any]:
"""Prepare the first acquisition camera outside every shared gate."""
"""Reserve authority before Popen, then commit without a dead-pipe gap."""
activation_started = time.monotonic()
if source_id not in CAMERA_SOURCE_PATHS:
raise ValueError("неизвестный camera source")
target = validate_private_ipv4(target_host)
@@ -674,29 +676,41 @@ class XgridsK1CameraGateway:
raise ValueError("observation session directory does not exist")
if not root.is_relative_to(self._repository_root):
raise ValueError("camera recording root must stay inside the repository")
with self._lifecycle_lock: # noqa: SIM117 - lock order is intentional
with self._lock:
self._require_open_locked()
if self._ffmpeg_path is None:
raise RuntimeError("локальный camera adapter FFmpeg не найден")
if self._producer is not None or self._recording_root is not None:
raise RuntimeError("camera acquisition producer уже активен")
if self._source_id is not None and (
self._source_id != source_id or self._target_host != target
):
raise RuntimeError("для camera gateway уже выбран другой source")
if self._source_id is None:
self._generation += 1
self._source_id = source_id
self._target_host = target
self._recording_root = root
self._recording_media_segment_count = 0
self._archive_summaries = []
self._expected_source_end_generation = None
self._phase = "selected"
self._error = None
self._revision += 1
reserved_generation: list[int] = []
def reserve() -> bool:
with self._lifecycle_lock: # noqa: SIM117 - lock order is intentional
with self._lock:
self._require_open_locked()
if self._ffmpeg_path is None:
raise RuntimeError("локальный camera adapter FFmpeg не найден")
if self._producer is not None or self._recording_root is not None:
raise RuntimeError("camera acquisition producer уже активен")
if self._source_id is not None and (
self._source_id != source_id or self._target_host != target
):
raise RuntimeError("для camera gateway уже выбран другой source")
if self._source_id is None:
self._generation += 1
self._source_id = source_id
self._target_host = target
self._recording_root = root
self._recording_media_segment_count = 0
self._archive_summaries = []
self._expected_source_end_generation = None
self._phase = "selected"
self._error = None
self._revision += 1
reserved_generation.append(self._generation)
return True
if not pre_prepare_fence(reserve):
raise ValueError("camera activation authority устарела до запуска adapter")
if len(reserved_generation) != 1:
raise RuntimeError("camera activation fence did not reserve exactly once")
authority_reserved = time.monotonic()
prepared = self._prepare_selected_producer()
ffmpeg_prepared = time.monotonic()
def commit() -> bool:
with self._lifecycle_lock:
@@ -719,7 +733,29 @@ class XgridsK1CameraGateway:
if not committed:
self._discard_prepared_producer(prepared, failure_code="stale-activation-commit")
raise ValueError("camera activation lineage устарела")
return self.snapshot()
readers_started = time.monotonic()
snapshot = self.snapshot()
logger.info(
"K1 camera acquisition producer activation timing",
extra={
"event_code": "k1_camera_activation_timing",
"camera_generation": reserved_generation[0],
"camera_authority_wait_ms": int(round(
(authority_reserved - activation_started) * 1_000,
)),
"camera_ffmpeg_prepare_ms": int(round(
(ffmpeg_prepared - authority_reserved) * 1_000,
)),
"camera_post_spawn_commit_ms": int(round(
(readers_started - ffmpeg_prepared) * 1_000,
)),
"camera_activation_total_ms": int(round(
(readers_started - activation_started) * 1_000,
)),
"device_command_sent": False,
},
)
return snapshot
def stop(self, generation: int) -> dict[str, Any]:
with self._lifecycle_lock:
+374 -64
View File
@@ -599,6 +599,18 @@ class _ActiveStreamRecoveryLineage:
target_port: int
@dataclass(frozen=True, slots=True)
class _CameraProducerActivationClaim:
"""Pre-Popen authority reduced to a bounded post-Popen local CAS."""
acquisition_id: str
evidence_session_id: str
start_operation_id: str | None
stop_operation_id: str | None
runtime_producer_generation: int
recovery_generation: int
@dataclass(frozen=True, slots=True)
class _PostRecoveryCameraRestartKey:
"""Exact recovered-PCL and camera progress CAS claimed by one worker."""
@@ -3848,7 +3860,7 @@ class XgridsK1CompatibilityService:
def _retire_terminal_prestart_control_failure(self) -> bool:
"""Automatically clear only proven local, pre-START terminal state."""
snapshot = self._application_control_session.snapshot()
snapshot = self._control_session_snapshot_without_physical_command()
self._retire_prepared_acquisition_on_terminal_control_failure(snapshot)
state = str(snapshot.get("state") or "unknown")
failure = snapshot.get("failure")
@@ -3909,6 +3921,13 @@ class XgridsK1CompatibilityService:
or self.runtime.snapshot().get("source_mode") != "idle"
):
return False
control = self._control_session_snapshot_without_physical_command()
if control.get("state") not in {
"connection-ready",
"workspace-ready",
"project-ready",
}:
return False
physical = self._physical_command_coordinator.snapshot()
if (
physical.get("status") not in {"empty", "resolved"}
@@ -3916,14 +3935,6 @@ class XgridsK1CompatibilityService:
or _physical_command_reports_active(physical)
):
return False
control = dict(self._application_control_session.snapshot())
if control.get("state") not in {
"connection-ready",
"workspace-ready",
"project-ready",
}:
return False
verified = control.get("verified_control")
supervisor = self._connection_supervisor.snapshot()
structurally_current = bool(
@@ -5618,6 +5629,14 @@ class XgridsK1CompatibilityService:
intent: Literal["select-device", "change-network"],
transition_gate_owned: bool,
active_binding: Mapping[str, object] | None = None,
runtime_snapshot: Mapping[str, object] | None = None,
physical_command_snapshot: Mapping[str, object] | None = None,
control_session_snapshot: Mapping[str, object] | None = None,
native_runtime_snapshot: Mapping[str, object] | None = None,
semantic_topology_snapshot: Mapping[str, object] | None = None,
identity_pin_snapshot: Mapping[str, object] | None = None,
provisioning_idempotency_snapshot: Mapping[str, object] | None = None,
network_ledger_snapshot: NetworkMutationLedgerSnapshot | None = None,
) -> list[str]:
"""Return one factual Bridge-only pre-START handoff admission policy."""
@@ -5631,14 +5650,44 @@ class XgridsK1CompatibilityService:
configured_mode = self._connection_mode
pending_required_mode = self._connection_reconfiguration_required_connection_mode
lifecycle_holders = set(self._application_control_process_lease_holders)
runtime = self.runtime.snapshot()
physical = self._physical_command_coordinator.snapshot()
control = self._application_control_session.snapshot()
ble_runtime = ble_runtime_snapshot()
semantic = self._semantic_topology_public_snapshot()
identity = self._device_identity_pin_public_snapshot()
idempotency = self._network_provisioning_idempotency_public_snapshot()
network_ledger = self._network_mutation_ledger.snapshot()
runtime = (
runtime_snapshot if runtime_snapshot is not None else self.runtime.snapshot()
)
physical = (
physical_command_snapshot
if physical_command_snapshot is not None
else self._physical_command_coordinator.snapshot()
)
control = (
control_session_snapshot
if control_session_snapshot is not None
else self._application_control_session.snapshot()
)
ble_runtime = (
native_runtime_snapshot
if native_runtime_snapshot is not None
else ble_runtime_snapshot()
)
semantic = (
semantic_topology_snapshot
if semantic_topology_snapshot is not None
else self._semantic_topology_public_snapshot()
)
identity = (
identity_pin_snapshot
if identity_pin_snapshot is not None
else self._device_identity_pin_public_snapshot()
)
idempotency = (
provisioning_idempotency_snapshot
if provisioning_idempotency_snapshot is not None
else self._network_provisioning_idempotency_public_snapshot()
)
network_ledger = (
network_ledger_snapshot
if network_ledger_snapshot is not None
else self._network_mutation_ledger.snapshot()
)
reasons: list[str] = []
if not transition_gate_owned:
@@ -5741,6 +5790,10 @@ class XgridsK1CompatibilityService:
self,
*,
allow_owned_network_holder: bool = False,
physical_command_snapshot: Mapping[str, object] | None = None,
runtime_snapshot: Mapping[str, object] | None = None,
control_session_snapshot: Mapping[str, object] | None = None,
native_runtime_snapshot: Mapping[str, object] | None = None,
) -> dict[str, object]:
"""Project one exact local-only retirement checkpoint.
@@ -5750,7 +5803,11 @@ class XgridsK1CompatibilityService:
ledger performs the final operation/revision/transport CAS.
"""
physical = self._physical_command_coordinator.snapshot()
physical = (
physical_command_snapshot
if physical_command_snapshot is not None
else self._physical_command_coordinator.snapshot()
)
record = physical.get("record")
with self._lock:
provisioning_active = self._provisioning_active
@@ -5761,9 +5818,19 @@ class XgridsK1CompatibilityService:
reconfiguration_active = self._connection_reconfiguration_intent is not None
pending_local_control_retirement = self._pending_local_control_retirement
lifecycle_holders = set(self._application_control_process_lease_holders)
runtime = self.runtime.snapshot()
control = self._application_control_session.snapshot()
native = ble_runtime_snapshot()
runtime = (
runtime_snapshot if runtime_snapshot is not None else self.runtime.snapshot()
)
control = (
control_session_snapshot
if control_session_snapshot is not None
else self._application_control_session.snapshot()
)
native = (
native_runtime_snapshot
if native_runtime_snapshot is not None
else ble_runtime_snapshot()
)
reasons: list[str] = []
retireable_ambiguous = bool(
@@ -5892,10 +5959,23 @@ class XgridsK1CompatibilityService:
self,
*,
allow_owned_network_holder: bool = False,
physical_command_snapshot: Mapping[str, object] | None = None,
runtime_snapshot: Mapping[str, object] | None = None,
control_session_snapshot: Mapping[str, object] | None = None,
native_runtime_snapshot: Mapping[str, object] | None = None,
supervisor_snapshot: ConnectionSupervisorSnapshot | None = None,
network_ledger_snapshot: NetworkMutationLedgerSnapshot | None = None,
provisioning_idempotency_snapshot: Mapping[str, object] | None = None,
semantic_topology_snapshot: Mapping[str, object] | None = None,
identity_pin_snapshot: Mapping[str, object] | None = None,
) -> dict[str, object]:
"""Project one exact fresh UUID checkpoint for local retirement reopen."""
physical = self._physical_command_coordinator.snapshot()
physical = (
physical_command_snapshot
if physical_command_snapshot is not None
else self._physical_command_coordinator.snapshot()
)
record = physical.get("record")
with self._lock:
provisioning_active = self._provisioning_active
@@ -5910,14 +5990,44 @@ class XgridsK1CompatibilityService:
fresh_devices = self._fresh_ble_devices_locked()
discovery_generation = self._ble_discovery_generation
generation_floors = dict(self._physical_retirement_reopen_generation_floors)
runtime = self.runtime.snapshot()
control = self._application_control_session.snapshot()
native = ble_runtime_snapshot()
supervisor = self._connection_supervisor.snapshot()
network_ledger = self._network_mutation_ledger.snapshot()
idempotency = self._network_provisioning_idempotency_public_snapshot()
semantic_topology = self._semantic_topology_public_snapshot()
identity_pins = self._device_identity_pin_public_snapshot()
runtime = (
runtime_snapshot if runtime_snapshot is not None else self.runtime.snapshot()
)
control = (
control_session_snapshot
if control_session_snapshot is not None
else self._application_control_session.snapshot()
)
native = (
native_runtime_snapshot
if native_runtime_snapshot is not None
else ble_runtime_snapshot()
)
supervisor = (
supervisor_snapshot
if supervisor_snapshot is not None
else self._connection_supervisor.snapshot()
)
network_ledger = (
network_ledger_snapshot
if network_ledger_snapshot is not None
else self._network_mutation_ledger.snapshot()
)
idempotency = (
provisioning_idempotency_snapshot
if provisioning_idempotency_snapshot is not None
else self._network_provisioning_idempotency_public_snapshot()
)
semantic_topology = (
semantic_topology_snapshot
if semantic_topology_snapshot is not None
else self._semantic_topology_public_snapshot()
)
identity_pins = (
identity_pin_snapshot
if identity_pin_snapshot is not None
else self._device_identity_pin_public_snapshot()
)
active_retirements = (
self._active_physical_retirement_documents(record)
@@ -6925,6 +7035,45 @@ class XgridsK1CompatibilityService:
binding_key,
)
def _control_session_snapshot_without_physical_command(self) -> dict[str, object]:
"""Read volatile control facts without recursively reloading the ledger."""
snapshotter = getattr(
self._application_control_session,
"snapshot_control_only",
None,
)
snapshot = (
snapshotter()
if callable(snapshotter)
else self._application_control_session.snapshot()
)
if not isinstance(snapshot, Mapping):
raise TypeError("application control session snapshot must be a mapping")
return dict(snapshot)
def _control_session_snapshot_with_physical_command(
self,
physical_command: Mapping[str, object],
*,
fallback_snapshot: Mapping[str, object],
) -> dict[str, object]:
"""Publish one control/physical join without a second ledger reload."""
snapshotter = getattr(
self._application_control_session,
"snapshot_with_physical_command",
None,
)
if callable(snapshotter):
snapshot = snapshotter(physical_command)
if not isinstance(snapshot, Mapping):
raise TypeError("application control session snapshot must be a mapping")
return dict(snapshot)
snapshot = dict(fallback_snapshot)
snapshot["physical_command"] = dict(physical_command)
return snapshot
def require_snapshot_runtime_id(self, expected_snapshot_runtime_id: str) -> None:
"""Reject an action issued by a browser bound to an older service.
@@ -6943,7 +7092,7 @@ class XgridsK1CompatibilityService:
self._retry_pending_local_control_retirement()
application_control = self._application_control.snapshot().as_dict()
application_control_session = _application_control_session_public_snapshot(
self._application_control_session.snapshot()
self._control_session_snapshot_without_physical_command()
)
runtime = self.runtime.snapshot()
camera_preview = self.camera_preview.snapshot()
@@ -6952,7 +7101,7 @@ class XgridsK1CompatibilityService:
self._reconcile_connection_supervisor(application_control_session, runtime)
if self._retire_orphaned_prestart_control_owner():
application_control_session = _application_control_session_public_snapshot(
self._application_control_session.snapshot()
self._control_session_snapshot_without_physical_command()
)
self._reconcile_application_control_process_lease(application_control_session)
# Preserve the terminal pre-START proof long enough to retire a local
@@ -6998,7 +7147,7 @@ class XgridsK1CompatibilityService:
# owner to idle. Continue the same atomic snapshot from that factual
# state instead of returning the stale pre-retirement failure row.
application_control_session = _application_control_session_public_snapshot(
self._application_control_session.snapshot()
self._control_session_snapshot_without_physical_command()
)
self._reconcile_acquisition(
runtime,
@@ -7013,12 +7162,12 @@ class XgridsK1CompatibilityService:
# control owner. Publish that factual post-reduction state in the same
# snapshot so passive BLE/read-only recovery is available immediately.
application_control_session = _application_control_session_public_snapshot(
self._application_control_session.snapshot()
self._control_session_snapshot_without_physical_command()
)
self._reconcile_application_control_process_lease(application_control_session)
if self._retire_terminal_prestart_control_failure():
application_control_session = _application_control_session_public_snapshot(
self._application_control_session.snapshot()
self._control_session_snapshot_without_physical_command()
)
self._reconcile_application_control_process_lease(application_control_session)
runtime = self.runtime.snapshot()
@@ -7038,14 +7187,35 @@ class XgridsK1CompatibilityService:
active_acquisition_checkpoint = (
self._active_acquisition_checkpoint_public_snapshot()
)
operator_retirement = self._physical_operator_retirement_projection()
ble_runtime = ble_runtime_snapshot()
operator_retirement = self._physical_operator_retirement_projection(
physical_command_snapshot=physical_command,
runtime_snapshot=runtime,
control_session_snapshot=application_control_session,
native_runtime_snapshot=ble_runtime,
)
physical_command["operator_retirement"] = operator_retirement
physical_command["operator_retirement_allowed"] = operator_retirement["allowed"]
physical_command["operator_retirement_reason_codes"] = operator_retirement["reason_codes"]
physical_command["operator_reconciliation_reopen"] = (
self._physical_operator_reconciliation_reopen_projection()
self._physical_operator_reconciliation_reopen_projection(
physical_command_snapshot=physical_command,
runtime_snapshot=runtime,
control_session_snapshot=application_control_session,
native_runtime_snapshot=ble_runtime,
supervisor_snapshot=supervisor_snapshot,
network_ledger_snapshot=ledger_snapshot,
provisioning_idempotency_snapshot=idempotency_snapshot,
semantic_topology_snapshot=semantic_topology_store,
identity_pin_snapshot=device_identity_pin_store,
)
)
application_control_session = _application_control_session_public_snapshot(
self._control_session_snapshot_with_physical_command(
physical_command,
fallback_snapshot=application_control_session,
)
)
ble_runtime = ble_runtime_snapshot()
with self._lock:
operation_phase = self._operation_phase
operation_message = self._operation_message
@@ -7318,11 +7488,27 @@ class XgridsK1CompatibilityService:
intent="select-device",
transition_gate_owned=False,
active_binding=active_binding,
runtime_snapshot=runtime,
physical_command_snapshot=physical_command,
control_session_snapshot=application_control_session,
native_runtime_snapshot=ble_runtime,
semantic_topology_snapshot=semantic_topology_store,
identity_pin_snapshot=device_identity_pin_store,
provisioning_idempotency_snapshot=idempotency_snapshot,
network_ledger_snapshot=ledger_snapshot,
)
change_network_reasons = self._connection_reconfiguration_safety_reasons(
intent="change-network",
transition_gate_owned=False,
active_binding=active_binding,
runtime_snapshot=runtime,
physical_command_snapshot=physical_command,
control_session_snapshot=application_control_session,
native_runtime_snapshot=ble_runtime,
semantic_topology_snapshot=semantic_topology_store,
identity_pin_snapshot=device_identity_pin_store,
provisioning_idempotency_snapshot=idempotency_snapshot,
network_ledger_snapshot=ledger_snapshot,
)
def reconfiguration_decision(
@@ -14622,8 +14808,13 @@ class XgridsK1CompatibilityService:
# deliberately bypasses this ordinary idempotence fast path.
return
camera_admission_started = time.monotonic()
physical = self._physical_command_coordinator.snapshot()
control = self._application_control_session.snapshot()
# ``physical`` above is the authoritative durable read for this
# admission attempt. Control facts are process-local; asking the
# public control snapshot for them would recursively reload the same
# physical ledger while this 10 Hz publisher callback is blocked.
control = self._control_session_snapshot_without_physical_command()
if control_mode == "plugin-commanded":
start_operation_id = (
@@ -14753,6 +14944,17 @@ class XgridsK1CompatibilityService:
time.monotonic() + CAMERA_POST_PCL_ACTIVATION_RETRY_SECONDS
)
raise
logger.info(
"K1 first-PCL camera admission timing",
extra={
"event_code": "k1_camera_pcl_admission_timing",
"evidence_session_id": out_dir.name,
"camera_pcl_admission_ms": int(
round((time.monotonic() - camera_admission_started) * 1_000)
),
"device_command_sent": False,
},
)
def _reset_live_data_plane_observation(self) -> None:
"""Make a stopped/replaced producer incapable of authorizing new data."""
@@ -22237,7 +22439,10 @@ class XgridsK1CompatibilityService:
local_start_operation_id=start_operation_id,
)
)
control = self._application_control_session.snapshot()
# Physical authority was loaded explicitly just above. Do not
# recursively reload it through the control projection while the
# first-PCL camera worker is trying to leave the ingress path.
control = self._control_session_snapshot_without_physical_command()
verified_control_value = control.get("verified_control")
verified_control = (
cast(Mapping[str, Any], verified_control_value)
@@ -22299,28 +22504,37 @@ class XgridsK1CompatibilityService:
camera,
evidence_session_id=out_dir.name,
)
activation_claims: list[_CameraProducerActivationClaim] = []
def reserve_activation(reserve: Callable[[], bool]) -> bool:
claim = self._reserve_camera_producer_activation_if_still_active(
acquisition_id=acquisition.acquisition_id,
evidence_session_id=out_dir.name,
start_operation_id=start_operation_id,
runtime_producer_generation=expected_runtime_generation,
reserve=reserve,
)
if claim is None:
return False
activation_claims.append(claim)
return True
def commit_activation(commit: Callable[[], bool]) -> bool:
if len(activation_claims) != 1:
return False
return self._commit_reserved_camera_producer_activation(
activation_claims[0],
commit,
)
if partial_same_session:
camera = self.camera_preview.retry_recording_producer(
DEFAULT_ACQUISITION_CAMERA_SOURCE,
target,
expected_generation=cast(int, camera["generation"]),
expected_recording_session=out_dir.name,
pre_retry_fence=lambda reserve: (
self._reserve_camera_restart_if_still_active(
acquisition_id=acquisition.acquisition_id,
evidence_session_id=out_dir.name,
start_operation_id=start_operation_id,
runtime_producer_generation=expected_runtime_generation,
reserve=reserve,
)
),
commit_fence=lambda commit: self._commit_camera_restart_if_still_active(
acquisition_id=acquisition.acquisition_id,
evidence_session_id=out_dir.name,
start_operation_id=start_operation_id,
runtime_producer_generation=expected_runtime_generation,
commit=commit,
),
pre_retry_fence=reserve_activation,
commit_fence=commit_activation,
committed_before_start=lambda committed: (
self._bind_live_perception_camera(out_dir.name, committed)
),
@@ -22330,13 +22544,8 @@ class XgridsK1CompatibilityService:
DEFAULT_ACQUISITION_CAMERA_SOURCE,
target,
out_dir,
commit_fence=lambda commit: self._commit_camera_restart_if_still_active(
acquisition_id=acquisition.acquisition_id,
evidence_session_id=out_dir.name,
start_operation_id=start_operation_id,
runtime_producer_generation=expected_runtime_generation,
commit=commit,
),
pre_prepare_fence=reserve_activation,
commit_fence=commit_activation,
committed_before_start=lambda committed: (
self._bind_live_perception_camera(out_dir.name, committed)
),
@@ -24378,6 +24587,104 @@ class XgridsK1CompatibilityService:
action=reserve,
)
def _reserve_camera_producer_activation_if_still_active(
self,
*,
acquisition_id: str,
evidence_session_id: str,
start_operation_id: str | None,
runtime_producer_generation: int,
reserve: Callable[[], bool],
) -> _CameraProducerActivationClaim | None:
"""Validate durable authority before Popen and freeze its local CAS."""
claims: list[_CameraProducerActivationClaim] = []
def reserve_and_capture() -> bool:
if not reserve():
return False
with self._lock:
claims.append(
_CameraProducerActivationClaim(
acquisition_id=acquisition_id,
evidence_session_id=evidence_session_id,
start_operation_id=start_operation_id,
stop_operation_id=self._acquisition_stop_operation_id,
runtime_producer_generation=runtime_producer_generation,
recovery_generation=self._active_stream_recovery_generation,
)
)
return True
accepted = self._camera_restart_action_if_still_active(
acquisition_id=acquisition_id,
evidence_session_id=evidence_session_id,
start_operation_id=start_operation_id,
runtime_producer_generation=runtime_producer_generation,
action=reserve_and_capture,
)
if not accepted:
return None
if len(claims) != 1:
raise RuntimeError("camera activation authority was not reserved exactly once")
return claims[0]
def _commit_reserved_camera_producer_activation(
self,
claim: _CameraProducerActivationClaim,
commit: Callable[[], bool],
) -> bool:
"""Commit a prepared FFmpeg candidate without durable reads or device I/O.
Durable physical/control/checkpoint authority was proved before Popen.
After Popen, a new STOP, acquisition replacement, runtime replacement or
recovery generation invalidates that frozen proof. The remaining gate
is deliberately in-memory so stdout/stderr readers start immediately.
"""
with self._camera_restart_commit_gate:
if self._camera_stop_priority_counts.get(claim.acquisition_id, 0) > 0:
return False
with self._acquisition_lifecycle_access():
runtime = self.runtime.snapshot()
with self._lock:
acquisition = self._acquisition
out_dir = self._acquisition_out_dir
current_start_operation_id = self._acquisition_start_operation_id
current_stop_operation_id = self._acquisition_stop_operation_id
recovery_generation = self._active_stream_recovery_generation
recovery_state = self._active_stream_recovery_state
local_current = bool(
acquisition is not None
and acquisition.acquisition_id == claim.acquisition_id
and acquisition.state
in {"starting", "awaiting_external_start", "acquiring"}
and out_dir is not None
and out_dir.name == claim.evidence_session_id
and current_start_operation_id
in {None, claim.start_operation_id}
and current_stop_operation_id == claim.stop_operation_id
and recovery_generation == claim.recovery_generation
and recovery_state
not in {
"reconnecting",
"blocked",
"fault",
"force-finishing",
"force-finished",
}
)
runtime_current = bool(
runtime.get("phase") == "live"
and runtime.get("source_mode") == "live"
and runtime.get("source_ready") is True
and runtime.get("producer_generation")
== claim.runtime_producer_generation
)
if not local_current or not runtime_current:
return False
return commit()
def _commit_camera_restart_if_still_active(
self,
*,
@@ -24421,7 +24728,10 @@ class XgridsK1CompatibilityService:
# only bounded in-process lineage checks and the camera-local commit
# remain.
physical = self._physical_command_coordinator.snapshot()
control = self._application_control_session.snapshot()
# The exact physical proof is already frozen for this pre-Popen
# fence. Only volatile MQTT dialogue facts are needed here; a nested
# physical reload adds no authority and delays camera reader startup.
control = self._control_session_snapshot_without_physical_command()
verified_control_value = control.get("verified_control")
verified_control = (
cast(Mapping[str, Any], verified_control_value)
@@ -960,6 +960,27 @@ class PhysicalCommandLedgerSnapshot:
return False
_PhysicalCommandStatFingerprint = tuple[int, int, int, int, int, int, int, int, int]
@dataclass(frozen=True, slots=True)
class _PhysicalCommandLedgerFilesystemFingerprint:
"""Cheap identity fence for one already-verified read-only snapshot.
The physical ledger remains fully reloaded for every mutation and proof.
Passive snapshots may reuse the in-memory, cryptographically verified
document only while the ledger directory, main file, archive directory and
every bounded archive entry retain their exact filesystem identities.
``ctime_ns`` deliberately prevents a same-size rewrite with restored mtime
from bypassing the fence.
"""
ledger_directory: _PhysicalCommandStatFingerprint | None
ledger_file: _PhysicalCommandStatFingerprint | None
archive_directory: _PhysicalCommandStatFingerprint | None
archive_entries: tuple[tuple[str, _PhysicalCommandStatFingerprint], ...]
def active_operator_retirements(
record: PhysicalCommandRecord,
) -> tuple[PhysicalCommandOperatorRetirement, ...]:
@@ -1255,12 +1276,24 @@ class PhysicalCommandLedger:
self._record: PhysicalCommandRecord | None = None
self._archive_history = _PhysicalCommandArchiveHistory()
self._corrupt = False
self._snapshot_filesystem_fingerprint: (
_PhysicalCommandLedgerFilesystemFingerprint | None
) = None
with self._lock, self._process_lock_locked():
self._reload_locked()
def snapshot(self) -> PhysicalCommandLedgerSnapshot:
with self._lock, self._process_lock_locked():
self._reload_locked()
current_fingerprint = self._snapshot_fingerprint_locked()
if (
current_fingerprint is None
or current_fingerprint != self._snapshot_filesystem_fingerprint
):
self._reload_locked()
if not self._corrupt:
self._snapshot_filesystem_fingerprint = (
self._snapshot_fingerprint_locked()
)
if self._corrupt:
return PhysicalCommandLedgerSnapshot(
status="corrupt",
@@ -3072,6 +3105,7 @@ class PhysicalCommandLedger:
self._record = plan.record
self._archive_history = plan.history
self._corrupt = False
self._snapshot_filesystem_fingerprint = None
return plan.record
def _publish_archive_plan_locked(
@@ -3157,6 +3191,10 @@ class PhysicalCommandLedger:
)
def _reload_locked(self) -> None:
# Every command/proof path calls the full loader. It must invalidate a
# passive snapshot fingerprint even when the subsequent transition is
# rejected, so the next read establishes a fresh verified generation.
self._snapshot_filesystem_fingerprint = None
try:
parent = self.path.parent.lstat()
_require_private_directory_metadata(parent, label="ledger directory")
@@ -3223,6 +3261,86 @@ class PhysicalCommandLedger:
self._archive_history = archive_history
self._corrupt = False
def _snapshot_fingerprint_locked(
self,
) -> _PhysicalCommandLedgerFilesystemFingerprint | None:
"""Capture a bounded metadata generation or decline cache reuse.
Failure never authorizes stale state: callers fall back to the complete
loader, whose established behavior is to mark unsafe evidence corrupt.
The process-wide flock is already held by every caller, serializing all
cooperating writers across Mission Core processes.
"""
try:
try:
ledger_directory_metadata = self.path.parent.lstat()
except FileNotFoundError:
ledger_directory_metadata = None
if ledger_directory_metadata is not None:
_require_private_directory_metadata(
ledger_directory_metadata,
label="ledger directory",
)
try:
ledger_metadata = self.path.lstat()
except FileNotFoundError:
ledger_metadata = None
if ledger_metadata is not None:
_require_private_regular_file(
ledger_metadata,
label="ledger",
empty=False,
)
if ledger_metadata.st_size > PHYSICAL_COMMAND_LEDGER_MAX_BYTES:
return None
try:
archive_directory_metadata = self._archive_dir.lstat()
except FileNotFoundError:
archive_directory_metadata = None
archive_entries: list[
tuple[str, _PhysicalCommandStatFingerprint]
] = []
if archive_directory_metadata is not None:
_require_private_directory_metadata(
archive_directory_metadata,
label="archive directory",
)
with os.scandir(self._archive_dir) as entries:
for entry in entries:
if len(archive_entries) >= PHYSICAL_COMMAND_ARCHIVE_MAX_SEGMENTS + 2:
return None
metadata = entry.stat(follow_symlinks=False)
if not stat.S_ISREG(metadata.st_mode):
return None
archive_entries.append(
(entry.name, _physical_command_stat_fingerprint(metadata))
)
archive_entries.sort(key=lambda item: item[0])
return _PhysicalCommandLedgerFilesystemFingerprint(
ledger_directory=(
_physical_command_stat_fingerprint(ledger_directory_metadata)
if ledger_directory_metadata is not None
else None
),
ledger_file=(
_physical_command_stat_fingerprint(ledger_metadata)
if ledger_metadata is not None
else None
),
archive_directory=(
_physical_command_stat_fingerprint(archive_directory_metadata)
if archive_directory_metadata is not None
else None
),
archive_entries=tuple(archive_entries),
)
except (OSError, ValueError):
return None
def _load_archive_history_locked(
self,
record: PhysicalCommandRecord,
@@ -3233,6 +3351,22 @@ class PhysicalCommandLedger:
)
def _physical_command_stat_fingerprint(
value: os.stat_result,
) -> _PhysicalCommandStatFingerprint:
return (
value.st_dev,
value.st_ino,
value.st_mode,
value.st_uid,
value.st_gid,
value.st_nlink,
value.st_size,
value.st_mtime_ns,
value.st_ctime_ns,
)
def _require_safe_edge_successor(
previous: PhysicalCommandRecord | None,
*,
@@ -661,6 +661,14 @@ class InteractiveApplicationControlSession:
)
self._set_phase_locked("stop-requested")
self._stop_requested.set()
logger.info(
"K1 STOP timing checkpoint",
extra={
"event_code": "k1_stop_dispatch_timing",
"operation_stage": "prepared-worker-released",
"device_command_sent": False,
},
)
except BaseException as preparation_error:
if coordinator is not None:
try:
@@ -781,41 +789,66 @@ class InteractiveApplicationControlSession:
def snapshot(self) -> dict[str, object]:
with self._lock:
transport_snapshot = self._live_transport_snapshot_locked()
phase = self._phase
return {
"mode": "interactive-canonical",
"inspection_only": self._inspection_only,
"inspection_promotion_allowed": self._inspection_promotion_allowed,
"state": phase,
"session_generation": self._run_generation,
"state_revision": self._state_revision,
"control_socket_open": phase not in {"idle", "completed", "closed", "failed"},
"can_open": self._can_open_locked(),
"can_enter_workspace": phase == "connection-ready",
"can_prepare_project": phase == "workspace-ready",
"can_start": phase == "project-ready",
"can_stop": phase == "scanning",
# Retained for wire compatibility with the v1alpha2 snapshot.
# Standby is now concluded solely from live K1 protocol state.
"can_confirm_standby": False,
"pending_operator_action": self._pending_operator_action_locked(),
"scripted_transitions": False,
"automatic_retry": False,
"outcome_unknown": self._outcome_unknown,
"scanning_observer_errors": self._scanning_observer_errors,
"failure": dict(self._failure) if self._failure is not None else None,
"dialogue": (
dict(self._dialogue_snapshot) if self._dialogue_snapshot is not None else None
),
"transport": transport_snapshot,
"verified_control": (self._verified_control_snapshot_locked(transport_snapshot)),
"physical_command": (
self._physical_command_coordinator.snapshot()
if self._physical_command_coordinator is not None
else None
),
}
physical_command = (
self._physical_command_coordinator.snapshot()
if self._physical_command_coordinator is not None
else None
)
return self._snapshot_locked(physical_command=physical_command)
def snapshot_control_only(self) -> dict[str, object]:
"""Return process-local control facts without reloading the durable ledger."""
with self._lock:
return self._snapshot_locked(physical_command=None)
def snapshot_with_physical_command(
self,
physical_command: Mapping[str, object] | None,
) -> dict[str, object]:
"""Join one already-verified physical snapshot to current control facts."""
with self._lock:
return self._snapshot_locked(physical_command=physical_command)
def _snapshot_locked(
self,
*,
physical_command: Mapping[str, object] | None,
) -> dict[str, object]:
transport_snapshot = self._live_transport_snapshot_locked()
phase = self._phase
return {
"mode": "interactive-canonical",
"inspection_only": self._inspection_only,
"inspection_promotion_allowed": self._inspection_promotion_allowed,
"state": phase,
"session_generation": self._run_generation,
"state_revision": self._state_revision,
"control_socket_open": phase not in {"idle", "completed", "closed", "failed"},
"can_open": self._can_open_locked(),
"can_enter_workspace": phase == "connection-ready",
"can_prepare_project": phase == "workspace-ready",
"can_start": phase == "project-ready",
"can_stop": phase == "scanning",
# Retained for wire compatibility with the v1alpha2 snapshot.
# Standby is now concluded solely from live K1 protocol state.
"can_confirm_standby": False,
"pending_operator_action": self._pending_operator_action_locked(),
"scripted_transitions": False,
"automatic_retry": False,
"outcome_unknown": self._outcome_unknown,
"scanning_observer_errors": self._scanning_observer_errors,
"failure": dict(self._failure) if self._failure is not None else None,
"dialogue": (
dict(self._dialogue_snapshot) if self._dialogue_snapshot is not None else None
),
"transport": transport_snapshot,
"verified_control": self._verified_control_snapshot_locked(transport_snapshot),
"physical_command": (
dict(physical_command) if physical_command is not None else None
),
}
def _run(self, generation: int) -> None:
executor: PhysicalAcceptanceDialogueExecutor | None = None
@@ -1028,6 +1061,14 @@ class InteractiveApplicationControlSession:
stop_dispatch_admission_deadline_reached
)
self._set_phase("stopping")
logger.info(
"K1 STOP timing checkpoint",
extra={
"event_code": "k1_stop_dispatch_timing",
"operation_stage": "pre-dispatch-validation-complete",
"device_command_sent": False,
},
)
executor.execute_canonical_stop(
stop_command,
stop_permit,
@@ -1038,6 +1079,14 @@ class InteractiveApplicationControlSession:
stop_dispatch_admission_deadline_reached
),
)
logger.info(
"K1 STOP timing checkpoint",
extra={
"event_code": "k1_stop_dispatch_timing",
"operation_stage": "correlated-application-response",
"device_command_sent": True,
},
)
self._validate_connection_binding_snapshot("stop-post-response")
self._set_phase("awaiting-standby-confirmation")
executor.maintain_post_stop_until_standby()
+6
View File
@@ -70,6 +70,12 @@ _EXTRA_FIELDS: Final = (
"camera_queue_rejection_reason",
"camera_retry_count",
"camera_generation",
"camera_pcl_admission_ms",
"camera_authority_wait_ms",
"camera_ffmpeg_prepare_ms",
"camera_post_spawn_commit_ms",
"camera_activation_total_ms",
"device_command_sent",
"websocket_close_code",
"transport_epoch",
"camera_append_error_name",
+12
View File
@@ -87,6 +87,12 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
"camera_queue_bytes": 12_000_000,
"camera_queue_segments": 96,
"camera_retry_count": 3,
"camera_pcl_admission_ms": 184,
"camera_authority_wait_ms": 91,
"camera_ffmpeg_prepare_ms": 42,
"camera_post_spawn_commit_ms": 7,
"camera_activation_total_ms": 140,
"device_command_sent": False,
"websocket_close_code": 4_008,
"transport_epoch": 7,
"unapproved_secret_field": "must-not-be-written",
@@ -108,6 +114,12 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
assert document["mqtt_loop_result_code"] == 7
assert document["mqtt_loop_phase"] == "post-publish-drain"
assert document["automatic_retry"] is False
assert document["camera_pcl_admission_ms"] == 184
assert document["camera_authority_wait_ms"] == 91
assert document["camera_ffmpeg_prepare_ms"] == 42
assert document["camera_post_spawn_commit_ms"] == 7
assert document["camera_activation_total_ms"] == 140
assert document["device_command_sent"] is False
assert document["side_effect_status"] == "unknown"
assert document["network_change_attempted"] is True
assert document["device_write_attempted"] is True
@@ -1047,6 +1047,33 @@ def service_with_fake_runtime(
return service, runtime
def test_passive_state_reuses_one_final_physical_snapshot(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
service, _runtime = service_with_fake_runtime(tmp_path)
coordinator = service._physical_command_coordinator # noqa: SLF001
original_snapshot = coordinator.snapshot
snapshot_calls = 0
def counted_snapshot() -> dict[str, object]:
nonlocal snapshot_calls
snapshot_calls += 1
return original_snapshot()
monkeypatch.setattr(coordinator, "snapshot", counted_snapshot)
state = service.state()
# Two earlier reads are deliberate pre/post settlement proofs. Presentation
# policy and the nested control-session projection must share the one final
# verified durable snapshot instead of recursively reloading the ledger.
assert snapshot_calls == 3
assert state["application_control_session"]["physical_command"] == state[
"physical_command"
]
def test_service_owns_one_wifi_association_probe_across_monitor_recreation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
@@ -10283,9 +10310,11 @@ def test_classified_scanning_cancels_stop_eof_and_preserves_or_rearms_camera_epo
target: str,
session_dir: Path,
*,
pre_prepare_fence: Callable[[Callable[[], bool]], bool],
commit_fence: Callable[[Callable[[], bool]], bool],
committed_before_start: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
assert pre_prepare_fence(lambda: True) is True
select_camera(source_id, target)
start_recording(session_dir)
assert committed_before_start is not None
@@ -13763,9 +13792,11 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
target: str,
session_dir: Path,
*,
pre_prepare_fence: Callable[[Callable[[], bool]], bool],
commit_fence: Callable[[Callable[[], bool]], bool],
committed_before_start: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
assert pre_prepare_fence(lambda: True) is True
events.append(("select", (source_id, target)))
camera_state["active_source_id"] = source_id
events.append(("record", session_dir))
@@ -13849,6 +13880,27 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
"snapshot",
snapshot_physical,
)
full_control_snapshot_calls = 0
control_only_snapshot_calls = 0
real_control_snapshot = control.snapshot
def snapshot_control() -> dict[str, object]:
nonlocal full_control_snapshot_calls
full_control_snapshot_calls += 1
return real_control_snapshot()
def snapshot_control_only() -> dict[str, object]:
nonlocal control_only_snapshot_calls
control_only_snapshot_calls += 1
return real_control_snapshot()
monkeypatch.setattr(control, "snapshot", snapshot_control)
monkeypatch.setattr(
control,
"snapshot_control_only",
snapshot_control_only,
raising=False,
)
runtime.pcl_frames = 1
frame = DecodedPointCloudView(
context=ConsumerFrameContext(
@@ -13863,6 +13915,8 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
positions_xyz=((0.0, 0.0, 0.0),),
)
full_snapshots_before_pcl = full_control_snapshot_calls
control_only_snapshots_before_pcl = control_only_snapshot_calls
service._observe_published_runtime_envelope( # noqa: SLF001
frame,
runtime.producer_generation,
@@ -13871,6 +13925,8 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
# Public manual controls stay closed until that worker proves an exact
# recording epoch, preventing a left/right selection race.
assert activation_entered.wait(timeout=2.0)
assert full_control_snapshot_calls == full_snapshots_before_pcl
assert control_only_snapshot_calls == control_only_snapshots_before_pcl + 1
activating_state = service.state()
activating_camera_streams = [
stream
@@ -13890,6 +13946,8 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
)
)
assert events == []
full_snapshots_before_camera_worker = full_control_snapshot_calls
control_only_snapshots_before_camera_worker = control_only_snapshot_calls
release_activation.set()
deadline = time.monotonic() + 2.0
while len(events) < 2 and time.monotonic() < deadline:
@@ -13899,6 +13957,8 @@ def test_confirmed_scanning_waits_for_first_authoritative_pcl_before_camera(
("select", ("sensor.camera.right", "192.168.1.20")),
("record", out_dir),
]
assert full_control_snapshot_calls == full_snapshots_before_camera_worker
assert control_only_snapshot_calls == control_only_snapshots_before_camera_worker + 2
assert camera_state["active_source_id"] == "sensor.camera.right"
assert camera_state["recording"] == {
"active": True,
@@ -14068,9 +14128,11 @@ def test_failed_post_pcl_camera_activation_retries_on_later_authoritative_frame(
target: str,
session_dir: Path,
*,
pre_prepare_fence: Callable[[Callable[[], bool]], bool],
commit_fence: Callable[[Callable[[], bool]], bool],
committed_before_start: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
assert pre_prepare_fence(lambda: True) is True
del commit_fence, committed_before_start
select_calls.append((source_id, target))
recording_calls.append(session_dir)
@@ -14827,10 +14889,12 @@ def test_stop_priority_overtakes_blocked_initial_camera_popen(
_target: str,
_session_dir: Path,
*,
pre_prepare_fence: Callable[[Callable[[], bool]], bool],
commit_fence: Callable[[Callable[[], bool]], bool],
committed_before_start: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
del committed_before_start
assert pre_prepare_fence(lambda: True) is True
candidate_blocked.set()
assert release_candidate.wait(3.0)
if not commit_fence(lambda: candidate_committed.append(True) or True):
+32
View File
@@ -193,6 +193,7 @@ class FakePhysicalCommandCoordinator:
self.resolutions: list[tuple[str, str | None]] = []
self.phase_probe: Callable[[], str] | None = None
self.snapshot_override: dict[str, object] | None = None
self.snapshot_calls = 0
def bind_control_session(self, binding: PhysicalCommandRuntimeBinding) -> None:
self.bindings.append(binding)
@@ -217,6 +218,7 @@ class FakePhysicalCommandCoordinator:
self.resolutions.append((f"{action}-not-dispatched", phase))
def snapshot(self) -> dict[str, object]:
self.snapshot_calls += 1
if self.snapshot_override is not None:
return self.snapshot_override
return {
@@ -433,6 +435,36 @@ def _successful_start_checkpoint_observer(
return None
def test_control_only_snapshot_reuses_preverified_physical_command() -> None:
coordinator = FakePhysicalCommandCoordinator()
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=FakeTransport, # type: ignore[arg-type]
physical_command_coordinator=coordinator, # type: ignore[arg-type]
)
control_only = session.snapshot_control_only()
assert control_only["state"] == "idle"
assert control_only["physical_command"] is None
assert coordinator.snapshot_calls == 0
verified_physical = {
"status": "resolved",
"requires_reconciliation": False,
}
joined = session.snapshot_with_physical_command(verified_physical)
assert joined["physical_command"] == verified_physical
assert coordinator.snapshot_calls == 0
regular = session.snapshot()
assert regular["physical_command"] == {
"status": "test",
"requires_reconciliation": False,
"automatic_replay_allowed": False,
}
assert coordinator.snapshot_calls == 1
def test_canonical_stages_require_operator_events_but_device_standby_does_not(
monkeypatch: pytest.MonkeyPatch,
) -> None:
+57
View File
@@ -6,6 +6,7 @@ import json
import sys
import threading
import time
from collections.abc import Callable
from io import BytesIO
from pathlib import Path
@@ -1416,16 +1417,60 @@ def test_source_end_none_epoch_cas_waits_for_canonical_archive_seal(
gateway.close()
def test_acquisition_candidate_never_spawns_before_authority_reservation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
session = tmp_path / "sessions" / "camera-pre-popen-denied"
session.mkdir(parents=True)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
popen_called = False
def forbidden_popen(*_: object, **__: object) -> object:
nonlocal popen_called
popen_called = True
raise AssertionError("FFmpeg must not start before authority reservation")
monkeypatch.setattr(camera_module.subprocess, "Popen", forbidden_popen)
try:
with pytest.raises(ValueError, match="authority"):
gateway.activate_recording_producer(
"sensor.camera.right",
"192.168.1.20",
session,
pre_prepare_fence=lambda _reserve: False,
commit_fence=lambda commit: commit(),
)
snapshot = gateway.snapshot()
assert popen_called is False
assert snapshot["active_source_id"] is None
assert snapshot["recording"]["active"] is False
finally:
gateway.close()
def test_acquisition_candidate_binds_epoch_before_any_reader_thread_starts(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
session = tmp_path / "sessions" / "camera-bind-before-reader"
session.mkdir(parents=True)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
caplog.set_level("INFO", logger="k1link.device_plugins.xgrids_k1.camera")
bound_generation: list[int] = []
reader_start_generation: list[int] = []
authority_reserved = False
def reserve_authority(reserve: Callable[[], bool]) -> bool:
nonlocal authority_reserved
assert authority_reserved is False
assert reserve() is True
authority_reserved = True
return True
def bind(committed: dict[str, object]) -> None:
recording = committed["recording"]
@@ -1436,6 +1481,7 @@ def test_acquisition_candidate_binds_epoch_before_any_reader_thread_starts(
def start_threads(producer: object) -> None:
generation = producer.generation # type: ignore[attr-defined]
assert authority_reserved is True
assert bound_generation == [generation]
reader_start_generation.append(generation)
@@ -1445,6 +1491,7 @@ def test_acquisition_candidate_binds_epoch_before_any_reader_thread_starts(
"sensor.camera.right",
"192.168.1.20",
session,
pre_prepare_fence=reserve_authority,
commit_fence=lambda commit: commit(),
committed_before_start=bind,
)
@@ -1452,6 +1499,16 @@ def test_acquisition_candidate_binds_epoch_before_any_reader_thread_starts(
assert bound_generation == [activated["recording"]["active_epoch"]]
assert reader_start_generation == bound_generation
assert activated["recording"]["media_ready"] is False
timing = [
record
for record in caplog.records
if getattr(record, "event_code", None) == "k1_camera_activation_timing"
]
assert len(timing) == 1
assert timing[0].camera_generation == bound_generation[0]
assert timing[0].camera_authority_wait_ms >= 0
assert timing[0].camera_ffmpeg_prepare_ms >= 0
assert timing[0].camera_post_spawn_commit_ms >= 0
finally:
gateway.close()
@@ -771,6 +771,71 @@ def test_immediate_archived_reconciliation_successor_fails_closed_on_archive_fau
)
def test_passive_snapshot_reuses_only_unchanged_verified_filesystem_generation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ledger = _ledger(tmp_path, monkeypatch)
_prepare_start(ledger)
real_reload = ledger._reload_locked
reload_count = 0
def counted_reload() -> None:
nonlocal reload_count
reload_count += 1
real_reload()
monkeypatch.setattr(ledger, "_reload_locked", counted_reload)
first = ledger.snapshot()
second = ledger.snapshot()
third = ledger.snapshot()
assert first == second == third
assert reload_count == 1
def test_passive_snapshot_observes_atomic_write_from_second_ledger_instance(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = _ledger(tmp_path, monkeypatch)
_prepare_start(first)
assert first.snapshot().record is not None
second = PhysicalCommandLedger(tmp_path / "repository", clock=_clock)
second.mark_dispatching(START_OPERATION)
observed = first.snapshot()
assert observed.record is not None
assert observed.record.stage == "dispatching"
assert observed.record.revision == 2
def test_passive_snapshot_ctime_fence_detects_same_size_archive_tamper(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ledger = _ledger(tmp_path, monkeypatch)
_predecessor, _current, archive_path = _install_resolved_start_rebind_rollover(
ledger,
monkeypatch,
)
assert ledger.snapshot().status == "resolved"
original_metadata = archive_path.stat()
tampered = bytearray(archive_path.read_bytes())
tampered[-1] = ord("{") if tampered[-1] != ord("{") else ord("}")
archive_path.write_bytes(tampered)
os.utime(
archive_path,
ns=(original_metadata.st_atime_ns, original_metadata.st_mtime_ns),
)
assert archive_path.stat().st_size == original_metadata.st_size
assert archive_path.stat().st_mtime_ns == original_metadata.st_mtime_ns
assert ledger.snapshot().status == "corrupt"
def test_classified_stop_ancestry_rejects_missing_confirmation_cycle_and_conflict(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,