From 238057ad5e9bb31f57f3e8c36696011398e6dfa9 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Mon, 7 Sep 2026 16:21:23 -0400 Subject: [PATCH] docs: the phone-logging decision, how to use it, and two build-apk traps DECISIONS.md gets the route and both rejected alternatives with what each would have cost; RUST.md gets a "Phone logging" section with the build command, where to read it on the phone, the end-to-end verification, and the two rig traps that cost an hour -- Gradle's merged-native-libs cache surviving build-apk.sh's `rm -rf jniLibs` (a --abi x86_64 APK packaged arm64 and aborted with what reads exactly like a Vulkan fault), and the 648 MB debug bench APK that cannot be installed at all. IRIS.md gets the client-core logging API with a before/after. Queue item ticked. Co-Authored-By: Claude Fable 5.1 --- docs/DECISIONS.md | 51 ++++++++++++++++++++++ docs/IRIS.md | 37 ++++++++++++++++ docs/RUST.md | 106 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 189 insertions(+), 5 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index cd5dfb0..b8ac0db 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -39,6 +39,57 @@ marked **DEFERRED** are ones the agent chose not to decide alone. why a fix (an OEM-specific named-family pin, or patching around fontique) is left as a follow-up rather than done in this pass. +## 2026-09-07 (how a phone log reaches Iris) + +- **The app sends its own log to `ai-server`, and Dev Updater shows it as + `ai-server`'s runtime log.** Iris has no `adb`/`logcat` on her phone, and + Android forbids one app reading another's logcat, so the app has to carry + its own copy and post it somewhere. `POST /client-log` on `ai-server` + re-emits each line into that server's own `tracing` output; Dev Updater + already runs `ai-server` as a `Managed` component, whose stdout its own + service script redirects to a file and reports through + `GET /apps/{key}/components/{name}/logs?kind=runtime`, which the phone + app's log dialog already offers as a **Runtime** tab for a `server` + component. So **no change to Dev Updater at all** -- one route on + `ai-server`, and the client in `client-core`. + + **Rejected: posting to Dev Updater's own server** (the first candidate). + It would need a new authenticated *write* route on a TLS surface whose + module doc says every route on it "is, or decides, the bytes that get + handed to `REQUEST_INSTALL_PACKAGES` next"; a per-app device-log store; + a change to `component_logs` so an APK component can have a runtime log; + a change to the phone app's `hasBothKinds = component.kind == "server"` + gate and to what `hasRuntimeLogs` means on the wire; and -- the real + cost -- a **second** enrollment for the iris app, since it has no CA or + token for Dev Updater and Dev Updater mints tokens per device by QR. + Five changes across two repos against one route, for the same line + landing in the same viewer. + + **Rejected: a share intent from a debug button** (a log file in the app's + external files dir, shared by hand). It works today and needs no server, + but every line costs Iris a manual export and a message, which is the + round trip through a person this was meant to remove. It is still the + fallback when the tunnel is down, and GrapheneOS's own per-app log export + already covers the crash case (that is how the `ToolInput.highlighted` + crash was reported). + +- **The ring is in `client-core`, not in the Android crate.** A bounded + in-memory ring (2000 lines or 256 KiB, whichever bites first) behind a + `log::Log` backend that *forwards* to whichever logger the platform + already installed, so `logcat` and a desktop terminal see exactly what + they saw before. The platform supplies only its own logger and its + destination. `Copy report` appends the ring to what goes on the + clipboard, and flushes the uploader first. + +- **The destination is baked in at build time, from the build machine's + own files** (`AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` plus the pinned CA) -- + all three or none, never two. The same trust boundary the transcript + config and the Compose APK's CA already use: nothing secret is + committed, and an APK is good for the server that built it. A build told + nothing still keeps its ring and still copies it; the diagnostics pane + says which of "not tried yet", "failing -- " and "no server + configured" it is, because otherwise all three look like silence. + ## 2026-09-06 (how a tool call looks, P1b) - **A card that never got a result says "no result", in yellow, and it is diff --git a/docs/IRIS.md b/docs/IRIS.md index f3eab5d..7b02b0e 100644 --- a/docs/IRIS.md +++ b/docs/IRIS.md @@ -8,6 +8,43 @@ capability that moved. Small and trivial changes do not go here. An entry gives the date, what changed, why, and a short before/after where it helps judge the change without the session that made it. Newest first. +## 2026-09-07: `client-core` carries the app's own log + +Not iris itself but the crate beside it, and it is a new public surface an +app author will use: `client_core::log_ring` and `client_core::log_upload`. +Because Iris's phone has no `logcat`, an app now keeps a bounded copy of +its own log and can send it to `ai-server`, where Dev Updater already +shows it. + +Before, an app installed a platform logger and that was the end of it: + + android_logger::init_once(config); // Android + // nothing at all on the desktop + +After, the platform's logger becomes the *inner* logger of a ring that +records everything alongside it -- `logcat` and a terminal see exactly +what they saw before: + + client_core::log_ring::install_process_logger( + Box::new(android_logger::AndroidLogger::new(config)), + LevelFilter::Debug, + )?; + let ring = client_core::log_ring::process_ring(); // 2000 lines / 256 KiB + ring.to_text(); // for a report + ring.summary(); // "1801 lines held, 12 dropped, last 20:09:24" + + // and, where the app has a server: + let upload = LogUpload::spawn(ring.clone(), transport, "iris-bench", Duration::from_secs(10)); + upload.flush_now(); // what `Copy report` calls + upload.status().summary(); // "not tried yet" / "failing -- " / "N lines sent" + +`process_ring` is a deliberate process-global, unusually for this project: +`log` already has exactly one backend per process, and a ring passed around +as a parameter would be a second answer to "which lines exist". Dropping +the `LogUpload` handle stops and joins its thread. The wire format is one +new route on `ai-server`, `POST /client-log`; the reasoning and the +rejected alternatives are in docs/DECISIONS.md, 2026-09-07. + ## 2026-09-07: `TextData` no longer bundles a font Iris's call: "remove the font for now; just match what compose does." diff --git a/docs/RUST.md b/docs/RUST.md index db6e13c..1d16949 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -89,6 +89,105 @@ Three things this says about the rig, since the rig is new: claimed of it -- asserted at the end of every draw, and the test checks both directions. +### Phone logging, 2026-09-07 (built and verified end to end) + +**The problem**: Iris tests these builds on a phone with no `adb`, and +Android forbids one app reading another's `logcat`, so a `log::info!` in +the iris app could not reach her at all. What she asked for was Dev +Updater, which she already reads. + +**The route, in one line**: the app keeps its own bounded log ring, posts +it to `ai-server`, and `ai-server` re-emits each line into its own +`tracing` output -- which Dev Updater *already* shows as that component's +**runtime log**, because it runs `ai-server` as a `Managed` service and +that service's script redirects stdout to +`$XDG_DATA_HOME/dev-updater/services/-/<...>.log` and +reports the path. **Nothing in dev-updater changed.** The alternatives and +what each would have cost are in docs/DECISIONS.md. + +What exists now: + +- `client_core::log_ring` -- `LogRing` (2000 lines / 256 KiB, whichever + bites first, with `dropped` reported rather than inferred), `RingLogger` + (a `log::Log` backend that records *and* forwards to the platform's own + logger), and `install_process_logger`. Reading does not consume, so the + report and the uploader are two readers of one ring. +- `client_core::log_upload` -- `LogUploader::flush_once` (batched at 500 + lines, retried from the same cursor on failure) and `LogUpload::spawn` + (a thread flushing every 10s, stopped by dropping the handle). + **Nothing in it calls `log!`**: those lines would land in the ring it is + draining. +- `POST /client-log` on `ai-server` -- `{source, lines:[{seq, at, level, + target, message}]}`, re-emitted at the client's own level under the + target **`ai_server::client_log`**. Under the crate's path deliberately: + a bare `client_log` target is dropped by `RUST_LOG=ai_server=debug`, the + filter AGENTS.md tells people to run with, so every line a phone sent + vanished with nothing saying so. Found by running it. +- `iris/android-app/src/app_log.rs` -- the platform half only: + `android_logger` as the logger to forward to, and the destination baked + in by `build.rs` from `AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` plus the pinned + CA (all three or none). `Copy report` appends the ring to the clipboard + text and flushes the uploader first; `Diagnostics` gains two lines -- + how many lines are held and when the last arrived, and what the uploader + last did. + +**How to use it.** Build the APK with the destination in the environment, +on the machine `ai-server` runs on: + + AI_APP_LOG_HOST=10.66.0.1 AI_APP_LOG_PORT=8443 \ + AI_APP_LOG_TOKEN= \ + ./build-apk.sh release + +Then read it on the phone: Dev Updater -> the ai-app project -> the +**server** component's log button -> the **Runtime** tab. The app's lines +are the ones tagged `ai_server::client_log`, each carrying `[ + #]` before the target and message. A build with +none of those variables set still keeps its ring and still puts it on the +clipboard from `Copy report`; the Diagnostics pane says so in as many +words. + +**Verified 2026-09-07, all four hops.** ai-server on 127.0.0.1:8455 with a +scratch config; the bench APK built `--abi x86_64` with +`AI_APP_LOG_HOST=10.0.2.2 AI_APP_LOG_PORT=8455 AI_APP_LOG_TOKEN=...`; +installed and launched on this checkout's emulator. Twenty seconds later +the server's log held + + INFO ai_server::client_log: [iris-bench 20:17:02.284 #0] + iris::android::view: iris: new_peer content_scale=2.625 + +and a scratch `dev-updater` (port 8492, `XDG_DATA_HOME=/tmp/du-test/data`) +with this checkout registered served exactly those lines back from +`GET /apps/ai-app-2/components/server/logs?kind=runtime` -- which is the +JSON the phone's Runtime tab renders. + +**Two rig traps this cost an hour to find, both in `build-apk.sh`, both +still there.** Written down rather than fixed because fixing them belongs +with whoever next touches that script: + +1. **Gradle's merged-native-libs cache survives `rm -rf jniLibs`.** The + script removes `app/src/main/jniLibs` before each build (its own + comment says why), but Gradle's `mergeReleaseNativeLibs` is *up to + date* against its cached inputs, so a build that switches ABI packages + the previous ABI. A `--abi x86_64` release APK contained + `lib/arm64-v8a/libmain.so`, installed fine, and aborted at startup with + `Could not get adapter!: NotFound { active_backends: VULKAN }` under + `libndk_translation` -- which reads exactly like the phone's own Vulkan + problem and is nothing of the kind. `rm -rf app/build/intermediates` + before the build is the workaround; check with + `python3 -c "import zipfile; print([i.filename for i in + zipfile.ZipFile('...apk').infolist() if i.filename.endswith('.so')])"`. +2. **The debug bench APK is 648 MB and will not install** + (`INSTALL_PARSE_FAILED_NOT_APK`): the debug `libmain.so` is 325 MB. + Use `release` on the emulator for this app, notwithstanding the general + rule that the emulator stays on debug -- there is nothing to measure + here, and the debug build cannot be installed at all. + +Also: the bench APK's package is `dev.iris.android.demo.bench`, not +`dev.iris.android.demo`. An older non-bench build left installed answers +to the second name, runs, looks right, and reports whatever *it* was built +with -- which is how "log upload: this build has no server configured" +came from a build that had one. + ### APK size (2026-09-07) Iris's question: the iris bench APK is about double the Compose bench APK @@ -366,11 +465,8 @@ closes it. drawn through the header in the other (docs/IRIS_TODO.md, 2026-09-07). Done 2026-09-07, e922b73 + d507ae4; the root causes and the test names are in that IRIS_TODO entry, and the short version is below. -- [ ] Phone logging through Dev Updater (Iris has no logcat; see - docs/TODO.md and the memory note): research how Dev Updater shows an - app's runtime log, design the smallest route (the app keeps its own - recent log; a debug button copies it; Dev Updater reads it), write - the decision in docs/DECISIONS.md, build it. +- [x] Phone logging through Dev Updater -- **done 2026-09-07**, see + "Phone logging" above and docs/DECISIONS.md's entry of that date. - [x] APK size: release profile tuned (`42af780`), -35% APK, -40% .so; see "APK size (2026-09-07)". **Iris's verdict, 2026-09-07: "remove the font for now; just match what compose does."** Done same day -- the six