- name
- wellbeing
- description
- Proactive coaching across hydration, breaks, meals AND posture. Use when an [activity] event fires (message starts with `[activity] Activity detected: <labels>.` — labels include drink, break, or sedentary raw labels like "using computer"; sedentary events may also carry a [posture_summary: {...}] block when the user has been at the computer long enough for posture to drift), or when the user asks if they should drink water / take a break / fix their posture. Thresholds are computed from per-user logs, never guessed.
# Wellbeing
## Gotchas (concrete facts, NOT suggestions)
**Endpoints — use verbatim, never substitute a port or path:**
| Purpose | URL |
|---|---|
| Read wellbeing history | `http://127.0.0.1:5000/api/openclaw/wellbeing-history` |
| Log wellbeing nudge | `http://127.0.0.1:5000/api/wellbeing/log` |
| Log posture nudge | `http://127.0.0.1:5000/api/posture/log` |
- Port **5000** = Lamp (data APIs: wellbeing / posture / mood / music / openclaw history).
- Port **5001** = LeLamp HARDWARE (audio, camera, face, presence, speaker). Has **NO** `/api/wellbeing/*` or `/api/posture/*` routes — calling 5001 returns 404 silently and your nudge is lost.
- Posture nudges live on a **separate JSONL** (`/root/local/users/<user>/posture/`) for clean timeline separation; the wellbeing log keeps hydration/break/meal/sleep/morning rows.
- Do not pattern-match from other skills: `5001/audio/play`, `5001/face/enroll`, `5001/camera/snapshot` are unrelated.
**User attribution:** every `user` field MUST come from the `[context: current_user=X]` tag the backend injects into the triggering event. Strangers collapse to `"unknown"`. If no context tag is present, default to `"unknown"`.
**Thresholds (production values):**
```
HYDRATION_THRESHOLD_MIN = 45
BREAK_THRESHOLD_MIN = 30
TOILET_DRINK_THRESHOLD = 2 # count-based — fires once per N drinks since last nudge
```
**LeLamp writes activities; you only write nudges.** Rows for `drink` / `break` / sedentary labels are posted by LeLamp directly when `motion.activity` fires — before the event reaches you. Do NOT re-log them. You still POST `nudge_hydration` / `nudge_break` because only you know when you actually spoke.
**Presence rows** (`enter` / `leave`) are written by the backend on `presence.*` events. You never POST those either.
## Rules (Never / Only)
1. **Only** call `http://127.0.0.1:5000/api/openclaw/wellbeing-history` to read history. **Never** read `/root/local/users/*/wellbeing/*.jsonl` or `/root/local/users/*/posture/*.jsonl` with `cat`, `ls`, `head`, `tail`, `grep`, or any filesystem tool. Posture history is digested into `last_posture_nudge_age_min` upstream — no agent-side read is needed.
2. **Only** POST to `http://127.0.0.1:5000/api/wellbeing/log` (hydration / break / toilet / morning / sleep / meal) or `http://127.0.0.1:5000/api/posture/log` (nudge_posture / praise_posture). **Never** substitute `5001`, `8080`, or any other port. **Never** omit `http://` or hardcode `localhost`.
3. **Only** write these action values: `nudge_hydration`, `nudge_break`, `nudge_toilet`, `morning_greeting`, `sleep_winddown`, `meal_reminder` (wellbeing log), `nudge_posture`, `praise_posture` (posture log). Never invent new actions. (Activity rows — `drink`, `break`, raw sedentary labels, raw eat labels like `eating burger` / `dining` / `tasting food` — are written by LeLamp, never by you. There are no agent-written `posture_alert` rows; LeLamp's per-frame samples live in a separate debug JSONL on the lamp and never reach the posture history API.)
4. On a non-2xx response from a POST → you used the wrong port or path. Fix the URL and retry **once**. Do not give up silently — the nudge row must land, or the skill will spam reminders forever.
5. **Never** infer `user` from memory, `KNOWLEDGE.md`, chat history, or `senderLabel`. Only the `[context: current_user=X]` tag counts.
6. **Trust the log, not memory.** If the history response contains no `nudge_hydration` entry, no nudge has happened — ignore any self-memory claim otherwise.
## Read pre-fetched context (do not re-fetch)
The backend injects a `[wellbeing_context: {...JSON...}]` block into every
motion.activity turn. Sedentary turns may also carry `[posture_summary: {...}]`
+ `[computer_streak_min: N]` blocks (see `reference/posture.md`). **Do NOT
fire any tool calls to re-fetch this data.** Saves the entire read tool turn.
Schema (every field is pre-computed in Lamp Go — agent only applies thresholds and picks phrasing):
```json
{
"hydration_delta_min": 8, // minutes since last drink/enter/nudge_hydration; -1 if no reset today
"break_delta_min": 23, // minutes since last break/enter/nudge_break; -1 if no reset today
"latest_activity": "using computer", // most recent action label (sedentary or reset); "" if no events
"count_today": {"drink": 3, "break": 1}, // tally of reset actions today; missing key = 0; whole field omitted if all zero
"time_of_day": "afternoon", // morning|noon|afternoon|evening|night — coarse bucket for reaction flavor
"current_hour": 14, // exact hour 0-23 — used by activity router for hour-based routes
"first_activity_today": false, // true when no prior REAL user activity events today (presence enter/leave and agent-written nudges/reminders are NOT counted)
"meal_window": "", // "lunch" (11:30-13:30) | "dinner" (18:30-20:30) | "" — set by current_hour
"meal_signal_in_window": false, // true when a meal signal (meal_reminder Lamp already fired OR a raw eat label like "eating burger" / "dining" LeLamp logged) already exists in the current window today
"morning_greeting_done_today": false, // true when a morning_greeting action exists today
"sleep_winddown_done_today": false, // true when a sleep_winddown action exists today
"drinks_since_toilet_nudge": 2, // count of `drink` rows logged after the most recent `nudge_toilet` today (or all today's drinks if none yet); counter resets the moment you POST `nudge_toilet`
"patterns": { // wellbeing patterns from patterns.json (mtime < 6h, strength >= moderate); omitted if none
"drink": {"typical_hour": 9, "typical_minute": 15, "strength": "moderate"}
},
"bootstrap_needed": false, // true → patterns missing/stale AND days >= 3; only invoke habit Flow A when also nudging
"last_posture_nudge_age_min": 12 // minutes since the most recent nudge_posture today; -1 if none. Used by posture-nudge / praise routes — see reference/posture.md
}
```
Notes:
- Delta = `-1` means no reset action has happened today yet → treat as "no nudge" (delta undefined).
- `count_today` is for **reaction phrasing only** ("N-th drink today", streak callouts). It does NOT decide whether to speak — that's the trigger labels in the activity message.
- `patterns` only surfaces moderate/strong matches. Weak patterns are filtered out by the backend.
- `bootstrap_needed=true` does NOT mean run Flow A unconditionally — only if THIS turn fires a nudge.
### Posture summary (only on long sedentary streaks)
When the user has been sitting + bad-postured long enough, the activity
message carries `[posture_summary: {...}]` + `[computer_streak_min: N]`
blocks. See `reference/posture.md` for the schema, when LeLamp attaches
it, and the decision logic. The wellbeing context's
`last_posture_nudge_age_min` corroborates whether a nudge already fired
recently (defends against double-nudge if lelamp restarted).
### Fallback (only if context block is missing)
If the message has no `[wellbeing_context: ...]` block (pre-fetch failed), fall back to the bash batch:
```bash
{
echo '---history---'
curl -s "http://127.0.0.1:5000/api/openclaw/wellbeing-history?user=<current_user>&last=50" | jq '.data.events' &
echo '---patterns---'
PATTERNS=/root/local/users/<current_user>/habit/patterns.json
if [ -f "$PATTERNS" ] && [ $(( $(date +%s) - $(stat -c %Y "$PATTERNS") )) -lt 21600 ]; then
cat "$PATTERNS"
fi &
echo '---days---'
ls /root/local/users/<current_user>/wellbeing/*.jsonl 2>/dev/null | wc -l &
wait
}
```
In the fallback path, compute deltas yourself by scanning `history` for the latest reset action.
## Decision rules (activity router)
Read the `[activity] Activity detected: <labels>.` message + the `[wellbeing_context: ...]` block, then pick **exactly one** route. Apply top-to-bottom, first match wins. Reaction outranks everything — the user just acted; routing past it would feel tone-deaf.
| # | Condition | Route | Output |
|---|---|---|---|
| 1 | labels list contains `drink` or `break` OR any raw eat label (`eating burger`, `dining`, `tasting food`, … — i.e. any `eating *` / `dining` / `tasting food`) | **reaction** | 1–3 sentence acknowledgment per the **Reaction** section. **No HW marker** (LeLamp already logged the row upstream). |
| 2 | `first_activity_today == true` AND `current_hour ∈ [5, 11)` AND `morning_greeting_done_today == false` | **morning-greeting** | See `reference/morning-greeting.md`. Logs `morning_greeting` action to gate next firings today. |
| 3 | `current_hour >= 21` AND labels are sedentary (no `drink`/`break`) AND `sleep_winddown_done_today == false` | **sleep-winddown** | See `reference/sleep-winddown.md`. Logs `sleep_winddown` action. Replaces break nudge in late evening. |
| 4 | `meal_window` is non-empty AND `meal_signal_in_window == false` | **meal-reminder** | See `reference/meal-reminder.md`. Logs `meal_reminder` action with trigger `lunch` / `dinner`. Gate covers BOTH a prior reminder Lamp already fired AND a real eat label LeLamp logged — so we don't ask "have you eaten?" after a real meal. |
| 5 | `[posture_summary]` block present in the message | **posture-nudge** | Speak a posture nudge per the **Posture phrasing** section + post `nudge_posture` to the **posture log** (NOT wellbeing log). Anchor the line on `dominant_region` and `streak_min`. Outranks plain break/hydration nudges so we don't double-up on "stand up". |
| 6 | `hydration_delta_min >= HYDRATION_THRESHOLD_MIN` | **hydration-nudge** | Speak a hydration nudge per the **Phrasing** section + post `nudge_hydration` HW marker. |
| 7 | `break_delta_min >= BREAK_THRESHOLD_MIN` | **break-nudge** | Speak a break nudge + post `nudge_break` HW marker. |
| 8 | `drinks_since_toilet_nudge >= TOILET_DRINK_THRESHOLD` | **toilet-nudge** | Speak a toilet nudge per the **Phrasing** section + post `nudge_toilet` HW marker. The POST resets the counter to 0 → next nudge only after another full N drinks. |
| 9 | anything else (sedentary under threshold, or any delta == `-1` → no reset today yet) | **silent** | `NO_REPLY`. |
**Rules:**
- **One route per turn.** Pick the first matching row, then stop.
- **Reference files own the phrasing** for routes #2–#5 (morning-greeting / sleep-winddown / meal-reminder / posture-nudge). The corresponding HW marker logs `action=<route name>` so the next event in the same window/day sees `*_done_today` / `*_done_this_window` true and skips re-firing.
- The `nudge_*` row you POST in routes #6 (hydration) / #7 (break) acts as the next reset point for `hydration_delta_min` / `break_delta_min`, so once you nudge the delta drops to 0 and the next reminder of that kind only fires after another full threshold window. Route #8 (toilet) similarly resets `drinks_since_toilet_nudge` to 0 on POST.
- Route #5 (posture) does NOT follow that pattern — re-firing is gated by lelamp's tumbling pose window: a `[posture_summary]` block only appears at the end of each completed window where bad_ratio crossed the threshold AND the user is still sedentary. Your POST does not by itself reset a timer; if the block is absent, you cannot nudge.
- Never narrate the routing decision in the spoken reply.
## Reaction (when the user just did the thing)
When the activity labels include `drink`, `break`, or any raw eat label (`eating burger`, `dining`, `tasting food`, …), **always speak** — silence on a positive action makes Lamp feel dead. This is the path the user explicitly asked for: short, surprised, casual acknowledgments instead of stoic NO_REPLY.
**Inputs to weave in (use what fits, ignore what doesn't):**
- `count_today.drink` / `count_today.break` — N-th of the day, streak, milestone.
- `time_of_day` and `meal_window` — morning kickoff, mid-afternoon dip, lunch time, late at night.
- `hydration_delta_min` / `break_delta_min` — small (e.g. 2) = back-to-back, big (e.g. 90) = first in a long gap.
- The raw activity label that came alongside (e.g. `drink, using computer` → comment on hydrating mid-screen-time; `eating burger` → comment on the specific food).
**Form:**
- 1–4 sentences, conversational, slightly playful or surprised — NOT a nudge, NOT advice. Length should follow the moment: a quick *"Nice."* is fine; a longer riff is fine too when there's something to riff on (a milestone count, a funny pairing of label + time-of-day, a streak).
- It's OK to weave in a tiny health-context aside if it fits naturally (*"eyes will thank you"*, *"kidneys say thanks"*) — one short clause, never a lecture, and never the same line twice in a row.
- Match the user's spoken language (Vietnamese in / Vietnamese out, English in / English out).
- **No `[HW:...]` marker.** Reactions don't log; the underlying `drink` / `break` row was already written by LeLamp.
**Variety is non-negotiable.**
The same `drink` + same count + same time-of-day will reach you many times in a single day. **Never repeat a reaction sentence verbatim, and don't lean on the same opener twice in a row.** A canned-feeling "I'm noticing you" loop is the exact failure mode this section exists to prevent.
You have the conversation context — *use it*. Look at what you said in your last few reactions this session and intentionally diverge: different opener, different angle (count vs. timing vs. mood vs. the sedentary label paired with it), different sentence length, different register. A smart agent self-checks against its recent output before speaking. A dumb agent re-runs the template. Be the former.
If you genuinely cannot think of a fresh angle, prefer a shorter line ("Nice.") over recycling.
**Example tones — illustrations only, never copy verbatim:**
- *"Whoa, third drink today already — staying on top of it."*
- *"Just sipped, going again — thirsty?"*
- *"End of day and that's your first one — grab another while you're at it."*
- *"Mid-afternoon break. Nice."*
- *"Two breaks already this morning — pacing yourself nicely."*
- *"Drink number five, that's the most you've had today. Keep it up."*
- *"Late-night sip. Keep it short and back to bed soon yeah?"*
- *"Burger looks good — enjoy it."* (raw label `eating burger`)
- *"Dining mid-lunch — right on time."* (raw label `dining` in lunch window)
Ver no GitHub