ADR-v2-155 — Fail-closed ThinkGear stream health before first physical-device qualification¶
Status: Proposed (2026-11-03); requires maintainer review before any runtime patch
Scope: Device-specific correctness hardening of the already-shipped, opt-in biosignal source; not a new capability or a claim of hardware support
Related: ADR-v2-152, ADR-v2-153, ADR-030, research, implementation spec.
Context and problem¶
The existing ThinkGearSource reads raw TGAM/ThinkGear packets into SampleBlock and exposes poor_signal as metadata. The same source can report rejected checksums, malformed payloads and discarded framing bytes. The live daemon and biosignal monitor use IntegrityGuard and IntegrityGate; biosignal field-test currently does not use their same composition. All parser/gesture verification is synthetic or fake-transport; real-device interoperability and accuracy are unverified.
A ThinkGear packet has no sample sequence or true sensor timestamp. t0 = accepted_raw_samples / 512 is an index in emitted data, not proof that no sample was lost. An ordinary timestamp-continuity guard cannot infer whether a rejected packet concealed raw data. A POOR_SIGNAL value is reported periodically, not per sample, so a later good value must never retroactively clear a bad or unknown interval.
The existing ThinkGear protocol defines 0x02 as a contamination/contact signal: zero denotes no detected contamination; any nonzero value denotes contamination, with 200 a documented no-contact case. The signal is an additional veto, not evidence by itself that the waveform is suitable.
Decision (proposed)¶
- Retain existing raw-only artifact semantics. Keep
0x80raw i16 as the only input to blink/jaw detectors. Never turn vendor eSense, attention, meditation or vendor blink-strength values into activation intents. Never infer neural intent, health or emotion. - Attach a typed, bounded, per-read health verdict to the source's accepted blocks. Read and validate all packet health changes in wire order. Any checksum/length/malformed frame, nonzero contact value, unknown contact state, stale contact report or device disconnect marks that entire affected read/batch untrusted; an end-of-batch good value cannot rescue the batch. Keep health facts separate from waveform samples and never persist raw signals to reporting artifacts. A batch without raw data may update health but may not generate an event.
- Compose a source-specific health veto with existing guards, not an alternative signal detector. The event gate is open only when source-health, stream integrity and waveform quality all permit it, and only after a clean rearm period. A source-health veto must close/release an already-held activation (where one exists), suppress pending double-blink state and prevent a stale event from appearing on recovery. Preserve the runner watchdog and existing global pause/activation safety. Missing or unrecognized provenance is treated as untrusted.
- First-device policy is intentionally conservative and initially fixed in code: receive at least one explicit
POOR_SIGNAL=0before admitting events; considerPOOR_SIGNAL != 0unhealthy; close if the most recent report is older than 3.0 seconds of an injected monotonic live clock; following any unhealthy/unknown state, require two fresh, separate zero-valued health reports and at least 1.0 second of contiguous valid raw samples before admitting. The current read/batch that completes this requirement remains vetoed; admit no earlier than the following clean batch. These are untested engineering thresholds, not product-validated defaults; they may be changed only in a reviewed follow-up with measured evidence, never tuned on evaluation samples. - Malformed/rejected data creates an uncertainty epoch, not a guessed time interval. Count discarded packets and discarded bytes, but never assume each bad packet represents one raw sample and never insert zero/repeated/interpolated samples to conceal transport damage. Invalidate a gesture that spans an uncertainty boundary, reset detector/filter state on gate transitions, and exclude untrusted exposure from the field-test denominator. Where a true lost-sample count is unknowable, label it
unknown. - Single shared live-policy composition. Make the daemon runner,
monitor, andfield-testconstruct the same source-health/integrity/quality stack.check-sourcestill performs its generic conformance checks but should add a ThinkGear-specific protocol-health qualification line, without claiming device support. PreserveBlockSourceas-is, avoiding public config, CLI or IPC changes unless a separate maintainer-reviewed interface change is needed. - Failure isolation. A missing
pyserial, no RFCOMM serial port, unexpected firmware payloads, stale quality, device disappearance, invalid block or health-gate error cannot crash normal dictation and cannot cause a new activation. A generic quality-gate exception must not silently disable the remaining source-health veto; fail closed for this path. - Privacy and evidence. Preserve off-by-default behavior and no network calls, telemetry, cloud dependency or automatic vendor SDK. Only local human-selected hardware can be opened. Shareable reports contain aggregate counts/exposure and non-identifying hardware metadata only; raw EEG/EOG and personalized calibration files remain local. For the first test, use a single volunteer as an engineering feasibility result, not a clinical trial or ADR acceptance evidence.
Important implementation boundary¶
Do not change the SampleBlock dataclass, its t0 contract, the generic event vocabulary, action mapping or core blink thresholds as a shortcut. An internal optional source-provenance interface on IntegrityGuard is acceptable only if it is backward-compatible and tested with every existing source type. No action or key injection is added to the driver itself.
All packet-level transitions occurring inside a single serial read() must affect that read's gate verdict. Conservative whole-batch veto is preferable to pretending that the final poor_signal applies to earlier samples; splitting batches is optional only if strictly proven necessary and bounded. If an invalid frame arrives without any raw block, it still invalidates pending detector state and cannot leave a held action indefinitely: coordinate the fail-closed transition with the runner watchdog/release path.
Alternatives¶
| Alternative | Upside | Failure | Decision |
|---|---|---|---|
| No change; trust generic signal-quality heuristics | Smallest patch | Ignores explicit contact-loss signal; nominal sample-index clock masks possible loss | Reject |
| Use vendor's built-in blink-strength / attention values | Easy demo | Opaque algorithm, proprietary heuristics, poor reproducibility | Reject |
| Assume one missing raw sample per rejected packet and synthesize a timestamp | Simple clock bookkeeping | Incorrect for metadata packets, multirow raw packets and lost chunks; false precision | Reject |
| New proprietary EEG SDK or remote service | May improve device interoperability | Duplicates validated source shape, might depend on account/network | Defer unless a documented offline incompatibility |
| Source-specific typed health veto + reuse existing gates | Conservative, testable with fake serial, works offline | Needs provenance and shared construction; may be overly strict | Selected |
Consequences and explicit trade-offs¶
- A headset that never emits
POOR_SIGNALor reports low nonzero values continuously will be ineligible for qualified activation under this initial policy. Reportunqualified/needs investigationinstead of silently bypassing the gate. This may reduce availability; that is the safety choice until evidence exists. - A valid packet stream may still have real-world dropouts. A passed conformance run demonstrates the limited interval measured, never universal reliability.
- One usability pilot cannot upgrade ADR-v2-152 from Proposed: its five-participant, multi-configuration, per-phase statistical and safety gates remain intact.
- A future alternate sensor with a different metadata contract must declare an explicit adapter and independent tests; this ADR is not a generic EEG driver mandate.
Acceptance tests before a reviewable runtime PR¶
- Fake-serial streams: contact unknown, fresh zero, nonzero after good, 200 no-contact, 0/200/0 interleaving in one batch, corrupt checksum before a valid blink, malformed length/payload, missing reports >3s, serial unplug/replug, idle/stall, multiple starts/stops, and oversized garbage; all events suppressed when health is untrusted.
- A double blink straddling a health transition is not accepted; no event leaks on rearm; a held activation releases exactly once even across errors, pause and watchdog.
field-test,monitor, and daemon apply the same health gate and report comparable reasons for the same packet sequence. Their normal synthetic/file/LSL/BrainFlow behavior remains unchanged.- Shareable field-test output has only counts/durations and no raw waveform, source path, per-event time, address or identifying data; critical errors remain in local diagnostics.
uv run python -m pytest tests/ -v,uv run ruff check src tests scriptsanduv run mypy src(advisory) verified; generated documentation and the ADR index updated via their generators, not manually.- A human hardware pilot and the larger acceptance study are separate future evidence stages, not prerequisites to reviewing a pure fake-transport patch.
Revisit / kill¶
Stop this device path (no launch/support claim) if the local host cannot access raw values without cloud/account, if contact health cannot be observed with sufficient freshness, if the supported firmware supplies no dependable raw stream, or if any recurrent stuck-action scenario remains after a fix. Log a reproducible limitation rather than bypassing the safety gate. Any change to these rules needs a dated amendment and maintainers' review.
Verification status: Design and code review only. No code patch, tests, physical board, human participant or device measurement was run for this ADR.