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:
1 parent
896c93a59a
commit
238057ad5e
3 files changed
+189
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user