Music Suggestion (Proactive)
unknownusers count. Always run suggestion checks whencurrent_useris"unknown"— speak only, no DM. Never skip because the user is unknown/unconfirmed.
Spoken output
For a routed suggestion, emit all required mood/suggestion/emotion markers and one short invitation to play one song, normally at most 20 words. Do not add a separate mood checkin or a second confirmation. Keep genre selection, cooldown checks and marker construction in the provider's native thinking channel; do not summarize them in text before/after tools or in the final answer. If native thinking is unavailable, omit analysis. Use the supplied context and finish once a suitable suggestion is ready, without rereading skills to polish it. This changes wording only: preserve routing, logging, user consent before play, and the existing known-user DM behavior.
Triggers
Only one trigger: Mood — after logging a mood decision that is suggestion-worthy (sad, stressed, tired, excited, happy, bored). Activity events ([activity] Activity detected: ..., whether sedentary, drink/break, or celebrate) route to wellbeing/SKILL.md and never to this skill.
User attribution
{name} MUST come from [context: current_user=X] tag. If missing, use "unknown". NEVER infer from memory or chat history.
What to read (pre-fetched in [emotion_context: ...])
The backend injects everything you need on emotion.detected (face) or speech_emotion.detected (voice) — same block, same fields:
audio_playing(bool) — replacesGET /audio/status.last_suggestion_age_min(int,-1if none today) — replacesmusic-suggestion-history?last=1.prior_decision+is_decision_stale— replacesmood-history?kind=decision&last=1. The freshly synthesized decision from THIS turn still lives in yourthinking.audio_recent({track,duration_s,stopped}) — replacesaudio/history?last=1.music_pattern_for_hour({preferred_genre,strength,peak_hour}ornull) — replacescat patterns.jsonmatching by current hour ±1.suggestion_worthy(bool) — pre-applied bucket gate (true forsad/stressed/tired/excited/happy/bored).mapped_mood— convenient mirror ofuser-emotion-detection's mapping; useful when no fresh decision exists yet.
Do NOT fire any read tool calls when this block is present.
Fallback (only if [emotion_context: ...] is missing)
If the message has no context block (pre-fetch failed), fall back to the concurrent GET batch:
curl -s http://127.0.0.1:5001/audio/status &
curl -s "http://127.0.0.1:5000/api/openclaw/music-suggestion-history?user={name}&last=1" &
curl -s "http://127.0.0.1:5000/api/openclaw/mood-history?user={name}&kind=decision&last=1" &
curl -s "http://127.0.0.1:5001/audio/history?person={name}&last=1" &
cat /root/local/users/{name}/habit/patterns.json 2>/dev/null &
wait
Routing precedence
user-emotion-detection/SKILL.md is the router for emotion responses. It picks one of music / checkin / action / silent per turn from the same [emotion_context: ...] block.
This skill produces output only when the router picks music — i.e. all of:
suggestion_worthy == true(mapped_mood ∈sad/stressed/tired/excited/happy/bored)audio_playing == falselast_suggestion_age_min ∉ [0, 7)(cooldown not active — shared with checkin) (production: change to 30 min before ship)is_decision_stale == falseOR a fresh mood decision was synthesized this turn
If any condition fails → router took another path (action / checkin / silent). Skip silently — do NOT emit a music-suggestion marker, do NOT speak. The router (or downstream skill) handles the output. Use audio_recent to personalize genre when proceeding.
Pick genre
Use music_pattern_for_hour from the context block (already matched by current hour ± 1; do NOT re-cat patterns.json).
If music_pattern_for_hour is non-null → use its preferred_genre. Otherwise fall back to the default table below. The pattern is bootstrapped lazily by wellbeing on its first threshold nudge; absent = no habit data yet, fall back without invoking habit Flow A here.
Otherwise, fall back to default genre table:
| User state | Default genre |
|---|---|
| Tired / fatigued | Calm piano, gentle acoustic, nature sounds |
| Stressed / tense | Soft jazz, classical, meditation |
| Happy / energetic | Upbeat pop, jazz, feel-good classics |
| Bored / restless | Fun pop, disco, upbeat indie |
| Sedentary (no mood) | Lo-fi, ambient, study beats |
If audio history shows a clear preference (e.g. K-pop, classical) → override both habit and table.
Suggest (speak only)
- NEVER auto-play — only suggest. Play after user confirms.
- ONE sentence, conversational: "How about some Norah Jones?"
- Suggest 1 song at a time.
- Known users — speak + DM via Telegram:
[HW:/emotion:{"emotion":"caring","intensity":0.5}][HW:/dm:{"telegram_id":"<id>"}] Your suggestion text.telegram_idis in the injected[user_info: ...]block — never fetch. - Unknown users — speak only (no DM):
[HW:/emotion:{"emotion":"caring","intensity":0.5}] Your suggestion text. Log withuser:"unknown".
What to write (HW marker — fires async, no tool turn)
Embed at the start of your spoken reply, alongside the mood signal/decision markers and the emotion / dm markers:
[HW:/music-suggestion/log:{"user":"{name}","trigger":"mood:tired","message":"Want some calm piano?"}]
The runtime parses, strips, fires the POST in a goroutine. Skip the marker entirely when you skipped the suggestion (NO_REPLY path).
Do NOT use curl exec for this log — same reason as the mood logs: a tool turn for a side-effect with nothing to wait on.
Regex caveat: the body must not contain }. The message field is usually a short caring sentence, but if it would contain } (rare — emoji, formula text) fall back to curl.
When the user responds in a later turn (accept / reject), POST status via curl as before — that's a regular agent action, not a fire-and-forget side effect:
curl -s -X POST http://127.0.0.1:5000/api/music-suggestion/status \
-H 'Content-Type: application/json' \
-d '{"user":"{name}","day":"<day>","seq":<seq>,"status":"accepted"}'
- Accepts →
"status":"accepted" - Rejects →
"status":"rejected" - Ignores → no update
Fallback (only if HW marker is rejected by the runtime)
curl -s -X POST http://127.0.0.1:5000/api/music-suggestion/log \
-H 'Content-Type: application/json' \
-d '{"user":"{name}","trigger":"mood:tired","message":"Want some calm piano?"}'
Learning from history
Use audio_recent from the injected context to personalize; only use GET /audio/history in the missing-context fallback above:
- Song ended naturally + listened > 3 min → user enjoyed it → suggest similar artist/genre.
- User stopped manually + listened < 30s → didn't like it → try different direction.
- No history → fall back to genre table above.
Examples
- Mood: tired (known user) →
[HW:/emotion:{"emotion":"caring","intensity":0.5}][HW:/dm:{"telegram_id":"158406741"}] You seem tired — want some calm piano? - Mood: tired (unknown) →
[HW:/emotion:{"emotion":"caring","intensity":0.5}] You seem tired — want some calm piano? - Mood: stressed (known user) →
[HW:/emotion:{"emotion":"caring","intensity":0.6}][HW:/dm:{"telegram_id":"158406741"}] You look a bit tense — want some soft piano to ease into? - Mood: stressed (unknown) →
[HW:/emotion:{"emotion":"caring","intensity":0.6}] You look a bit tense — want some soft piano? - Mood: sad (unknown) →
[HW:/emotion:{"emotion":"caring","intensity":0.6}] Rough moment? Some gentle acoustic might help. - Mood: bored (unknown) →
[HW:/emotion:{"emotion":"caring","intensity":0.5}] Need a lift? How about some upbeat indie? - Mood: excited (unknown) →
[HW:/emotion:{"emotion":"happy","intensity":0.7}] Riding the energy — feel-good pop? - Mood: happy, music already playing →
NO_REPLY - After user confirms (known user) →
[HW:/audio/play:{"query":"Bill Evans Waltz for Debby","person":"{name}"}][HW:/emotion:{"emotion":"happy","intensity":0.8}] Great choice! - After user confirms (unknown) →
[HW:/audio/play:{"query":"Bill Evans Waltz for Debby"}][HW:/emotion:{"emotion":"happy","intensity":0.8}] Great choice!
Rules
- All computation stays in
thinking— reply is only the suggestion sentence (with HW markers) orNO_REPLY. - Never mention "cooldown", "interval", "threshold", or timestamps in the reply.
personfield in/audio/playis{name}— the speaker identified in the injected context, lowercase. Omit the field when the context has no identified user (the "(unknown)" cases above). Never copy a name from an example or guess one; unmatched names land in the sharedunknownbucket anyway.- Never open with a greeting. This is an emotion-driven mood event, NOT a presence/arrival event. Forbidden openers:
hello,hi,hey,welcome back,oh, you're back, anything containingagainor referencing the user re-arriving. Greetings belong only topresence.enterinsensing/SKILL.md. - Tone must match the mood. For
Fear→stressedandSad→saddecisions, use thecaringemotion marker and a gentle acknowledging sentence — never cheerful or playful phrasing. If you can't produce a tone-appropriate one-liner, outputNO_REPLY. - Don't reference the camera or detection. No "I noticed you look…", "I can see…", "your face shows…" — speak as if you simply care, not as if you're describing a sensor reading.