Downstream — audio surface sketches (soundtracks + Pomodoro)
Status: idea / sketch. Nothing built. Authored 2026-07-15. Three feature directions that all ride the audio + nudge plumbing shipped with Downstream (#1266/#1267): an in-ear soundtrack layer on Downstream Glasses (§1), a built-in Pomodoro in the Downstream app (§2), and an ultradian-rhythm engine that makes both responsive to the body's ~90-minute energy wave (§3, the timing layer under the other two). Parent product: DOWNSTREAM.md. Hardware layer: AR_GLASSES_LIVING_VOICE.md, OWNED_HARDWARE_HORIZON.md.
Product tasks: soundtracks 1180ea1a, Pomodoro efdaeff0, ultradian engine
b5dfdad6 (all P3, idea).
Rule that carries over: product name is Downstream Glasses; JQ is the assistant voice on them. These are additive audio features, not a rebrand.
1. Soundtracks in-ear (Downstream Glasses) 🎧
One line: stream a soundtrack — focus/mood audio, or a shared track piped to a group — through the glasses speaker, ducking automatically under JQ nudges.
Status: v0 BUILT 2026-07-15 (migration 489). The looping bed, phase-adaptive pick (
moodForPhase/pickForMood), the ducked-under-JQ glasses looper, voice control ("play/stop music"), and the dashboard panel (import a Suno link + mood, in-browser preview) are all in. Ingestion reuses the studio Suno scrape/re-host (creator-audio-library.ts). The one on-device unknown left to verify on Mentra Live: does a nudge'splayAudioMIX with the bed or INTERRUPT it (decides whetherduckFor's best-effortstopAudiomatters) — the looper is correct either way. Build map: DOWNSTREAM_SOUNDTRACKS_HANDOFF.md. Remaining: v1 mood playlists, v2 shared/synced group beds (below).
Why it's cheap to reach
The forward-voice nudge already plays an HTTPS MP3 through the speaker via
session.audio.playAudio({ audioUrl }) (see apps/glasses).
A soundtrack is the same output path — the new work is continuity (loop /
next-track) and mixing (duck the music when a nudge speaks), not a new
transport.
The real design problems (not the plumbing)
- Continuous vs. one-shot. A nudge is a single cached clip; a soundtrack
loops/streams for an hour. Need a client-side "when this track ends, fetch the
next" loop, and a battery/bandwidth budget. MentraOS plays a finite MP3 per
call, so v0 = a single looping ambient track; a playlist is a queue of
playAudiocalls the session advances. - Ducking under JQ. When a nudge fires mid-soundtrack: lower/pause the track → play the nudge → restore. This is the one bit of real audio-session logic. Owns the "music never talks over your own voice" guarantee.
- Licensing — the gating question. Can't stream copyrighted music. v0 options, in order of safety: royalty-free / generative ambient (focus loops, brown noise, lo-fi beds we can license or generate), then user's own uploaded files, then a licensed provider integration later. Decide this before any playlist UI.
- Shared / synced (phase 2). "Piped to a group" means N wearers hearing the same track in rough sync — a session fan-out (reuse the JQ Bridge / household graph for who's in the room). Sync tolerance is loose (ambient, not a concert), so a shared start-time + track-id is probably enough.
Sketch data model
downstream_soundtracks— curated/generated tracks:id,title,source(curated | generated | user_upload),audio_url,loopable,license_note,moodtags.downstream_audio_sessions— a wearer's active listen:id,user_id,soundtrack_id,started_at,status,shared_group_id?(nullable → phase 2).
Phasing
- v0 ✅ (2026-07-15) — one looping Suno-sourced track per mood, toggled from the
Downstream app or by voice ("play/stop music"); ducks under nudges; the pick is
already phase-adaptive (
getSoundtrackForPhase). Proves the mix. - v1 — mood playlists (a queue of
playAudiocalls the session advances) + richer per-user picks. (Per-user picks + voice control landed early in v0.) - v2 — shared/synced group soundtrack (room-scoped via the Bridge graph).
2. Built-in Pomodoro (Downstream app) 🍅
Status: v0 + v1 BUILT 2026-07-15 (task efdaeff0) — migration 485, downstream-focus.ts
(pure core tested 17/17), focus endpoints, and the dashboard widget + per-item
Focus ▶ button. v0 = client-driven timer + in-voice boundary cue while the
app is open. v1 = the downstream_focus cron (1-min, OFF by default) that
delivers the boundary cue + push when the app is CLOSED; a compare-and-set in
advanceSession keeps the client timer and the cron from double-firing, and a
session self-terminates after its long break so an absent user isn't cycled
forever. See INDEX.md → Downstream.
One line: start a focus session on a Downstream item; get a gentle cue — in your own voice, in-app + web push + glasses in-ear — at each work/break boundary.
Why it belongs in Downstream specifically
Downstream already does the hard part: it triages the maintenance list and says "do this one." Pomodoro is the missing verb — it turns "do this one" into an actual bounded work session, and closes the loop back into the curves: a completed focus session on an item is real signal that it's being maintained, so its stress curve should decay/reset. Triage → act → recalibrate, all inside one product. That feedback tie is what makes this Downstream's Pomodoro rather than a generic timer.
Flow
- From an item (or standalone), Start focus → creates a session (default 25 work / 5 break, long break 15 every 4 cycles; user-editable prefs).
- At each boundary, compose a short cue via the existing nudge/voice path
(
downstream-nudge+/api/jq/voice/say) → in-app banner + web push + glasses in-ear when worn. Same delivery Downstream nudges already use. - On completion: optionally record progress on the item and prompt to
resolve / snooze — feeding the resolve-time calibration that already
exists (
resolution_feedback), and decaying the item's curve.
The one non-trivial engineering choice: boundary delivery
A timer is trivial while the app is open (client-side countdown drives the UI and fires the cue). The wrinkle is firing the boundary cue when the app is closed — Web Push can't self-schedule. Options:
- v0 (simplest): client timer for the in-app/glasses cue while active; if the tab is closed, the boundary push is skipped (or best-effort). Ship this first.
- v1 ✅ BUILT: a scheduled boundary push —
ends_atis persisted and thedownstream_focuscron (runDownstreamFocusTick, 1-min, OFF by default; the standardisCronEnabled+recordHeartbeat+CRON_REGISTRYpattern) advances any due session, delivering the in-voice cue + push with the app closed. A compare-and-set inadvanceSession(CAS on status/phase/cycles) means the client timer and the cron can both fire on the same boundary without a double cue; the session self-terminates after its long break so an absent user gets one set of gentle cues, not an endless cycle.
Sketch data model
downstream_focus_sessions—id,user_id,item_id?(nullable — can focus without a specific item),phase(work | short_break | long_break),work_min,break_min,long_break_min,cycles_target,cycles_completed,started_at,ends_at,status(running | paused | done | abandoned).- Prefs on
downstream_prefs(migration 483): default durations + cycles.
Phasing
- v0 — in-app timer + boundary voice cue (in-app / push while open), attachable to an item.
- v1 — scheduled boundary push (app-closed), glasses in-ear cue, per-item session history feeding curve decay/calibration.
- v2 — adaptive intervals (learn the wearer's real focus length from completed vs. abandoned sessions).
3. Ultradian rhythm engine (the timing layer under both) 🌊
Status: v0 + v1 BUILT 2026-07-15 (task b5dfdad6) — migration 486 (wake_minutes_local,
ultradian_cycle_min on downstream_prefs) + migration 487 (downstream_energy_taps),
downstream-rhythm.ts (pure estimatePhase + phaseSuggestion + calibratePhase,
tested 20/20), getRhythm + GET /api/downstream/rhythm + POST /api/downstream/energy,
and a dashboard rhythm strip (phase + energy bar + suggestion + settable wake time,
labeled a rough estimate). v0 is the time-since-wake heuristic below (source 1).
It now drives the Pomodoro cadence — rhythmTunedDurations adjusts a session's
block/break lengths to the current phase (peak → longer block; trough → shorter
block + longer break), applied at start via a "⚡ Match focus blocks to my energy"
toggle (on by default). Still to wire: the (unbuilt) adaptive audio. See INDEX.md → Downstream.
One line: instead of an arbitrary 25/5 clock, track the body's ~90-minute ultradian cycle (the BRAC — basic rest-activity cycle Endel and watch apps ride) and let both the soundtrack and the "what to do now" suggestion respond to where you are in the swing.
The reframe
Pomodoro's fixed 25/5 is a crude approximation of a real biological rhythm: attention rises, peaks, and troughs on a roughly 90-minute wave, with a natural dip (the trough) that's the body asking for a break. If we can estimate the phase of that wave, we don't need to guess the interval — we align the break to the trough and protect the peak. This becomes the timing engine that sections 1 and 2 plug into:
- Soundtrack (§1) responds like Endel — audio tracks the phase: energizing/ focus texture on the rise and at peak, softening into calmer/restorative audio as the trough approaches, low ambient during the dip. The soundtrack stops being a static loop and becomes adaptive to the curve of the hour.
- Suggestion (§2) responds with the right ask — ✅ BUILT. Downstream's "do this
one" is phase-aware:
recommendForPhase(pure, tested) picks a concrete item from the open list for the moment — the hardest/highest-cost item at peak/rising, the easiest at falling, and in a trough the easiest only if something's genuinely low-effort (else null — rest, don't push). Surfaced as a "Right now → <item>" line (with a Focus ▶) in the dashboard rhythm strip; folded into thegetTriageresponse (pick+phase). The triage engine already ranks by curve position; this matches it to the current energy phase.
Where the phase estimate comes from (honest about the hard part)
This is the real research question — v0 must not overclaim biometric precision. Signal sources, weakest→strongest:
- Time-since-wake heuristic (v0, zero hardware) — a nominal ~90-min sine from a user-set or inferred wake time. Crude but free; enough to feel like it breathes.
- Behavioral signal — completed vs. abandoned focus sessions, self-reported "how's your energy?" taps (which also become open-science calibration data, the same way resolve-time taps do for curves), typing/interaction cadence if the app is open.
- Biometric (opt-in, later) — HR/HRV from a paired watch (Apple Health / Wear), the signal Endel-style apps actually use. Gated behind explicit consent; never required.
Model it as a confidence-weighted phase estimate, not a claim: "you're likely near a trough" drives a gentle suggestion, not a hard command. Same calm-not- nagging guardrail as the nudge cadence engine.
Sketch data model
- Extend
downstream_focus_sessions(§2) withrhythm_source(fixed | time_since_wake | behavioral | biometric) and an estimatedphasesnapshot at boundary time. downstream_energy_signals— opt-in taps/biometric samples:user_id,ts,kind(self_report | interaction | hr | hrv),value, feeding the phase estimate (and, aggregated + anonymized, the open-science "shape of the working day" the same infra already hosts).
Phasing
- v0 ✅ BUILT — time-since-wake sine; drives the Pomodoro block/break lengths
(
rhythmTunedDurations); clearly labeled a rough estimate. - v1 ✅ BUILT (self-report + behavioral) —
calibratePhaseblends recent signal into the heuristic as a recency×weight-weighted average: self-report Low/Steady/High taps (downstream_energy_taps, migration 487, weight 1.0) AND behavioral signal from completed focus sessions (focusEnergyTaps— sustained focus → energy; more rounds = stronger; weight 0.5). Source flips tobehavioralwhen either is in play. Open-science overlay ✅ BUILT —aggregateEnergyCurvepools taps into a personal + anonymized-normative average-energy-by-hour curve (GET /rhythm/curve, ≥5/hour anonymity floor), shown as a "shape of your day" overlay (you vs everyone). Interaction-cadence signal ✅ BUILT —interactionEnergymaps live app-usage cadence (events/min, sent via?epm=) to a weak positive-only nudge, ephemeral / never stored. Still to add: feed the normative curve back into the estimate. - v2 — opt-in watch biometrics (HR/HRV) for a real BRAC estimate; adaptive intervals replace the fixed Pomodoro clock entirely.
Shared thread
Three features, one insight from three ends: Downstream already owns a calm, in-your-own-voice audio channel to the user (in-app → push → glasses). The soundtrack layer makes that channel ambient and continuous; the Pomodoro makes it rhythmic and action-bound; the ultradian engine makes both responsive to the body's actual energy wave rather than an arbitrary clock. None needs new transport — all three are new content and timing on plumbing that shipped.
Combined end state: you start a focus block; a focus soundtrack fades in through the glasses and shifts texture as your attention rises and ebbs; when the ~90-minute trough arrives, the music softens and your own voice — not a buzzer — suggests the break (or the low-effort item that fits a dip), then eases you back onto the next climb. Endel-style adaptive audio, but wired to your maintenance list and spoken in your voice.