# Low Passage Mind Host An Android app that turns a phone or tablet into a **mind host**: an OpenAI-compatible `/v1` endpoint on the LAN, installed and configured over USB, then untethered and left to run. It is part of [[Low Passage]]. The phone is a **mind host or a consort, not an Erlang station**; the device evidence is in [[Low Passage Research]]. ## Scope - Android 12+, arm64, physical device. QVAC does not run on emulators. - One model per app instance for v0; multiple models later if the device can hold them. - OpenAI-compatible API: `/v1/models`, `/v1/chat/completions` (blocking and SSE), `/health`. Embeddings optional. - Bearer-token auth; LAN only; no public exposure. - Provisioning cosmetics: wallpaper, dark mode, screen timeout. A hollowed device cannot set these from its own settings, and a node that is meant to sit on a shelf does not get to depend on a person with a Google account holding the phone. - Not a chat UI, not a station, not a consort, not a model trainer. ## What the app contains | Part | Responsibility | |---|---| | QVAC SDK | Load the GGUF model, run completion and streaming, unload it. | | HTTP server | Ktor or NanoHTTPD on a configurable port, default `11434`. | | Foreground service | Persistent notification, wake lock, `START_STICKY`, battery-optimization exemption. | | Model manager | Download or import a GGUF, preflight memory, set `ctx_size`, load/unload. | | Config | Ship URL, mind ID, token, model alias, port, context size, capacity, heartbeat interval. Stored in app-private storage; writable over ADB. | | Registration | `POST` its `describe` payload to the ship's `mind_pool`; heartbeat on an interval; retry with backoff. | | UI | Start/stop, model loaded, IP and port, last heartbeat, logs. | | Cosmetics | Sets the wallpaper from a bundled image or a pushed file, and asks for dark mode and a screen timeout, so provisioning finishes on the device that received it. | | Boot receiver | Optional restart after reboot, subject to OEM policy. | ## Provisioning over USB This is the sequence the tender's `enroll:provision` station runs in [[Low Passage]]: the app is what gets installed, not what does the installing. 1. Build and sign `lowpassage-mind-host.apk`. 2. Enable Developer options and USB debugging; authorize the PC's RSA key. 3. `adb install -r lowpassage-mind-host.apk`. 4. Push config, either as a file or as intent extras: ```sh adb push mind-host.json /sdcard/Android/data/dev.lowpassage.mindhost/files/config.json # or adb shell am start -n dev.lowpassage.mindhost/.ConfigActivity \ --es ship_url http://ship.local:7433 \ --es mind_id android-01 \ --es token <token> \ --es model qwen3-3b-q4 ``` 5. Grant the permissions the service needs: ```sh adb shell pm grant dev.lowpassage.mindhost android.permission.POST_NOTIFICATIONS adb shell dumpsys deviceidle whitelist +dev.lowpassage.mindhost adb shell cmd appops set dev.lowpassage.mindhost RUN_IN_BACKGROUND allow ``` 6. Start it once and confirm it loaded: ```sh adb shell am start-foreground-service -n dev.lowpassage.mindhost/.MindService adb logcat -s LowPassageMindHost ``` 7. Verify from the PC over the phone's Wi-Fi address: ```sh curl http://<phone-ip>:11434/v1/models -H "Authorization: Bearer <token>" ``` 8. Untether. Confirm the registration and heartbeat in `mind_pool` and on the bridge. USB is only the installer and config channel. The production transport is Wi-Fi. ## Device cosmetics A hollowed device cannot set its own wallpaper. "Wallpaper & style" is a Google app: it calls Play Services, and its "My photos" tab hands the photo choice to the gallery. Scoop both out and the setting does not degrade, it crashes — which is exactly what happened on the first test device. Dark mode and the screen timeout survive the scoop, but a device that sleeps after thirty seconds is not a node, and one that has no way back to a dark theme is unpleasant to look at while somebody debugs it. So the app carries these, even though they are not really its business: - **Wallpaper** — `WallpaperManager.setBitmap` from a bundled image, or from a path pushed over adb. `SET_WALLPAPER` is a normal permission, so any app may do this; no Play Services, no picker, no gallery. It is a handful of lines. - **Dark mode** — `UiModeManager.setNightMode` where the platform permits it, with `adb shell cmd uimode night yes` as the provisioning fallback, because that one's permission story is less certain than the wallpaper's. - **Screen timeout** — `Settings.System.SCREEN_OFF_TIMEOUT` behind `WRITE_SETTINGS`, granted once during provisioning. A node should stay awake long enough to be worked on. **One texture per ingestion mode.** A scooped node and an implanted one are different things to find in a drawer, and the wallpaper says which is which without unlocking anything: the scooped device wears the node itself, the implanted one wears a variant that still shows the life it was borrowed from. The app bundles both and picks by a setting the tender writes at provisioning, so a device that gets re-ingested — hollowed after a year of being somebody's phone — changes its own face on the next pass. The rule underneath is the one the rest of the design follows: a capability belongs to whatever holds the permission for it. The wallpaper lives in a system service; the app is a hand that can reach in. Shipping the hand is cheaper than shipping a gallery. ## Runtime - The service binds the Wi-Fi interface on the configured port and requires the bearer token on every model call. - It registers an endpoint like `http://192.168.4.42:11434/v1` with the ship's `mind_pool`, including `kind: model`, model alias, context size, capacity, latency, availability, and token reference. - Heartbeat every 30 seconds; a missed heartbeat marks the mind `offline` in the pool, and the bridge shows it. - The app exposes `GET /health` for a cheap readiness probe. - `/control/load` and `/control/unload` are optional and token-protected; otherwise model changes are done over ADB. ## Failure and recovery | Failure | Behaviour | |---|---| | Wi-Fi drop | Heartbeat fails; the pool marks the mind offline; the app retries with backoff. | | Process killed | Foreground service restarts; boot receiver is a best-effort addition, subject to OEM policy. | | Thermal throttling | The app reports `degraded`; the pool reduces routing; a cool-down period is optional. | | Model OOM | Preflight refuses the load; the app reports the error rather than crashing. | | Token rotation | Config update over the LAN control endpoint, or re-provision over USB. | | App update | `adb install -r` while tethered, or a signed in-app update path later. | ## Security - Use a dedicated device with no personal accounts. - One bearer token per mind; never log it; store it in app-private storage. - LAN only. No port forwarding to the internet. TLS is optional on a trusted LAN; if the LAN is not trusted, put the endpoint behind a reverse proxy or use TLS. - USB debugging is powerful. Keep the device managed; revoke USB authorization when provisioning is done, and re-authorize only for the next provisioning session. - The `/control` endpoints must require the same token as the model endpoints. ## Device floor | Role | RAM | Practical model | Notes | |---|---|---|---| | Grunt mind, stripped | 4 GB | 1B Q4 | **measured**: 8.25 t/s over twelve minutes, 77 °C peak, ~15% lost to throttling — [[Device Report — Pixel 3a XL]] | | Grunt mind | 6–8 GB | 1B–3B Q4 | 3–7 tok/s; thermal throttling after 5–10 min | | Better grunt mind | 8–12 GB | 3B–4B Q4 | 8–10 tok/s on a 2022 flagship | | Small primary | 12–16 GB | 3B–7B Q4 | 7B at 6–8 tok/s; the only phone class where 7B is pleasant | Android reserves 40–50% of RAM for the system, so an 8 GB phone gives about 4 GB to the model. Storage: at least 4 GB free for the model and logs. Keep the device charging and ventilated. **Context is the second axis, and it is measured in time rather than memory.** The cache costs 32 KB per token — 256 MB at 8K — which no phone in this table is short of; prompt processing at 16–19 tokens a second is what a station actually pays, because a 4K domain is three and a half minutes before the first token arrives. Size the domain, not the window: the specification has the rule under force design, and the numbers behind it are in [[Device Report — Pixel 3a XL]]. The 4 GB row is measured rather than estimated, and it argues with the band beneath it. A Pixel 3a XL with no accounts, Play Services disabled and 109 of 241 packages stripped held 1.86 GB available and ran a 1.24B model at 9–10 tokens a second. What decides is **free memory after stripping**, not nominal RAM: the same device read 0.76 GB available as found, which would have refused it. ## Test device Two devices are in hand, and they are at opposite ends of the range. **Measured — Pixel 3a XL** (`bonito`), the app's proving ground for the low end: SDM670, 3.68 GB RAM, 1.86 GB available once stripped, Android 12. It runs a 1.24B Q4 at 9.1 t/s sustained with the binaries alone, which means the app is being written for a device that has already been shown to work without it. Full numbers in [[Device Report — Pixel 3a XL]]. **Target — Pixel 10 Pro XL** (`mustang`), the device the app is actually aimed at: - **Pixel 10 Pro XL** (`mustang`), Tensor G5 (`laguna`), Android 17 / API 37, `arm64-v8a`. - **16 GB RAM**, ~5.9 GB available with the system running; 8 GB swap. - **228 GB storage**, 158 GB free. - **PowerVR DXT-48-1536**, OpenGL ES 3.2, Vulkan compute 1.3.1. Not Adreno, so QVAC's OpenCL path may not apply; test Vulkan first, CPU fallback second. - Battery 71%, USB powered, health good, 27.3 °C. On paper this is the "small primary" class: 12–16 GB is the only phone class where 7B is pleasant. Measured, it hands about 5.9 GB to the model with the system running, which is comfortable for 3B–4B Q4 and tight for 7B, so a 7B load needs the memory preflight. The class only predicts; the measurement decides. ## Alternatives - **PicoClaw APK** — a consort, not a mind host. It runs an agent harness and consumes model providers; it does not serve a model. Useful if the phone should act, not think. - **Termux + llama.cpp** — an experiment, not a product. No foreground-service guarantees, Android background limits, manual builds. Use F-Droid Termux if you try it. - **PocketPal / MLC Chat / other local LLM apps** — most are chat UIs and do not expose an OpenAI-compatible server or a headless service mode. - **Ollama on Android** — not officially supported; Termux and the same background limits. - **USB tethering or ADB port forwarding** — debugging and fallback only. Not the production transport. - **Linux SBC or mini PC** — more reliable and more RAM per dollar; the preferred pool member when space and power allow. - **Human mind** — a person with a console; a different kind of mind, not a model host. ## Build toolchain ### Host status, 2026-11-08 Verified on the development host: - **Android Studio 2026.2.2.8** is installed and has completed its first run, so its config files exist under `C:\Users\thant\AppData\Roaming\Google\AndroidStudio2026.2.2\options\`. - **Its bundled JBR is 25**, which the Android Gradle Plugin will not accept. The project pins JDK 17 instead; `JAVA_HOME` stays on 25. - **Temurin JDK 17.0.21** is installed at `C:\Program Files\Eclipse Adoptium\jdk-17.0.21.9-hotspot`. - **Android SDK** at `C:\Users\thant\AppData\Local\Android\Sdk`: platform-tools 37.0.1 (`adb` 1.0.41), platforms `android-35` and `android-36`, build-tools `35.0.0` and `36.0.0`, command-line tools 23. `ANDROID_HOME` and `ANDROID_SDK_ROOT` are set. - **No global Gradle pin.** `~/.gradle/gradle.properties` does not exist, deliberately: the JDK 17 pin belongs to the project, not the machine. - **Node 24.19** is installed. The Kotlin route does not need it; the Expo/JS route needs Node 22.21+ or 24.9+. - **Device access works.** `adb devices` enumerates an attached phone once USB debugging is authorized; the first test target was a Pixel 10 Pro XL. ### Install | Item | Why | |---|---| | Android Studio (latest) or the command-line tools | SDK manager, platform-tools, build-tools, and device deployment. | | JDK 17 (Temurin) | AGP-compatible compiler. The project pins this one for itself; the machine's `JAVA_HOME` stays on the newer JDK, because a JBR 25 or a JDK 25 is not accepted by AGP. | | SDK Platform 35/36, Build-Tools, Platform-Tools, Command-line Tools | Build and deploy. Accept licenses with `sdkmanager --licenses`; in the current command-line tools `sdkmanager` is deprecated in favor of `android sdk`, and package IDs use a slash (`platforms/android-35`, `build-tools/35.0.0`). | | QVAC Kotlin SDK Maven archive | `qvac-sdk-android-llm-0.23.0-maven.zip` from the `sdk-v0.23.0` GitHub release; extract it to a local Maven repository. | | JitPack repository | Serves `bare-rpc-kotlin`, which the QVAC Kotlin artifacts depend on. | | Kotlin 2.4.20 Gradle plugin | QVAC Kotlin artifacts are compiled with Kotlin 2.4; AGP 9 can use the plugin without applying it. | | Gradle wrapper | AGP 9 wrapper; no separate Gradle install. | | USB driver for the phone | Windows needs the OEM or Google USB driver for ADB. | | A physical Android 12+ arm64 device | QVAC does not run on emulators. | | `keytool` + `apksigner` | Release signing; debug signing is automatic. | | ~20 GB free disk, 16 GB RAM | SDK, Gradle caches, models. | **Optional:** NDK `29.0.14206865` only if you build QVAC's native modules or its Kotlin e2e runner; the Maven archive should carry prebuilt libraries. `scrcpy` for device control, `curl`/`jq` for API tests, and `vulkaninfo` on the device to confirm GPU support. ### Gradle setup - `settings.gradle.kts`: `google()`, `mavenCentral()`, JitPack for `com.github.holepunchto.*`, and the local extracted QVAC Maven repository. - `app/build.gradle.kts`: `implementation("io.tether:qvac-sdk-android-llm:0.23.0")`; Kotlin 2.4.20 plugin; `minSdk 31`, `compileSdk 35/36`; NDK only if needed. - The QVAC service runs in the app's private `:qvac_worker` process; the foreground service talks to it. ### Project JDK pin `JAVA_HOME` stays on the machine's modern JDK. The Android project pins its own: - `gradle.properties` in the project root: ```properties org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-17.0.21.9-hotspot ``` - In Android Studio, the project's Gradle JDK is Temurin 17. - The project's `.idea/gradle.xml` records the same once the project is opened. Do not put this in `~/.gradle/gradle.properties` or the IDE's default project settings; that would pin every Gradle build on the machine to JDK 17. ## Build plan 1. **Spike 1 — the server.** Kotlin app, QVAC SDK, Ktor, foreground service. Load a 0.6B model. `adb install`, untether, call `/v1/models` and `/v1/chat/completions` from the PC over Wi-Fi. 2. **Spike 2 — registration.** Config push, token auth, `mind_pool` registration and heartbeat; confirm the bridge shows the mind. 3. **Spike 3 — the real model.** A 3B model on an 8 GB phone; measure throughput, thermal throttling, and uptime over an hour. 4. **Spike 4 — survival.** Wi-Fi drop and reconnect, process kill, battery exemption, boot restart, OEM background policy. 5. **Productize.** Model manager, error taxonomy, logs, versioning, a provisioning script, and the cosmetics pass — wallpaper, dark mode, screen timeout — that lets a hollowed device finish setting itself up. ## Acceptance criteria - Installed over USB, configured, untethered, and reachable over Wi-Fi. - Serves `/v1/models`, `/v1/chat/completions`, and `/health` with a bearer token. - Registers in `mind_pool`; the bridge shows the mind, its model, and its health. - Survives a Wi-Fi drop and a process kill without manual intervention. - Runs a 3B model for an hour with an honest thermal report. - No personal accounts; token auth; LAN only. - A hollowed device with no accounts, no Play Services and no gallery leaves provisioning with its wallpaper, dark mode and screen timeout set by the app alone. ## Open questions - The exact QVAC Kotlin SDK calls for completion, streaming, and unload — verify before committing to the app's design. - Which OEMs kill the foreground service anyway, and what setting is required on each. - Registration protocol: plain HTTP to the ship, or mDNS discovery plus HTTP. - One model per instance, or a model switcher with memory preflight. - TLS on the LAN, or a reverse proxy, or trusted-LAN-only. - Whether `/v1/embeddings` is worth exposing for small RAG work. - How to update the app and model without USB once the device is deployed.