NataCoach / product & system design Wiki Brain Personas Coach Console ↗

06 — Training Experience (Session Mode)

How @NataCoachBot runs a workout: Nata's structured program, the chat-based set-card runner, the weight-proposal engine, RPE capture, substitutions, and what a finished session leaves behind.

Session Mode is where the prime directive — the user does less — earns its keep. A client should be able to complete a coached strength session with nothing but ✓ taps. Every number on screen was proposed by the system from Nata's program, the client's history, and this morning's recovery from Health Sync; the client's only decisions are exceptions.


1. Program structure: what Nata authors in the Coach Console

Nata builds programs in the Coach Console program builder. A program is structured data, never free text — three reasons:

  1. The weight-proposal engine needs machine-readable targets (sets, rep ranges, RPE, progression rules) to compute numbers.
  2. Analytics (volume, adherence, e1RM trends) must be deterministic SQL over events, per locked decision 6 in the brief.
  3. Substitutions need movement-pattern and equipment tags to offer valid swaps automatically.

The hierarchy:

erDiagram
 PROGRAM ||--o{ MESOCYCLE : "contains 1..n"
 MESOCYCLE ||--o{ WEEK : "4-6 incl. deload"
 WEEK ||--o{ SESSION_TEMPLATE : "e.g. A / B / C"
 SESSION_TEMPLATE ||--o{ EXERCISE_SLOT : "ordered slots"
 EXERCISE_SLOT }o--|| EXERCISE : "references library"
 EXERCISE ||--o{ SUBSTITUTION : "equipment-tagged swaps"
 SESSION_TEMPLATE ||--o{ SESSION_INSTANCE : "one per performed day"
 SESSION_INSTANCE ||--o{ SET_EVENT : "immutable log"

Nata defines session templates once per mesocycle; the builder stamps them into weeks and applies week-level rules (loading weeks progress, the final week auto-deloads). An exercise slot — the atomic unit — looks like this (Marta's Session A, slot 1):

slot:
  order: 1
  exercise: goblet_squat          # from Nata's exercise library
  pattern: squat                   # movement pattern, drives swap logic
  is_compound: true
  sets: 3
  rep_range: [8, 10]
  target_rpe: [7, 8]
  rest_sec: 90
  progression: double_progression  # fill the rep range, then +1 load step
  load_step_kg: 2                  # dumbbell increment at Marta's gym
  starting_load_kg: 10             # Nata-set for meso week 1 (postpartum-conservative)
  cue: "Chest tall, elbows inside knees."
  demo_video: s3://exercises/goblet-squat-20s.mp4
  substitutions: [db_box_squat, leg_press, split_squat]
  nata_override: null              # e.g. "cap at 16 kg until pelvic-floor check-in"

The exercise library (about 120 movements at pilot) carries per-exercise metadata Nata fills in once: form-cue one-liner, 20-second demo clip, equipment requirements, the technique checklist used by Form Check, and equipment-tagged substitutions.

Level Owner Typical values (pilot)
Program Nata "Return to Strength" (Marta), "Recomp Hybrid" (Denys)
Mesocycle Nata 5 weeks: 4 loading + 1 deload
Week Builder (auto) week_in_meso 1–5, is_deload flag on week 5
Session template Nata Marta: A/B/C full-body ×3; Denys: 2 strength + 2 zone-2
Exercise slot Nata 4–6 slots per strength session

2. Starting a session: the Morning Brief proposes, the user taps

Nobody opens a menu to "log a workout." The Morning Brief (see 02-user-experience.md) already knows today is a training day and proposes the session, pre-adjusted for recovery:

NataCoachBot  07:31
Morning, Marta ☀️ Whoop recovery 78% — green.
Today: Session A (squat, row, press + core), ~35 min.
Everything's set up — weights proposed from last Tuesday.
[  Start when ready ]   [ Move to another day ]   [ Preview session ]

Tapping Start (whenever she actually reaches the gym — the button stays live all day) drops her into the chat-based runner: one set card message at a time, edited in place as she progresses. In Phase 2 the same button opens the Mini App runner instead; chat remains the always-working fallback (hotel Wi-Fi, old phones — see 11-roadmap.md).

Capability Chat runner (Phase 1) Mini App runner (Phase 2)
Set cards, buttons, logging inline keyboards native UI, swipe between sets
Rest timer message with 1–2 edits (30 s left, "Go") live countdown + haptic/sound
Demo videos link opens clip inline autoplay loop
Session overview / jump around "Preview session" list full session board
Offline tolerance Telegram's own queueing local state, syncs on reconnect

3. The per-set card

One Telegram message per set, four buttons, zero typing in the happy path:

NataCoachBot  18:04
🏋️ Goblet Squat — set 1 of 3
"Chest tall, elbows inside knees."   20-sec demo
Proposed: 12 kg × 8 reps
[ ✓ Done as proposed ]  [ ± Adjust ]
[ ⇄ Swap exercise ]     [  Can't do it ]
  • ✓ Done as proposed — logs exactly the proposed load × reps. One tap. This is the designed-for path; in pilot we target ≥ 75% of sets logged this way.
  • ± Adjust — quick-tap steppers (weight −1/+1 step, reps −2…+2) or type 10x9. Adjusting affects this set's log and feeds the engine; it does not nag or ask why.
  • ⇄ Swap exercise — opens the slot's substitution list filtered by the session's equipment profile (§5).
  • ✋ Can't do it — one-question triage (§5). Pain always routes to Nata.

Rest timer. After each logged set the bot posts ⏱ Rest 90 s, edits it once at 30 s left…, and again to Go — set 2 👇 alongside the next card. (Telegram rate-limits edits; two edits per rest period is deliberate.)

RPE capture — once per exercise, not per set. After the final set of an exercise, one emoji row:

How did goblet squats feel overall?
[ 🟢 easy ]  [ 🟡 solid ]  [ 🟠 hard ]  [ 🔴 maxed ]
Tap Stored RPE Meaning vs target RPE 7–8 Engine reaction next time
🟢 easy 6 clear headroom two 🟢 in a row skip ahead one progression step
🟡 solid 7 on target progress per rule
🟠 hard 8 top of target progress per rule, watch closely
🔴 maxed 9–10 over target hold load, −1 rep target; two 🔴 in a row −5% and flag to Nata

Per-set RPE is a logging chore real lifters skip; per-exercise RPE is one tap and enough signal for autoregulation. (Assumption we'll validate in pilot week 3.)

4. The weight-proposal engine

This is the centerpiece of "the user does less": the client never computes a number. The engine runs when a session instance is created (at Morning Brief time) and re-runs if Health Sync data arrives late.

proposeSlotTarget(slot, user):
  # 1. Baseline: last completed performance of this exercise (from SET_EVENTs)
  base = lastPerformance(user, slot.exercise)        # load, reps, rpe
  if none: base = slot.starting_load_kg × bottom of rep_range

  # 2. Progression rule vs week-in-mesocycle
  target = slot.progression.apply(base, weekInMeso)
  #   double_progression: last RPE ≤ 6 → jump reps toward top of range;
  #   all sets at top of range at RPE ≤ 8 → load += load_step, reps → bottom
  if week.is_deload: target.load ×= 0.85 ; target.sets −= 1

  # 3. RPE autoregulation (table in §3)
  if base.rpe ≥ 9: hold load, target.reps −= 1

  # 4. Today's recovery modifier (Health Sync readiness 0–100)
  r = todayReadiness(user)          # Whoop recovery / normalized Garmin, see 04-data-collection.md
  if r <= 33:                       # red day (bands per 02 §2.2 / 04 §5.2: red ≤ 33)
      if slot.is_compound: target.load ×= 0.90 ; target.sets = max(1, target.sets − 1)
      else:                target.sets −= 1
      progressionState.freeze()     # today doesn't advance the ladder
  elif r < 67:                      # yellow day
      progressionState.hold()       # repeat last targets, no new load/reps

  # 5. Nata override always wins
  if slot.nata_override: target = clamp(target, override)

  # 6. Round to what physically exists in the user's equipment profile
  target.load = roundToEquipment(target.load, user.equipment_profile, slot.exercise)
  return target + provenance        # every number carries its "why"

Every proposal carries provenance; a Why this weight? button renders it in one sentence ("12 kg because you hit 3×10 @ solid last week — that graduates you up one dumbbell"). Trust is the feature.

roundToEquipment rounds to the nearest available implement or plate combination: Marta's gym has dumbbells in 2 kg steps above 10 kg; barbell work rounds to 2.5 kg total; on red days ties round down.

Worked example: Marta's goblet squat, mesocycle 2

Slot: 3 × 8–10 @ RPE 7–8, double progression, +2 kg step, Nata's starting load 10 kg.

Date Readiness Engine math Proposal Result RPE tap
Tue Jul 7 (W1) 71% 🟢 starting load, bottom of range 10 kg × 3×8 as proposed 🟢 easy (6)
Tue Jul 14 (W2) 68% 🟢 RPE 6 jump to top of range 10 kg × 3×10 as proposed 🟡 solid (7) load increase earned: next target 12 kg × 3×8
Tue Jul 21 (W3) 29% 🔴 12 kg × 0.90 = 10.8 kg nearest of {10, 12} 10 kg; sets 3 − 1 = 2; progression frozen 10 kg × 2×8 as proposed 🟢 easy (6) — correct on a red day
Tue Jul 28 (W4) 78% 🟢 frozen target resumes 12 kg × 3×8 as proposed 🟡 solid (7)
Tue Aug 4 (W5, deload) 74% 🟢 12 × 0.85 = 10.2 10 kg; sets 3 − 1 = 2 10 kg × 2×8 as proposed 🟢 easy (6)

The red-day message makes the adjustment feel like coaching, not a downgrade:

NataCoachBot  07:31  (Tue Jul 21)
Recovery 29% — rough night, I saw. We're not skipping, we're trimming:
goblet squats today at 10 kg × 2×8 instead of the planned 12s.
The 12s aren't going anywhere — they're waiting for a green day. 💛
[  Start when ready ]  [ Rather rest today ]

Five workouts, zero numbers computed by Marta, one tap each morning. That is the product.

5. Substitutions, hotel gym mode, and "Can't do it"

Swap list. Each slot lists 2–4 substitutions tagged by movement pattern and required equipment. Tapping ⇄ Swap exercise shows only the swaps valid for the session's equipment profile (full_gym / hotel_gym / bodyweight_band).

Hotel gym mode (Denys). When Denys taps I'm traveling in the Morning Brief (or his Garmin location/calendar hints at it and the bot asks), the whole session re-resolves against hotel_gym: barbell RDL dumbbell RDL, lat pulldown band pulldown, etc. Loads are recomputed with a per-substitution transfer hint (e.g. db_rdl ≈ 35% of barbell load per hand), proposed one notch conservative on first use, then calibrated from his RPE tap. His barbell progression state is frozen, not lost — home-gym numbers resume when he's back. See the travel-week walkthrough in 10-personas.md.

"Can't do it" triage — exactly one question:

What's the issue with this one?
[ 🧰 No equipment ]   [  Something hurts ]   [ 🥵 Too hard today ]
Answer System response
🧰 No equipment show swap list for the slot; remember the gap in this equipment profile
⚡ Something hurts safety escalation — skip the slot and every remaining slot sharing its movement pattern, log a pain.reported event, notify Nata immediately regardless of autonomy dial (locked decision 7); Nata follows up personally
🥵 Too hard today −10% load or offer the easiest regression; engine notes it for next proposal

6. Session summary events Wiki Brain

Closing message after the last exercise:

Session done — 34 min 💪
Volume: 2,742 kg across 11 sets (last Tuesday: 2,510 kg)
PR: Goblet squat 12 kg × 8 — your heaviest set since the restart
Effort: mostly 🟡, one 🟠 — exactly where week 4 should sit
Nata: "Strong day. Protein with dinner tonight — Thursday we press."

The coach sentence is generated by the persona layer (Nata's tone guide + this user's Wiki Brain) and follows the Coach-in-the-loop autonomy dial; PRs and volume are deterministic SQL, never LLM arithmetic.

Everything the session produced is immutable events in Postgres:

Event Key payload fields
session.started session_instance_id, template, equipment_profile, readiness
set.logged exercise, weight_kg, reps, proposed_weight_kg, proposed_reps, adjusted:bool
rpe.recorded exercise, rpe (6/7/8/9–10), emoji_tap
exercise.swapped from, to, reason
session.cant_do slot, triage_answer
pain.reported slot, free_text if given escalation
session.completed duration_min, total_volume_kg, pr_flags

The nightly distillation job (Karpathy ingest workflow, 08-llm-wiki-brain.md) folds these into training-history.md and exercises/*.md — per-exercise pages get new best sets and cue history, log.md gets the dated entry, contradictions (e.g. "says squats easy but adjusted down twice") are flagged for lint.

7. Session state machine and full transcript

stateDiagram-v2
 [*] --> Proposed
 Proposed --> Active: user taps Start
 Proposed --> Skipped: declined or expires at 22:00
 Active --> SetCard: load next slot
 SetCard --> Logging: Done as proposed or Adjust
 SetCard --> Swapping: Swap exercise
 SetCard --> Triage: Cant do it
 Swapping --> SetCard: substitute chosen
 Triage --> SetCard: equipment or too-hard resolved
 Triage --> Escalated: pain reported
 Logging --> Rest: sets remaining in slot
 Logging --> RPECapture: final set of exercise
 Rest --> SetCard: timer done
 RPECapture --> SetCard: next exercise
 RPECapture --> Summary: last exercise done
 Active --> Paused: no input for 15 min
 Paused --> SetCard: user returns same day
 Paused --> AutoClosed: end of day, partial session logged
 Summary --> [*]
 Escalated --> [*]
 Skipped --> [*]
 AutoClosed --> [*]

Transcript — Marta, Tue Jul 28 (W4, readiness 78%), first two exercises of Session A:

Marta         18:03  [taps  Start when ready]
NataCoachBot  18:03  Let's go — Session A, 5 exercises, ~35 min.
NataCoachBot  18:03  🏋️ Goblet Squat — set 1 of 3
                     "Chest tall, elbows inside knees."   20-sec demo
                     Proposed: 12 kg × 8 — the 12s you earned two weeks ago 🙂
                     [ ✓ Done as proposed ] [ ± Adjust ] [ ⇄ Swap ] [  Can't do it ]
Marta         18:06  [✓ Done as proposed]
NataCoachBot  18:06   Rest 90 s      → (edited) 30 s left… → (edited) Go — set 2 👇
NataCoachBot  18:08  🏋️ Goblet Squat — set 2 of 3 · Proposed: 12 kg × 8
Marta         18:10  [✓ Done as proposed]
NataCoachBot  18:12  🏋️ Goblet Squat — set 3 of 3 · Proposed: 12 kg × 8
Marta         18:14  [✓ Done as proposed]
NataCoachBot  18:14  How did goblet squats feel overall?
                     [ 🟢 easy ] [ 🟡 solid ] [ 🟠 hard ] [ 🔴 maxed ]
Marta         18:14  [🟡 solid]
NataCoachBot  18:14  Logged. That's a rep-for-rep PR at 12 kg 🎉 Next up:
NataCoachBot  18:15  🏋️ One-Arm DB Row — set 1 of 3 (each side)
                     "Pull the elbow to your hip, not your armpit."   demo
                     Proposed: 14 kg × 10
                     [ ✓ Done as proposed ] [ ± Adjust ] [ ⇄ Swap ] [  Can't do it ]
Marta         18:18  [✓ Done as proposed]
NataCoachBot  18:18   Rest 75 s
NataCoachBot  18:20  🏋️ One-Arm DB Row — set 2 of 3 · Proposed: 14 kg × 10
Marta         18:23  [✓ Done as proposed]
NataCoachBot  18:24  🏋️ One-Arm DB Row — set 3 of 3 · Proposed: 14 kg × 10
Marta         18:27  [± Adjust] → taps reps −2 → logs 14 kg × 8
NataCoachBot  18:27  14 × 8 logged — grip goes before back on rows, totally normal.
                     How did rows feel overall?
Marta         18:27  [🟠 hard]
NataCoachBot  18:27  Noted — I'll hold rows at 14 kg next week and we'll rebuild the last set.
                     Next: incline push-ups…
[… exercises 3–5, then the session summary from §6 …]

Two exercises, six working sets, one adjustment, two emoji taps — and the engine now knows exactly what to propose next Tuesday.


Related: 02-user-experience.md (Morning Brief, notifications) · 04-data-collection.md (readiness score) · 07-video-feedback.md (Form Check from inside a session) · 08-llm-wiki-brain.md (distillation) · 09-admin-analytics.md (program builder, overrides, escalations).