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
@@ -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.