Changelog¶
For the canonical history, see the companion repo's commit log and the watch repo's commit log.
Companion v1.16.1 + Watch v1.5.1 — Wake-word & watch-code fixes¶
A round of fixes and small tweaks on top of the Home Assistant release:
- Custom wake words accept more than one again. The Custom trigger words field (Home Assistant and Ask Agent) silently dropped everything after the first line, so you could only ever set one wake word. Fixed — add one per line, or comma-separated.
- "Code display time" setting (Settings → ⌚ Watch). A voice-recalled code used to vanish after a few seconds when launched via Quick Launch; now you choose how long it stays (default 60 s, or Stay until back).
- Open your codes from a watch button. Settings → ⌚ Watch → shortcuts now offers Codes, so a long-press (or SELECT) can jump straight to your saved codes list — alongside Notes, Tasks, Alarms, and the chooser menu.
- Explicit dates no longer drift. Saying a specific date — "schedule dentist July 29th at 3 pm" — could land on the wrong day when that date had just passed; it's now honoured as spoken. Bare times ("remind me at 3 pm") still roll forward to the next occurrence as before.
Update both apps for the full set (the code-display timeout and the Codes shortcut need Watch v1.5.1).
Companion v1.16.0 — Control Home Assistant by voice¶
Point Wristotle at your self-hosted Home Assistant — base URL + a long-lived
access token under Settings → 🏠 Home Assistant — then speak from your watch:
"hey home assistant turn off the kitchen lights", or just "home assistant,
is the garage door open". The command text goes to HA's own
conversation agent
(/api/conversation/process), which runs its own natural-language
understanding and acts on your devices; its reply comes back on the watch. No LLM
and no cloud in the middle — it's your server, your token, your network (LAN /
Tailscale / self-signed http:// all work). Add custom wake words like
"jarvis" if you like, and enable a spoken reply under Settings → 🔊
Speech. Everything (URL, language, wake words, and — if you opt the secret in —
the token) rides along in Backup & Restore. Companion-only update.
Companion v1.15.1 — Cloud voice replies fixed¶
Speak-on-watch with a cloud or self-hosted TTS provider was silent with some servers. The cause: those endpoints stream a WAV whose length field is written as zero (the length isn't known when the header is sent), and the companion trusted that and decoded no audio. It now reads the audio that's actually there, so cloud / self-hosted text-to-speech speaks again. Local (on-device) TTS was unaffected. Companion-only update.
Companion v1.15.0 + Watch v1.5.0 — Save QR codes & barcodes on your watch¶
Keep your loyalty cards, membership barcodes, and QR codes on your wrist. Scan a card with the phone's camera (no Google Play Services needed) or type one in, and it syncs to the watch to show full-screen at the checkout — even offline. Works with QR, Code 128, EAN-13, and UPC-A. Give each code a name, then recall it by voice — "show my qr code" or "show my clubcard"; on the watch they also live under the menu (long-press UP → Codes). Edit a code's name, alias, or contents anytime, and everything (including the voice names) rides along in Backup & Restore. Update both apps for the full feature.
Companion v1.14.3 — Ask Agent works with slow local models¶
Settings → ✨ Ask Agent has a new Response timeout you can raise, so slow on-device models that "think" before answering (Gemma thinking, R1-style reasoning) aren't cut off mid-reply by the old fixed timeout. The default is unchanged, so nothing changes unless you raise it. While the model is working, the watch now shows "→ thinking…" and waits instead of erroring early.
Companion v1.14.2 + Watch v1.4.1 — Find My Phone false-positive fix¶
Find My Phone no longer triggers by accident. Dictating something that merely mentions your phone — "make a note to find my phone charger", "remind me to find my phone", or asking the agent a question about your phone — no longer rings it. It now only rings when you actually ask to find it. (Both the watch's local command matching and the phone's intent classifier were over-eager; both are fixed. Update both apps for the complete fix.)
Companion v1.14.1 — Calendar event fixes¶
- Spoken dates like "July twenty ninth" now set the right date and time — compound numbers ("twenty ninth") were being cut down to "twenty".
- You no longer need to say "called" to name an event: "create an event dentist appointment tomorrow at 3pm" now titles it "Dentist appointment".
Companion v1.14.0 + Watch v1.4.0 — Find my phone, but louder¶
"Find my phone" now rings from the phone at full alarm volume and vibrates, so you can hear it even when your media volume is muted — the old ringer was silent in that case. Tap, Stop, or swipe the notification to silence it (it also auto-stops after 30 s). Update both apps for this to be the default; on an older companion you can switch the find-phone source back to PebbleKit JS in your Pebble app's settings.
Sports data — FIFA World Cup 2026 (2026-06-22)¶
Added the FIFA World Cup 2026 — ask about any of the 48 national teams: "when does Argentina play next", "did Brazil win", "live score for France". No app update needed — it arrives via the daily sports-data refresh, or instantly from Settings → Sports → Update sports data. (Group standings aren't available yet; scores, fixtures, and results work.)
Companion v1.13.1 — Morning Brief backup fix¶
Fixes a bug where your Morning Brief section choices (new in v1.13.0) were lost on backup and restore. They're now saved and restored correctly.
Companion v1.13.0 — Choose what's in your Morning Brief¶
You can now pick which sections your morning brief includes — meetings, alarms, messages & notifications, reminders, tasks, and notes — in Settings → 🔔 Notifications → Morning Brief. Turn off what you don't care about; the brief skips reading that data entirely (so switching off messages also stops it reading your notification tray).
Watch v1.3.3 + Companion v1.12.2 — Richer bug-report diagnostics¶
The diagnostics bundle now includes your watch's model, firmware, and the Wristotle watch-app version, plus the Pebble companion app version, battery-optimisation and exact-alarm status, and device RAM — so problems are much faster to pin down. Update both apps for the full set.
Watch v1.3.2 — Dictation works again on the smaller watches¶
Fixes a memory regression introduced by the v1.3.0 cards update: on the 64 KB watches (Pebble 2, Pebble 2 Duo, Pebble Time, Pebble Time Round) the watch app had too little memory left to start a dictation session, so dictation failed with "Dictation unavailable." v1.3.2 frees up watch memory so dictation works again — no companion or Core Devices changes needed. Update the watch app.
Watch v1.3.1 + Companion v1.12.1 — Clearer dictation errors¶
When watch dictation can't start or comes back empty, the watch now tells you what to check — for example that voice dictation is off or not set up in your Pebble app — instead of always showing "Didn't catch that."
Update the watch app for the clearer messages; the companion update is optional.
Companion v1.12.0 + Watch v1.3.0 — Replies as cards on the watch¶
- Full-screen cards — replies now show as a card on the watch: a coloured band for the action (green create, red delete, blue lookup…) and an icon for the feature. Sports, weather, and meetings are on by default.
- Rich weather card — temperature, condition, humidity, and wind at a glance; "weather in Tokyo" or a bare "what's the weather" (which now grabs a fresh location when needed).
- Pick which replies become cards — Settings → Watch → Watch cards lets you switch cards on or off per type; reminders, tasks, notes, alarms, the agent, and the morning brief can all be enabled there.
Requires updating both the companion and the watch app.
Companion v1.11.1 — Reminder text no longer truncated¶
Fixes a bug where reminder, reschedule, and meeting titles were cut off when the wording contained "in" / "at" / "on" — "remind me to put in a Whole Foods order" kept only "Put". Now only a genuine trailing time is trimmed, so the rest of what you said is preserved (thanks to the reporter on Codeberg #14). No watch update needed.
Companion v1.11.0 — Big Bash cricket + self-updating sports data¶
- Big Bash League — cricket now covers Australia's BBL: "BBL standings", "did the Scorchers win".
- Sports data updates itself — leagues, team names, and behind-the-scenes fixes now refresh automatically in the background, so new additions reach you without an app update.
- Update on demand — Settings → Sports → Update sports data pulls the latest immediately; the button enables only when a newer version is available.
No watch update needed.
Companion v1.10.2 — Speak sports replies¶
Sports answers (F1, scores, standings) can now be read aloud on your watch, just like Weather and the other lookups.
There's a new Sports toggle under Settings → 🔊 Speech → Lookups — it's pre-ticked, so once the master Speech switch is on you'll hear sports replies straight away. No watch update needed.
Companion v1.10.1 — Formula 1 session fixes¶
Formula 1 answers now show the right session and tell you which one it is:
- Ask for qualifying, sprint, or pole — "who won the last qualifier", "F1 qualifying results", "who got pole", "F1 sprint results".
- Every F1 answer is labelled with its session (e.g. "Spanish Grand Prix · Qual"), so you always know what you're looking at.
- "Who won the last race" now gives the most recent completed session — so mid-weekend you get a result (qualifying) instead of "not run yet".
No watch update needed.
Companion v1.10.0 — Formula 1 + cricket¶
Sports grows beyond team-vs-team:
- Formula 1 — "when is the next F1 race", "who won the grand prix" (podium), "last race standings" (full finishing order), and "driver standings" / "constructor standings".
- Cricket — IPL and the Cricket World Cup: "IPL score", "did RCB win", "IPL standings".
- Turn sports on/off — untick any sport you don't follow in Settings → Sports; it returns no info and won't match spoken names.
Plus polish from real-world testing: standings show a clearer top-10 (F1 constructors show the full field), short division codes like "AL East" work, and ambiguous names resolve better ("India" → the cricket side, not Indiana). Favorites, sport priority, and your on/off choices all travel in Backup & Restore. No watch update needed.
Companion v1.9.0 — Sports¶
Ask Wristotle about your teams: next game, last result, live score, or league standings — across soccer (Premier League, La Liga, Serie A, Bundesliga, Ligue 1, MLS) and the US major leagues (NBA, NFL, MLB, NHL).
- "when do the Warriors play next" · "did Arsenal win" · "live score for Manchester City" · "NBA standings" · "National League West standings".
- Settings → Sports — save favorite teams so "did we win?" works without naming one, and set a sport priority so an ambiguous name like "City" resolves to the sport you follow (Manchester City vs Oklahoma City).
- Favorites and sport priority are included in Backup & Restore.
Like Weather, this is companion-side and only touches the network when you ask — see Privacy → Sports queries. No watch update needed.
Companion v1.8.6 — Reproducible F-Droid builds¶
Companion-only patch release. No functional changes — same app as 1.8.2.
Groundwork for the F-Droid listing: the build is now fully reproducible, so F-Droid can rebuild the app from source and confirm it matches the release you download here, byte for byte (verified on F-Droid's build servers). This was entirely a build-system change — how the on-device speech libraries are compiled and packaged (stripped at link time, built without OpenMP so they come out identical on every build server) — and the app itself behaves exactly the same. Supersedes the intermediate 1.8.3–1.8.5 build-fix releases.
Companion v1.8.2 — Packaging fix¶
Companion-only patch release. No functional changes — same app as 1.8.1.
A build-system fix so the app builds cleanly on F-Droid's servers: removed the Gradle "foojay" toolchain auto-provisioning (which downloaded a JDK over the network at build time). Nothing changes for you on the app side.
Companion v1.8.1 — Voice scheduling fixes¶
Companion-only patch release. No watch-side or wire-protocol changes.
A batch of fixes to voice reminders, alarms, and meetings — plus two under-the-hood safety fixes.
Alarms
- Voice alarms work again. "Set an alarm for 5 minutes" / "for 7 a.m." confirmed the right time but then silently failed to actually set the alarm — that's fixed.
Times resolve the way you'd expect
- A bare hour now means the next one o'clock everywhere. At 2:30 p.m., "remind me at 8" / "set an alarm for 8" → 8 p.m. today, not tomorrow morning. "At 1 a.m." (with the a.m. spoken) still means tomorrow 1 a.m.
- "Is there a reminder at 8?" now checks the 8 p.m. where you'd have set one, instead of 8 a.m. The same resolution applies whether you're creating, rescheduling, cancelling, or asking — consistently.
Reminder & meeting titles
- A reminder with only a time ("set a reminder for 8 p.m.") is no longer titled "8pm" — it gets a clean Wristotle Reminder placeholder. Same for a bare day: "remind me Friday at 8 p.m." no longer becomes a reminder called "Friday".
- "Remind me Friday at 8 p.m. to call mom" now titles it Call mom (the leading day/time clause is stripped properly).
- "Schedule a meeting at 3 p.m. with Alex" now keeps Alex as the attendee (a trailing period from dictation used to drop it).
- App-generated placeholders are consistent: Wristotle Reminder / Meeting / Alarm — used only when you didn't name one.
Under the hood
- Closed a spot where a self-hosted weather provider's API key could be written to the device log, and removed a path that could have wiped your conversation history on a future database change.
- Cloud speech-to-text now gets the same hallucination cleanup the on-device engine already had (repeated-phrase loops, "*DING*"-style noise).
Watch v1.2.1 — Fix: original Pebble 2 / Pebble 2 HR connection¶
Watch-only patch release. No companion or wire-protocol changes. Update if you have an original Pebble 2 or Pebble 2 HR.
The v1.2.0 on-watch speaker release accidentally broke the watch↔phone connection on the original Pebble 2 and Pebble 2 HR: after updating, the watch app reported that the companion wasn't loaded even though it was running.
The speaker support added in v1.2.0 was enabled for the whole "diorite" hardware family. But that family is shared between the speaker-equipped newer hardware and the original, speaker-less Pebble 2 / Pebble 2 HR — and on the original hardware the speaker's audio buffer used up enough memory that the watch could no longer open its messaging channel to the phone. The app still launched, so it looked like a connection problem rather than an out-of-memory one.
v1.2.1 limits the speaker to Pebble Time 2 (emery) only, restoring the original Pebble 2 / Pebble 2 HR to exactly how they behaved before v1.2.0. (Thanks to the reporter on issue #2 for the clear diagnostics.)
Companion v1.8.0 — Stats page¶
Companion-only minor release. No watch-side or wire-protocol changes.
A new Settings → 📊 Stats card that surfaces what you've been asking Wristotle for, when, and how well it understood. Everything is computed locally from the existing conversation history and on-device stores — nothing leaves your phone, and there are no new permissions or collection points. The header chip says it explicitly.
Six panels:
- Streak hook — Day N with Wristotle, total voice queries, words dictated, approximate dictation time (135 WPM).
- Top intents (last 30 days) — a clean bar chart of what you've been doing. Unknown and Error don't get top billing.
- When you talk to Wristotle — 24-hour activity chart in your local time + peak hour + busiest day-of-week.
- How well it works — success rate over the last 30 days and lifetime, plus the NLU classifier's average confidence.
- Where Wristotle stumbles — the three lifetime intents with the lowest success rate, severity-tinted, with absolute X of Y succeeded counts and a hint pointing at Settings → 🎓 Learning where you can teach Wristotle new phrasings.
- On your phone — current notes / completed tasks / pending reminders / alarms. Named honestly: deleting a row reduces the count. (True lifetime counters that survive deletes are a follow-up.)
Sub-10-query installs collapse to a single Day 1 stub so a fresh install doesn't read as a wall of zeros. The 30-day window degrades gracefully to whatever's still on disk under your 💬 Conversation retention setting (10 days by default).
Internals worth noting:
- The whole Stats stack — data class, source seam, repository,
bucket densification, word counter, accuracy math — lives in
:wristotle-corecommonMain. The Android side is just a Room + prefs adapter. An eventual iOS port reuses every line of the computation. - 9 new unit tests in commonTest covering the empty case, lifetime totals, handler sort, bucket densification, 30d-vs-lifetime independence, the stumbles surface, and word-counter edge cases. Pass on Android-host AND iOS Sim.
Companion v1.7.3 — TTS test buttons + stream-close hardening¶
Companion-only patch. No watch-side or wire-protocol changes.
- Test buttons on the Speech card now actually work. "Speak on watch (primary only)" and "Play 440 Hz tone on watch" were producing garbled audio because they skipped a pre-warm step that real voice replies always sent. Now they mirror the dispatch path.
- Defensive stream cleanup —
TtsStreamer.speak()now closes the watch's TTS stream in aNonCancellablefinally on every exit (success, synth failure, BLE drop, coroutine cancellation), so the watch can never get stuck mid-stream waiting for a close that never came. - Internal: 33 new unit tests covering provider gating, composite
fallback, sentence splitting, URL append, and Intent-enum sync —
all in
:wristotle-corecommonTest so they run on Android-host AND iOS Sim. No user-visible change, but the TTS feature now has a real safety net under any future refactor.
Companion v1.7.2 — SetAlarm compound durations + dotted AM/PM¶
Companion-only patch. No watch-side or wire-protocol changes. Closes codeberg #12.
Two SetAlarm bugs that SetTimer was already handling correctly:
- "Set an alarm for one hour and four minutes from now" used to fail
with "Couldn't understand the time". SetAlarm fed the query to
prettytime-nlp, which anchors on the verb phrase and can't sum
compound relative units. Now sums
<n> <unit>pairs the same way SetTimer does — both intents share a single duration-parsing helper so they can't drift apart again. - "Set an alarm for 7:51 p.m." failed because the dots in "p.m." broke the AM/PM tokenizer. SetAlarm now normalises "a.m." / "p.m." → "am" / "pm" before parsing. Affects the same way for "6:30 a.m.", "11:15 P.M.", etc.
Both inputs from the bug report now resolve correctly:
set an alarm for one hour and four minutes from now→ +1h4mset an alarm for 7:51 p.m.→ 19:51
Companion v1.7.1 — TTS settings now travel in backups + diagnostics¶
Companion-only patch. No watch-side or wire-protocol changes.
Closes two gaps in v1.7.0's Speech feature:
- Backup & Restore now includes everything on the Settings → 🔊 Speech card. Mode (Local only / Local primary / Cloud primary), HTTP endpoint URL, model, voice, and per-intent toggles all survive a phone reset. The HTTP API key rides only when you tick the new "TTS provider API key" secret checkbox — off by default, same shape as every other secret in the backup ZIP.
- Diagnostics (Settings → 🔧 Diagnostics → Build bug report)
captures the TTS provider's current state. Master toggle, mode,
HTTP URL, model, voice, key state (
(set)/(empty)— never the key value), and the list of intents you've opted in to. Makes "Speak on watch is on but Reminders don't speak"-style reports immediately diagnosable without needing screenshots.
API keys are never included in any log, exception, error message, or the diagnostics report. That's a standing rule across every provider in the codebase (Anthropic / OpenAI-compat / Weather / MCP / STT / TTS).
Companion v1.7.0 + Watch v1.2.0 — Speak replies on the watch (experimental)¶
Companion + watch release. New opt-in setting; nothing speaks by default. Hardware-gated to Pebble Time 2 (emery) and Pebble Round 2 (diorite) — every other model still receives the chunks but silently drops them.
Settings → 🔊 Speech (marked "Experimental" on the landing list) holds the whole feature:
- Master switch + per-intent picker. Pick exactly which intents the watch should read aloud — Ask Agent replies, morning brief, weather, world time, reminder confirmations, task adds, note creations, every CRUD-style action grouped by noun. Off by default everywhere.
- Engine picker. Three modes: Local only (Android
TextToSpeech— whichever engine your phone has installed), Local primary (Android first, HTTP fallback), Cloud primary (HTTP first, Android fallback). - HTTP engine config. Any OpenAI-compatible
/v1/audio/speechendpoint works — OpenAI'stts-1, self-hosted Piper, OpenedAI Speech, LiteLLM proxies, etc. Base URL + API key + model + voice fields with empty defaults; the URL accepts both…/v1and…/v1/audio/speech(we won't double-append). - Test surface in the same card. Free-form phrase input + four
buttons: speak via the configured primary on the watch, play a 440 Hz
tone on the watch, speak via the phone's
AudioTrackfor A/B against the watch path, and play the tone on the phone. Run all of them without flipping the master switch on.
Time-to-first-audio depends heavily on the engine:
- Local Android TTS (GrapheneOS
speechserviceson my dev phone): ~4 s — Android synthesizes at roughly 50% real-time and that's the floor. - Cloud (OpenAI
tts-1): ~1 s — synth throughput hits ~6.7× real-time, BLE delivery is the next bottleneck after that.
How it works under the hood, briefly:
- Companion pre-warms the watch the moment it commits to speaking,
before the synth round-trip. Watch enters BUFFERING immediately so
tts_is_active()is true; quick-launch's auto-exit timer defers instead of popping the chat window during the ~1–4 s synth. - Long replies are split into sentences (
.!?+ newlines + a first-sentence comma when the leading clause is long enough). Synth is pipelined-sequential — only one synth call is in flight at a time (AndroidTextToSpeechlisteners collide on parallel calls), but sentence N+1's synth kicks off the moment sentence N starts streaming. - Pacing on the wire is ring-usage-based, not wall-clock-based. Caps the watch ring at ~11.5 KB so it never overflows on cross-sentence hand-offs, and the companion catches up quickly when it falls behind.
- Watch tail-drain delays
speaker_stream_close()by 1.5 s after the ring goes empty so the speaker's own internal queue finishes before the stream is shut down. Without this, the last word of every phrase was truncated.
Pebble Round 2 (diorite) was added late in the cycle — the original
spike targeted emery only. The macro on the watch now guards on
defined(PBL_PLATFORM_EMERY) || defined(PBL_PLATFORM_DIORITE); UI on
the companion warns about the hardware gate so people on aplite /
basalt / chalk / flint / gabbro know the toggle is a no-op for them.
Companion v1.6.2 — Voice fixes (punctuation, word numbers, cancel timer)¶
Companion-only patch. Four small voice-handling bug fixes; no protocol or schema changes on either side.
- Sent text messages preserve trailing punctuation. Saying "text dad
did you have lunch?" now actually sends
?. Pre-fix, every per-word pass throughcleanNameTokenstripped trailing punctuation — meant for contact-name hygiene ("john,"→"john"), wrong for the body. - Word-form durations cover 11–19 and 21–99 in both spacings. "Set a
timer for eleven minutes" used to return
Couldn't understand the durationbecause the lookup table jumped fromtentofifteentotwenty. Now the table is generated, so the teens and every compound (twenty-one/twenty one…ninety-nine) all resolve. - Same coverage on the time parser. Reminder / SetAlarm / Reschedule / Calendar / CreateEvent all flow through the same parser, which had drifted: it only knew 1–12 plus multiples of 5. "Set an alarm for thirteen minutes" works for the first time. Closes codeberg #11.
- "Cancel timer" stops lying. Android has no programmatic
DISMISS_TIMERintent — timers can only be cancelled from the Clock app. The cancel handler now says so directly ("Timers can't be cancelled by voice — open the Clock app") instead of insisting "No reminder matching timer".
Companion v1.6.1 — :wristotle-core KMP refactor¶
Internals-only patch. ~170 files lifted into a shared
:wristotle-coreKotlin Multiplatform module that builds for both Android and iOS Simulator. No user-visible changes; foundation work for a future iOS companion.
Twelve platform-interface seams (KeyValueStore, Logger, HttpClient, WatchTransport, Telephony, AlarmScheduler, Notifier, CalendarReader, ContactsResolver, AppLauncher, FileSystem, TimeParser) keep Android-only code on the Android side; everything else lives in commonMain and compiles for iOS in CI on every commit.
Companion v1.6.0 — Morning brief catches dismissed notifications¶
Companion-only minor release. Adds an opt-in storage surface for the morning brief; no protocol or schema changes on the watch side.
Also the first release under AGPLv3 — see the companion README for the per-commit cutoff. v1.5.0 and earlier remain MIT in perpetuity.
The morning brief's Messages line has always read from your live notification tray — useful, but it loses anything you swiped before brief time. If you got a text at 11pm, cleared it before bed, and asked for the brief at 7am, that text was gone.
Optional persisted log. Turn on Settings → 🔔 Notifications → Include dismissed notifications to record what arrived today so the brief still surfaces it even after you've cleared it.
- Default OFF. Snapshot semantics stay the default; nothing changes for users who don't flip the toggle.
- When ON: Wristotle stores
package id + conversation key + timestampfor every actionable notification posted today. No body, no title, no sender is ever read or written. - Auto-prunes after 7 days; Clear log wipes it on demand.
- The brief reads the union of currently-in-tray notifications and today's log, deduped per conversation, so a notification still sitting in your tray AND a notification you've already swiped both land in the count.
Conversation count vs message count. Same dedupe-by-conversation rule as before — five SMS from one friend reads as one. When a thread is chatty enough that the raw number matters, the brief now shows both:
Messages: 1 conversation (5 msgs) from WhatsApp.
When the two numbers match, the parenthetical drops and the line stays calm:
Messages: 2 from WhatsApp, Signal.
New Settings category. The toggle lives under a new top-level 🔔 Notifications section so future notification-related settings (channel mutes, DnD overrides) have a natural home.
Closes companion task #169.
Companion v1.5.0 — Persistent reminders¶
Companion-only minor release. New voice intent flavour; no protocol or schema changes on the watch side.
Some reminders you really don't want to miss — meds, an important call, the oven. Wristotle now supports a persistent flavour of reminder that follows the existing flow on the watch and keeps nagging your phone afterward until you explicitly stop it.
Voice triggers — any of:
- "persistent reminder to take meds at 9pm"
- "nag me to call mom at 5"
- "keep reminding me to drink water at noon"
- "remind me persistently to check the oven at 6"
What you'll see
- On the watch — same timeline pin as a plain reminder. Fires once at the scheduled time.
- On the phone — a "Persistent reminders" notification channel posts at the same moment and re-fires every 10 minutes (default — see Settings → Reminders → Persistent reminders) until you tap Stop, run out of nags, or cancel the reminder by voice. The body shows "Persistent reminder · N more nags after this" so you can see how many remain.
Settings → Reminders → Persistent reminders
- Nag interval — 5 / 10 / 15 / 30 minutes.
- Max nags — 3 / 5 / 7 / 10. The nag chain stops automatically when it runs out so an unattended phone can't spam itself.
What clears a nag
- Tap Stop on the notification → cancels the whole chain.
- Say "cancel reminder to call mom" → cancels the watch pin AND the phone nag chain in one go.
- Tap the notification body → opens Wristotle and clears that nag, but the next one in the chain still arrives. Use this when you want to acknowledge but keep being reminded.
Boot survives — if your phone reboots mid-chain, Wristotle re-arms the pending nags during the next start.
Closes codeberg #10.
Companion v1.4.1 — Self-hosted endpoints over HTTP work again¶
Companion-only patch release. Fixes codeberg #9. No protocol or schema changes on the watch side.
If you point Ask Agent, an STT provider, or an MCP server at a plain
http:// URL — a LAN box, a Tailscale CGNAT address, a self-signed
endpoint behind a reverse proxy — Android was rejecting the request
with "Cleartext HTTP traffic to … not permitted" before Wristotle
ever saw it. The request never left the device.
Wristotle now declares usesCleartextTraffic="true" in its manifest,
matching what every other self-hosted-first Android client does (Core
Devices' Pebble app, Jellyfin, Home Assistant Companion, Nextcloud).
The URL field itself is the user's explicit opt-in — if you type
http://, you meant it.
HTTPS endpoints are unchanged. This only unblocks the cleartext
case for users who explicitly type an http:// URL into one of the
provider settings.
Companion v1.4.0 — Morning brief¶
Companion-only minor release. New voice intent; no protocol or schema changes on the watch side.
Ask "morning brief" (or "what's on my plate today" / "brief me" / "summary of today") and Wristotle reads back a single multi-line response covering, in order:
- Meetings — today's calendar events with start times. Capped to
the first three by start time; the rest fold into a
+N moretail. - Alarms — enabled alarms set for today. Single alarm shows the exact time; multiple show the first three.
- Messages — count of currently-posted notifications from
messaging apps (WhatsApp, Telegram, Signal, Slack, Discord,
Teams, Messenger, Gmail, AOSP / Google / Samsung Messages),
deduplicated per conversation. Notifications from apps the brief
doesn't recognise still get a
+N othertail so you know how full your tray is. - Reminders — count of timeline pins due today.
- Tasks — pending tasks across all your lists.
- Notes — notes created since midnight; single note inlines the first ~40 chars of the body.
Each line is short by design — the watch chat surface tops out at ~280 characters, and the brief is the kind of thing you skim, not read. Empty sections drop out; on a packed day the tail sections truncate first so the time-anchored items (meetings / alarms) stay visible.
Unread messages read live from notification access — there's no persistence, so a notification you've already dismissed never appears in the brief. The same Notification Access permission already grants media-control reads; nothing new for the user to enable. If Notification Access isn't granted the Messages line simply doesn't show up.
A more accurate persisted-log approach (tracking each notification from post until you explicitly act on it) is sketched as a follow-up — the snapshot path covers the common case without a new privacy commitment.
Companion-only patch release. Two user-visible fixes and a diagnostics improvement; no protocol or schema changes.
Relative alarms fire at the right time (closes #8)¶
"set an alarm for an hour from now" used to land at the current
minute instead of one hour later. prettytime-nlp can't quantify
the indefinite article in the verb-phrase shape, so it fell back
to "now". Two-part fix: the time parser now rewrites a / an +
unit to 1 + unit before parse, and SetAlarmSlots strips the
verb-phrase preamble first so prettytime sees only the time
fragment. Same fix lifts "in a minute", "a day from now",
and similar relative shapes.
Voice Input card hides once dismissed (closes #7)¶
The Permissions tab's Voice Input card used to leave an empty frame after Core Devices users dismissed the scope-note explanation. The inner rows were already gated on the BLE companion, but the card title + frame remained. Now the entire card disappears once a Core Devices user dismisses the note — microPebble users are unaffected since Wristotle's voice provider still participates for that companion.
Diagnostics surfaces voice-provider detection internals¶
The diagnostics export's Default voice provider line now also
dumps the raw Settings.Secure.voice_recognition_service value,
the parsed package + class, and the expected package + class so
"I set the binding via ADB but the app still says denied" reports
can be triaged from the bundle without a rebuild. Useful side
finding: Android 16 normalises the stored value to the short
form (pkg/.short.class.Name) some time after the ADB write —
the existing parse handles both shapes correctly.
Watch v1.1.0 + Companion v1.3.0 — Watch polish + opt-in color theme + chat bubbles¶
Watch v1.1.0 plus a companion bump that adds the user-facing toggle. No backwards-incompatible changes — older companions still drive new watches and vice versa; the colour toggle defaults off.
Watch side (v1.1.0)¶
Every menu row across Notes, Tasks, Alarms, and the Quick Launch chooser now carries a leading entity icon — a page for notes, an empty / filled checkbox for tasks, a clock for alarms, the action's glyph in Quick Launch (mic for dictation, page for notes, checkbox for tasks, clock for alarms). The empty states ("No notes yet", "No tasks", "No alarms / Set one by voice") show the same icon so each screen retains a visual identity even when there's nothing in the list.
Chat entries now sit inside soft rounded bubbles — a thin foreground outline with comfortable padding around the text, so each query / response pair reads as a discrete unit at a glance.
Behind the icons, a new design-tokens header (utils/wt_theme.h)
names every font, color, spacing, and icon-resource constant the
watch uses so a future design swap lands in one place. The
typography hierarchy is the same six system fonts the watch was
already using, now with intent-based names (WT_FONT_DISPLAY /
_TITLE / _BODY / _SUBHEAD / _BODY_SM / _CAPTION).
Bug fix — picking "Dictation" from the Quick chooser menu used to silently swallow the SDK dictation push (a Pebble window-routing race when the chooser pops + a new window pushes in the same event loop tick). Now deferred by one tick after the pop, so all four chooser actions (Dictation / Notes / Tasks / Alarms) launch reliably.
Companion side (v1.3.0)¶
Settings → 📱 Watch gains a new "Color theme" switch (off by default). When enabled and the connected watch is colour-capable (Pebble Time, Time Steel, Time Round, Time 2, or Emery):
- Menu rows light up with coloured icon variants — red bell, green checkbox-filled, deep teal checkbox-empty, amber page, blue mic, purple gear. Each glyph's hue picks from a deepened Pebble palette so the icons read as grounded rather than washed out.
- Highlight bg on the selected menu row switches to
GColorLightGray— the one neutral that doesn't clash with any of the five accent colors, so dark icons remain legible while highlighted. - Chat bubbles fill with
GColorIcterine(a warm yellow tint). Text stays foreground for contrast. - Chat progress strip glows
GColorChromeYellowduring dictation, agent activity, and tool calls.
Black-and-white watches (Pebble Classic, Pebble 2) stay
monochrome regardless of the switch — every accent token collapses
to foreground on those platforms via PBL_COLOR-conditional
macros, so a single call site renders correctly across all seven
Pebble platforms.
Icons are generated programmatically by
tools/generate_ui_glyphs.py (Python + Pillow, deterministic) —
both B&W and colored variants come out of one script run. Adding a
new glyph is one drawer function + one entry in the script's color
table.
Companion v1.2.0 — Folder sync for Notes + Conversations¶
Companion-only minor release. Closes issue #5.
Settings → 📝 Notes and Settings → 💬 Conversation each gain a new "Sync to folder" card. The same architecture under both — pick any folder on the phone and Wristotle writes every entry there as a file. The picked folder's existing sync stack (Obsidian, Joplin, Logseq, Dropbox, Drive, Syncthing, git, plain file browsers, etc.) handles whatever happens next.
Defaults:
- Notes — one file per note, CommonMark Markdown, attachments in
Wristotle/notes/attachments/. - Conversations — append every query + response to a single
daily-log file (
Wristotle/conversations/conversations.md), same formats / granularity options as Notes; you'd be drowning if it defaulted to one file per query.
Both cards expose:
- Format: Markdown (default — CommonMark-clean, no Obsidian- flavour wikilinks so it round-trips every tool), Plain text, or JSON.
- Granularity: one file per entry, or append to a single file.
- Delete cascade toggle — when an entry is removed in Wristotle, remove the file in the vault.
- Sync now button — manual pass for the cases where the reactive subscription wouldn't fire (format change applied, fresh setup, recovering after a folder move).
Filenames are ISO timestamps (2026-06-05T17-30-21-123Z.md) rather
than the entity's Room id — keeps a fresh install from silently
clobbering vault files written by a previous install. Audio
attachments are linked by their filename so you can identify a
recording even when looking at the markdown out of context.
Under the hood, the abstraction landed under a new sync/ package
with a generic FileSyncRenderer<T> interface; adding a brand-new
entity type takes one pure-Kotlin renderer file plus a handful of
wiring lines.
Companion v1.1.0 — Claude web search + smarter aliases + reminder polish¶
Companion-only minor release. Three additions and two bug fixes.
Additions:
- Claude built-in web search. Settings → ✨ Ask Agent →
Anthropic gains a "Web search (Claude built-in)" Switch. When
on, Claude can search the web mid-reply for fresh facts (news,
sports, today's prices) and return a cited answer. Off by
default — Anthropic bills $10 per 1,000 searches on top of
normal token costs, so the toggle is opt-in. Wires Anthropic's
web_search_20260209server tool (the dynamic-filtering version) into every chat request and silently drops theserver_tool_use/web_search_tool_resultblocks during parsing so only the cited final answer reaches the watch. OpenAI-compatible users get the universal answer via an MCP web-search server — see the Ask Agent troubleshooting section. - Compound-word-tolerant app aliases. App aliases now resolve
compound-vs-split STT inconsistencies automatically. An alias
stored as
audiobookstill matches when dictation transcribesaudio book(and vice versa), so users don't need to add both spellings. Direct map hits still win — if you intentionally pin both spellings to different apps, each spoken form resolves to its exact alias. The fix is read-side only; existing aliases keep working as-is, no re-save needed. App-label matching (against installed launcher labels) has the same shape of issue and is filed as a follow-up — it needs a second index-key column plus a re-scan. - App aliases card gains an add-alias guide. The collapsible info blurb under the card header now reads as a numbered walkthrough ("type the phrase as you'd say it → pick the app → Save") and explicitly calls out the compound-word tolerance.
Fixes:
- Reminder title leaks the time phrase (issue
#4).
"Remind me in two hours to check the tables" used to produce
a reminder titled "In two hours to check the tables" — the
schedule was correct but the title kept the leading time clause.
Fixed by adding a strip pass for time clauses sitting between
the prefix and the task-introducing "to". A subtler underlying
bug —
STRIP_PREFIXESmatching the "to" inside "tomorrow" — got fixed at the same time with\b…\bword-boundary guards on the optional connectors. - Ask Agent help text guides reasoning-model users. When users
point Ask Agent at a Qwen3 / DeepSeek-R1 / QwQ endpoint and see
<think>…</think>blocks in the watch response, the system-prompt help text now flags the failure mode and points at the inference- server fix (vLLM--enable-reasoning --reasoning-parserflag). Full walkthrough in the new Ask Agent section of the Troubleshooting page. Wristotle stays parser-naive by design — reasoning-model output belongs at the boundary it's emitted from, not in the client.
Companion v1.0.4 — MCP server card overhaul¶
Companion-only patch. User-facing UX additions to Settings → MCP servers plus a small bouquet of bug fixes uncovered while debugging a "linking the MCP server fails" report.
UX:
- Edit existing server — each row gets a pencil icon that opens a dialog pre-filled with the current name / URL / Authorization header / transport mode. Saving evicts any cached connection so the next refresh picks up the new values; no app restart needed.
- Enable / disable switch per row — flips the existing
enabledflag that the Ask Agent loop was already filtering by. Disabled rows dim their description and grey out the refresh button; the agent skips them entirely on the next voice query. - Save-time scheme guard — the Authorization field gains a placeholder
(
e.g. Bearer sk-…) and, on Save, a "Looks like a raw token" dialog fires when the value doesn't start with a recognised scheme (Bearer,Basic,Token,Digest,ApiKey,api-key). Closes the 401 failure mode where users pasted a raw token and the server returned a confusing "Unauthorized".
Bug fixes:
- Refreshing an MCP server that returns 401 no longer crashes the app. The settings card now shows a Failed: Unauthorized chip on the row.
- Malformed
Acceptheader that some strict MCP servers were rejecting (Wristotle was sendingapplication/json, text/event-stream; application/jsoninstead ofapplication/json, text/event-stream). - Ktor engine leak when an MCP connect attempt threw — the partial HTTP client is now closed instead of hanging around until GC.
Companion v1.0.3 — F-Droid build pipeline hardening¶
Companion-only patch. Internals — no user-facing surface changes.
Follow-up to v1.0.2's build-system prep. Three CI / build changes that close the remaining reproducibility and supply-chain gaps before the F-Droid listing PR goes in:
- AGP 8.3+ silently bundles
META-INF/version-control-info.textproto(the build's git commit SHA) into the APK, which broke two-build inner-content equality.vcsInfo.include = falseon the release buildType turns it off. - The CI release workflow now hard-fails if the signing key's
SHA-256 fingerprint changes — defence against an accidental key
rotation that would break the in-place upgrade path on every
user's phone (and break F-Droid's
AllowedAPKSigningKeyspin). - The CI workflow also publishes a
sha256sums.txtalongside the APK on every release page so downloaders can verify what they got.
Plus a small cleanup: removed every reference to private knowledge files from tracked source so the public repo stays self-contained.
Companion v1.0.2 — F-Droid build prep¶
Companion-only patch. Internals — no user-facing surface changes.
Build-system work so the F-Droid listing path can move forward without further code churn on our side:
speech-whisper's cmake block now passes-Wl,--build-id=noneand-ffile-prefix-mapso the whisper.cpp.sofiles stay byte-identical across rebuilds (no per-build random build-ID, no absolute laptop paths leaking into the binary).- Compose's baseline-profile generation tasks are now disabled —
the generated
baseline.profmis non-deterministic and we don't ship it today. - The static
versionCode/versionNamefallback inapp/build.gradle.ktsnow tracks the released tag (so F-Droid's scanner reads a meaningful value instead of the old1/"1.0"defaults). The pre-push hook warns if those drift away from the tag being pushed. - New
fastlane/metadata/android/en-US/directory with descriptions, per-version changelogs, screenshots, and a 512×512 icon — used by F-Droid (and by Triple-T-style publishers) to render the listing.
Two back-to-back clean release builds now produce APKs with identical
inner content (the v2 signing block byte-differs by design, which
apksigcopier handles on F-Droid's side).
Watch v1.0.1 — bug fix¶
- Fixed Quick Launch blocking scroll on multi-sentence Ask-Agent responses (issue #1). UP / DOWN now scroll the chat as normal and reset the auto-exit timer so the reply can't disappear while you're reading.
Companion v1.0.1 — bug-report diagnostics expansion¶
Companion-only patch. Internals — no new user-facing features.
The diagnostics bundle (Settings → 🐛 Bug reports → Build report) now includes sections that would have shaved minutes off triaging the first post-launch issue: which BLE companion is active, STT-provider mode + state, Ask-Agent provider state, MCP-server inventory, the watch-settings mirror snapshot, and the conversation-audio capture state. API keys / URLs / wake words / system prompts / MCP URLs are never exposed; the existing Redact PII toggle additionally hides STT base URL and MCP server names.
Companion v1.0.0 + Watch v1.0.0 — leaving alpha 🎉¶
Visual tour of the 1.0 surface →
Six months in. The voice-command surface is wide; the dispatch path is well-tested; the data model has been through a real backup/restore round-trip; the audit cycle removed the obvious sharp edges. Calling it stable.
What "1.0" means going forward:
- Minor bumps add user-facing features (each gets a
features.jsonentry + a changelog section + a Help-page row). - Patch bumps fix bugs and ship internals (testing, CI, refactors). No
features.jsonentry; changelog summarises. - No breaking changes without a deprecation cycle. The backup ZIP, the slot map, the
wakeup_serviceregistry shape, the AppMessage wire keys — these are the load-bearing contracts and they don't move under your feet.
The next several minor releases are queued in wristotle-companion/TODO.md — recurring alarms (day-of-week mask), Phase C of MCP integration (Wristotle's intents exposed as tools the agent can call), Maestro UI flow tests, multi-language Whisper picker, custom STT providers' inline progress, and friends.
Headline: first-launch welcome wizard + Settings → 🌟 Setup card¶
New users open Wristotle and see a multi-step welcome dialog walking through the optional setup that unlocks the full app:
- Download a Whisper model (microPebble only — Core Devices users skip this step automatically)
- Download the on-device intent classifier (skipped on memory-constrained phones)
- Grant the four runtime permissions Wristotle needs (Contacts, SMS, Phone, Microphone)
- Scan installed apps for the "open Spotify" / "play Audible" index
Each step has three buttons: Open settings (drops you on the matching Settings sub-screen so you finish with the familiar UI), Skip this step (advance within the wizard, action stays pending), Skip wizard (dismiss permanently). The wizard is dynamic — a power user with everything granted sees only Welcome → Final.
The same checklist persists in Settings → 🌟 Setup, surfaced at the top of the Settings drill-down. Completed actions disappear automatically; the card collapses to "✓ You're all set up" when nothing's pending. A bonus "Quality wins" section nudges users toward contact aliases (mishears), app aliases, Speech provider configuration, and Ask Agent setup. A "Show welcome again" button re-arms the wizard for users who skipped too eagerly.
Settings tab gains 🌟 Setup at the top¶
SettingsCategory.Setup joins the drill-down enum as the first row. The existing 12 categories (Watch / Conversation / Notes / …) keep their positions; 🌟 Setup leads.
Backup behaviour¶
The wizard-dismissed flag lives in its own setup_state SharedPrefs file deliberately — not in app_preferences. So restoring a backup ZIP on a fresh OS install re-shows the wizard for the new environment, and SetupHealthProvider re-derives the pending list against the restored state. Most of the time the wizard either skips most steps (model IDs preserved, aliases preserved) or goes straight to Final.
Internals (no user-facing change)¶
SetupHealthProvideris fully unit-testable — 11 JVM tests cover every trigger condition (Core Devices hides Whisper, low-RAM hides NLU, granted permissions / scanned apps / active models / configured speech-provider / configured ask-agent / existing aliases each drop their row, sort order pinned, power-user-everything-done case → empty list).- Promoted
appIndexDaoto aWristotleApplicationproperty so the provider can readlatestScanAt()directly without going through the indexer.
Companion v0.20.1 — alpha¶
Companion-only release. No watch changes. Bug fixes + internals.
Fixes¶
- SendMessage greedy "that" split — dictating "text John we tested the package that we built" used to greedy-split on the "that" deep inside the body and call findContact on a sentence-long fake name (which then surfaced as "Contact not found"). The conjunction split now requires the contact half to actually resolve before it's accepted; otherwise the multi-word loop picks up the real first-word name.
- Conversation audio replay missing in cloud-primary mode — the audio sink lived on
WhisperRecognizerso dictations transcribed by the cloud HTTP recognizer didn't save a WAV. Moved capture above the Recognizer layer (CapturingAudioSource); now fires once per session regardless of which inner recognizer ran. - Backup summary dialogs were incomplete — the export + restore success dialogs only listed the 9 entity counts (notes / tasks / conversations / nlu / mcp / audio / pins / aliases / contact aliases). Now also show "Settings included / applied" and (when ticked) "Secrets included / applied" lists. Also fixed the "App preferences" checkbox label, which was misleading.
Internals (no user-facing change)¶
- Three-agent code audit (reuse / quality / efficiency) on the v0.20.0 STT-providers feature. Net -81 lines:
- Extracted shared
List<ShortArray>.flatten(totalSamples)helper (was duplicated across WhisperRecognizer / HttpRecognizer / CapturingAudioSource). - Lifted
providerErrorMessage+HttpFailureBucket+WRISTOTLE_USER_AGENTfrom:app/agent/LlmHttp.ktinto:speech/util/HttpUtil.ktso both modules share. Caught a latent JSON-parsing bug while writing direct tests for the helper (an object-with-blank-message would surface raw JSON as the "provider error"). - Moved
SimpleHttpfrom:app/util/to:speech/util/so HttpRecognizer can share the one HTTP-setup pattern five clients now use (LLM × 2 + weather × 2 + speech). ResolvedContactslot now carries the fullContactobject instead of just the name — eliminates ~2 ContentResolver cursor queries per SendMessage dictation.- 31 new test cases (14 HttpUtil, 6 CapturingAudioSource, 8 CompositeRecognizer, 3 SendMessageSlots regression + ResolvedContact stash).
Companion v0.20.0 — alpha¶
Companion-only release. No watch changes.
Cloud + self-hosted speech-to-text¶
Until v0.20.0 the only transcription option was the on-device Whisper bundled with the companion — fine for short watch commands but unhelpful for users who already run a self-hosted Whisper server on their homelab (or want sub-second cloud latency for longer dictation). v0.20.0 adds a Speech provider picker under Settings → 🧠 Models that wires a single OpenAI-compatible /v1/audio/transcriptions HTTP client alongside the local recognizer.
One client covers ~every provider worth supporting: OpenAI, Groq (the fast / free-tier-friendly default we'd point most people at), Cloudflare Workers AI, self-hosted Speaches and whisper.cpp HTTP server, LiteLLM proxies — anything that speaks the OpenAI transcription shape. Config is the same base URL + bearer key + model triple already used by the Ask Agent feature. Bearer auth is omitted on a blank key so Tailnet-only self-hosted servers behind a homelab work out of the box.
Three modes for the recognizer choice:
- Local Whisper only — default; preserves the pre-v0.20.0 behaviour.
- Local primary, cloud fallback — Whisper on-device first; if it errors (model not loaded / inference failure) the HTTP endpoint takes over.
- Cloud primary, local fallback — HTTP first; if the network call fails (timeout / non-2xx / no model active) the local Whisper steps in.
The card includes a "Test connection" button that posts 200 ms of silence to verify your endpoint accepts the request shape + key before the first real dictation — a configuration error surfaces here instead of as a dead voice query later. The chosen mode picks up on the next voice query without an app restart.
Backup carries the mode + base URL + model with the non-sensitive settings; the API key is gated behind a separate "include secrets" checkbox (same shape as the existing weather / Ask Agent secret handling).
Companion v0.19.0 — alpha¶
Companion-only release. No watch changes.
Import custom Whisper models¶
The Speech card already downloaded curated whisper.cpp models from a built-in catalog; v0.19.0 adds a sideload path for everything else. Settings → Speech → Imported models opens the system file picker — pick any .bin, give it a display name, and it joins the active-model pool. Useful for community fine-tunes, language-specific variants, distilled checkpoints, or your own conversions that aren't in the catalog.
The first four bytes of the picked file are validated against ggml/GGUF magic before the file is copied in, so garbage picks (PDFs, JPGs, half-downloaded HTML) get rejected up front with a clear error. Imported models live in the same on-disk shape as catalog downloads, so the native loader doesn't care where a model came from.
Two small UX fixes shipped alongside: the Settings-tab attention badge now clears when an imported model is active (previously it only checked catalog rows), and a "Browse whisper.cpp models on HuggingFace →" link sits in the section header so the discovery step has a starting point.
Companion v0.18.0 + Watch v0.10.0 — alpha¶
Alarms with phone / watch / both destinations¶
Re-do of the alarm feature from the ground up. Previously voice "set an alarm" fired the system clock via AlarmClock.ACTION_SET_ALARM — phone-only, set-only (Android exposes no API to enumerate or cancel scheduled alarms). The v2 design adds first-class watch alarms via Pebble's wakeup_service, surfaces a per-alarm destination choice (Phone / Watch / Both) at creation, and brings voice cancel + a list view.
Companion UI — Settings → ⏰ Alarms & Reminders (renamed from Reminders; the existing Reminders card sits alongside the new Alarms card). The Alarms card has a default-destination dropdown for voice-created alarms and an editor dialog with a TimeInput, optional label, and destination radio. Cancel = delete (one-shot alarms have no recurring re-arm story).
Voice — "set an alarm for 7am" creates an alarm at the default destination. "cancel the alarm" / "cancel 7am alarm" cancels the watch leg of matching alarms; phone-leg alarms can't be cancelled programmatically and the response says so. The watch's keyword router lumps cancel-verb queries into the reminder family — added a deterministic Cancel → CancelAlarm upgrade so "cancel the alarm" doesn't get answered by the reminder cancel handler.
Watch — new alarm/ module with persisted registry (wakeup_service + persist_*, 8-slot budget, ±60 s global spacing). On-fire alarm window with vibration + dismiss. On-watch list view with per-row cancel. LONG_PRESS_ACTION_ALARMS = 4 extends the configurable-buttons enum: bind SELECT / long-press UP / long-press DOWN to the alarm list directly, OR open via the Quick menu chooser's new 4th row.
Out of scope (v2) — recurring alarms (one-shot only — re-create for repeats), backup integration (wireEpoch can't be restored across devices, so alarm data is intentionally device-local).
Watch v0.9.1 — alpha¶
Watch-only polish release. No companion changes.
Chat-empty surface + hint bar¶
The chat-empty welcome screen was hardcoding "Hold UP for notes" / "Hold DOWN for tasks" — accurate for v0.8.x, misleading after v0.9.0 made those buttons configurable. Rewrote in two parts:
- A larger title ("I'm listening.") sits above a smaller body that describes what the chat-surface buttons can open, without claiming which button does what. A "Configure in app settings." line points users at the binding controls.
- The bottom hint bar now reads "Wristotle ready" at rest. A thin progress strip animates above the text while a query is in flight — invisible when idle, sliding marquee while "Thinking…" or an agent tool status is showing.
Companion v0.17.0 + Watch v0.9.0 — alpha¶
Configurable chat-surface buttons¶
The watch's three chat-surface buttons are now each user-bindable. SELECT (single press), long-press UP, and long-press DOWN can each open Dictation, Quick menu (chooser), Notes, or Tasks. Defaults preserve the existing muscle memory — SELECT to speak, UP opens a quick chooser, DOWN goes to Tasks.
Configure anywhere: the gear-icon settings page in the Pebble app (PebbleKit JS settings via Clay) or the Wristotle Android companion's Watch settings card (Settings → ⌚ Watch). Both paths talk to the same persisted slots on the watch.
The Quick menu reuses the existing Quick Launch chooser — no new menu surface was added on the watch.
Companion v0.16.3 — alpha¶
Companion-only release. No watch changes.
Help page auto-opens on first launch after install/update¶
WhatsNewState compares BuildConfig.VERSION_NAME against the last-seen version persisted in SharedPreferences. On the first launch after an install or upgrade, the app routes you to Settings → ❓ Help with the matching feature entry highlighted and scrolled into view ("✨ NEW IN THIS RELEASE" chip + accent border). The persisted version is written immediately on construction so a crash between launch and the first UI tick doesn't loop the auto-open.
Companion v0.16.2 — alpha¶
Companion-only release. No watch changes.
In-app Help page¶
Settings → ❓ Help — a new landing in the Settings drill-down with two surfaces:
- Tips — four discovery hints (custom wake words, confirm-before-sending, contact aliases for mishears, backup encryption) with "Open docs ↗" deep-links into the most relevant section of this site.
- Feature history — every user-facing release, newest first, with version + date, a single-sentence description, an optional sample voice command, and a docs deep-link. Companion + watch versions render together for paired releases (e.g. "v0.14.0 + Watch v0.8.0 · 2026-05-31"). Searchable — type "weather", "backup", "v0.10" to filter both tips and timeline.
A "Need help troubleshooting? ↗" button at the bottom opens the troubleshooting page directly.
Internals (no user-facing change)¶
The Help page's feature list is driven by a JSON file (data/features.json) checked into both the docs repo and the companion repo. A codegen script + a Gradle verification task + the pre-push hook enforce that the in-app HelpTimeline.kt and the JSON stay in sync — so a release can't ship a stale Help page without one of the three layers refusing.
Companion v0.16.1 — alpha¶
Companion-only release. No watch changes. Pure test infrastructure.
Internals (no user-facing change)¶
Added a JVM voice-pipeline integration test layer + 47 NLU plumbing tests (CosineSimilarity math, WordPiece Tokenizer, ExampleBank ranking, EmbeddingIntentClassifier ranking + auto-warm + rebuild). Routing regressions like the v0.16.0 "Jarvis" wake-word miss are now caught at unit-test time in under a second rather than on the watch. Three production seams carved out to make these tests possible: VoicePipeline extracted from PebbleListenerService, Embedder interface (production MiniLmEmbedder is one impl), EmbeddingIntentClassifier takes its seeds as a constructor argument. No behavioural change.
Companion v0.16.0 — alpha¶
Companion-only voice-input release. No watch changes.
Ask Agent — custom trigger words (incl. wake-word form)¶
Until now AskAgent only fired on the literal "ask agent …" / "ask claude …" (etc.) lead-ins. Settings → ✨ Ask Agent → Custom trigger words now lets you add your own — one per line.
Two shapes are recognised:
- Verb form — "ask <your-word> …" / "hey <your-word> …". Works for every built-in subject (
agent,claude,ai,llm,assistant,bot,chatbot,chatgpt,gpt) and every custom you add. - Wake-word form — bare "<your-word> …" at the start, e.g. "Jarvis, what's the weather?". Custom words only. Built-ins like
agent/aideliberately don't bare-match because they're too generic to fire safely on their own.
Add the words you'd actually call your assistant by — "jarvis", "buddy", "hal", "alfred" — and the next "Jarvis, …" query routes straight to the LLM instead of being treated as an unknown intent.
Companion v0.15.7 — alpha¶
Companion-only catalog trim + correctness release. No watch changes.
Whisper catalog: dropped Medium + Large v3 Turbo¶
The v0.15.6 catalog expansion added Medium (English) q5_1 and Large v3 Turbo q5_0 to the All models view for higher-RAM phones. They had to come back out: even on a Pixel 10a, the Large v3 Turbo model takes ~17 seconds to transcribe a 3-5 second clip, which exceeds the watch's 15-second session timeout — every voice query failed silently. Medium was in the same neighbourhood. The largest remaining option is the unquantised Small (English) full (~466 MB on disk, ~1.4 GB RAM).
For users who had Medium / Large v3 Turbo downloaded under v0.15.6, the model file stays on disk untouched but won't appear in the catalog. You can delete it manually via Settings → Models → Whisper → (any installed model) → Delete, or — once a future release adds an "Unlisted models" view — directly from there.
Internals (no user-facing change)¶
The large-encoder path in the native Whisper bridge now bypasses the adaptive audio_ctx truncation (only safe for encoders ≤ 768-state — tiny/base/small). The 2-minute warm-up is now skipped on large encoders too. Both are dormant for the recommended Small tier; they're in place so a future "make large viable on faster hardware" attempt can re-enable Large without re-hitting either pitfall.
Companion v0.15.6 — alpha¶
Companion-only catalog expansion. No watch changes.
More Whisper models for higher-RAM phones¶
The model picker (Settings → Models → Whisper) used to recommend the full unquantised Small (English) at 466 MB for the Accurate tier — too RAM-hostile for most non-flagship phones to load without paging. The recommended Accurate pick is now the Small (English) q5_1 quantised variant at ~190 MB, which actually fits in a typical 4-6 GB phone's available memory.
For higher-RAM phones, the All models view now exposes heavier options: - Medium (English) q5_1 — ~514 MB on disk, needs ~1.5 GB RAM - Large v3 Turbo q5_0 — ~574 MB on disk, needs ~1.7 GB RAM - Plus the unquantised Small (English) full for users who want it explicitly
Faster q4_0 quantisation tier¶
Three new q4_0 variants joined the catalog (Tiny / Base / Small). On memory-bandwidth-bound CPUs (i.e. the older Android phones Wristotle targets), Whisper's own benchmarks show q4_0 is the fastest format at ASR-quality identical to the higher bitrates. Same binary, smaller download.
Memory footprint at a glance¶
Each model row now shows the rough RAM cost alongside the download size, e.g. "190 MB · ≈570 MB RAM · English". Rule of thumb: roughly 3× the on-disk size at runtime. Lets you pick something your phone can actually hold instead of finding out the hard way after a slow download.
Companion v0.15.5 — alpha¶
Companion-only refactor + correctness release. No watch changes.
enrichResolvedContact correctness fix¶
The confirm-before-dispatch flow stashes a "resolved contact" alongside the spoken name so the prompt shows what's actually about to be dialled / texted. After the v0.15.4 slot-keys consolidation, this one helper was inadvertently left reading raw string keys — which would have silently broken the confirm prompt's contact display if any of the keys had been renamed later. Migrated to SlotKeys along with the rest of the codebase.
MCP server "Authorization" header — typed classifier check¶
When no NLU model is installed, voice queries fall back to a "download an NLU model" hint. That hint used to check for a magic "stub" string on the classifier; a rename in the model code would have silently disabled the hint. Now uses a typed isStub: Boolean property — a future rename is a compile error.
Internals (no user-facing change)¶
PebbleListenerService refactored from 697 → 607 LOC. Audio file leaks fixed when a confirm prompt is overwritten by a re-entrant dictation (the prior dictation's .wav is now logged + deleted instead of stranded on disk). Service no longer iterates the full message dictionary in the log line for microPebble fan-out messages it's about to drop.
Companion v0.15.4 — alpha¶
Companion-only refactor release. No watch changes.
CreateEvent: "with X" + explicit title finally coexist¶
Asking for "schedule a meeting with Alex called standup tomorrow at 3pm" used to create a calendar event titled just "Standup" — the attendee got dropped on the floor. Now it creates "Standup with Alex" (and the watch confirm prompt shows the same string). When only a title is spoken it stays "Standup"; when only an attendee is named it stays "Meeting with Alex"; when both are spoken they're now joined.
Internals (no user-facing change)¶
A pass through the 24 voice-intent slot extractors consolidated 24 different sets of raw string keys into one SlotKeys object, deduplicated regex patterns shared between MediaSeek / SetTimer / Weather / WorldTime, and added regression tests pinning the ordering of the prefix-hint rule list.
Companion v0.15.3 — alpha¶
Companion-only privacy fix + UI scaffold release. No watch changes.
MCP "Authorization" header now masks¶
The MCP server settings card used to render the Authorization header field as plain text — the value was visible while typing and to anyone looking over your shoulder. It's now a masked field with the same eye-toggle reveal as the backup password and the Ask Agent API key fields.
Internals (no user-facing change)¶
Extracted reusable scaffolds for password fields, confirm dialogs, info dialogs, and the alias-rows dialog shared between App Aliases and Contact Aliases. Several perf wins on lower-end phones: download progress no longer recomposes the whole app shell, conversation history audio-existence checks moved off the main thread, settings tab no longer pays for flows it isn't reading.
Companion v0.15.2 — alpha¶
Companion-only build-config tweak. No user-visible changes.
Quiet warning suppression for some pre-built native libraries that AGP can't strip. APK size unchanged.
Companion v0.15.1 — alpha¶
Companion-only refactor + behavior fix release. No watch changes.
Ask Agent — watch timeout race fixed¶
When an LLM call took 16-29 seconds, the watch's response timer would expire while the companion was still waiting, leaving the chat surface silent even though the answer eventually arrived. The agent loop now emits a → thinking… status line on the watch at the start of every LLM round (which re-arms the watch timer), and the LLM read timeout is capped at 14s (under the watch's 15s ceiling) so a genuine slowdown surfaces as a fast failure instead of silent timeout.
Internals (no user-facing change)¶
Settings cards' equality guards consolidated. HTTP boilerplate extracted into a single util/SimpleHttp.kt shared by the four outbound JSON-API clients (Anthropic, OpenAI-compatible, open-meteo, OpenWeather). MCP database, AskAgent settings, and diagnostics settings now build lazily — first Settings tap pays the init, not cold start.
Companion v0.15.0 — alpha¶
Companion-only release. No watch changes.
Backup & Restore — per-category opt-in tree¶
The backup feature now lets you pick exactly what travels in (and out of) the ZIP, with secrets defaulting OFF.
- Per-category checkboxes on both export and restore. Notes, conversations, learned NLU examples, contact aliases, app aliases, pins, reminder/weather/agent settings, and MCP servers are each individually toggleable. The dialog shows live counts (e.g. "Notes (7)", "Audio recordings (12 · 4.3 MB)") so you know what's in the box.
- Secrets default to OFF. Weather API key, MCP
Authorizationheaders, and Ask Agent API keys are now separate ticks under Secrets, all unchecked by default. They only travel if you explicitly opt in. - Plain-text-secrets guard. If you tick a secret without setting a backup password, you get a one-time confirm dialog before the ZIP is written — keys go in clear text otherwise.
- MCP servers are now backed up. Previously skipped. Restored as disabled so you can review them before letting the agent talk to them.
- Restore picker mirrors export. The preview dialog shows what's in the ZIP and lets you tick which categories to actually merge in — so you can restore notes-only from a full backup without disturbing your current settings.
The on-disk schema bumps from 1 → 2; older ZIPs still restore (they're treated as "everything selected" for back-compat).
Companion v0.14.0 + Watch v0.8.0 — alpha¶
Paired release. AskAgent's per-round status line on the watch needs both apps — update both together.
Ask Agent (LLM passthrough + MCP tool calling)¶
The biggest add since the alpha started: voice-trigger any LLM, optionally backed by Model Context Protocol tool servers.
- New voice intent. "ask agent <question>" / "ask claude <question>" (or "ask the agent …" / "hey agent …") routes the rest of your sentence to a configurable LLM and reads back the answer.
- Two providers, one Settings card. Settings → ✨ Ask Agent lets you pick Anthropic (paste a Claude API key) or OpenAI-compatible (one endpoint URL covers OpenAI itself, OpenRouter, Ollama, llama.cpp, vLLM, …). Per-provider key + model fields; flip between them without losing either key.
- Visible system prompt with a watch-friendly default. The system-prompt field is pre-filled with "Reply in 1-2 short plain-text sentences…" so the LLM doesn't return giant markdown tables the watch can't render. Edit freely; "Reset to default" puts the baseline back.
- MCP tool calling. Add HTTP MCP servers under Settings → 🔌 MCP servers (e.g. GitHub's MCP server for repo/issue/PR control). When at least one is enabled, AskAgent runs an agent loop — the LLM picks tools, the companion executes them, results feed back into the next round, up to 5 rounds total before answering. The watch shows a per-tool status line in the hint bar (
→ github.get_me) while each tool runs, so you see what's happening instead of waiting silently. - Trust model. API keys are stored on the phone (local SharedPreferences). Queries + tool calls go directly from your phone to your chosen LLM provider and MCP servers — no Wristotle servers in the path. Off by default; you only pay when you say "ask agent …".
Watch (paired)¶
- Agent status line. New
agent_statusAppMessage key routes per-tool status to the bottom hint bar (the same slot that shows "SELECT to speak" / "Thinking…"). Stays out of the chat surface so the final answer still lands in the chat bubble cleanly.
Companion v0.13.0 + Watch v0.7.1 — alpha¶
Weather is the headline; the watch update is a small defensive fix you'll probably never notice.
Weather (companion)¶
- Voice weather. "what's the weather in Tokyo" / "weather in London" / "what's the weather" (no city — uses your phone's location). Returns a short condition + temperature on the watch. Covers cities, countries, and common abbreviations.
- Two providers. Defaults to open-meteo (free, no API key needed, no setup). In Settings → Weather you can switch to OpenWeather and paste your own free-tier API key — direct control over your own quota.
- °C / °F toggle on the same Settings page. Defaults to your locale's convention.
- Optional Location permission (only the bare "what's the weather" form needs it). Grant from Permissions → Weather (Location); without it the bare form just hints you to name a city. Coarse (~3 km) accuracy, used only when you ask — no background tracking.
- This is the first voice command that uses the network. The full data flow + opt-out is documented in Privacy → Weather queries.
Watch (defensive fix)¶
- Drop a stale companion response when the chat window has already been popped — prevents a use-after-free that could have rebooted the watch on edge-case quick-launch races. Existing flows are unaffected.
Companion v0.12.0 + Watch v0.7.0 — alpha¶
Paired release. World clock needs both apps (the watch forwards the query, the companion answers it) — update both together.
World clock¶
- Ask the time anywhere. "what time is it in Tokyo" / "time in London" / "what time is it in India" tells you the current time there, plus the day and the offset from your own zone — e.g. "It's 7:42 AM in Tokyo (Wed, 13h ahead)". Resolved entirely on the phone from Android's time-zone database; no network.
- Plain "what time is it" (no place) still answers instantly on the watch for your own clock — only the "in <place>" form goes to the phone.
- Cities, countries, and common abbreviations (UK, NYC, LA) all resolve.
Calculator¶
- Quick maths by voice. "what's 25 plus 17", "12 times 8", "96 divided by 4", "what's 15% of 80", "20% off 50" — worked out on the phone, no network or calculator app. Operator words and % of / % off are understood. Dividing by zero or an expression it can't parse comes back as a short "couldn't work that out" rather than a wrong answer.
Thinking indicator (watch)¶
- The watch now shows it's working while it waits for a reply from the phone: the bottom hint bar animates "Thinking…", and if the reply takes more than ~5 seconds you get a single buzz so you know it hasn't stalled.
- The hint bar was restyled to a white background with black text and a thin divider line.
Companion v0.11.0 + Watch v0.6.1 — alpha¶
Paired release. The alarm/timer fix touches both apps — update both together.
Alarms & timers¶
- Set an alarm by voice. "set an alarm for 7am" / "wake me up at 6:30" creates an alarm in your phone's clock app (silently — the watch confirms it).
- Set a timer by voice. "set a timer for 10 minutes" / "timer for 30 seconds" / "set a timer for 1 hour 30 minutes" opens the Clock app's Timers tab with the countdown running, so you can see and cancel it.
- Both go through the Confirm before send prompt (if you've enabled it) — a misheard time or duration is exactly what it catches.
Notes for power users¶
- Set-only by design. Android has no API for one app to list or cancel another app's scheduled alarms/timers, so there's no "cancel my alarm" / "what alarms do I have" — manage existing ones in the Clock app. (The timer command opens the Clock app precisely so you can cancel a running timer there.)
- Alarm vs. timer is decided by what you say, not just the keyword: a relative duration ("for 10 minutes") is always a timer; a clock time ("7am") is always an alarm. This holds even when dictation drops the word "timer" → "time".
- The watch's local "what time is it" command now matches "time" as a whole word, so "set a timer…" is no longer answered locally as the current time — it forwards to the phone.
Companion v0.10.1 — alpha¶
Companion-only. Watch stays at v0.6.0.
See and prune what Wristotle has learned¶
When Settings → Learning → Learn from my voice commands is on, every successful command is added to the intent classifier's example bank so similar phrasings are recognised more confidently next time. Until now there was no way to see what got stored — only a single "Clear learned examples" button.
- Settings → Learning → Saved phrases (N) opens a dialog grouped by intent (Call / Send message / Reminder / Cancel reminder / Find phone) with every phrase Wristotle has learned. The button only shows when N > 0.
- Each row has a delete icon — remove individual phrases without nuking the whole bank.
- The dialog header makes it clear the data stays on the phone, but it's now visible to you: a learned "call John Smith" keeps "John Smith" in the bank as part of the phrase.
- Deleting a phrase rebuilds the classifier centroids immediately, same as the bulk-clear.
Nothing else changed — wire protocol, watch app, and learning behaviour are all unchanged from v0.10.0.
Companion v0.10.0 — alpha¶
Companion-only. Watch stays at v0.6.0.
Contact aliases¶
Pin a spoken phrase to a specific contact, for when dictation mishears a name or you want to use a nickname. The classic case: Whisper hears "text mom" as "next mom", the contact slot ends up as next, and the confirm prompt shows [NO_CONTACT] [next]. Add next → Mom in Settings → Learning → Contact aliases and the mishearing transparently routes to the right person.
- Always resolves to the current name + number — rename the contact in your address book or change their number, and the alias keeps pointing at them.
- Conflict warning — if your alias phrase already matches an existing contact, the form flags it. Aliases always win over the normal contact matcher, so you'll know upfront that you're shadowing them.
- Backed up + relinks across devices — aliases ride in the manifest with name + number snapshots. On restore, dead lookup keys are re-bound by exact-name then by phone number, so aliases survive moving to a new phone or re-importing contacts from VCF.
Closes the loop on the [NO_CONTACT] display change from v0.9.1: that told you what failed; this lets you fix it without editing Contacts.
Companion v0.9.1 — alpha¶
Companion-only. Watch stays at v0.6.0.
Confirm prompt shows the resolved contact¶
The confirm prompt (when Confirm before send is on) now shows the actual contact name about to be dialled / texted, not just the word Whisper transcribed. Three shapes:
[John Smith]— resolved against your Contacts.[NO_CONTACT] [next]— your contact lookup didn't find anything for that phrase. Second bracket echoes the spoken value so you can see WHY (usually a Whisper mishearing of the verb landing in the contact slot — "text" misheard as "next").[NO_NAME]— no contact name was extracted from your dictation at all (e.g. just "call").
The confirm gate still fires on NO_CONTACT / NO_NAME so you can BACK out cleanly rather than getting a context-free "Contact not found" reply.
Closes issue #2.
Companion v0.9.0 + Watch v0.6.0 — alpha¶
Paired release. Watch and companion both got new code; install both together.
Tasks — a new voice-controlled checklist¶
- Voice CRUD from the watch. Four new commands round-trip with a new Tasks tab in the companion:
- "add task buy milk" / "add buy milk to my tasks" / "new task call the dentist" — create a task
- "what are my tasks" / "what are my completed tasks" / "completed tasks" — list with an optional filter
- "complete buy milk" / "mark buy milk done" / "finish X" / "done with X" — substring-match against pending tasks; ambiguous matches ask "Multiple matches: A, B — which one?" and you re-say with more text. "Complete the last task" / "latest" / "most recent" picks the newest pending task without text matching.
- "delete task X" / "remove X from my tasks" — same matching, removes the row.
- On-watch list. Long-press DOWN on the chat screen → chooser (Pending / Completed / All) → drill into the list. SELECT on a pending row marks it done; completed rows show with a
[x]prefix and SELECT is a no-op for v1 (delete via voice). Empty state reads "No tasks" / "No completed tasks". - Companion Tasks tab. New teal tab between Notes and Permission. Inline composer + collapsible Completed (N) section + swipe-to-delete + clear-all. Five tabs total now.
- Backup integration. Tasks ride alongside notes / conversations in the existing per-entity JSON ZIP export + import. Re-importing the same backup is idempotent — dedupes on
(createdAt, text)and preserves the completed state.
Notes for power users¶
- Anti-rule worth knowing: "add task to call the dentist" now correctly creates a task (not a meeting). The PrefixHints rule sits above CreateEvent because the latter's noun set includes "call".
- Whisper variants handled: pluralisation ("add task" → "add tasks"), trailing-noun ↔ leading-noun inversion ("add task to buy oysters" → "add to my tasks buy oysters"), past-tense filter modifier ("completed tasks" routes to ListTasks, not CompleteTask). If you find another one Whisper produces consistently, it's a one-line addition to the regex.
- Voice-to-reopen a completed task isn't shipped — use the companion app's checkmark icon or "add task X" fresh.
- Tasks are separate from Reminders. If you want a time-triggered alert, use "remind me to X at 5pm"; if you want a checklist item, use "add task X".
Companion v0.8.5 — alpha¶
Companion-only. Watch stays at v0.5.1.
One path for SMS + messaging apps¶
Intent.SendMessagenow covers every send-a-message path. SMS, WhatsApp, Telegram, and Signal all route through one handler against one registry — same slot shape (app/contact/body), same confirm prompt format, same learning surface. SMS still sends programmatically viaSmsManager.sendTextMessage(no compose screen, no tap), so existing "text Mom hi" / "tell Dad I'll be late" phrasings keep working exactly as before. Architectural cleanup; no behaviour change for users on the SMS path.- WhatsApp / Telegram / Signal voice commands are recognised but gated. Saying "WhatsApp Mom on my way" / "Telegram Dad" / "Signal Alex" now lands cleanly on the confirm prompt ("action: whatsapp / details: [Mom] on my way"), but dispatch returns "Can't send to WhatsApp yet" — clearer than the previous "Unknown command". The deep-link path opens a pre-filled draft but doesn't actually send for sideloaded callers; we'd need an AccessibilityService to drive the Send button. That permission ask is heavyweight (system-wide screen access), so it's deferred behind the more important question: do users want that trade?
Notes for power users¶
- Under the hood: removed
Intent.Sms,SmsHandler,SmsSlots. Their logic lives inSendMessageHandler+SendMessageSlots+MessagingTargets.Sms. Existing learned phrases survive via a Room migration (nlu_examplesschema 1 → 2 rewritesintent='Sms'rows to'SendMessage'). - Whisper-variant aliases for WhatsApp now include "what's up" / "what's app" / "whatsap" — when Whisper mishears "WhatsApp John" as "What's up John", it still routes correctly.
Companion v0.8.4 — alpha¶
Companion-only. Watch stays at v0.5.1.
Small follow-ups¶
- Path hints follow the new Settings drill-down. Three in-app messages still pointed at the old flat Settings layout — "Settings → Models", "open Settings and tap Scan installed apps", "Download a Whisper model in Settings first". Updated to "Settings → Models → Intent classifier", "Settings → Learning → Installed apps → Scan", and "Settings → Models" respectively so the path the watch chat or in-app error tells you actually matches what's in the UI.
Companion v0.8.3 — alpha¶
Companion-only. Watch stays at v0.5.1.
Reliable open <app> from the watch¶
- Cold-start
open <app>finally works — say "open Spotify" / "open YouTube" / "open Maps" with the phone locked + the app force-stopped, and the target app foregrounds. Previously Android 14+'s background-launch restriction silently blocked these (the companion reported "Opened …" but the target stayed in the background — the long-standing Reddit complaint). - The grant. Wristotle now declares the "Display over other apps" permission, and the Permissions tab has a new Reliable background app launch (optional) card with a button to grant it. Wristotle never actually draws any overlay — only the permission grant is what unlocks Android's BAL allow-list. Off by default; flip it on if you want reliable cold-start launches.
play <app>partial improvement. The grant also makesplay <app>reliably open the target app cold, but auto-resuming playback depends on the target media app. Most major media apps (Spotify, Tempo, etc.) only accept playback commands from Google Assistant — we can open them but you may still need to tap play. The honest story is on the voice commands page.
Notes for power users¶
- The fix is
SYSTEM_ALERT_WINDOW. Confirmed empirically across YouTube, WhatsApp, Tempo, Spotify, and Maps. Investigated alternatives that didn't work:RoleManager.ROLE_ASSISTANT, MediaBrowserService binding,MEDIA_PLAY_FROM_SEARCHintents. All gatekept by either the OS (ROLE_ASSISTANTexemption is for system-privileged installs only) or by the target app (MediaBrowser whitelists by signing cert). - Permissions tab now lists 4 cards: Watch Bridge (existing), Voice Input (existing), Media Control (existing), and Reliable background app launch (new). The new card is optional and the badge logic doesn't require it.
Companion v0.8.2 — alpha¶
Companion-only. Watch stays at v0.5.1.
Bug fixes driven by live testing¶
- Permissions tab dot finally clears under Core Devices. The companion was caching its own process as the "active BLE companion" because of a self-bind that slipped past the detector. That misclassified Core Devices users as Unknown, which lit up Whisper-related UI (Voice Input rows, Audio Capture card) and held a red dot on the Permissions tab even after granting all the actual permissions. The detector now refuses self-binds outright and heals any pre-existing pollution on the next cold start — existing users recover automatically without clearing data.
- Settings tab badge respects the active companion. Under Core Devices the Settings tab no longer badges over "no Whisper model installed" — there's no reason to install one when Whisper isn't in the dictation path.
- Settings landing shows which category needs attention. When the Settings tab badges, a small red dot now sits next to the specific category row (Models, etc.) so you can see what to tap into.
- "Amend my notes" now appends. Whisper consistently mishears the verb
appendasamend— that was silently failing classification. Both spellings now route to the AppendNote handler. Same for "amend my last note", "amend to the notes: …", etc.
Companion v0.8.1 + Watch v0.5.1 — alpha¶
Update both apps — the watch fix only takes effect when paired with a companion that's reading the new flag, and the companion's new hint only fires when the watch's classifier is the stub. Mixed versions still work but you'll miss the fixes.
Bug fixes driven by r/pebble alpha feedback¶
- "Remind me ..." and "Cancel ..." now trigger the confirm prompt. The Watch was using a legacy voice channel for these two commands that didn't carry the per-query confirm flag, so they bypassed the gate even with the toggle on. Both channels now ship the flag.
- Calendar (and other natural-language) commands without the Intent model give a useful hint. Instead of falling back to "Unknown command:
" , the watch chat now shows "Need the Intent classifier model. Install in the companion: Settings → Models." Install it from there to enable calendar / paraphrased commands.
Polish¶
- "Confirm before send" is now the consistent name in all three surfaces — the watch's Clay config UI (formerly "Confirm action"), the companion's Watch settings card (formerly "Confirm action before running it"), and the docs site. Same toggle, no behaviour change.
Companion v0.8.0 — alpha¶
Companion-only. No watch app update needed; the watch stays at v0.5.0.
Reminders without a time now work¶
- "Remind me to buy milk" used to dead-end with "Couldn't understand the time." Now it sets a reminder 30 minutes from now (configurable) and shows it back so you can see what landed.
- Pick your default. New Settings → Reminders → Default time when none spoken — 5 / 10 / 15 / 30 / 45 / 60 / 90 min, default 30. Changes apply to the next reminder you dictate.
- Plays nicely with Confirm before send. If you have that toggle on, the confirm prompt shows the defaulted time so you can cancel + redictate if 30 minutes was the wrong default for that particular reminder.
Settings tab — drill-down navigation¶
- The Settings tab was scrolling well past the fold by the time Notes, Reminders, and Backup all landed. Tap a category from the new landing list to drill into just that screen — fewer cards, more focus.
- Categories: ⌚ Watch · 💬 Conversation · 📝 Notes · ⏰ Reminders · 🧠 Models · 🎓 Learning · 💾 Backup & Restore · 🔧 Diagnostics.
- System back returns to the landing.
Visual polish¶
- Per-tab colored bottom navigation. Each bottom-nav tab has its own brand color now — Conversation blue, Notes amber, Permission green, Settings purple — so the selected tab and indicator pill are immediately recognisable.
- "Permissions" → "Permission" to fix a label that was wrapping onto two lines on narrower phones.
Companion v0.7.0 + Watch v0.5.0 — alpha¶
Update both apps — confirm-before-dispatch uses a fresh wire protocol that only works when both halves are on this release.
Confirm before dispatch¶
- Catch misheard commands before they fire. Turn on Settings → Watch → Confirm before send and any voice command that does something — call a contact, send an SMS, set / cancel / reschedule a reminder, add a calendar event, open an app, start media playback — pauses on the watch with a "Action:
/ Details: " summary before it runs. - SELECT to send, BACK to cancel. A cancelled command logs in your Conversation history as "Cancelled" so you can see exactly what you backed out of (and can re-dictate).
- Configurable timeout + default action. Pick how long the prompt waits before auto-resolving — 5s / 10s / 15s (default) / 30s / 1m / Never — and choose whether the timeout cancels (default) or sends through. Both live under Settings → Watch → Confirm before send once the main toggle is on.
- Read-only commands stay snappy. "What time is it", "battery", "what's on my calendar today",
pause,next,play,find phone, list reminders — none of these prompt for confirmation even with the setting on. The prompt only appears for actions that send, call, write, or launch. - Off by default. Flip it on if you've ever had a misheard contact name send the wrong SMS.
Smaller fixes that came along¶
- "text, Mom hi" no longer becomes "Text text,?" Whisper occasionally inserts a leading comma after the verb; the parser now strips it cleanly before resolving the contact.
- "Cancelled reminder" reaches the cancel handler. The cancel verb regex now accepts past-tense and present-participle forms, so the watch's "Cancelled" auto-reply doesn't accidentally classify as an unrelated query.
Companion v0.6.0 — alpha¶
Companion-only. No watch app update needed; the watch stays at v0.4.0.
Backup & Restore¶
- Export your data to a ZIP. A new Settings → Backup & Restore card lets you save your notes, conversations, reminders, app aliases, learned phrases, and settings out to a file you pick (Drive, OneDrive, Files, anywhere). You can also include your dictation audio recordings if you want them in the backup.
- Optional password protection. Set a password on export and the ZIP is encrypted with AES-256. Leave it blank for a plain ZIP. If you lose the password the backup cannot be restored — write it down somewhere safe.
- Restore on a new phone (or the same one). Pick the ZIP and the companion shows you what's inside before doing anything. Confirm and it merges the backup into whatever's already on the device — existing notes / conversations / phrases stay; duplicates are skipped; settings are overwritten with the backup's values.
- Per-entity restore summary. When the restore finishes you see a small table with Total · Imported · Duplicates · Failed counts per category (Notes, Conversations, Learned phrases, Audio, Reminders, Aliases) so you know exactly what happened.
- Multi-clip notes round-trip correctly. If you use the "separate WAV per append" notes setting, every clip is preserved through export and restore.
Notes for power users¶
- The backup format is per-entity JSON inside the ZIP (not raw database files), so it stays readable across future companion versions even when database schemas change. You can inspect a plaintext backup with
unzip+jqif you want to see exactly what was saved. - Whisper / NLU model files themselves are not in the backup — they're large and re-downloadable from the Models card. Your active model selection is in the backup so the Models card will prompt to redownload after restore.
Companion v0.5.0 + Watch v0.4.0 — alpha¶
Most of what's below touches both apps — Notes and Quick Launch action need new wire protocol and a new on-watch screen, so update both to companion v0.5.0 and watch v0.4.0. The app-aliases work is companion-only.
Voice notes (update both apps)¶
- Take a note by voice — "take a note: pick up bread" or "make a note that the wifi password is 1234" creates a note. Speaking is the fast path; you can also type one from the new Notes screen in the companion.
- Append to your last note — "add to my last note: also milk" or "append to my note: bullet two — gluten free" grows the most recent note instead of creating a new one.
- Audio is kept (configurable) — your dictation audio is saved alongside the text and playable from the companion's Notes screen. Choose between a single continuous WAV per note (each append extends it) or a separate WAV per append, in Settings → Notes. Retention is per-note configurable.
- Browse notes on the watch — long-press UP from the chat screen to open the on-watch Notes list. Tap a row to see the full body with its timestamp. (Also reachable through Quick Launch — see below.)
- Manage notes on the phone — the new Notes tab shows the full list with edit, delete, and inline audio playback.
Quick Launch action (update both apps)¶
- A new setting under Settings → Watch → Quick Launch action lets you pick what your watch's Quick Launch shortcut does:
- Dictate (today's behaviour) — go straight into dictation.
- Notes — go straight into the on-watch Notes list.
- Show menu — open a two-row chooser so both Dictation and Notes are one click away.
- Available from both PKJS Clay and the companion's Watch settings card.
App aliases (companion-only)¶
- Override Whisper mishearings and nicknames for
open <app>. If Whisper hears "Yotube" for YouTube, or you want "GMA" to mean Gmail, add an alias under Settings → Apps → Aliases and that phrase will route to the right app from then on. - One-tap quick-add from conversation history. Misrecognised
open …rows in the conversation log have an inline button to save the missed phrase as an alias for the app you actually meant — no need to type it again. - Dangling aliases auto-prune. If you uninstall the target app, the next app rescan drops aliases that point nowhere, so the dropdown stays clean.
Fixes & cleanup (update both apps)¶
- Debug logging now defaults to OFF everywhere — watch, Clay, and companion. Previous releases shipped with the watch + companion defaults set to ON while Clay's default was OFF, so fresh installs disagreed about whether logs were enabled. Existing settings are preserved; new installs ship quiet. (both)
- Clay's "skip retry dialog" toggle now actually persists to the watch. It was a silent no-op due to a messageKey mismatch between Clay and the watch's
package.json. (watch) - Quick Launch "Show menu" no longer silently falls back to Dictation. The watch's settings inbox handler was only mapping two of the three enum values; now all three (Dictate / Notes / Menu) are honoured. (watch)
- The Models card uses a tiered picker that doesn't keep growing as new Whisper / NLU model variants ship. (companion)
Companion v0.4.0 + Watch v0.3.1 — alpha¶
The reminder features below are companion-only (no watch app update needed for them). The dictation reliability work and cleanup fixes touch both apps — update the companion to v0.4.0 and the watch app to v0.3.1 to get all of them.
Reminder interactions (companion-only)¶
- List your reminders — "what are my reminders", or ask about a specific time ("is there a reminder at 2pm") and it answers for that hour. Reminders stay listable for 24 hours after they fire, then auto-clear.
- Cancel a specific reminder — "cancel the gym reminder", "cancel my 5pm reminder". Matches by title or time; a target that matches nothing says so instead of cancelling the wrong one. (Bare "cancel that" still cancels the most recent.)
- Reschedule / snooze — "push my reminder to 6pm", "snooze for 10 minutes", "reschedule my gym reminder to noon".
Faster, more reliable dictation¶
- No more spurious "Didn't catch that" from a slow first dictation. The Whisper model is now pre-warmed at startup, and a cancelled dictation aborts its inference immediately instead of making the next one wait — so back-to-back dictations no longer pile up and time out.
Fixes & cleanup (update both apps)¶
- "schedule a meeting…" now reliably creates a calendar event instead of occasionally falling through to "Unknown command". (companion)
- Reminder titles are no longer truncated when they contain words like "chat" or "finish". (companion)
- Plugged several memory leaks — recognizer/model handles and the on-device intent ONNX session — that could accumulate over cancelled dictations and model switches. (companion)
- The watch no longer leaves a companion-response timeout running after the app closes. (watch v0.3.1)
v0.3.0 — alpha¶
Voice calendar¶
- Ask about your calendar — "when is my next meeting", "what are my next 3 meetings", "what's on my calendar tomorrow", "do I have a meeting on May 25". Reads your phone calendar (via
CalendarContract), covering a 30-day window with recurring events expanded to individual occurrences. - Create events by voice — "schedule a meeting tomorrow at 3pm", "add a meeting with Alex Friday at noon called standup", "book a half hour meeting Monday at 9". The title defaults to "Meeting" when you don't say one, and the duration defaults to 60 minutes ("for one hour" / "30 minutes" / "half hour" to override). The watch echoes back what was created so a misheard time is visible.
- Both need the new Calendar permission (asked in the first-run flow / Permissions tab). Companion-only — no watch app update required.
- Deleting events by voice isn't supported yet; it's planned alongside a confirm-before-acting step.
Fixes¶
- Contact matching now applies a similarity floor, so a poorly-transcribed name no longer silently snaps to a dissimilar contact.
v0.2.0 — alpha¶
Watch settings, from the phone¶
- Manage the watch app's settings from the companion — a new Settings → Watch section mirrors the settings that previously lived only in the BLE companion's Clay config screen (vibration toggles, dictation confirmation, find-phone / reminders / cancel routing, quick-launch auto-exit, skip-retry-dialog, debug logging). The watch stays the source of truth; the companion fetches a snapshot and writes changes back over AppMessage.
- Edits are staged behind a Save to watch button (no auto-save on each toggle).
- The section re-fetches fresh each time you open it.
- Keep the Wristotle watch app open while loading or saving — Pebble only syncs with the app that's currently running. If the watch app is too old to support this, the card tells you to update it.
Fixes¶
- A dictation with no Whisper model installed now returns a clear "No speech model installed — download one in Wristotle settings" instead of the placeholder "hello world".
Under the hood¶
- Release notes on Codeberg are now auto-generated from the commit log between tags, so each release page lists what actually changed.
v0.1.0 — alpha¶
Initial public release. Both repos cut at the same tag.
Companion features¶
- On-device speech recognition via whisper.cpp. Multiple downloadable models (tiny/base/small) with quantized variants. Active model swap without app restart.
- On-device intent classification via MiniLM-L6-v2 INT8 (~23 MB). Natural-phrasing support for
call/text/reminder/cancel/find phone/open <app>/play <app>/pause/next/previous/seekintents. - Implicit learning — successful commands optionally added to the intent classifier's example bank for better future recognition. Toggle in Settings → Learning → Intent.
- Conversation history — Room-backed chat-style log of every command and response, with user-configurable retention (1 / 10 / 20 / 30 days, default 10).
- Optional dictation audio capture — last 5 WAVs saved to app-private storage, off by default. Inline playback button on chat rows.
- System-wide voice input provider — Wristotle can be set as Android's default
SpeechRecognizer, so any app that uses the standard speech API transcribes through us. - Cross-app media control —
play <app>/pause <app>/next <app>/previous <app>work across any media app that registers aMediaSessionorMediaButtonReceiver. Three-tier targeting strategy (session controller → targeted MEDIA_BUTTON broadcast → system key event) handles Flutteraudio_service-style apps that register lazily. - App launcher —
open <app>/launch <app>/fire up <app>work against a locally-indexed list of installed launcher apps. Scan from Settings → Learning → Installed apps. - In-app diagnostics export — Settings → Diagnostics builds a markdown bug-report bundle (app/device/permission/model/AppIndex/conversation/log state), copies to clipboard, opens Codeberg new-issue page. Privacy-redacted by default.
- Three nested Settings sections with collapsible info icons — Models (Speech + Intent), Learning (Installed apps + Intent), Conversation (History + Audio).
- R8 minification + resource shrinking on release builds (24 MB APK).
- arm64-v8a-only ABI filter to keep APK size down.
Watch features¶
- DictationSession-based voice input with vibration feedback (configurable). Quick-launch (long-press a button) opens straight into dictation with optional auto-exit.
- Chat-style on-watch history (last 5 entries).
- Local commands for time, battery, steps, vibrate, find-phone — handled fully on-device without companion roundtrip.
- Configurable routing for find-phone, reminders, cancel — companion or PKJS, with auto-fallback.
- DictationSession auto-recreate after
FailureSystemAborted— recovers from stuck SDK state without requiring a watch reboot. - Skip-retry-dialog setting — when on (default), dictation failures land in the chat instead of trapping the user on the mic view under the SDK's "Error / Try Again" modal.
Fixes shipped in v0.1.0¶
- Whisper subtitle-annotation hallucinations (
*Door opens*,[laughter],(silence)) on silence input are filtered before reaching the chat. - Rapid retry storm after zero-audio sessions (upstream pipe-race amplified by microPebble's collectLatest) is suppressed in
WhisperRecognitionService— cooldown window of 500 ms. MediaPause/MediaNext/MediaPreviousaccept an app body — "pause Spotify" pauses Spotify specifically, not whichever session is most recently active.- AppIndex.lookup has reverse-substring matching — "absorbed" → "Absorb" recovers from Whisper mishearing.
Known limitations¶
- "Watch firmware DictationSession is wedged" still requires a watch reboot in some cases — the SDK-level recreate landed in v0.1.0 but doesn't fully cover BLE-bridge-side state. See Troubleshooting → Dictation isn't available.
- App alias UI for cases where Whisper consistently mishears an app name (e.g. "absolved" → never resolves to "Audible") is queued but not yet shipped.