Files
NODEDC_MISSION_CORE/docs/adr/0015-k1-physical-state-recovery.md
T
DCCONSTRUCTIONS 0ca7316a24 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.
2026-08-14 14:57:50 +03:00

310 lines
17 KiB
Markdown

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