wip(k1): checkpoint connection recovery rewrite

Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
DCCONSTRUCTIONS
2026-08-14 14:57:50 +03:00
parent aff331082f
commit 0ca7316a24
157 changed files with 152962 additions and 4036 deletions
+58 -7
View File
@@ -111,6 +111,27 @@ handle from the operator's scan and connects that exact selected handle in the
following network action. A fallback lookup remains only for non-UI callers
that did not perform discovery first.
The public advertisement cache is deliberately separate from an admitted
device session. Rows from the latest explicit scan generation remain stable
without wall-clock expiry while the operator completes the form. They still
grant no mutation authority without exact retained-handle capture and live
GATT validation. An admitted selected session ends only on proven disconnect,
explicit stop, app/backend restart, selection of another K1, or connection-mode
switch. A later scan may replace unselected candidates but never auto-connects
any of them.
An explicit Quick Connect to Bridge request for that same device can therefore
continue when K1 no longer advertises after AP activation. The preconditions
are: no active acquisition, no pending evidence cleanup, no active local source,
and a terminal/released control session. A mode switch closes the old selected
session first; the operator then scans, selects the K1, and creates a clean new
GATT session for Bridge. Before the station command the code performs the
normal internal `7f02` baseline read and allows exactly one 99-byte `7f01`
write. A powered-off or unreachable peripheral ends that attempt. An
unobserved post-write result is recorded as terminal `outcome-unknown`; it is
never retried automatically and never blocks a later distinct explicit
scan-select-connect attempt.
The 2026-07-20 prepared-host acceptance installed the exact firmware provider,
found one expected K1 candidate, emitted one AP-enable write, observed AP-ready
and completed one CoreWLAN association without an iPhone or manual credential.
@@ -136,12 +157,19 @@ mode. It never fragments or retries the payload automatically.
A completed GATT write only proves transport completion. It does not prove that
the K1 joined Wi-Fi or began beaconing. The application polls `7f02`; the
observed response frame contains a fixed-width mode slot, an address slot, a
status byte at offset 50 and the AP-ready flag at offset 51. The stale AP
observed response frame contains a fixed-width text slot, an address slot, a
status byte at offset 50 and the AP-ready flag at offset 51. The text slot is
not a uniform mode enum: AP state uses the `WIFI_AP` control literal, while the
2026-08-08 FW 3.0.2 Bridge observation returned the joined network name. The stale AP
baseline reports `WIFI_AP / 192.168.56.1 / byte51=0`; the physically observed
ready transition reports the same mode/address with `byte51=1`.
For Bridge/Direct Connect, acceptance requires at least one of:
For Bridge/Direct, acceptance requires the post-write `7f02` text slot to match
the exact requested network name and the address slot to contain a valid
non-AP private IPv4. This proves the desired target even when the K1 was
already joined to the same network before the explicit idempotent command. A
legacy literal-only `WIFI_CLIENT` observation retains the older conservative
cross-family rules and requires at least one of:
1. `7f02` reports a non-AP IPv4 address;
2. the same address appears as a new router/ARP client after the write;
@@ -150,6 +178,15 @@ For Bridge/Direct Connect, acceptance requires at least one of:
Do not infer success from a write callback alone.
An interrupted attempt with no exact post-write network-name observation is not
made successful by a write callback, changed DHCP address, router/ARP row or
reachable endpoint. Likewise, an already AP-ready baseline alone cannot prove
the outcome of an interrupted Quick-to-Quick attempt. Such an attempt remains
`outcome-unknown` in historical audit and is never replayed automatically. It
does not create a permanent mutation barrier: after the old active operation
and cleanup have terminated, a later explicit operator scan, selection, and
connect is a distinct session with its own single reviewed write.
The Bridge/Direct Connect address is a DHCP lease, not configuration and not
device identity. Mission Core re-reads `7f02` without writing before every new
LAN control session, implicit-host acquisition and factory-calibration read.
@@ -157,6 +194,10 @@ If the value changes, it rotates `device_session_id`; it never retargets an
active acquisition. A correlated MQTT `DeviceInfo` response supplies the live
model/firmware/serial identity barrier.
The joined network name is used only for exact in-process comparison with the
current explicit request. Durable network audit stores the normalized semantic
family and never stores or publishes the raw network name.
The 2026-07-20 reboot/power-cycle check observed the startup race directly:
one read returned the earlier `.54` lease while that exact address had no ARP or
application endpoint; a later read returned `.52`, where exact probes found
@@ -166,7 +207,9 @@ and why a BLE lease observation alone is not reported as live DeviceInfo.
For Quick Connect, host association is not admitted until the canonical
byte-51 ready flag is observed. CoreWLAN then searches only for the exact
device-profile SSID for at most 15 seconds and performs at most one association.
device-profile SSID for at most 30 seconds and performs at most one association.
AP-ready is a device-state barrier, not proof that the host has already observed
the RF beacon; a retained successful run required 18.142 seconds of discovery.
## Safety, recovery and stop conditions
@@ -176,9 +219,17 @@ device-profile SSID for at most 15 seconds and performs at most one association.
secure store. Missing or mismatched firmware material fails before the AP
write. Never extrapolate this provider to another firmware or model.
- The macOS adapter materializes a device-scoped Keychain item from the exact
firmware source, then performs one association. Standard Wi-Fi Keychain and
native prompt paths remain compatibility fallbacks, not the reviewed
zero-touch path. It never asks the browser for a password.
firmware source before the BLE write, then performs one association using
only that exact profile. Standard Wi-Fi Keychain lookup, native password
prompts and post-write profile rewrites are prohibited. It never asks the
browser for a password. Preflight reads are non-interactive and validate the
exact SSID/source inside the helper before K1 changes network state.
- The prepared-host laboratory adapter launches the reviewed Swift source only
through `/usr/bin/xcrun swift`. Runtime `swiftc` compilation to an ad-hoc
executable is prohibited because its unstable process identity regressed
Keychain ACL and CoreWLAN behavior. Product packaging still requires a
prebuilt, properly signed helper with a stable designated identity and
explicit CoreWLAN authorization.
- Do not alter Deco settings, scan the subnet, or guess any credential.
- If the status does not change, do not retry automatically.
- If the supplied credentials are wrong, reconnect over BLE and overwrite them
+3 -4
View File
@@ -110,12 +110,11 @@ directly and use their sibling metadata receive timestamps when present.
## Connect and stream live
1. Power K1 to its normal steady-green standby state.
2. Confirm the manual power checklist in **Парк → Локальное устройство**.
3. Run the real six-second BLE scan and select the intended device from the
2. Run the real six-second BLE scan and select the intended device from the
complete visible-device list.
4. Enter the existing router SSID/password and explicitly authorize the reviewed
3. Enter the existing router SSID/password and explicitly authorize the reviewed
provisioning write. The backend does not retry the write automatically.
5. Enter the required project name, confirm operator presence, closed LixelGO,
4. Enter the required project name, confirm operator presence, closed LixelGO,
storage/power and steady green, then choose **Запустить сканирование и
локальный приём** once.
6. Mission Core emits operations 16, waits for their correlated device
File diff suppressed because it is too large Load Diff
@@ -136,7 +136,7 @@ The current XGRIDS contribution maps its proven internal workflow into those
platform states without changing the wire protocol:
```text
confirm power -> scan BLE -> select candidate -> enter Wi-Fi
scan BLE -> select candidate -> enter Wi-Fi
-> provision once -> receive LAN address -> start source
-> wait for first point frame -> streaming
```
+115 -13
View File
@@ -1,6 +1,6 @@
# ADR 0013: explicit K1 local connection matrix
- Status: amended 2026-07-20; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
- Status: amended 2026-08-08; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
- Date: 2026-07-19
- Extends: ADR 0004, ADR 0005 and ADR 0012
@@ -47,6 +47,23 @@ for the previous session. An active acquisition is never retargeted in place.
The later correlated MQTT `DeviceInfo` response supplies model, firmware,
serial and vendor identity; IP equality alone cannot identify a K1.
The 2026-08-08 physical Bridge trace corrected the earlier field model: the
first `7f02` text slot is `WIFI_AP` in AP state but contains the joined network
name in FW 3.0.2 station state. Mission Core therefore normalizes that station
response to `WIFI_CLIENT` internally and admits Bridge/Direct only when the raw
post-write name exactly matches the current explicit request plus a valid
non-AP private address. The raw name is not persisted in the secret-free
network audit or published through API state.
The limitation still applies when recording an interrupted attempt that has no
exact post-write network-name observation. A changed private DHCP address alone
cannot identify the selected network. An already AP-ready baseline likewise
cannot prove the outcome of an interrupted Quick-to-Quick attempt. Mission Core
therefore records that attempt as terminal `outcome-unknown` and never replays it automatically. The
historical uncertainty is not a permanent barrier: after the old active
operation and cleanup have ended, a later explicit operator scan, selection,
and connect is a distinct session with its own one reviewed write.
Product decision on 2026-07-20: Bridge/direct-LAN is the continuing route.
Quick Connect remains visible and executable on an already prepared host, but
is not a deployment dependency or portability claim.
@@ -86,12 +103,65 @@ and the following cold Swift/CoreWLAN process missed the beacon. The corrected
implementation holds the selected `BleakClient` open through bounded native
SSID discovery and the single association call.
BLE discovery and the selected device action form one host session. A physical
run proved that immediately rediscovering the same K1 by its CoreBluetooth UUID
can fail even though the preceding scan exposed it. Mission Core retains the
non-serializable `BLEDevice` handle process-locally and uses that exact handle
for the next selected network action; it never exposes the handle through API
state or treats the macOS UUID as durable device identity.
One explicit six-second BLE discovery and the later Apply action form one
operator intent without a second discovery. A physical run proved that
immediately rediscovering the same K1 by its CoreBluetooth UUID can fail even
though the preceding scan exposed it. Mission Core retains the non-serializable
`BLEDevice` handle process-locally and uses that exact handle for Apply; it never
exposes the handle through API state or treats the macOS UUID as durable device
identity. UI row selection itself performs no GATT or backend I/O.
The operator-visible candidate list, local selected draft and admitted active
session are separate contracts. Every explicit scan replaces the candidate
set. Selection only binds a local form to one result from the latest admitted
generation. Wall-clock age does not remove that generation while the operator
completes the form. Apply admits only its exact retained handle and live GATT
validation may create the active session; a remembered UUID is never mutation
authority. Proven disconnect, explicit stop, app/backend restart, another
explicit Scan, or a committed mode transition revokes the applicable candidate
or live session. Rediscovery never auto-connects.
Quick Connect to Bridge is an explicit topology transition, not another scan
heuristic. Selecting Bridge — or choosing another K1 while Bridge is already
selected — sends one idempotent local `reset_scenario` CAS. It seals retained
receiver/camera/control ownership, invalidates candidates and credentials and
retires old physical lineage truthfully, while sending no device command, BLE,
host-network write or automatic Scan. The next explicit Scan starts the clean
discovery flow, while Apply remains the topology and device-mutation boundary.
The operator selects one discovered K1, sees the Bridge credentials immediately
and submits once. The new GATT
session reads internal baseline `7f02` and emits exactly one reviewed 99-byte
station write. A connect failure ends that attempt. An ambiguous post-write
failure is terminal `outcome-unknown` audit, not a permanent cross-session
fence. There is no automatic BLE or network-write retry.
Likewise, an exact REST `network_applied` result with unready/unknown control
spends that Apply and its credentials without making the read-model attempt a
permanent topology lock. Recommended Verify is pinned to the backend
current/configured target. A separately explicit new intent still requires
current server policy: Bridge prepares `select-device` before a later fresh
scan; Quick and Direct run an admitted fresh scan, select only its latest row
and create a new idempotency Apply; a mode or same-mode new-device transition
uses one idempotent local-only `reset_scenario` before that fresh scan. None of
these new-intent UI paths reuses the old
intent or runs as a hidden frontend/mutating continuation. The service-owned
same-intent read-only bootstrap declared below is the sole post-ACK exception.
The Apply REST call returns as soon as the exact durable
`network_applied` proof is available. The service may continue the same
intent's supervised control bootstrap read-only after that ACK. This performs
no BLE/host mutation or retry and creates no frontend Verify, Scan, Apply or
blocking Apply loader. While the exact child is accepted/running, the UI may
show only a passive **Сеть настроена · подтверждаем управление** indicator and
must keep every recovery action disabled. Later control state arrives only as
backend presentation convergence; terminal unready/unknown state then exposes
the explicit server-policy recovery choices.
Bridge and Quick Connect were physically accepted as separate paths before
this amendment. The combined Quick Connect to Bridge transition has automated
contract coverage but remains a distinct physical acceptance gate; it must not
be reported as field-accepted until one redacted live run records both sides of
the transition.
The corrected host boundary derives a non-secret, device-scoped profile ID from
the selected SSID. The reviewed client contains per-device `WiFiAP_SSID` and
@@ -110,13 +180,40 @@ that opaque source before any BLE write. A missing provider fails closed. The
browser, API, argv, logs, manifests and evidence never receive the secret; the
importer's short-lived mutable buffer is zeroized after the Keychain handoff.
The 2026-08-06 field regression established that process identity is part of
this prepared-host contract. Runtime `swiftc` compilation produced an ad-hoc
helper with an unstable designated identity. macOS then requested Keychain
authorization repeatedly and the same process context failed to expose the
exact K1 SSID through CoreWLAN even after K1 had acknowledged AP-ready. That
runtime-compiled route is rejected. The laboratory adapter uses the previously
physically accepted Apple-signed interpreter path,
`/usr/bin/xcrun swift <reviewed-source>`, and validates the source path before
launch. A portable product implementation still requires a packaged,
precompiled and properly signed helper with a stable bundle identifier,
designated requirement, Location/CoreWLAN authorization and Keychain ACL; the
current prepared-host path does not claim that packaging work is complete.
Before any BLE write, the helper's preflight is non-interactive. It first checks
Keychain item existence through metadata, then validates the selected profile's
SSID and `exact-firmware-profile` provenance inside the helper without returning
secret data. Provider material is also read with interaction disabled if a
missing device profile must be materialized. The association phase accepts only
that already materialized exact profile. It never
falls back to the system Wi-Fi Keychain, rewrites a profile opportunistically,
or opens a password/authorization dialog after K1 has changed network state.
An unavailable or unauthorized profile therefore fails closed with a precise
reason code and no automatic device retry.
The host-network boundary, rather than the XGRIDS frontend, owns platform
association. Browsers expose no Wi-Fi join API, and Apple's iOS
`NEHotspotConfiguration` consent flow is unavailable on macOS. The current
implementation therefore uses a short-lived Swift/CoreWLAN + macOS Keychain
helper; Windows Credential Manager and Linux Secret Service adapters remain
separate platform work. The helper performs repeated read-only exact-SSID scans
inside one 15-second discovery window and at most one association. It never
inside one 30-second discovery window and at most one association. The larger
window covers the physically observed 18.142-second beacon-discovery case;
AP-ready confirms K1 state but does not prove that macOS has already observed
the RF beacon. It never
repeats the BLE command, guesses a password or treats `7f01` as a credential-read
command. The credential-bearing 99-byte station-provisioning frame and fixed
100-byte AP-enable frame are separate reviewed payloads.
@@ -128,12 +225,13 @@ The owner also observed no explicit device/account pairing in the normal
LixelGo onboarding flow; this is consistent with a firmware-defined AP secret,
but does not establish account-wide authorization for arbitrary scanners.
Connection verification refreshes the session-scoped lease with the same
read-only BLE status operation. It does not write a characteristic, re-provision
Apply may read BLE baseline internally while establishing a new selected
session. Selection never does so, and the normal flow has no mandatory or hidden
"verify without write" recovery step. The baseline read does not re-provision
Wi-Fi, scan the subnet, change a host route, or touch VPN configuration. The
later canonical MQTT session supplies the real data-plane connection and live
`DeviceInfo` identity check. A BLE lease observation is therefore not by itself
a claim that MQTT/RTSP is reachable.
later canonical MQTT
session supplies the real data-plane connection and live `DeviceInfo` identity
check; BLE status alone is not a claim that MQTT/RTSP is reachable.
## Consequences
@@ -148,6 +246,10 @@ a claim that MQTT/RTSP is reachable.
therefore not scheduled for this Quick Connect path.
- Direct Connect requires an already-running hotspot and a controller route;
Mission Core does not create or manage that hotspot.
- Discovery never auto-connects devices. Mode, selection and input are local
only. App restart, disconnect, explicit stop and mode transition require a
fresh explicit scan-select-Apply session. Apply performs no hidden rescan or
Verify and may cross at most one device-mutation boundary.
- The application-control, START/STOP and raw-first acquisition protocol is
unchanged after a target address is admitted.
- Direct Connect remains explicitly pending one owner-operated physical
@@ -0,0 +1,177 @@
# ADR 0014: long-lived macOS host-association observer
Status: planned production boundary; software contract may be developed behind
a disabled feature flag.
Related acceptance item: `CONN-66` in
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
## Context
Mission Core must distinguish a K1 that is configured for a network from a Mac
that is currently attached to the same network. Route, TCP, DeviceInfo,
control and data evidence are bound to a host-path epoch; a Wi-Fi switch,
sleep/wake cycle or observer restart must invalidate that epoch before any late
TCP/MQTT result can restore command authority.
The current laboratory implementation is fail-closed but not a production
observer. One normal connection-monitor poll samples the host path before and
after its TCP probe. Each sample synchronously invokes:
```text
/usr/bin/xcrun swift plugins/xgrids-k1/macos/associate_wifi.swift
```
under one process-local lock with a 30-second timeout. At the one-second
monitor interval this can launch two Swift processes per second. A failed
cycle can occupy the lock for roughly sixty seconds, and cancellation of the
Python `asyncio.to_thread()` waiter does not terminate the native process or
thread. Physical-command validation shares this observation path. Shorter
timeouts, cached shell output or automatic fallback would hide rather than
remove the lifecycle defect.
## Decision
Production host-association evidence will come from one signed, long-lived,
read-only agent in the user's macOS login session.
- The agent owns one `CWWiFiClient` for its process lifetime.
- It observes CoreWLAN link/association/power events and macOS sleep/wake.
- It never scans BLE, changes Wi-Fi, reads K1 credentials, reconnects MQTT or
sends START/STOP.
- It is packaged in a minimal container app and registered with `SMAppService`;
it is not a `LaunchDaemon` and is not launched through `xcrun` at runtime.
- The required Wi-Fi event entitlement and Location authorization are checked
before K1 network mutation is offered. Missing authorization produces
explicit unavailable evidence, not a crash loop or guessed association.
- The existing Wi-Fi mutator remains a separate component under the exclusive
network process lease. Observer authority and mutation authority are never
combined.
The backend communicates with the observer through bounded local IPC. Each
backend session supplies a random HMAC key. SSID and BSSID remain inside the
agent; only an opaque continuity token is returned and it cannot be correlated
between backend processes. The token material is interface plus BSSID; SSID is
used only to report evidence quality. This keeps one AP identity stable when
macOS alternates between `ssid+bssid` and `bssid-only` disclosure.
## Observer contract
```text
schema_version: missioncore.macos-host-association/v2
agent_instance_id: random 128-bit process instance
sequence: uint64
association_epoch: uint64
interface_name: string | null
wifi_interface: true | false | null
state: associated | not-associated | inactive | not-wifi | unavailable
evidence_quality: ssid+bssid | bssid-only | not-wifi | unavailable
continuity_token: 64 lowercase hex | null
reason_code: string | null
observed_monotonic_ns: uint64
sample_age_ms: uint32
cause: initial | link-change | association-change | power-change |
permission-change | will-sleep | did-wake | poll-correction |
observer-restart
```
`sequence` changes for every event or heartbeat. `association_epoch` changes
when interface, power, state, SSID or BSSID changes. Sleep and wake each create
a barrier even if the visible network looks unchanged afterward. A new agent
instance, IPC reconnect, sequence rollback/gap, malformed frame or timeout is
also a discontinuity.
The backend adds its own `observer_session_epoch`; the effective host-route
fingerprint includes the agent instance, observer session, association epoch
and opaque token. A response from an old session or sequence is discarded.
Unknown schema/state or incomplete evidence is `unavailable` and immediately
revokes host authority.
## Timing and failure semantics
- Heartbeat: 1 second.
- Maximum cached-snapshot age: 750 ms.
- Snapshot RPC deadline: 250 ms.
- Initial handshake deadline: 2 seconds.
- Two missed heartbeats or one invalid IPC frame revoke authority immediately.
- Reconnect backoff: 250 ms, 500 ms, 1 s, 2 s, then at most 5 s.
- There is no automatic fallback to the Swift source runner.
- Agent loss affects only read-only host evidence. It never triggers a K1
network write, MQTT reconnect or physical command.
- A discontinuity first marks the supervisor host path unavailable and rotates
its epoch. Recovery then requires fresh route, TCP and DeviceInfo/control
evidence in that order.
## Delivery phases
Phase A is safe without signing or a physical K1:
1. Define the Python observer protocol and validate the v2 schema.
2. Add a fake/in-memory transport and backend session/sequence validator.
3. Implement immediate epoch invalidation and bounded cached lookup.
4. Inject the observer into the monitor behind a disabled feature flag.
5. Implement the Swift reducer and local transport as a testable Swift package.
6. Test sleep/wake, timeout, event gaps, delayed replies, crash/restart and
manual network changes.
7. Expose secret-free observer health and next action to the UI.
8. Prove 10,000 samples launch no child process and cause no lock starvation.
Phase B requires the actual Mac signing and permission environment:
1. Package and register the user-session agent.
2. Obtain the Wi-Fi events entitlement and complete Location onboarding.
3. Run the observer in shadow mode beside the current fail-closed probe.
4. Cut over only after the physical fault matrix and an eight-hour soak show no
unexplained divergence.
## Acceptance gate
- No `xcrun`, `swift` or `swiftc` occurs on the observer path.
- One agent and one CoreWLAN client serve one login session.
- Snapshot p99 is below 50 ms, hard deadline 250 ms, monitor-cycle p99 below
1.5 seconds.
- No mutex is held across native or IPC calls.
- Sleep, wake, agent restart, sequence gap and timeout always invalidate the
effective host epoch.
- Late TCP/DeviceInfo evidence from an old epoch is rejected.
- SSID, BSSID and credentials never enter IPC logs, API state or artifacts.
- Quick-to-Bridge, Bridge-to-Quick, manual Wi-Fi switch, Wi-Fi off/on,
router loss/return with the same SSID/IP, backend restart and Location denial
all revoke control authority within two seconds and recover only through
fresh route, TCP and DeviceInfo evidence.
Until this gate passes, the current association probe remains explicitly a
laboratory implementation and `CONN-66` remains open.
## Laboratory containment while Location evidence is hidden
The source-runner helper can return `association-identity-unavailable` on a
connected Mac when macOS privacy rules hide SSID and BSSID from the CLI child
process. Rotating a random fallback token on every one-second poll made a
stable route and a successful TCP probe mutually impossible: every following
sample revoked the preceding endpoint result as a fictitious network switch.
Until the signed observer above replaces the source runner, the laboratory
probe uses one random, process-scoped token for the same interface and
unavailable-evidence scope. This is not promoted to association evidence:
- the public evidence quality remains `unavailable`;
- interface, source address, kernel route, availability, a proven different
BSSID and process restart remain epoch barriers;
- endpoint reachability alone remains `configured-unverified`;
- only fresh exact DeviceInfo/control evidence can grant control authority;
- `CONN-66`, sleep/wake and same-subnet network-switch acceptance remain open.
For an already reachable lease whose exact DeviceInfo identity and control
session remain healthy, a temporary helper timeout or privacy-limited
association sample may retain the preceding proven association fingerprint
only while the kernel route fingerprint, interface, source, intent and target
are unchanged. That retained sample still performs TCP contact and a second
kernel-route check, refreshing only route/TCP observation TTLs. Endpoint loss,
control loss, control-proof expiry, target/intent change, a proven association
identity change or any raw route change revokes immediately. A
`configured-unverified` path does not receive this bridge and remains bounded
by the existing technical-failure debounce and transport TTL.
This containment removes the false per-poll epoch churn observed on the field
Mac without claiming that the planned production observer has been delivered.
+309
View File
@@ -0,0 +1,309 @@
# ADR 0015: explicit K1 recovery beside the one-intent connection flow
Status: accepted product, recovery and presentation contract; executable
coverage and remaining hardware acceptance are tracked in
`docs/k1-connection-acceptance.manifest.json`.
Related acceptance items: `CONN-16` through `CONN-19`, `CONN-28`, `CONN-29`,
`CONN-65`, and `CONN-68` through `CONN-78` in
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
## Problem
Loss of K1 power, the router, Mac Wi-Fi, MQTT control or the backend does not
prove whether K1 is physically scanning. Retained points, an open TCP port and a
historical START are insufficient. Replaying START or STOP after an ambiguous
dispatch boundary can create a second physical edge.
The durable physical-command ledger, exact read-only classification and
fail-closed supervisor must remain. They must not make ordinary connection slow
or surprising. In particular, selecting a device must not secretly connect,
Verify, retire/reopen history or delay network credentials.
## Decision
### Existing product surface
K1 connection stays in the existing device plugin section headed
**Подключение XGRIDS LixelKity K1**. The surrounding job, entity and lifecycle
models do not change. This is novelty A: an improvement to an existing product
surface. A separate wizard, modal flow and mandatory preflight/recovery surface
are rejected.
The section reuses canonical shared `Button`, `TextField`, `ActivityIndicator`
and `StatusBadge`. It creates no shared entity and uses no raw local HTML
controls or literal local status colors.
### One-intent normal flow
The normal flow is:
1. choose Bridge, Direct Connect or Quick Connect locally;
2. press the explicit Bluetooth search action;
3. wait for exactly one six-second discovery;
4. press **Выбрать** on one result;
5. enter Bridge/Direct credentials immediately, or review the Quick Connect
summary;
6. press **Применить** once.
Opening the section, changing mode, selecting a row and every
SSID/password keystroke perform zero browser-controller, device or host I/O.
They create no backend operation and show no operation loader. An admitted fresh
selection retains the selected card and exposes applicable inputs immediately.
A candidate without current draft authority is omitted or presented only as
non-actionable evidence; it never receives a misleading disabled primary.
Each explicit search owns exactly one bounded six-second discovery. It performs
no connect, Verify, selection or mutation. Results are never auto-selected.
One Apply owns the normal connection intent. It may commit the local desired-mode
draft under backend CAS and may cross at most one reviewed K1 mutation boundary.
Its frontend handler performs no hidden Scan, Verify, reconnect, retirement,
reopen, candidate substitution or retry. Quick, Bridge and Direct use the same single primary
**Применить** action; credentials are required only for Bridge and Direct.
Ordinary Bridge Apply never opts into changing the controlling Mac's Wi-Fi
association. Host switching is a separate future consequential operator action,
not an Apply substep. K1 provisioning can therefore succeed as
`network_applied` while control is `control_not_ready`. That result must not
repeat that intent's BLE write. Recommended separately explicit read-only
Verify/recovery may establish route, endpoint and DeviceInfo/control evidence
for the applied topology; a new intent remains separately policy-gated.
The exact REST response owns completion of the Apply mutation. A snapshot with
`connection_attempt.phase=network_applied` is accepted immediately when
`control_state` is `control_not_ready` or `unknown`; the controller does not
wait for WebSocket/poll convergence or call the full connection-ready
requirement. The service may continue supervised same-intent control bootstrap
after this fast durable ACK, but only read-only: no BLE/host mutation, mutation
retry, new UI action or second Apply. This is not a hidden frontend Scan or
Verify. Exact connection-ready remains mandatory before control or physical
START. This separation spends the old intent before a delayed state channel
could invite its duplicate replay. `connection_attempt` is a read model, not
permanent lifecycle authority; current server policy may admit a separately
explicit new intent.
While the exact service-owned bootstrap child is `accepted` or `running` and
projects `safe_next_action=wait-for-current-attempt`, the UI shows only one
passive **Сеть настроена · подтверждаем управление** indicator. It enables no
Verify, mode change, Scan, row or Apply action. Terminal unready/unknown child
state then exposes the separately explicit policy-gated recovery choices.
`network_applied` plus unready or unknown control spends the old Apply and gates
ordinary mode change, Scan, row selection and Apply. It first waits passively
for an exact active service child; after terminal settlement it presents an
explicit recovery choice, regardless of browser-local mode. It never authorizes
automatic or same-intent replay. Recommended Verify is pinned to the backend
`serverBound` current/configured transport and mode; it never falls back to a
selected browser row and is not a prerequisite for every new intent.
Current server policy may admit a distinct, explicit new-intent path. Bridge
uses `prepare-select-device`, a local-only CAS with zero device/host I/O; only
after its success may the operator initiate a fresh six-second Scan. Quick and
Direct use explicit policy-gated `scan-ble`, then the latest fresh row and a new
idempotency Apply. A mode change requires backend `mode_selection` authority and
then a fresh explicit Scan. No recovery choice performs hidden Scan, selection,
Verify, provisioning or continuation of the old Apply, and the browser never
manufactures authority.
### Freshness and outcome semantics
Apply is admitted only for the exact selected transport, completed discovery
generation, backend runtime, desired-mode revision, reconfiguration intent and
policy snapshot. Authority drift before dispatch is a terminal, zero-device-I/O
`stale` result. The UI keeps the result understandable, labels it explicitly and
offers a new explicit six-second search. It never starts that search itself.
A failure before the reviewed mutation boundary is `not-dispatched` or
`failed`, with zero K1 mutation. A lost response, timeout, power failure or
process death after dispatch is `outcome-unknown`, with
`safe_to_retry=false`. The durable network-attempt ledger prevents replay.
Credentials are never reused automatically. A later operator Apply is a new
intent and must pass all current gates.
## Physical safety remains separate
Network attempts are disposable; physical START/STOP ambiguity is durable:
- START and STOP never replay automatically;
- control loss does not prove scanning stopped;
- local receiver/camera/ingress cleanup is not physical STOP;
- a wrong K1/transport/profile/project cannot reconcile the record;
- READY records cessation without rewriting historical command outcome;
- exact same-project SCANNING may mint one single-use confirmed STOP permit on
the still-open exact control binding;
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
only host-owned resources, records `timed_out` / `standby-unknown`, preserves
the unresolved ledger and keeps every mutation fenced.
The composite supervisor and physical-command ledger can disable Apply before
device I/O. Their denial does not turn mode, selection or input into recovery.
### Explicit read-only recovery
Recovery is a distinct, explicitly requested exceptional action. It is never a
continuation of row selection or Apply. The browser supplies neither endpoint,
substitute transport nor ledger authority. The backend pins the durable record's
exact transport, identity/profile, operation/revision, acquisition/project,
topology revision and host epoch.
The non-reconnecting observation is:
```text
topology-probed
-> pre-start-control-opened
-> device-info-requested (ordinal 1; exactly one publish)
-> device-info-verified
-> awaiting-passive-fresh-status
-> cessation | active-same-project | foreign-active | inconclusive | failed
```
It publishes exactly one canonical DeviceInfo request and then accepts only a
fresh non-retained DeviceStatus from the same socket generation after that
barrier. It publishes no status solicitation, DeviceConfig, time sync,
workspace, project, START or STOP; it never scans, reconnects, provisions or
continues into Apply.
Canonical READY records cessation/standby. Initialized SCANNING may rebind only
when operation/acquisition, identity/profile, transport, host epoch and project
all match; it exposes one separate single-use confirmed STOP checkpoint.
Foreign, stale or inconclusive evidence changes no topology or authority.
### Explicit retirement and reopen
`physical-command.retire-unavailable` is a separately confirmed local durable
recovery action for one unresolved target that is truly unavailable or replaced.
Admission requires stable idempotency identity and exact backend runtime,
operation, ledger revision and transport CAS plus safe lifecycle ownership. It
preserves the original unknown outcome, activates the exact-transport deny,
performs zero device/host I/O and starts no discovery.
`physical-command.reopen-retired-reconciliation` is also separately confirmed.
It requires an exact fresh same-transport candidate, stable `reopening_id`, exact
runtime/revision/retirement/transport/discovery CAS and safe lifecycle ownership.
It preserves retirement audit, removes only that retirement's active deny and
performs zero device/host I/O. The explicit recovery intent may then run one
exact read-only observation. **Выбрать** never invokes retirement, reopen or
Verify. The only Apply exception is an internal, request-bound local reopen
checkpoint for an explicit scenario reset plus its exact successor Scan. It is
ordered after network PREPARED and before the sole dispatch edge, remains
invisible in the wizard and grants no command authority. The same applied
intent may then settle it read-only from fresh DeviceInfo plus non-retained
READY/SCANNING evidence.
FW 3.0.2 BLE `7f02` contains no stable DeviceInfo identity. Mission Core cannot
prove during BLE-only discovery that the same physical unit has a new
CoreBluetooth UUID. This remains an explicit protocol/hardware gap.
### Bounded durable audit rollover
An explicit local scenario reset must not become unavailable merely because
closed retire/reopen history filled the 64 KiB hot ledger. Before a transition
would exceed that bound, Mission Core durably publishes the complete previous
ledger as a private, owner-only, content-addressed archive segment and then
atomically publishes a compact v4 main record. The main record retains every
active retirement deny, the newest lost-response retire/reopen checkpoint, and
all reconciliation/confirmation proof required by the current physical
operation. Compaction never changes a device outcome and performs no device,
network or host I/O.
Archive segments form a predecessor hash chain with exact sequence and byte
accounting. Reload verifies directory and file ownership/mode, rejects symlink
traversal, bounds total segments and bytes, reparses every embedded ledger and
fails closed for a missing, replayed, reordered or tampered segment. Operation,
reconciliation, verification, confirmation, retirement and reopening identities
remain globally one-use across the hot record and archive. The archive segment
is fsynced before the main-file replace: a crash may leave only an inert orphan,
while retry of the same CAS reuses identical bytes and cannot duplicate the
referenced chain.
Scenario reset asks the ledger to build the exact prospective retirement or
prepared→not-dispatched plan before closing any local receiver, camera,
control-session or network ownership. That shared planner applies the same hot
serialization, compaction, segment, count and total-byte bounds as commit. When
rollover is required, preflight may idempotently prepublish only the immutable
content-addressed predecessor; the main revision/CAS and physical disposition
remain unchanged. This also proves owner/mode, symlink and content-collision
conditions before teardown.
Archive publication is restart-safe at the hard-link boundary. A process death
after destination link and directory fsync but before temporary-name unlink may
leave exactly two private names for one inode. Retry removes only a strictly
named, owner-only temporary alias whose bytes and inode exactly match the
expected destination and whose link count is exactly two, fsyncs that cleanup,
then reuses the destination. Any unrelated hard link, extra temporary, symlink,
metadata mismatch or byte mismatch remains a fail-closed corruption condition.
## Failure and restart semantics
- UI entry, mode, selection, input, polling, refresh and layout changes
start no device operation.
- Search starts only when pressed, runs once for six seconds and terminalizes.
- Apply starts only when pressed, uses one exact fresh candidate and may perform
at most one K1 mutation.
- Candidate/runtime/intent drift is explicit stale, never hidden rescan.
- Post-dispatch uncertainty is explicit outcome-unknown, never automatic replay.
- K1 power loss revokes the active session without inventing standby.
- Wi-Fi loss and WAN loss are distinct: local LAN control may survive WAN loss;
route/association loss revokes only dependent host/control evidence.
- Browser refresh restores no live local selection and causes no I/O.
- Backend restart restores durable audit and safety ledgers, but no live BLE,
control or operator intent.
- Mac sleep/restart rotates host/runtime authority and rejects late work.
## Acceptance
- Mode, selection and input result in zero controller calls.
- Each Search click issues exactly one scan with duration `6`; no effect, timer,
selection or Apply path calls Scan.
- Every result keeps the same ordinary **Выбрать** action. Selection retains
the card, shows applicable inputs immediately, shows no loader and calls no
controller. After an explicit committed scenario reset and its successfully
completed successor Scan, this includes the exact UUID used by the retired
prior scenario; the row never exposes a reconnect/reopen/Verify CTA.
- During unresolved physical recovery, a completed explicit Scan still renders
passive BLE evidence but cannot substitute a foreign target for the durable
recovery record. Exact recovery remains a separate established-session
action outside the cold result list; ordinary **Выбрать** never invokes its
reopen or read-only Verify. A new network flow first requires explicit reset
and a successor Scan.
- Bridge/Direct show SSID/password; Quick Connect does not.
- Exactly one primary **Применить** owns the connection request. Its frontend
handler calls no Scan/Verify/reopen helper and it permits at most one device
mutation. For an exact reset-owned retired UUID, the backend may append only
the internal local settlement checkpoint described above before dispatch.
A later SCANNING settlement grants only explicit STOP authority and never
restarts the reset-owned receiver, camera, writer or acquisition.
- Stale/pre-dispatch and unknown/post-dispatch outcomes are visibly distinct.
- Applied-but-unready/unknown spends the old Apply and gates ordinary mode,
Scan, selection and Apply behind an explicit recovery choice; recommended
Verify has only a server-bound backend target and no browser fallback.
- A new intent remains possible only through current backend policy. Bridge
uses explicit local-only `prepare-select-device`; Quick/Direct use an explicit
admitted Scan and latest fresh row; mode change requires `mode_selection`.
Each route starts no hidden frontend or mutating continuation and ends in a
later fresh Scan/new idempotency Apply. The declared service-owned
same-intent read-only bootstrap after the durable ACK is the sole continuation
exception and creates no UI action.
- The exact Apply REST snapshot with `phase=network_applied` completes the
network intent for both `control_not_ready` and `unknown`, without requiring
connection-ready or waiting for WebSocket/poll convergence.
- A service-owned supervised control bootstrap may continue read-only after
that ACK. It performs no BLE/host mutation or retry and creates no frontend
Scan/Verify/new-Apply action or blocking Apply loader. Its exact
accepted/running state may own one passive settling indicator only.
- Operator error copy comes only from an allowlisted public error-code mapping;
unknown/raw messages use a canonical secret-free fallback and never render
credentials, SSIDs, payloads or stack traces.
- No timeout, disconnect, refresh, restart or state update starts a continuation
or replays an ended action.
- Supervisor, identity pin, network-attempt ledger, physical-command ledger,
process/BLE lease and one-use recovery STOP remain authoritative.
- The plugin uses shared `Button`, `TextField`, `ActivityIndicator` and
`StatusBadge`; contract tests reject raw local controls and literal colors.
- Geometry and long-copy tests keep all actions reachable without overlap.
- Bridge and Quick Connect retain separate real-hardware acceptance.
This ADR does not itself declare hardware coverage. The manifest may mark a
scenario software-covered only when named executable tests cover the software
invariant; remaining K1/macOS/router and Quick Connect gaps stay explicit.
@@ -0,0 +1,93 @@
{
"schema_version": "missioncore.k1-connection-acceptance/v1",
"canonical_document": "docs/20_K1_CONNECTION_SUPERVISION_CANON.md",
"meaning": {
"software-covered": "The listed automated tests cover the software invariant; this is not hardware acceptance.",
"partial": "At least one software layer is covered and an explicit remaining gap is listed.",
"planned": "The scenario is specified but does not yet have adequate executable coverage."
},
"scenarios": [
{"id":"CONN-01","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 Quick-to-Bridge evidence"]},
{"id":"CONN-02","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 Bridge-to-Quick evidence"]},
{"id":"CONN-03","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-dispatch fault injection"]},
{"id":"CONN-04","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-dispatch fault injection"]},
{"id":"CONN-05","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_device_identity_pin_store.py"],"remaining":["two-K1 hardware evidence"]},
{"id":"CONN-06","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_ble_scanner.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["CoreBluetooth hardware evidence"]},
{"id":"CONN-07","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 wait-beyond-TTL acceptance"]},
{"id":"CONN-08","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real app/backend restart reconnect acceptance"]},
{"id":"CONN-10","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-prepare power-loss fault injection"]},
{"id":"CONN-11","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["process-kill acceptance at the prepared boundary"]},
{"id":"CONN-12","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 post-dispatch power-loss acceptance"]},
{"id":"CONN-13","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 observation-loss evidence"]},
{"id":"CONN-14","status":"software-covered","test_files":["tests/test_xgrids_semantic_topology_store.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware host-association loss"]},
{"id":"CONN-15","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hard-power hardware evidence"]},
{"id":"CONN-16","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_physical_command_ledger.py"],"remaining":["end-to-end passive Scan policy after acquisition power loss","exact-target read-only recovery/rebind integration","Bridge hardware power-loss acceptance","Quick Connect recovery not exercised"]},
{"id":"CONN-17","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py"],"remaining":["transport dispatch integration","restart acceptance"]},
{"id":"CONN-18","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_application_mqtt.py"],"remaining":["facade exact-target resolved-active rebind","single-use explicit recovery STOP presentation/action integration","same-project Bridge hardware acceptance","Quick Connect recovery not exercised"]},
{"id":"CONN-19","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_application_mqtt.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["facade exact-target resolved-active READY cessation integration","Bridge reboot hardware acceptance","Quick Connect recovery not exercised"]},
{"id":"CONN-20","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["router-loss hardware evidence"]},
{"id":"CONN-21","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["same-SSID router-return evidence"]},
{"id":"CONN-22","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["manual macOS switch evidence"]},
{"id":"CONN-23","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real Quick AP leave/return"]},
{"id":"CONN-24","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_ble_scanner.py"],"remaining":["macOS sleep/wake hardware acceptance"]},
{"id":"CONN-25","status":"software-covered","test_files":["tests/test_connection_supervisor.py"],"remaining":["route-race integration evidence"]},
{"id":"CONN-26","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["wrong-service integration evidence"]},
{"id":"CONN-27","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_mqtt.py","tests/test_xgrids_application_session.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["half-open MQTT hardware acceptance"]},
{"id":"CONN-28","status":"partial","test_files":["tests/test_xgrids_application_mqtt.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["full facade policy for passive Scan with unknown/active physical state","physical-record transport_ref pinning across bounded observation and wrong-K1 no-topology-change","resolved-active SCANNING one-STOP integration","real K1 passive READY/SCANNING DeviceStatus acceptance"]},
{"id":"CONN-29","status":"planned","test_files":[],"remaining":["durable external-active takeover contract","operator-confirmed same-binding STOP"]},
{"id":"CONN-30","status":"planned","test_files":[],"remaining":["browser/app close clean-session acceptance at every stage"]},
{"id":"CONN-31","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_semantic_topology_store.py"],"remaining":["restart integration acceptance"]},
{"id":"CONN-32","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from a prepared network mutation"]},
{"id":"CONN-33","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from a dispatching network mutation"]},
{"id":"CONN-34","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from an observing network mutation"]},
{"id":"CONN-35","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_semantic_topology_store.py"],"remaining":["restart integration acceptance"]},
{"id":"CONN-36","status":"planned","test_files":[],"remaining":["restart acceptance proving no old live session restoration"]},
{"id":"CONN-37","status":"planned","test_files":[],"remaining":["corrupt historical audit quarantine without permanent K1 block","operator diagnosis UI"]},
{"id":"CONN-38","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_ble_runtime_arbiter.py"],"remaining":["two-service integration acceptance"]},
{"id":"CONN-39","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py"],"remaining":[]},
{"id":"CONN-40","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_session.py"],"remaining":["real wrong/failed DeviceInfo evidence"]},
{"id":"CONN-41","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["MQTT fault-injection integration"]},
{"id":"CONN-42","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["camera and point stall integration"]},
{"id":"CONN-43","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["late packet integration evidence"]},
{"id":"CONN-44","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_session.py"],"remaining":["late DeviceInfo integration evidence"]},
{"id":"CONN-45","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_control_process_lease.py"],"remaining":["durable physical-command integration"]},
{"id":"CONN-46","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_camera_gateway.py"],"remaining":["combined MQTT/camera late-producer integration"]},
{"id":"CONN-47","status":"software-covered","test_files":["tests/test_xgrids_application_control_process_lease.py"],"remaining":["two-service integration acceptance"]},
{"id":"CONN-48","status":"software-covered","test_files":["tests/test_xgrids_camera_gateway.py"],"remaining":["drain-timeout integration evidence"]},
{"id":"CONN-49","status":"software-covered","test_files":["tests/test_connection_supervisor.py"],"remaining":["long-running fault-injection acceptance"]},
{"id":"CONN-50","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["macOS sleep/wake hardware acceptance"]},
{"id":"CONN-51","status":"software-covered","test_files":["tests/test_xgrids_macos_wifi.py","tests/test_connection_supervisor.py"],"remaining":["compiled association observer"]},
{"id":"CONN-52","status":"software-covered","test_files":["tests/test_xgrids_device_identity_pin_store.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["two-identity hardware evidence"]},
{"id":"CONN-53","status":"software-covered","test_files":["tests/test_xgrids_semantic_topology_store.py","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["restart browser acceptance"]},
{"id":"CONN-54","status":"planned","test_files":[],"remaining":["historical unknown audit does not block fresh explicit connect","restart browser acceptance"]},
{"id":"CONN-55","status":"partial","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["automated browser geometry matrix"]},
{"id":"CONN-56","status":"partial","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["automated long-copy browser geometry"]},
{"id":"CONN-57","status":"planned","test_files":[],"remaining":["operator-confirmed physical-ledger archive and identity rotation"]},
{"id":"CONN-58","status":"software-covered","test_files":["tests/test_xgrids_ble_runtime_arbiter.py","tests/test_xgrids_application_control_process_lease.py","tests/test_ble_scanner.py","tests/test_wifi_provisioning.py","tests/test_xgrids_ap_activation.py"],"remaining":["two-service CoreBluetooth hardware acceptance","native cleanup fault injection on macOS"]},
{"id":"CONN-59","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_network_provisioning_idempotency_journal.py"],"remaining":["prove failed audit admission releases active ownership for a new explicit attempt"]},
{"id":"CONN-60","status":"software-covered","test_files":["apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["manual browser confirmation-dismissal acceptance"]},
{"id":"CONN-61","status":"planned","test_files":[],"remaining":["legacy unresolved record terminalization without BLE or cross-session block","process-kill acceptance"]},
{"id":"CONN-62","status":"software-covered","test_files":["tests/test_web_validation_security.py"],"remaining":["manual browser refresh/close acceptance"]},
{"id":"CONN-63","status":"planned","test_files":[],"remaining":["composite policy denies active contention but ignores terminal historical network audit","manual policy presentation acceptance"]},
{"id":"CONN-64","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 control/data loss acceptance"]},
{"id":"CONN-65","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["resolved-active same-project one-STOP UI action acceptance","real restart/browser host-route and passive DeviceStatus acceptance","Quick Connect recovery not exercised"]},
{"id":"CONN-66","status":"planned","test_files":[],"remaining":["compiled or long-lived macOS association observer","long-running monitor latency/fault acceptance"]},
{"id":"CONN-67","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 repeated same-mode and cross-mode reconnect acceptance"]},
{"id":"CONN-68","status":"partial","test_files":["tests/test_xgrids_application_session.py","tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["Bridge STOP-ack plus Wi-Fi-loss hardware rerun","Quick Connect recovery not exercised"]},
{"id":"CONN-69","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["resolved-active same-project one explicit STOP browser acceptance","Bridge hardware rerun with redacted evidence","Quick Connect recovery not exercised"]},
{"id":"CONN-70","status":"software-covered","test_files":["apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge one-scan/select/immediate-credentials/Apply acceptance","Quick Connect recovery not exercised"]},
{"id":"CONN-71","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge STOP-deadline fault injection","Quick Connect recovery not exercised"]},
{"id":"CONN-72","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge existing-plugin-section acceptance","manual two-tab browser acceptance","Quick Connect live acceptance remains separate"]},
{"id":"CONN-73","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_semantic_topology_store.py","tests/test_xgrids_device_identity_pin_store.py"],"remaining":["real cold Bridge and two-K1 identity-mismatch/restart evidence","Quick Connect live acceptance remains separate"]},
{"id":"CONN-74","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_network_provisioning_idempotency_journal.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge Apply with no hidden discovery/Verify","post-dispatch hardware fault injection","Quick Connect live acceptance remains separate"]},
{"id":"CONN-75","status":"software-covered","test_files":["tests/test_xgrids_connection_scenario_reset.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_application_control_process_lease.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["real disconnected/idle desired-mode draft no-I/O acceptance with unresolved durable physical history plus live-owner denial","real pre-START orphan and backend-runtime credential invalidation acceptance","manual top-right emergency-reset acceptance","Quick Connect live acceptance remains separate"]},
{"id":"CONN-76","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_application_control_process_lease.py","tests/test_xgrids_ble_runtime_arbiter.py","tests/test_xgrids_camera_gateway.py","tests/test_cli.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real separate explicit retirement confirmation while selection and Apply remain mutation-free","same-hardware/new-CoreBluetooth-UUID cannot be identified before provisioning because FW 3.0.2 BLE 7f02 exposes no stable DeviceInfo identity","Quick Connect live acceptance remains separate"]},
{"id":"CONN-77","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real retired exact-UUID selection remains local and Apply denied","real separately explicit reopen followed by READY and same-project SCANNING outcomes","Quick Connect live acceptance remains separate"]},
{"id":"CONN-78","status":"software-covered","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["manual Bridge and Quick one-intent timing acceptance","manual top-right idle/pending accessible-label acceptance","real stale-before-dispatch and unknown-after-dispatch fault injection","real fast REST network_applied plus delayed service-owned read-only control-bootstrap convergence","real Bridge prepare-select-device and Quick/Direct scan-new-intent recovery acceptance","manual canonical shared-control visual acceptance"]}
]
}
+393
View File
@@ -0,0 +1,393 @@
# K1 connection lifecycle and recovery runbook
Canonical model: [`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
This runbook is the operator-facing projection of that model. Technical terms in
the internal-safety sections are engineering evidence; they are not wizard copy.
## One operator wizard
The connection surface is one progressive wizard, not a recovery dashboard.
Its only model-bearing heading is **Подключение XGRIDS LixelKity K1**. Inside
the wizard the two step names are exactly **Подключение** and **Сеть**.
### Cold entry
On a clean cold entry show only:
- the connection-mode selector;
- Step 01 **Подключение** with the explicit Bluetooth search action.
Historical K1 identity never adds a reconnect choice to cold entry. If local
session ownership or an older connection scenario exists, one explicit
`reset_scenario` CAS first closes only that local scenario. Only after the reset
is accepted does clean Step 01 expose **Найти по Bluetooth** as a separate
click; reset never starts Scan itself.
Do not render Step 02 yet and do not start discovery automatically. The mode
selector and same-mode **Подключить новый K1** escape remain available through
every other lifecycle state. The first gesture may supersede another pending
local action. While that one bounded scenario reset itself owns `mode`, all
three reset entry points show pending and dispatch no B intent. Each accepted
gesture sends one idempotent local `reset_scenario` CAS: it queues behind old
local lifecycle ownership, seals retained receiver/camera/control resources,
invalidates candidates/drafts/credentials and retires the old physical lineage
without resolving its outcome. It sends no BLE, device or host network command,
MQTT publish, Verify, provisioning, START, STOP or automatic Scan. Opening and
polling the surface still do nothing. Scan, Verify, provisioning and START
retain separate backend gates.
The top-right refresh-shaped utility is the same explicit emergency reset, not
a passive state refresh. Its accessible label is **Сбросить подключение**; while
the request owns the current action it reads **Сбрасываем подключение** and
does not dispatch a second reset until that bounded request settles. It remains
available to supersede any other local action. The accepted revision always
returns the new local scenario and even a dirty browser selector to canonical
**Bridge**, clears the old browser/backend presentation and source owners, and
leaves Scan as a separate click. It performs no hidden Scan, Verify, Connect,
START, STOP, BLE or network write and never substitutes a passive `state.read`
for the reset mutation. After settlement the product surface contains no prior
UUID, result count, **Повторить поиск**, reconnect error, selected device,
credentials or recovery card; it returns to **Найти по Bluetooth**. Late
Scan/Verify settlements from the retired scenario cannot repopulate it.
The retained reset marker fences only work that belonged to the retired
scenario. A newly correlated post-reset network attempt that fails or has an
unknown outcome must immediately render its current recovery/error surface;
reload must neither hide that new failure nor resurrect the prior prompt.
### Step 01 — Подключение
Step 01 **Подключение** is visible immediately.
1. Bluetooth discovery starts only after the operator presses the search
action.
2. For the full bounded search, show an activity indicator and a visible
seconds countdown in the same step.
3. After search completes, show the result count or the empty result. Every
connectable result row keeps the same one enabled **Выбрать** action,
including an exact UUID used in an earlier scenario. That action is
local-only and never invokes reopen or Verify. Fresh results never render a
reconnect CTA, a disabled competing primary or an old/new-device decision.
A successful admitted Scan first settles only the reset marker whose
id/revision/mode it captured at action entry, recording the later admitted
discovery generation while retaining the marker for idempotent reset replay.
Failed/cancelled Scan and an older Scan racing a newer pending reset leave
the marker active and do not expose stale recovery authority.
4. After ordinary **Выбрать**, retain the chosen device card and reveal only
the applicable local draft inputs. Selection itself has no loader and makes
no controller call.
5. Only an authoritative successful connection outcome renders Step 01 green
as **Подключение установлено** and advances the normal connection flow.
The wizard never labels a candidate as saved, original, retired or physically
ambiguous and never exposes ledger, CAS, retirement, reopen or reconnect
terminology in the search list. Exact recovery belongs only to a previously
established session after an actual interruption.
### Step 02 — Сеть
Step 02 **Сеть** exists only after Step 01 has a confirmed green connection.
- If the selected device is already usable on the chosen connection path,
show the network result without asking for credentials.
- If backend policy has safely admitted explicit network setup, show the exact
retained device context and SSID/password fields here, never beside the
candidate list.
- One explicit submit owns any exact hidden revalidation and at most one
reviewed network write.
- **Изменить сеть** belongs only to this step and starts no Bluetooth search
when its form opens.
- A stale tab, changed runtime/binding or policy denial fails before a write and
never exposes a foreign candidate.
## Ordinary selection
The ordinary **Выбрать** action is presentation simplification, not relaxed
safety. It creates only a browser-local candidate draft and performs no
controller I/O. It never selects an internal recovery path:
- an ordinary fresh candidate becomes only the selected local draft;
- an exact prior candidate uses the same **Выбрать** action as every row;
- no candidate selection retires old authority, reopens a ledger record, calls
Verify, connects GATT, scans again or changes topology or the device;
- a foreign, stale, non-connectable or policy-denied candidate remains
unavailable and changes no topology, ledger or device.
After an explicit committed scenario reset, only its successfully completed
successor Scan may make an exact previously retired transport eligible for a
new network draft. Apply still captures that exact current-generation handle,
validates the live GATT baseline and crosses at most one reviewed write edge.
Selection performs no recovery action. At the final Apply boundary, the backend
may append one exact request-bound local reopen checkpoint after network
PREPARED and before write dispatch. That checkpoint preserves physical
retirement/original-outcome audit, performs no device I/O and authorizes no
START or STOP. The same applied intent then uses fresh DeviceInfo plus a
non-retained DeviceStatus to settle READY as standby or identity-bound SCANNING as
active, without a visible Verify step or command replay. In the SCANNING case
it materializes only explicit STOP authority; it does not restart the retired
receiver, camera, evidence writer or acquisition.
### Exact internal recovery for an established session
For an exact target belonging to a previously established session with one
active retirement,
`physical-command.reopen-retired-reconciliation` requires:
- `operator_confirmed=true`, bound to a separately explicit session-recovery
action outside cold entry and the Bluetooth result list;
- a stable `reopening_id` and reason
`device-returned-for-explicit-reconciliation`;
- exact `expected_snapshot_runtime_id`, `expected_revision`,
`expected_retirement_id`, `expected_transport_ref` and
`expected_discovery_generation` CAS;
- a current connectable candidate and safe lifecycle/process ownership.
The transaction changes only the local durable ledger. It appends reopen audit,
preserves the retirement and unknown command outcome as history, restores the
original unresolved `dispatching` or `observing` stage and removes only that
retirement's active deny. It performs zero BLE, Wi-Fi, MQTT, DeviceConfig,
ModelingStatus, workspace, project, START or STOP I/O and starts no Scan.
The same still-current session-recovery action may then own one exact read-only
Verify. Ordinary **Выбрать** never invokes either half. Recovery never replays
historical START/STOP and never silently provisions:
- fresh non-retained READY resolves standby;
- fresh exact same-project SCANNING resolves active and permits only the
separately guarded stop path;
- identity, GATT, CAS, route/control or policy failure leaves the outcome
unknown and ends the established-session recovery without entering the new
connection wizard.
If the action response is lost, refreshed state may continue the same click
only when it proves that exact `reopening_id` audit was committed and every
original runtime, candidate and authority fence still matches. A second tab,
new discovery generation, new retirement or different reopening identity cannot
inherit the continuation.
### Internal retirement
`physical-command.retire-unavailable` is a local durable primitive for an
unresolved target that is truly unavailable or replaced. It may run only from
its separately confirmed recovery/reset path, never from ordinary candidate
selection, UI entry, polling or a timer. Admission requires explicit
confirmation, stable `retirement_id` and exact backend runtime,
operation, revision and transport CAS while every local owner is safe.
Retirement preserves the complete old attempt and unknown command outcome,
activates an exact-transport deny and performs zero device I/O or automatic
discovery. Retirement history remains durable even if an exact later recovery
action uses the reopen transaction. The wizard exposes no retirement
transaction or historical label. Any plain-language exact recovery CTA belongs
only to the established-session surface when backend authority permits it.
Current FW 3.0.2 BLE `7f02` does not expose stable DeviceInfo identity. The same
hardware under a new CoreBluetooth UUID cannot be recognized before DeviceInfo
becomes available. This remains an explicit protocol/hardware acceptance gap;
the wizard must not speculate.
## Session and freshness rules
A scan result is an unselected presence candidate owned by the latest explicit
scan generation. Wall-clock age does not remove its row while the operator is
reading or completing the form. A successor Scan, explicit scenario reset,
runtime-owner teardown or proven exact-target GATT failure invalidates it. The
row itself is never network authority: Apply still requires the exact captured
CoreBluetooth object and live GATT validation before any write.
The selected session ends on proven disconnect, explicit lifecycle stop, a
committed network transition, selection of another device, backend restart or
proven native cleanup. A later connection always requires an explicit search
and **Выбрать**. Polling can update presentation but starts neither operation.
Bridge, Quick Connect and Direct Connect are separate topologies. In
any state, changing the mode or choosing another K1 in the same mode sends one
local scenario-reset CAS. It can wait for and supersede live/recovery ownership,
seal retained local producers and retire unresolved old lineage, but performs
zero device/host I/O and starts no Scan. Scan, Verify, provisioning and START
remain independently fenced until an explicit candidate intent crosses its
reviewed transition. No old host route, endpoint, control, data or BLE authority
crosses a committed reset boundary.
## Active scanning: transient host-path recovery
This is the sole automatic read-only rebind exception. It exists only after
Mission Core itself has a composite-confirmed START and still owns the exact
acquisition/runtime/device/connection/evidence lineage. It does not apply on a
cold connection screen, after backend restart, to an external SCANNING K1 or to
an unresolved/foreign START.
When the Mac loses Wi-Fi/route or the data socket while that acquisition is
running, the active scanning pane shows a neutral spinner and
**Восстанавливаем соединение** with attempt/elapsed time. Do not show a red
terminal operation banner for the expected late failure of the superseded old
control socket. Keep the acquisition and evidence session owned while the
backend retries exact route/TCP and inspection-only DeviceInfo/status proof.
The recovery loop never sends BLE, changes Wi-Fi, writes DeviceConfig, repeats
START or sends STOP. Outcomes are:
- exact same-device/same-project initialized `SCANNING`: silently resume the
point stream/control binding and, when necessary, CAS-restart the dead or
stalled acquisition-owned right-camera FFmpeg epoch;
- fresh `READY`: interrupt/seal host-owned acquisition resources truthfully,
without STOP;
- fresh `SCAN_OVER`: persist cessation, interrupt/seal locally and retain a
read-only `awaiting READY` fence that denies a new START;
- wrong identity/same IP, changed lineage or failed camera CAS: remain blocked
for explicit operator handling; and
- device/system fault or unsafe status: show a truthful terminal fault, with no
command retry.
While state is `reconnecting` or `blocked`, expose **Завершить локально**. The
action `acquisition.force-finish-local` requires the current snapshot runtime,
acquisition id/state revision, recovery generation, producer generation match,
an idempotency key and explicit confirmation. It cancels recovery first, then
seals only local receiver/camera/control/perception owners. It preserves the
physical START ledger and sends no STOP. If a connection-mode reset races this
action, the shared lifecycle gate makes cleanup idempotent; the loser cannot
overwrite the new mode or revive the old acquisition.
If receiver, camera or evidence sealing fails, the recovery generation is
still irrevocably cancelled first. The force-finish operation ends with a
visible `local-cleanup-failed` result whose retryability applies only to local
finalization; the terminal acquisition retains `cleanup_pending` and blocks a
replacement session. A later explicit local stop or exact connection-scenario
reset may retry those host resources. It must not retry START, STOP, BLE or a
network write, and a late success from the retired recovery generation remains
fenced.
## Failure matrix
| Event | Product result | Operator path |
| --- | --- | --- |
| Cold entry | Mode plus Step 01 and explicit Scan; zero device I/O before Scan | Start search explicitly |
| Disconnected/idle mode or same-mode new-device request with unresolved durable physical history | Local session/audit lineage is retired under one reset CAS; zero device/host I/O and no automatic Scan | Start the clean Step 01 search explicitly; the old physical outcome remains auditable |
| Mode reset while live, reconnecting or terminal cleanup still owns local sources | Reset supersedes recovery and locally seals receiver/camera/control; previous K1 may still scan | Wait for the bounded local cleanup or retry the same reset if local sealing fails |
| Search running | Step 01 spinner and visible countdown | Wait or let the bounded search end |
| Search finds no candidates | Step 01 reports no matches | Repeat search explicitly |
| Search finds one or many candidates | Every connectable row has one enabled **Выбрать**, including the exact prior UUID | Select one row; no reconnect or recovery action appears in search results |
| Wall-clock time passes after Scan before selection | Latest-generation rows remain stable; no operation starts | Select normally; exact capture and live GATT will gate Apply |
| A new Scan/reset/runtime teardown or exact-target GATT failure invalidates the generation | Old rows disappear or the attempted action fails cleanly before mutation | Run one explicit new search if needed |
| Selection is rejected by identity, GATT, CAS, lifecycle or safety policy | Loader ends; nothing changed; Step 02 remains absent | **Повторить** or **Выбрать другое** |
| Selection completes exact device connection | Step 01 turns green | Continue in Step 02 **Сеть** |
| Network setup is safely required | Credentials appear only in Step 02 | Submit once |
| Network write becomes ambiguous after dispatch | Attempt ends unknown; no replay | Wait for cleanup, then create a distinct explicit attempt |
| Device powers off or BLE disconnects | Live selection and authority revoke after proof | Search and select explicitly after the device is available |
| Router, Mac Wi-Fi or MQTT control is lost while idle/pre-START | Host/control authority revokes; data may remain evidence only | Restore reachability, then use the same wizard flow |
| Mac Wi-Fi/route is briefly lost during one composite-confirmed owned acquisition | Active pane remains neutral **Восстанавливаем соединение**; no START/STOP/network retry | Wait for exact automatic read-only rebind or press **Завершить локально** |
| Active recovery returns READY or SCAN_OVER | Local receiver/camera seal without STOP; SCAN_OVER remains fenced until fresh READY | Start another scenario only after backend policy reports it safe |
| Active recovery sees another K1 on the same IP or changed lineage | Recovery blocks fail-closed; no camera/data resurrection | Finish locally or explicitly choose/reset connection scenario |
| A physical START/STOP edge is unresolved | Mutation stays fenced; no technical wizard ceremony | Search/select remains explicit; backend admits only a safe exact path |
| Exact actively retired UUID is present after committed reset and successor Scan | The row exposes the same enabled **Выбрать** as every candidate | Select locally; Apply remains exact-handle/live-GATT gated and may append one internal local settlement checkpoint before its sole write; audit remains append-only and START/STOP stay denied until fresh read-only classification |
| Another candidate is selected while old authority is unavailable and no reset-owned new scenario exists | Selection stays local and Apply remains denied | Start an explicit new connection scenario, then Scan and select again |
| Browser refresh or backend restart | No automatic operation and no restored live selection | Begin from the cold progressive wizard |
## Physical START/STOP safety remains separate
The simplified wizard never weakens physical-command safety:
- loss of control does not prove that K1 stopped recording;
- START and STOP are never replayed automatically;
- local receiver/camera/ingress cleanup is not physical STOP;
- an ambiguous post-dispatch command remains unknown until exact fresh proof;
- read-only recovery is pinned to the durable transport, identity/profile,
host epoch and project;
- each observation publishes exactly one DeviceInfo request and may classify
only a fresh non-retained DeviceStatus after that barrier;
- READY records cessation without inventing a successful STOP;
- SCAN_OVER records cessation without inventing STOP, but keeps a durable
read-only fence until a later fresh unbound READY observation;
- exact same-project SCANNING may mint one single-use, separately confirmed STOP
checkpoint; it does not send STOP automatically;
- a wrong transport/device/project changes no topology or ledger state;
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
only host-owned resources, yields `timed_out` / `standby-unknown`, preserves
the unresolved ledger and keeps mutation fenced.
Engineering logs and state APIs retain these distinctions. The connection
wizard projects only the ordinary progressive flow and a non-technical terminal
selection result.
## No automatic action rule
None of these events may scan, select, reconnect, Verify, provision, START or
STOP:
- opening or resizing the connection surface;
- backend event delivery or state polling;
- an acknowledged scenario reset (it may perform only its explicit local
retirement, never any listed device/network action or automatic Scan);
- candidate list refresh after an ended search;
- browser refresh, sleep/wake or backend restart;
- timeout, disconnect or a historical audit record.
The only exception is the service-owned active-stream read-only rebind above.
It is triggered by the already-owned receiver's transport loss, not UI entry or
polling, and is limited to route/TCP, DeviceInfo/status inspection, receiver
resubscribe and exact local camera-epoch restart. It never performs discovery,
provisioning, START, STOP or any device/network write.
Only the currently pressed search, distinct exact recovery CTA, network submit
or separately guarded acquisition control may own corresponding I/O. Ordinary
**Выбрать** owns only a browser-local draft and never owns a loader. Every
loader belongs to the explicit action that created it and ends with it.
## Hardware acceptance order
Software tests do not replace a real K1/macOS/router run. Accept sequentially:
1. Open cold and prove mode plus Step 01 and its explicit Scan action are
visible, while Step 02 is absent and no discovery starts automatically. With both empty and
unresolved durable physical history, change the mode and prove one local
reset CAS, zero device/host calls and no automatic Scan. Repeat from active,
reconnecting and terminal `cleanup_pending` states; prove local sources are
sealed, the old K1 is not claimed stopped, and a local cleanup failure leaves
the exact reset retryable. With an exact prior connection, prove cold entry
contains no historical reconnect prompt; after one reset CAS and zero Scan,
a separate clean **Найти по Bluetooth** action remains clean after reload.
2. Start discovery and prove the spinner and seconds countdown remain visible
for the bounded search, then the exact result count appears.
3. With multiple advertisements, prove every ordinary connectable row keeps
exactly one enabled **Выбрать** action and none auto-selects or auto-connects.
Repeat with the exact prior UUID after reset and prove it has the same
**Выбрать** action, with no reconnect/reopen/Verify path.
4. Select a Bridge device, including that prior UUID, and prove the card remains visible through
**Подключение…**, then Step 01 turns green before Step 02 **Сеть** appears.
5. Prove network fields never coexist with candidate rows, and one explicit
submit owns at most one write.
6. Wait beyond the legacy candidate TTL and prove both the latest-generation
unselected rows and an admitted selected session remain stable; then prove a
missing exact handle/live GATT failure blocks Apply before any write.
7. Exercise identity, GATT, stale-CAS, lifecycle-busy, disconnect and power-off
failures; each ends the loader, leaves Step 02 absent and offers only ordinary
retry/choose-another copy.
8. Retire an unresolved target in controlled fault injection, perform one
scenario reset and rediscover its exact UUID in the successor Scan. Prove its
sole action is **Выбрать**, selection performs no I/O and Step 02 appears
immediately. Apply once and prove exact current-generation handle capture,
live GATT baseline, exactly one request-bound append-only physical reopen
checkpoint and at most one network write. The original retirement/outcome
audit remains immutable; the service-owned continuation uses only DeviceInfo
and non-retained status, with zero START/STOP and no browser Verify. Inject
failed and outcome-unknown network results; each current error/recovery
surface remains visible after reload.
9. Try a different device while old authority is unavailable and prove its
ordinary selection triggers no hidden retirement/reopen/Verify and cannot
bypass the durable target.
10. Prove no row labels a device saved/original/retired, says
**Переподключиться**, or exposes physical-state/ledger terminology. The model name
appears only in the top heading; step names remain **Подключение / Сеть**.
11. During a composite-confirmed live acquisition, remove host Wi-Fi for longer
than the old control keepalive and restore it. Prove neutral reconnecting,
same-lineage SCANNING resume, raw-writer continuity, exact camera epoch
restart when stalled, and zero START/STOP/BLE/network mutation. Repeat with
READY, SCAN_OVER, wrong identity and permanent loss plus
**Завершить локально**.
12. Repeat idle/pre-START Bridge network loss, Mac Wi-Fi switch, sleep/wake,
hard K1 power loss, STOP deadline and backend restart; prove zero automatic
command or retry outside the sole active-stream exception.
13. Repeat the entire acceptance separately for Quick Connect before claiming
Quick coverage.
The current software contract is not real-hardware acceptance. The acceptance
manifest lists executable coverage and the remaining Bridge/Quick field gaps.