# RECEIVER-HARDENING-PLAN — v1.0.4 (webhook receiver survival)

> JIT plan (AGENTS.md). Post-checkpoint execution artifact: this file carries the
> full wave so a fresh context can execute it. Gate: app JVM tests + assembleDebug/
> Release + emulator smoke + `traceable-reqs check` green, then release v1.0.4 per
> docs/RELEASING.md.

## Field evidence (2026-07-07, live adb diagnosis on enlyzeam / motorola_razr_plus_2024)

Operator's Index 01 dictation never reached the app's webhook. Root-caused by logcat +
process forensics — **our side, NOT Pebble's #287**:

- Pebble app sent a well-formed `TranscriptionOnly` POST to `127.0.0.1:8646` and got
  **`ECONNREFUSED`** (`E IndexWebhookApi: Failed to connect to /127.0.0.1:8646 … ECONNREFUSED`).
- Our receiver answers `401` to a token-less probe when the app is up (verified twice via
  `adb forward tcp:18646 tcp:8646` + curl) — so the receiver binds and gates correctly.
- Our app process was **~36 min old** (`/proc/<pid>/stat` field 22 = 39073678 jiffies ÷100
  vs uptime): it **cold-started at ~02:55**, MainActivity foregrounded 02:56:06.557, webhook
  refused 02:56:06.573 — 16ms later. The foreground service that hosts the receiver **was
  not running** while the app was backgrounded.
- Motorola OEM battery management (`MotoBatteryCareService` in the logs) kills background
  FGS. Current manifest has `dataSync` type + `startForeground` + `START_STICKY` but:
  **no BOOT_COMPLETED receiver** (dead after reboot until the app is opened) and **no
  battery-optimization exemption** (OEM kills it). So the "always-on phone server" isn't.

Coverage gap: `REQ-HAZARD-DICTATION-LOSS` assumes the receiver is UP (covers answer-200-
then-lose). Nothing covers "receiver process not running → ECONNREFUSED → dictation lost."

## Tasks

- **T1 — battery-opt exemption** (highest impact on Moto): add
  `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission (manifest); a UI prompt on the Hosts
  screen (mirror the existing POST_NOTIFICATIONS runtime ask in MainActivity) that fires
  the `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` intent, gated on
  `PowerManager.isIgnoringBatteryOptimizations`. Non-fatal if declined; surface state.
- **T2 — boot auto-start** (REQ-HAZARD-RECEIVER-DOWN): `RECEIVE_BOOT_COMPLETED` permission
  + a `BootReceiver` (exported, `BOOT_COMPLETED` + `QUICKBOOT_POWERON`) that calls
  `LinkService.start(context)`. The FGS-start-from-background-receiver is legal because
  dataSync is a permitted post-boot FGS type; verify on the Android 15 target. START_STICKY
  already covers OS-reclaim restarts (LinkService.kt:176) — this closes the reboot hole.
- **T3 — receiver liveness UI** (the silent-failure gap, same class as #287): surface
  "listening on :8646 · last received <ago>" on the Hosts voice card. WebhookServer already
  owns the bound state; thread a `lastReceivedAt` + `bound` into `VoiceState`
  (LinkService.kt:47) so a dead/never-hit receiver is visible, not silent.
- **T4 — hazard + test** (REQ-HAZARD-RECEIVER-DOWN): activate the REQ (currently seeded
  inactive in traceable-reqs.toml); KNOWN-HAZARDS §1.2 entry; a JVM test asserting the
  boot receiver targets LinkService + a WebhookServer test that a bound receiver reports
  liveness. FGS-survival itself is device-behavior (documented, live-verified), not unit-
  testable — note that in the hazard mapping.

## Gate + release

- Version bump all three (app versionCode 5 / versionName 1.0.4, host Cargo.toml +
  manifest.toml stay 1.0.3 unless host changes — this wave is app-only, so host untouched;
  confirm `spt adapter update` isn't expecting a host bump).
- app JVM + assembleDebug/Release (release-cert signed) + emulator smoke (ci/emulator-
  smoke.ps1) + traceable-reqs check green.
- Release per docs/RELEASING.md; **APK only changed** — adapter.spt unchanged, but re-cut
  is harmless. Operator must sideload the 1.0.4 APK (Obtainium).
- Re-verify on enlyzeam over adb: toggle battery-opt exemption, reboot the phone, confirm
  the receiver answers 401 without opening the app.

## Open notes

- adb tooling is installed on enlyzeam at `C:\temp\platform-tools\` (ssh `decid@enlyzeam`);
  device `ZY22K2S8V5` authorized. `adb forward tcp:18646 tcp:8646` + curl is the receiver
  probe.
- Android 15 dataSync 6h/day cap (LinkService.kt:46) is a separate latent limit — note if
  it bites during the reboot-survival test.
