Phone Awareness¶
Phone Awareness gives Kwasi visibility into the attention layer of Lawrence's day — where screen time goes — so its picture of the day is grounded in what actually happened rather than only what was typed into Telegram.
Sensory-only by design
Kwasi reads phone signals. It does not send notifications to the phone, dismiss them, open apps, or actuate anything.
Shipped 2026-07-07 with two halves: notification capture and app-usage capture. The notification half was removed on 2026-08-22 — see Why notification ingest was removed. What remains is the usage half, which is where the signal was.
The feature is inert unless PHONE_INGEST_SECRET is set — the endpoint returns 503 and nothing is stored.
Shape¶
It reuses the health-ingest shape end to end: a sideloaded Android app pushes batches to a token-gated FastAPI endpoint, and there is no server-side polling loop for collection.
flowchart TD
subgraph Phone["Android phone (bridge-android/)"]
US["UsageStatsManager\ndaily poll"]
SW["PhoneSyncWorker\nWorkManager"]
US --> SW
end
SW -->|"X-Phone-Secret\nPOST /phone/usage/ingest"| API["app/routers/phone.py"]
API --> DB[("app_usage_daily")]
DB --> TOOLS["get_app_usage_summary\nget_attention_snapshot"]
DB --> DIGEST["_phone_digest_loop"]
DB --> REFLECT["Reflection Engine\nattention + rhythm block"]
Why notification ingest was removed¶
The notification half ran in learning mode — a firehose to a diagnostic table — from 2026-08-07 to 2026-08-22, precisely so the whitelist could be tuned against real data before going live. The data answered a different question than the one it was collected for.
6,762 rows over 16 days decomposed as:
| Bucket | Rows | Share |
|---|---|---|
android — a single repeating "Data warning" |
5,628 | 83.2% |
| Calendar events | 321 | 4.7% |
| Media transport controls (YouTube Music) | 216 | 3.2% |
| WhatsApp + Telegram "messages" | 244 | 3.6% |
| 31 other apps | ~350 | 5.2% |
Two findings made the whitelist question moot:
- 83% of the corpus was one notification. All 5,628
androidrows carried the identical title "Data warning" — a mobile-data-limit warning reposting ~352×/day. A phone setting, not a whitelist problem. - The messaging stream carried counters, not content. All 159 WhatsApp rows had one distinct title, and every body matched
^[0-9]+ (new )?messageat an average of 25 characters. All 83 Telegram rows had one distinct title (empty) and two distinct bodies. Thesendercolumn read"WhatsApp"for every WhatsApp row and was empty for every Telegram row.
The extraction code was not at fault: KwasiNotificationListener already dropped FLAG_GROUP_SUMMARY before anything else, so those rows were the per-conversation child notifications, and they still carried only counters. That is the signature of message previews being disabled at the source, in which case there is nothing on the wire to read.
Consequences recorded so the reasoning is not re-litigated:
Whitelist.senderswas dead against real data. A rule like{"package":"com.whatsapp","senders":["Mohsin"]}could never match, becausesenderwas the app name.- Calendar notifications duplicated a source Kwasi already reads live via the Google and Outlook Calendar APIs.
- The remaining signal — interruption frequency without content — did not justify the surface: a
NotificationListenerService, an on-ingest mini-model classifier, two tables with TTL pruning, a server-side redaction backstop, and a whitelist UI.
What was not the reason: privacy, cost, or the Bridge failing. The pipeline worked exactly as designed and delivered 6,762 rows of noise.
If notification signal is ever wanted again, the thing worth capturing is dwell time — onNotificationRemoved was never implemented, so nothing recorded how long a notification sat before being cleared. That is an attention signal that needs no message content at all.
Storage¶
One table. See Storage Layer for full column listings.
| Table | Retention | Key |
|---|---|---|
app_usage_daily |
indefinite | unique (date, package, device) |
app_usage_daily stores one row per app per day, with the day-level screen_unlocks, total_foreground_ms, and timezone denormalised onto every row for that date. That keeps usage in a single table with no summary side-table; reads take MAX(screen_unlocks) for a date.
The dropped tables still exist in production
phone_events (0 rows) and phone_events_diagnostic (6,762 rows) are no longer created or written, but were not dropped from the production database — the same call made when proposed_skills was retired. The rows are a record of what happened, and dropping them gains nothing.
What Kwasi does with it¶
Two read tools, registered in _UTILITY_FNS so every domain agent has them — "what pulled on me today?" and "my screen time" arrive from any domain, not a phone-flavoured one:
| Tool | Returns |
|---|---|
get_app_usage_summary(days=7) |
Total screen time, unlock count, per-app breakdown |
get_attention_snapshot() |
One-shot last-24h view — screen time and top apps |
Both are read-only and fail soft: a storage error returns a short string rather than raising, because the phone feature must never break a normal turn.
Nightly digest — _phone_digest_loop posts a deterministic (no LLM) summary of yesterday at PHONE_DIGEST_TIME local. See Background Loops → Phone Digest Loop.
Reflection — the Reflection Engine appends an attention + rhythm block built from get_attention_snapshot() + get_app_usage_summary(days=7).
Why the block goes into the conversations slot
It is appended to the existing conversations placeholder rather than added as a new {placeholder} in the reflection prompt. The reflection prompt is Langfuse-managed with a code fallback — a new placeholder that the managed version doesn't carry would be silently dropped. The profile's "Patterns & Rhythms" section absorbs the signal naturally. Fail-safe: empty string on any error.
Configuration¶
| Env var | Default | Effect |
|---|---|---|
PHONE_INGEST_SECRET |
— | Gates the whole feature. Unset → endpoint 503, nothing stored |
PHONE_DIGEST_ENABLED |
true |
Nightly digest loop |
PHONE_DIGEST_TIME |
21:30 |
Local time for the digest |
The Bridge side¶
The same sideloaded app that carries the Health Connect reader (bridge-android/):
UsageCollector— a dailyUsageStatsManagerpoll.PhoneSyncWorker— WorkManager job that flushes to the server.PhoneIngestClient— posts withX-Phone-Secret.- A four-tab Compose UI (Status / Phone / Health / Setup) with a pause switch and permission-health cards.
Setup friction (Galaxy S26 / Android 16 / One UI 8): the app must be added to Samsung's "Never sleeping apps" or the WorkManager job is throttled. Usage access is granted under Special app access → Usage data access.
A Bridge older than 2026-08-22 will get 404s
The notification endpoint was removed, not disabled. An un-updated Bridge posting to /phone/notifications/ingest receives a 404; its usage sync is unaffected. Rebuild and sideload to stop the failed posts.
Privacy posture¶
- The feature is inert without
PHONE_INGEST_SECRET. - Only aggregate usage is stored — no notification content is captured at all since 2026-08-22.
- A pause toggle in the Bridge UI stops capture at the source.
db_querycannot read the tables that hold credentials (vault_credentials,context) — see Raw Database Ops.