halkyon/docs/adr/0038-halkyon-meditate.md
Tizian 「ティツィアーン」 c36bd83c6a
feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling
RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person
chose in Android, and puts the text it returns where the cursor stood,
as Dictation does; cancelled, nothing. Only where none is installed, or
it can't be opened, does speech_to_text listen in line as before.

halkyon_app: VoiceInputApp (one listen, an answer once closed) on the
channel app.halkyon/voice_input, which DeviceSpeechInput asks first at
every listen, answering SpeechStart.finished so that Dictation doesn't
listen again; speech_to_text isn't touched on that route, so our app
asks for no microphone. SpeechInput.listen and Dictation.start take the
prompt the voice input app shows (the field's hint).

Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult,
the first of EXTRA_RESULTS) attached in MainActivity, which hands it
activity results, and RECOGNIZE_SPEECH among the manifest's queries.
The privacy pages of Learn and Meditate say so.

Our own Whisper and Meditate's dream recordings are dropped (D18).

Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input.
Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 11:48:42 +01:00

29 KiB

ADR 0038: Halkyon Meditate, what it does and how it looks

  • Status: proposed 3 Nov 2026 by the agent, the developer having asked to "continue as far as possible without my interjection"; every decision below is the agent's recommendation unless it says the developer's, and is put to the developer with the first version. Built the same day: steps 1 to 4; the dream journal (D14, the developer's request of the same day) as step 5.
  • Builds on: ADR 0033 (Todo's shell, which Meditate copies as Train copied Todo's, ADR 0030, D12); ADR 0030, D7, D14, D15 and D61 (sealed rows, the person's key, What's new); ADR 0031, D5 and D7 (Meditate's icon: Lucide's sprout, cobalt through pacific into fern); ADR 0014 and 0016 (the module and the register).

Context

On 3 Nov 2026 the developer asked for Halkyon Meditate, following "the basic approach as the other applications": the bar and the rail, the theme, the settings and account screens; and to look at eleven screenshots of 7Mind's Android app (1 Nov 2026, ~/Downloads/7mind-screenshots/). On 2 Nov they chose fern as Meditate's backdrop (ADR 0031, D5). What the screenshots show:

  • Three tabs: For you, Explore, Profile.
  • For you: a greeting with the person's name; a band of newly added content, each a card with its kind (Meditation, Sound, Sleep Story, Movement, Morning Stories, a course of several tracks), its length and its speaker; Your favorites and Recently played, each a row of the same cards with More.
  • Explore: a search field, four goals as large tiles (reduce stress, sleep better, learn to meditate, take care of yourself), categories as a row of icons (autogenic training, breathing, dream journeys, knowledge, meditation, morning stories, movement, progressive muscle relaxation, routines, sleep stories, sounds), and topics as a list of cards with a line each (sleep, strength, personal development, relaxation, feelings and relationships, daily life, silence and sounds).
  • Profile: minutes, sessions and days as large figures; the week's days as dots with the days in a row; Favorites, Downloads and Your path; then Account, Subscription, Notifications, Language, Appearance, the streak's visibility, help, legal information.
  • No player is shown; 7Mind's content is recorded speech by its speakers, behind a subscription.

Halkyon has no recorded meditations, and none may be taken from 7Mind (its content, texts, names and pictures are its own). Everything below is Halkyon's own.

Decisions (3 Nov 2026, the agent's, to be confirmed)

  • D1 Meditate's frame is Todo's and Train's (ADR 0033, D1; ADR 0030, D60): go_router with a StatefulShellRoute, each tab keeping its own stack, halkyon_ui's NavigationFrame (the bar below medium windows, floating above the foot at first and flush with it, a fade above it, since 7 Nov 2026 (ADR 0030, D60's amendment), a rail with Meditate's icon on top from medium on), halkyon_app's first start, sign-in and account, What's new after an update, About, English and German. Copied from Todo's and renamed, as Train's was (ADR 0030, D12), Todo's own features left out.
  • D2 Three tabs: Today, Explore, Profile, as 7Mind has three, Today opening (four since D14, the Journal between Explore and Profile). Today (not For you: nothing is chosen for the person by anyone) starts a sitting at once, shows the week and the streak, the favourite practices and the ones done last. Explore holds every practice, by kind and by what it is for, with search. Profile is every Halkyon app's (HalkyonProfile, decided 2 Nov 2026): the person's own tab first, then Meditate's Practice tab with the figures, the week and the sittings. The cog to Settings stands atop Today and Profile, as Train's does atop Home and Profile. Over four tabs with the timer as one: the timer is one tap from Today and from Explore, and a sitting is a screen over the tabs (D6), as Train's recording is.
  • D3 The washes are fern (the developer's, 2 Nov: ADR 0031, D5): every tab and the sitting's screen wash down from the top in PaletteColor.fern; buttons and links stay cobalt, as in every app.
  • D4 What is practised: Halkyon's own practices, guided by text and bells, and the silent timer. Without recorded speech, a practice is a sitting of a set or chosen length with cues: short lines of guidance shown on the sitting's screen one after the other, spread evenly over the length chosen (so a practice scales to each of its lengths), and later read out by the device's text-to-speech (see Later). The practices are written for Meditate in English and German (practice.dart holds what each is, the ARBs its words), so their licence is the app's. Version 0.1.0 has six: counting breaths, a three-minute pause, a body scan, open awareness, kind wishes (loving-kindness) and settling for sleep, under the kinds Breathing, Body, Awareness, Kindness, Sleep and Pause, and the goals Calm down, Sleep, Focus and Begin; breath at the nostrils, walking, the 4-7-8 breath and box breathing come next, the breathing ones with a paced ring. A practice's ID never changes once released. The silent timer is a sitting of any length from 1 to 120 minutes with a bell at the start, at the end and, if chosen, every 1 to 30 minutes between. Recorded guidance (the instance's or the person's own files) is a later step: it needs the instance to hold and serve files, their licences checked, and their downloads kept on the device.
  • D5 The bells are made for Meditate: three sounds (a singing bowl, a small bell, a wooden block) synthesised by apps/meditate/tool/bells.py (Python's standard library alone: decaying partials with the bowl's beating) into assets/bells/*.wav, so they carry the app's licence and no one else's; played with audioplayers (already in the workspace's lock, Learn's). Over recorded bells from freesound.org and the like, whose licences differ file by file.
  • D6 A sitting is a screen over the tabs: a ring filling as the time passes, what is left inside it counted down by the seconds begun (5:00 until a second has passed), the practice's cue of the moment, Pause and Resume, and End (also the close button and back). Time is counted from the clock, not from a ticker, so a sitting paused by the system or the app going to the background is right as it comes back. The screen stays on while it runs where the person's setting says so (on by default, wakelock_plus, as the lock already holds it). Ended early, it asks whether to keep what was sat (Keep, Discard, or Sit on); a sitting under a minute is never kept and ends without asking. At the end, the bell, and Done with what was sat. Bells while the app is in the background are a later step (a scheduled notification's sound), as is reading the cues out.
  • D7 What is kept: sealed rows, synced, as Todo's and Train's (ADR 0033, D5; ADR 0030, D7, D15): the person's key (content_key), one settings row (meditate_settings: the bell, the interval and starting bells, the screen kept on, the favourite practices, whether the streak shows) and a row a sitting (sitting: the practice or the timer, when it started as a wall-clock time with the device's offset, how long it was planned and sat). Everything is in the sealed payload; the instance sees that rows exist, their sizes and when they changed. Called sitting, not session, as Serverpod's Session is every endpoint's. A person's data goes with them (deleteMeditateData, the sync spaces' cascade, as Todo's).
  • D8 The figures are worked out on the device from the sittings: minutes, sittings, days with a sitting, the days in a row (up to today, or up to yesterday while today has none yet, so a streak doesn't break before the day is over), and the week's days as dots from the week's first day in the person's language. The streak can be hidden (Settings, as 7Mind's Streak visibility), as some find streaks a pressure.
  • D9 Today greets by the time of day (no name: the account has a handle, not necessarily a first name), offers Sit now with 5, 10 and 20 minutes and the timer's own length, the practice of the day (one after the other by the date, the same on every device), the week with the streak, the favourites and the last three sittings.
  • D10 Explore is a search field (titles, summaries and kinds, every word typed, without case or accents), the goals as four tiles (chosen again to let go), the kinds as chips, and every practice as a card, narrowed by what is chosen. A practice's page says what it is, its length (a choice of lengths where it scales), how it goes (its cues in order), its favourite heart and Start.
  • D11 Settings as Todo's (ADR 0033, D4): Sync's card first, the operator's card, then Meditate's own Sitting card (the bell, the starting bell, the interval bell's default, the screen kept on, the timer's default length) and Practice card (the streak shown), the account, What's new and About, the version below.
  • D12 No reminders and no notifications in the first version: a daily reminder at a time the person sets is the first thing to add after it (Todo's notifications show the way).
  • D13 Development ports of its own: API 8280, Insights 8281, web 8282, PostgreSQL 8290 (Learn's and Todo's are 8080 to 8090, Train's 8180 to 8190), so Meditate's server runs beside the others as a member of Learn's register.

The dream journal (3 Nov 2026, the developer's request)

  • D14 A dream journal, asked for by the developer on 3 Nov 2026 with what an entry holds, that it is sealed and synced as the sittings, the Journal's list, search and filters, Today's morning card and its switch in Settings, the empty state's word on sealing, quick writing, and optional figures in Profile. What follows beyond that is the agent's recommendation, to be confirmed:
    • An entry (Dream): the night, named by the morning after it (a date at midnight UTC, as every day the app counts) and by default today's date at any hour, so "last night" even when written at 1 o'clock; a title, empty where none; the text; the marks lucid, nightmare and recurring, as a set; clarity 1 to 5 or none; the mood on waking from five (glad, calm, puzzled, sad, uneasy: few enough to choose half awake, a mixed or neutral one left out as none says it); free labels (people, places, themes, one list rather than kinds of label, each once whatever its case, those used before offered by how often); and when it was first written, kept through edits. Marks and moods by name in the payload (schema 1), so never renamed; a value a version doesn't know reads as none.
    • Its own table, dream, a row a dream, sealed for its table and row as the sittings (D7), rather than rows in sitting or one row of all dreams: a dream is changed and deleted on its own, and a row each keeps the sync's merges to the one changed.
    • Where it lives: a fourth tab, Journal, between Explore and Profile (Today, Explore, Journal, Profile), as the developer suggested: a dream is written every morning and searched later, a list of its own like Explore's, which under Profile would be two taps deep and crowd the person's figures; four destinations still fit the bar on a phone. Its icon is Lucide's moon-star. A dream opens over the tabs, as a sitting and Settings do, for the whole screen while writing.
    • The Journal: the newest month first, each month a card with its dreams the newest night first; a search over title, text and labels as Explore's (every word, without case or accents); the marks as filter chips, a dream shown where it carries every one chosen; + above the bar as Train's buttons. Empty, it says what it is for and that every entry is sealed on the device before it syncs.
    • Writing: the text field first, focused, the rest (night, title, marks, clarity, mood, labels) folded below it for a new dream and unfolded for a kept one; Save once there is text. A kept dream opens to read, with Edit and Delete (asked first); leaving a dream written but not saved asks first.
    • Today's card from 4 to 12 o'clock while last night has no dream, below the greeting, leading to a new dream; switched off in Settings' Journal card (dreamPrompt, on at first, in the sealed settings: schema 1 stays, as a reader that doesn't know it reads it as at first).
    • Profile's Practice shows, once the journal holds a dream, a small Dream journal card: the dreams of this month and the lucid ones of all.
    • Not done: reminders to write (D12), dreams linked to sittings, figures over time.
  • D15 Dreams spoken (3 Nov 2026, the developer's request): speech-to-text in the journal's editor, as Learn's assistant field takes questions spoken (Halkyon Learn's ADR 0021, D67: the device's own speech recognition, speech_to_text). The agent's recommendations, to be confirmed:
    • Shared, not copied: Learn's SpeechInput and DeviceSpeechInput move into halkyon_app with their fake, Learn importing them from there unchanged; beside them Dictation, generic, which writes what is heard where the cursor stands (in place of a selection, set apart by a space on either side where the text has none) and shows the words so far. Learn's button stays Learn's: it sends, and replaces from where it started. Todo and Train link the plugin now but declare no microphone, so nothing there listens.
    • In the editor: a microphone under the text, where the device recognises speech (none on Linux, as Learn's); listening, it turns into Stop with "Listening…" and the words heard beside it. It listens in the app's language, else the device's own.
    • A dream is longer than a question: it restarts by itself while the button stays on, rather than offering to continue: half awake, a tap after every pause would interrupt the telling, and the button says throughout that it listens. Each listen may pause 30 s and last 10 minutes (Apple's recogniser stops sooner, Android's after a few seconds' silence whatever is asked), and after three listens running hear nothing it stops by itself, so a phone left on the table doesn't listen on.
    • Privacy: on Android Meditate asks for recognition on the device (DeviceSpeechInput(preferOnDevice: true): Android 12's on-device recogniser where the language is installed, else the recogniser with prefer offline); should that fail before a word is heard, it listens as Android otherwise does from then on, which may send the sound to Google or the phone's maker. Apple's and Windows' recognisers choose for themselves (the plugin's on-device flag on Apple fails outright where unsupported, so it isn't asked). The instance never hears it. The privacy page says so where Meditate's server serves it (privacyPagePoints in meditate_base.dart, with what Meditate keeps sealed), as Learn's does.
    • Today's card offers Speak it beside it, opening a new dream already listening.
    • The platforms declare it as Learn's do: RECORD_AUDIO and the recognition service's query on Android, the microphone's and speech recognition's usage texts on iOS and macOS, and macOS's audio-input entitlement.
  • D18 Dictation through the phone's voice input app (7 Nov 2026, the developer's decision, amending D15 for Learn's, Todo's and Meditate's dictation alike): on Android the microphone opens the app handling RecognizerIntent.ACTION_RECOGNIZE_SPEECH (FUTO Voice Input, or Google's), whichever the person made the default in Android itself, and puts the text it returns where the cursor stood, as D15's dictation does (halkyon_app's DictationInsertion); cancelled, nothing. Only where none is installed, or it can't be opened, does D15's in-line recogniser (speech_to_text) listen; where neither exists the microphone stays hidden. Text only: no sound is kept, and our app asks for no microphone on this route, as the voice input app records.
    • Why: FUTO Voice Input recognises on the device with Whisper, but offers no SpeechRecognizer (RecognitionService) API, registering only a placeholder service, so speech_to_text can't reach it; it does handle the intent, in a floating window, and returns text. Our own Whisper (whisper.cpp in a package of ours, the whisper-spike branch) is not pursued: a model to download and a native build per CPU, for what an installed app already does. FUTO's code is under the FUTO Source First License, which doesn't go with our AGPL, so it is used as a separate app only, never linked or copied. And the dream's recordings (audio kept beside the text, synced end to end encrypted in a table of their own) are dropped: the transcript is enough, and the intent returns text, not audio.
    • How: VoiceInputApp in halkyon_app (one listen, an answer once closed: its own shape, as SpeechInput streams words and Dictation listens again after each pause), on each app's VoiceInput.kt over the channel app.halkyon/voice_input (installed by queryIntentActivities, listen by startActivityForResult, the first of EXTRA_RESULTS), with EXTRA_LANGUAGE_MODEL free form, EXTRA_LANGUAGE the app's language and EXTRA_PROMPT the field's hint (Meditate: What did you dream?); each manifest declares the intent among its <queries> (Android 11 on). DeviceSpeechInput asks it first at every tap, answering SpeechStart.finished, after which Dictation doesn't listen again. While it is open the button shows Stop and nothing else listens; Stop drops what it returns later.
    • Consequences: on a phone with Google's app, which handles the intent too, its dialog listens now rather than the in-line recogniser, one utterance a tap, and D15's recognition on the device (preferOnDevice) applies only where no app handles the intent; which recognises on the device is then that app's choice.

The operator's library (3 Nov 2026, the developer's request)

  • D16 Guided content the operator provides, asked for by the developer on 3 Nov 2026, structured as 7Mind's catalogue is (items with a title, a category, a description, a speaker and recordings; categories; courses of ordered lessons). Only the structure is taken: nothing of 7Mind's (texts, recordings, names, ids) is in the repository, its tests or its fixtures, whose entries are made up. The agent's recommendations, to be confirmed:

    • A folder and a manifest. The operator mounts a folder into Meditate's server and names it in MEDITATE_LIBRARY_DIR: recordings and pictures, and library.yaml, YAML rather than JSON because operators write it by hand and want comments (a commented, made-up example: halkyon_meditate_server/library.example.yaml). An item: id (the operator's slug, a-z, 0-9 and -, which sittings, favourites and lessons done refer to, so never changed), title, category, description, speaker, language (BCP 47), image, one or more recordings under audio (each a file, a label telling them apart by length or voice, and a duration), and licence and credit, required, with an optional licence-url. A course: id, title, description, category, language, image and lessons in order, each an item with an optional title of its own. The duration is the manifest's, in seconds or m:ss, not read from the file: reading MP3's, AAC's and Opus's lengths without a decoder is unreliable, and a wrong length would count wrong sittings; a check that reads it can come with the deploy's image.
    • Categories: Meditate's eleven keys (meditation, sleep-story, sound, movement, autogenic-training, progressive-muscle-relaxation, routine, dream-journey, knowledge, morning-story, other), named by the app in English and German; and the operator's own under categories: with a key and an English and a German name, never one of Meditate's keys: an instance's library is its own, and "other" alone would hide what it offers.
    • Checked as read, refusing what fails, with the reason: an item without a licence or credit, a recording or picture outside the folder (a .., an absolute path or a link leading out, each resolved), missing, or of another type (recordings: MP3, M4A, AAC, Ogg, Opus, FLAC; pictures: JPEG, PNG, WebP), a wrong id, category or language, no duration, an id taken twice, a course pointing at an item not served; the rest is served. A manifest that doesn't read at all leaves the library as it was served (an edit half done), one removed ends it. bin/check_library.dart <folder> reads a folder as the server would, before it is deployed, and exits 1 on any problem.
    • In memory, not in the database: the manifest is the library's whole truth and small, read as the server starts and again every ten minutes by a future call (LibraryReloadFutureCall, as Learn's dictionaries are reread), an unchanged folder costing a hash of the manifest and a stat of each file. No table, no migration, and nothing to keep in step.
    • For signed-in people alone (LibraryEndpoint.catalogue, requireLogin): the operator's licences may not allow offering it to the public.
    • The files on Serverpod's web server, /library/<served name>, by ranges (relic's StaticHandler: Range, If-Range, ETag), as the platforms' players fetch a recording. A file is asked for by a served name the reading gave it (a hash of its place, size and time with its extension); no request's path reaches the file system. Since players can't all send the session's header, the catalogue brings an access: an hour's start plus 12 hours, signed with a key the server makes as it starts (a restart ends every access; the app asks again), in the URL's query. Without one: 401; one not signed here or ended: 403; any other name: 404. Starting from the hour, the access is the same for everyone given it within that hour, so a fetch names no account (the privacy page says so). Rejected: an endpoint streaming the bytes (Serverpod's endpoints answer whole, no ranges), Caddy serving the folder (it can't check a session or an access without a plugin).
    • In the app: Explore shows the library below Meditate's six practices, with chips of the categories it uses and the same search, its courses (lessons done counted) then its items; an item's page has its recordings, Play, the heart, keeping it on the device, for a sound a silent sitting with it looped (the timer's length), and its credit and licence; a course's page lists its lessons, ticked once done. The player (GuidePlayer on audioplayers, beside the bells') plays, pauses and seeks; played to its end, an item is kept as a sitting of its whole length under library/<id>, so the figures count it, and as its course's lesson done; stopped before, nothing. Playing on with the screen off (Android's foreground service and media notification, iOS's audio background mode, as audio_service does) is for later: not cheap to do well.
    • The person's, sealed and synced: favourites of the library in the settings' list beside the practices', as library/<id>; lessons done in a new table, lesson_done, a row each under an ID keyed by the account, the course and the item, so two devices doing it make one row; both name the operator's ids, so a lesson whose course went counts again should it return.
    • Offline: the last catalogue is kept in the person's folder (library.json), and a recording is kept on the device only on the person's asking, removed again on it (and with their folder); a kept one plays from there.
    • No library, nothing: an instance without one shows no section in Explore and no credits in Settings.
    • Credits: Settings lists every item's credit and licence beside About (Library credits), and each item's page shows its own.
    • The deploy, later (step 6): the folder mounted read-only into Meditate's container (/srv/meditate-library:/library:ro, MEDITATE_LIBRARY_DIR=/library), Caddy forwarding /library/* to the web server with the rest, check_library compiled into the image so an operator checks a folder on the host.
  • D17 What a recording speaks (5 Nov 2026, the developer's request): an item's page says its spoken language; where the library has several, its cards do too and Explore offers them, several at once, none meaning every one, kept in the person's sealed settings (libraryLanguages). Languages group by their primary subtag (de-CH with de). A library with one language asks nothing, and a choice no longer offered is dropped, so that a choice never hides a library.

Steps

  1. The shell (3 Nov): Todo's copied and renamed, its features left out, the three tabs (D1, D2, D3), Settings (D11), the icon (ADR 0031, D7).
  2. The data (3 Nov): the server's models and its migrations, the device's key, settings and sittings (D7).
  3. The practices and the sitting (3 Nov): the catalogue (D4), the bells (D5), the sitting's screen (D6), Explore (D10).
  4. Today and Profile (3 Nov): D8, D9.
  5. The dream journal (3 Nov): D14, the dream table and its migration, the Journal tab, a dream's screen, Today's card, Settings' Journal, Profile's figures; and dreams spoken (D15).
  6. The operator's library (3 Nov): D16, the server's reading, endpoint, files and check, the lesson_done table and its migration, Explore's section, an item's and a course's pages, the player, keeping on the device, the credits.
  7. Released and deployed (later, as Todo's step 5): apps/meditate/release.dart, a key of its own, the F-Droid listing, a snap, deploy/compose.meditate.yaml at meditate.halkyon.eu (its .env values the developer has set already, 2 Nov), its database and role, redeploy.sh and backup.sh taking it in.

Migrations

  • initial: Serverpod's tables, its auth modules', halkyon_base's and the OIDC module's as they stand, as Train's first migration (ADR 0030); only CREATE TABLE, CREATE INDEX and foreign keys, nothing to transform.
  • initial also holds the sync engine's tables (offline_sync_*), as the module is in config/generator.yaml from the start.
  • sittings: Meditate's three tables, content_key, meditate_settings and sitting (CREATE TABLE, each an id UUID made by gen_random_uuid_v7(), which the migration defines again with CREATE OR REPLACE, a nullable spaceId, and the envelope's columns), an index on each spaceId (CREATE INDEX … USING btree) so deleting a person doesn't read the tables, and each spaceId's foreign key to offline_sync_spaces deleting with the space (ON DELETE CASCADE); then the version rows of every module (INSERT … ON CONFLICT DO UPDATE into serverpod_migrations). Purely additive: no row exists to transform.
  • dreams (D14): the table dream as sitting is (CREATE TABLE "dream": id a UUID v7 by default, the function defined again with CREATE OR REPLACE, unchanged; a nullable spaceId; keyId, payload not null and nonce, the envelope), the index dream_space_idx on spaceId, its foreign key to offline_sync_spaces("id") with ON DELETE CASCADE, so a person deleted takes their dreams with their space; then the version rows. A new, empty table: nothing existing changes.
  • library (D16): the table lesson_done as dream is (CREATE TABLE "lesson_done" with the same columns, id a UUID v7 by default, the function again CREATE OR REPLACE), the index lesson_done_space_idx on spaceId (btree), its foreign key to offline_sync_spaces("id") ON DELETE CASCADE, so a person deleted takes their lessons done with their space; then the version rows. The library itself has no table. A new, empty table: nothing existing changes.

Later

  • Recorded guidance (D4), the cues read out by text-to-speech, bells in the background (D6), a daily reminder (D12), sittings written to Health Connect as mindfulness sessions (Android 14's MindfulnessSessionRecord), courses of Meditate's own practices in order; for the library (D16), playing on with the screen off, durations read from the files, the pictures shown.

Consequences

  • The practices are text, short and translatable, so a new one is a change to practice.dart and its words in the two ARBs; a practice once used keeps its ID, as sittings name it.
  • Without recorded voices, Meditate is a timer with guidance more than a library; recorded guidance (D4) is what would make it more, and an instance's library (D16) brings it where its operator offers one.
  • An operator's library is theirs to license: Meditate refuses an item without a licence and credit, but can't check that they hold.