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 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 16:21:23 -04:00
1 parent 896c93a59a
commit 238057ad5e
3 files changed
+189 -5

No files matched your search

+51
View File
@@ -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 -- <why>" 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
+37
View File
@@ -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 -- <why>" / "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."
+101 -5
View File
@@ -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/<key>-<component>/<...>.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=<a token that server accepts> \
./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 `[<source>
<the app's own clock> #<seq>]` 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