The app hands its log to Dev Updater on the phone, not through ai-server
Iris's call once the upload route was working: put it in Dev Updater properly. So the app now exposes its own ring through a ContentProvider at `<applicationId>.devlog` -- Dev Updater's contract, written down in that project's README, not something invented here -- and Dev Updater's phone app reads it on the same device and forwards it to its own build machine. No tunnel, no token, no second enrolment, and any app that server delivers can implement the same and get the same Runtime tab. `DevLogProvider.java` plus `devlog.rs` are the platform glue only: a flat `String[]` across JNI, a `MatrixCursor` on the Java side, and `nativeReady` telling Rust the authority the provider actually registered, so the Diagnostics pane can name somewhere a reader can query rather than composing a guess. `LogRing::newest_seq()` is the one addition in `client-core`: an in-memory ring starts again at zero, so it is what lets a reader notice the process restarted instead of silently skipping everything since. Deleted with it, so there is one mechanism: `client_core::log_upload`, `POST /client-log` on ai-server, the `AI_APP_LOG_*` baking (which left `build.rs` with nothing to do), and the uploader on both Android clients. Kept: the ring, `RingLogger`, `install_process_logger`, and the Diagnostics line -- whose second half is now `devlog provider: content://<authority>`. Verified end to end on this checkout's emulator: iris's own `iris::android::view` startup lines read out of the provider by the shell, forwarded by Dev Updater's Runtime tab, and served back from `GET /apps/android-app/components/app/logs?kind=runtime`. A component whose package has no provider says so in as many words. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e10582a2cd
commit
06b8a1f4b0
15 files changed
+618
-842
No files matched your search
+61
-2
@@ -46,7 +46,61 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
unaffected (still resolves monospace correctly, confirmed unchanged).
|
||||
docs/RUST.md's "Platform fonts (2026-09-07)" has the full account.
|
||||
|
||||
## 2026-09-07 (how a phone log reaches Iris)
|
||||
## 2026-09-07 (a phone log reaches Iris through Dev Updater's own tab)
|
||||
|
||||
**Supersedes the "how a phone log reaches Iris" entry below, same day.**
|
||||
Iris's call once the route was working: put it in Dev Updater properly
|
||||
rather than smuggling the lines through `ai-server`'s log.
|
||||
|
||||
- **The app exposes its own log on the device, and Dev Updater reads it
|
||||
there.** A `ContentProvider` at `<applicationId>.devlog`, one table of
|
||||
lines queried with `?since=<seq>` so a poll is incremental, plus a
|
||||
`status` row (`held`, `dropped`, `newest_seq`). Dev Updater's phone app
|
||||
polls it while the component's **Runtime** tab is open and forwards what
|
||||
is new to its own build machine, into that APK component's runtime log
|
||||
-- so the same tab renders both kinds and the history outlives the
|
||||
phone. No tunnel, no token, no second enrolment: the two apps are on the
|
||||
same phone.
|
||||
|
||||
**It is a contract, not a feature for iris.** Written down in
|
||||
dev-updater's `README.md` ("An app's own log"), so any app that server
|
||||
delivers gets the tab by implementing it; the Compose app in `app/` can
|
||||
do the same later. That is the reason it beat the route below on its
|
||||
second look -- the earlier one only ever worked for the one project that
|
||||
had a server, and put a phone's lines under a *different component* than
|
||||
the one they came from.
|
||||
|
||||
- **Read access is `protectionLevel="normal"`, and that is a real trade.**
|
||||
`signature` is what this wants and is not available: Dev Updater and the
|
||||
apps it delivers are built on one machine but signed with different
|
||||
locally generated keys, so a signature permission would be held by
|
||||
nothing at all. What `normal` costs is that any app on that phone which
|
||||
requests `dev.updater.permission.READ_DEVLOG` by name can read another
|
||||
app's dev log. Accepted because these are development builds on a
|
||||
development phone and the alternative was no log; stated in the manifest
|
||||
beside the declaration and in dev-updater's README so it is not
|
||||
rediscovered as a surprise.
|
||||
|
||||
- **The provider polls rather than notifying.** `notifyChange` was not
|
||||
implemented: the ring is filled by a `log::Log` backend on whatever
|
||||
thread logged, and giving that a route to a `ContentProvider` means
|
||||
plumbing a callback through `client-core` for every platform. Dev
|
||||
Updater's contract therefore says it polls (about a second, only while
|
||||
the tab is open), which is what keeps implementing the contract cheap --
|
||||
a provider that does notify loses nothing.
|
||||
|
||||
- **What was deleted, so there is one mechanism**: `client-core`'s
|
||||
`log_upload` module, `POST /client-log` on `ai-server`, the
|
||||
`AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` baking in `iris/android-app/build.rs`
|
||||
(which left that file with nothing to do, so it is gone too), and the
|
||||
uploader fields on both Android clients. Kept: the ring, `RingLogger`,
|
||||
`install_process_logger`, and the Diagnostics line counting what is
|
||||
held. The upload-status line there is now **"devlog provider:
|
||||
content://<authority>"** -- named from what the provider registered
|
||||
rather than composed from the package here, so a screenshot of that pane
|
||||
is evidence the contract is live and says which package's log it is.
|
||||
|
||||
## 2026-09-07 (how a phone log reaches Iris) -- superseded, see above
|
||||
|
||||
- **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
|
||||
@@ -60,7 +114,9 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
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).
|
||||
**Rejected: posting to Dev Updater's own server** (the first candidate,
|
||||
and what the entry above went on to build -- the estimate below was
|
||||
right about the work and wrong about it being too much).
|
||||
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;
|
||||
@@ -90,6 +146,9 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
|
||||
- **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) --
|
||||
*gone; the provider above replaced it.* What is worth keeping from it is
|
||||
the reason it went: an APK good only for the server that built it cannot
|
||||
be built in this VM for Iris's phone, which is the case that mattered.
|
||||
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
|
||||
|
||||
+19
-12
@@ -46,10 +46,9 @@ scrolling never consults the one in `ViewConfiguration`.
|
||||
## 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.
|
||||
app author will use: `client_core::log_ring`. Because Iris's phone has no
|
||||
`logcat`, an app now keeps a bounded copy of its own log and hands it to
|
||||
Dev Updater on the device.
|
||||
|
||||
Before, an app installed a platform logger and that was the end of it:
|
||||
|
||||
@@ -68,17 +67,25 @@ what they saw before:
|
||||
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"
|
||||
// and, for whatever hands the log out of the process:
|
||||
let (lines, next) = ring.since(cursor); // inclusive of `cursor`
|
||||
ring.newest_seq(); // None for a ring nothing was written to
|
||||
|
||||
`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.
|
||||
as a parameter would be a second answer to "which lines exist".
|
||||
|
||||
**Amended later the same day.** `client_core::log_upload` and
|
||||
`ai-server`'s `POST /client-log` are **gone** -- an app no longer sends
|
||||
its log anywhere. It exposes it on the device instead, and Dev Updater
|
||||
reads it there: on Android that is a `ContentProvider` at
|
||||
`<applicationId>.devlog`, which is Dev Updater's own contract (its
|
||||
`README.md`, "An app's own log") rather than anything iris-specific.
|
||||
`LogRing::newest_seq()` is the one addition that went with it: a reader
|
||||
holding a cursor uses it to notice the process **restarted**, since the
|
||||
ring is in memory and a new process starts again at sequence zero.
|
||||
The reasoning and the rejected alternatives are in docs/DECISIONS.md,
|
||||
2026-09-07.
|
||||
|
||||
## 2026-09-07: `TextData` no longer bundles a font
|
||||
|
||||
|
||||
+76
-71
@@ -89,83 +89,77 @@ 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)
|
||||
### Phone logging, 2026-09-07 (rebuilt on Dev Updater's own tab)
|
||||
|
||||
**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.
|
||||
**The route, in one line**: the app keeps its own bounded log ring and
|
||||
**exposes it on the device** through a `ContentProvider`; Dev Updater's
|
||||
phone app -- on the same phone -- reads that while the component's
|
||||
**Runtime** tab is open and forwards what is new to its own build machine,
|
||||
into that APK component's runtime log. No tunnel, no token, no second
|
||||
enrolment.
|
||||
|
||||
**It is Dev Updater's contract, not iris's feature.** Written down in
|
||||
dev-updater's `README.md` under "An app's own log", so any app that server
|
||||
delivers gets the tab by implementing it. The shape:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| authority | `<applicationId>.devlog` |
|
||||
| read permission | `dev.updater.permission.READ_DEVLOG` (`android:readPermission`) |
|
||||
| `lines?since=<seq>` | held lines with `seq >= since`, ascending: `seq, t_ms, level, target, message` |
|
||||
| `status` | one row: `held, dropped, newest_seq` |
|
||||
|
||||
`newest_seq` is `-1` for an empty ring and is what makes a **restart**
|
||||
visible: the ring is in memory, so a new process starts again at zero and
|
||||
a reader holding a cursor would otherwise skip everything since, silently.
|
||||
`insert`/`update`/`delete` throw. `notifyChange` is **not** implemented --
|
||||
the ring is filled by a `log::Log` backend on whatever thread logged, and
|
||||
routing that to a provider means a callback through `client-core` for
|
||||
every platform, so Dev Updater polls (about a second, only while the tab
|
||||
is open) and the contract says so.
|
||||
|
||||
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.
|
||||
bites first, with `dropped` reported rather than inferred), `since(seq)`,
|
||||
`newest_seq()`, `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 provider are two readers
|
||||
of one ring.
|
||||
- `iris/android-app/src/devlog.rs` and
|
||||
`app/src/main/java/dev/iris/android/demo/DevLogProvider.java` -- the
|
||||
platform glue only (the sharing rule): a `String[]` across JNI, a
|
||||
`MatrixCursor` on the Java side, and `nativeReady` telling Rust the
|
||||
authority the provider actually registered under.
|
||||
- `iris/android-app/src/app_log.rs` -- `android_logger` as the logger to
|
||||
forward to, and the Diagnostics pane's two lines: how many lines are
|
||||
held, and **`devlog provider: content://<authority>`** (or "declared,
|
||||
not created yet", since Android creates a provider lazily). Named from
|
||||
what the provider registered rather than composed from the package here,
|
||||
so a screenshot of that pane is evidence the contract is live.
|
||||
- On the Dev Updater side (its own repo, commit `013d711`): the tab for
|
||||
every component rather than only a server's, `DevLog.kt`'s reader and
|
||||
cursor, `POST /apps/{key}/components/{name}/runtime-log`, and
|
||||
`$XDG_DATA_HOME/dev-updater/devlogs/<key>-<component>.log` with one
|
||||
rotation at 4 MiB.
|
||||
|
||||
**Superseded 2026-09-07, and left standing rather than edited**: Dev
|
||||
Updater is growing an on-device runtime-log reader, so this whole route --
|
||||
`log_upload`, `POST /client-log` and the `AI_APP_LOG_*` baking -- is being
|
||||
removed rather than kept current. The app's *server* destination no longer
|
||||
comes from any of it: that is the enrolment link (the queue item above).
|
||||
What follows describes the route as built.
|
||||
**Deleted with it, so there is one mechanism**: `client-core`'s
|
||||
`log_upload`, `POST /client-log` on `ai-server`, the `AI_APP_LOG_*` baking
|
||||
in `iris/android-app/build.rs` (which left that file with nothing to do,
|
||||
so it is gone), and the uploader fields on both Android clients.
|
||||
|
||||
**How to use it.** Build the APK with the destination in the environment,
|
||||
on the machine `ai-server` runs on:
|
||||
**How to use it.** Build and install the bench APK; open Dev Updater ->
|
||||
the ai-app project -> the **app** component's log button -> the
|
||||
**Runtime** tab. Nothing to configure: the tab finds the provider from the
|
||||
package the component installs. A component whose package exposes none
|
||||
says so in as many words, which is a different sentence from an empty log.
|
||||
|
||||
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.
|
||||
**Verified 2026-09-07 end to end** -- see "Verified" below.
|
||||
|
||||
**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
|
||||
@@ -193,7 +187,9 @@ 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.
|
||||
came from a build that had one. The devlog authority carries the
|
||||
`applicationId` for exactly that reason: the two packages each get their
|
||||
own and neither can read the other's log.
|
||||
|
||||
### `iris::input`/`iris::frame` diagnostics, 2026-09-07
|
||||
|
||||
@@ -637,8 +633,16 @@ closes it.
|
||||
- [ ] Scroll clamped at both ends, and Compose's impulse velocity
|
||||
estimator with min/max fling velocity (docs/IRIS_TODO.md, 2026-09-07
|
||||
later). After the culling fix lands (same file).
|
||||
- [ ] **APK runtime logs in Dev Updater (Iris, 2026-09-07: "please add
|
||||
android / apk runtime log support to dev updater").** Supersedes the
|
||||
- [x] **APK runtime logs in Dev Updater (Iris, 2026-09-07: "please add
|
||||
android / apk runtime log support to dev updater").** **Done
|
||||
2026-09-07** -- dev-updater `013d711`, and this repo's provider half;
|
||||
"Phone logging" above is the account, docs/DECISIONS.md the decision.
|
||||
What was designed and what was built agree except in one place: the
|
||||
provider does **not** call `notifyChange` (the reason is in both), and
|
||||
the tab is drawn for every component rather than only where a provider
|
||||
resolves, since its absence would be the one thing that could not say
|
||||
which of the several reasons there was nothing to read. The design as
|
||||
written: Supersedes the
|
||||
ai-server `POST /client-log` route, which becomes the second mechanism
|
||||
and is deleted once this works (`log_upload.rs`, `app_log.rs`'s
|
||||
upload half, the route). Design: Android forbids reading another
|
||||
@@ -684,10 +688,11 @@ closes it.
|
||||
Dev Updater needed no change: its Enroll button already opens the minted
|
||||
link with `ACTION_VIEW`, and Android offers the chooser between this app
|
||||
and the Compose one.
|
||||
**The log upload was deliberately left out of it**: Dev Updater is
|
||||
growing an on-device runtime-log reader instead (Iris, 2026-09-07), so
|
||||
`log_upload`, `POST /client-log` and the `AI_APP_LOG_*` baking are on
|
||||
their way out whole rather than being rewired first.
|
||||
**The log upload was deliberately left out of it**: Dev Updater grew an
|
||||
on-device runtime-log reader instead (Iris, 2026-09-07), so
|
||||
`log_upload`, `POST /client-log` and the `AI_APP_LOG_*` baking went out
|
||||
whole rather than being rewired first -- done the same day, "Phone
|
||||
logging" above. `build.rs` had nothing left to do and is gone with them.
|
||||
- [ ] `iris/android-app/build-apk.sh`: clear Gradle's merged-native-libs
|
||||
cache when the ABI changes (the x86_64 trap), and make the debug bench
|
||||
APK installable (648 MB) -- RUST.md's logging section names both.
|
||||
|
||||
Reference in new issue
Block a user