From 135950c8edc41f32ca461c7f3defa1058d3a9d9b Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 29 Aug 2026 23:43:19 -0400 Subject: [PATCH] Say when a session wants you, and stop calling a stop an error Three things Bryan asked for, and one the second of them exposed. **Notifications.** A session that asks a question or finishes a turn now says so on the phone, per session, switchable from its settings screen. The switch is stored on the backend rather than the phone, because it is a fact about the session: one that runs unattended overnight should be quiet on every device, and answering that question again on each device is how two of them come to disagree. It is on by default -- a notification nobody wanted is turned off in one tap, where one that never arrived is not diagnosable at all. Which moments count is `notification_for`, and the asymmetry in it is the point. *Waiting on a person* is worth saying however it was reached. *Finished* is only worth saying when this server watched the work happen: sessions settle into idle for several reasons that are not "your work ended", including every one of them being adopted at startup, and announcing those would put "finished" on the phone for the whole config on every backend restart. That is the failure that makes somebody switch the feature off, so it has a test naming every transition rather than the two that work. The stream is `GET /notifications`, live only and with no cursor -- the one place this server does not offer to catch a client up. A notification is a claim about now; replaying "your turn" from an hour ago sends somebody to a session that may have been answered from another device since, and a notification that is wrong costs the trip *and* the credibility of the next one. What was missed is still on the session list, which says what is waiting without claiming to be news. On the phone it is a foreground service, because Android has had no long-lived background service since 8.0 -- it is what Syncthing does, and Discord is not a counter-example since it takes a push from Google, which would mean this backend talking to Google about somebody's sessions. The ongoing notification Android charges for it sits on an `IMPORTANCE_MIN` channel: no sound, no status-bar icon, bottom of the shade. `specialUse` rather than `dataSync`, which is what it looks like: Android 15 caps dataSync at six hours a day, and a connection that stops listening after six hours misses the overnight run it exists for. **A stop is not an error.** The CLI reports an interrupted turn exactly as it reports a broken one -- `is_error` on a `result` -- so pressing Stop showed "the turn ended with an error" for doing what the button says. The line cannot distinguish them; what does is that this side asked, so the driver says so before the request goes out and the translator spends that on the next result. The test's second half is the one that matters: the naive fix passes the first half and silences every genuine failure after it. **Every status says which one it is.** The session screen's status row named only `exited` and left the rest blank, so idle and "nobody could read it" looked identical -- and a just-stopped turn showed nothing, which reads as the app having lost the session rather than as the stop having worked. The words are the session list's own, so a state is not called two things depending which screen you are on. Red on a quota bar now starts at 90%. **`GET /sessions/{id}`**, which the notification switch found missing. A screen opened from a list row carries the row the list last fetched: fine for a title, wrong for a switch, which is *set to* something. Caught on the emulator, where the switch read on against a backend that said off, with nothing on screen to say which was true. The screen now reads the session when it opens, and until that answers the switch is disabled and says so -- a two-position control cannot say "I do not know", so it does not pretend to. Verified on the emulator with the app backgrounded: the service holds the stream (`isForeground=true types=0x40000000`), a finished turn posts "Finished" and a question replaces it with "Waiting for you" on the same tag, turning the switch off silences it with no restart, and turning it back on from the phone reaches config.ron. The interrupt is a translator test rather than a live turn, which is where that logic is anyway. --- app/androidApp/src/main/AndroidManifest.xml | 25 ++ .../src/main/kotlin/com/example/aiapp/Api.kt | 30 ++ .../kotlin/com/example/aiapp/MainActivity.kt | 21 ++ .../kotlin/com/example/aiapp/Notifications.kt | 273 ++++++++++++++++++ .../kotlin/com/example/aiapp/SessionScreen.kt | 29 +- .../example/aiapp/SessionSettingsScreen.kt | 74 ++++- .../main/kotlin/com/example/aiapp/Theme.kt | 2 +- server/Cargo.lock | 1 + server/Cargo.toml | 5 +- server/src/config.rs | 19 ++ server/src/routes.rs | 71 ++++- server/src/session/claude.rs | 4 + server/src/session/claude/translate.rs | 76 ++++- server/src/session/mod.rs | 239 ++++++++++++++- 14 files changed, 849 insertions(+), 20 deletions(-) create mode 100644 app/androidApp/src/main/kotlin/com/example/aiapp/Notifications.kt diff --git a/app/androidApp/src/main/AndroidManifest.xml b/app/androidApp/src/main/AndroidManifest.xml index 202e1e8..4ce4bd8 100644 --- a/app/androidApp/src/main/AndroidManifest.xml +++ b/app/androidApp/src/main/AndroidManifest.xml @@ -10,6 +10,14 @@ dev-updater's manifest for the full story. --> + + + + + + + + +