Skip to content

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)

  1. Retain existing raw-only artifact semantics. Keep 0x80 raw 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.
  2. 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.
  3. 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.
  4. First-device policy is intentionally conservative and initially fixed in code: receive at least one explicit POOR_SIGNAL=0 before admitting events; consider POOR_SIGNAL != 0 unhealthy; 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.
  5. 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.
  6. Single shared live-policy composition. Make the daemon runner, monitor, and field-test construct the same source-health/integrity/quality stack. check-source still performs its generic conformance checks but should add a ThinkGear-specific protocol-health qualification line, without claiming device support. Preserve BlockSource as-is, avoiding public config, CLI or IPC changes unless a separate maintainer-reviewed interface change is needed.
  7. 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.
  8. 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_SIGNAL or reports low nonzero values continuously will be ineligible for qualified activation under this initial policy. Report unqualified/needs investigation instead 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 scripts and uv 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.