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