Skip to content

feat!: autonomous, faithful session detail (program/free/rowing) + today one-shot (#23)#24

Merged
stozo04 merged 6 commits into
mainfrom
fix/issue-23-session-actual-weight-telemetry
Jun 19, 2026
Merged

feat!: autonomous, faithful session detail (program/free/rowing) + today one-shot (#23)#24
stozo04 merged 6 commits into
mainfrom
fix/issue-23-session-actual-weight-telemetry

Conversation

@stozo04

@stozo04 stozo04 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Resolves #23. Breaking change to the session --json contract.

session is now a faithful, verbatim passthrough and autonomous: given only a training_id it figures out what the session was — a program/Coach session, free weights, or a rowing/ski free session — and emits the raw Speediance payloads under one uniform shape. A new today command makes "give me the client's workout for today" a single call. The calling agent never needs to know the session type.

Refined across review: started as a per-rep-weight correctness fix, became a dumb verbatim passthrough, and now adds in-binary type dispatch so no agent (or human) has to route by type.

Uniform output — {training_id, kind, info, detail}

kind"program" | "free" | "", and tells a type-agnostic consumer how to read the two verbatim payloads:

kind info detail
program cttTrainingInfo (incl. completionRate) cttTrainingInfoDetail — per-exercise + per-rep arrays
free freeTraining totals (totalCapacity, totalEnergy, totalDistance for rowing/ski, trainingCount) freeTrainingDetail — usually [] (free lifts have no per-rep data)
"" null null (no session in either namespace)

info/detail are the endpoints' data payloads verbatim — original field names and values (leftWatts, forceControlScore, weights, leftBreakTimes, …), carried as raw JSON so any field (now or after a future app update) flows straight through. No renaming, no derivation, no synthesized weight, no --telemetry. Absence is preserved (omitted fields stay omitted).

Autonomous dispatch (the headline)

  • session <id> probes the program namespace, falls back to the free namespace (freeTraining/freeTrainingDetail, discovered by exploring the live API), and reports which answered via kind.
  • today [--date today|yesterday|YYYY-MM-DD] — one call returns every session on the day, each fully resolved (array of the same shape). It reads each record's authoritative numeric type from the list, so dispatch is collision-free.
  • --free / --program are strict overrides: a trainingId can identify different sessions in the program vs. free namespaces, so these force one when an id is ambiguous (selection only — no content reshaping).
  • workouts rows gain a kind field for filtering.

Live-verified on a real account

session 940759  → kind=program, info.completionRate=100.0, 6 exercises (per-rep arrays)
session 2702951 → kind=free, info.totalDistance=1505.5, existBoatingSkiDataGraph=true, detail=[]   (a rowing session)
today --date 2026-06-17 → [ {2702951, kind=free, …} ]
today --date 2026-06-18 → [ {940759,  kind=program, …} ]

Why free lifts look sparse

Exploring the API showed Free Lifts (incl. rowing/ski) live at freeTraining/<id> and expose session-level totals onlyfreeTrainingDetail is [] and actionList is empty/absent even for a real weight free-lift. There is no per-rep breakdown server-side to deliver; the tool surfaces everything that exists.

Tests

Auto-detect free; today across mixed types in one day; collision overrides (--free/--program force the intended session); mutually-exclusive flags; no-data-anywhere → kind:"", nulls; program passthrough stays verbatim (all raw keys present, sparse set preserved, no derived/renamed fields, no --telemetry); workouts exposes kind; nil payloads → null.

go build / vet / test -race green; gofmt -l clean; golangci-lint 0 issues.

⚠️ Breaking change

session --json now emits {training_id, kind, info, detail} (was {training_id, completion_rate, exercises[...]} originally, then {training_id, info, detail}). The derived weight/weight_source and the --telemetry flag are gone; info/detail are verbatim and polymorphic by kind. New: today command, --free/--program flags, and a kind field on workouts rows.

🤖 Generated with Claude Code

stozo04 and others added 2 commits June 18, 2026 16:38
…on (#23)

`session <id> --json` reported the *planned* weight, not what was lifted: for a
completed program session the API leaves each rep's top-level `weight` null, so
the CLI fell back to the exercise `maxWeight` and stamped it on every set —
fabricating flat, wrong per-set weights that looked logged. It also discarded the
richest data the API returns (per-rep, per-side power/ROM/tempo + form scores)
living in a nested `trainingInfoDetail` the loader never read.

- Stop using `maxWeight` as the per-set weight. Derive the scalar from the real
  per-rep telemetry: `mean(weights[])` (already per-attachment, so correct for
  both dual-handle and single-rope with no handle-count branch), falling back to
  `capacity/reps`, else `0.0`.
- Add a `weight_source` marker (`actual` | `derived_avg` | `unavailable`) so a
  consumer can never mistake a derived average for a logged weight — the
  anti-corruption guard for this exact bug.
- Always emit per-set `capacity` (was parsed but dropped).
- Add an opt-in `--telemetry` flag that surfaces per-rep, per-side arrays
  (weights, watts, amplitude, rope speed, finished/break times, timestamp),
  per-set `weight_avg_per_handle`, and per-exercise `scores`/`max_weight`/
  `max_weight_count`. Single-attachment moves omit the right-side fields.

Guards: TestSessionWeightNeverUsesMaxWeight (negative-assertion regression — a
null per-rep weight must never become the planned maxWeight), plus
TestSessionTelemetryExposesPerRepDetail and the CLI-level TestSessionTelemetryJSON.
Docs (SKILL.md/AGENTS.md/README.md/GOAL.md §9.3) updated to keep advertised ==
actual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
… derived fields)

Supersedes the telemetry-flag design earlier on this branch. `session <id>` is now
a dumb, complete, lossless dump of the session's Speediance data: it fetches BOTH
session endpoints and emits each one's data payload verbatim, with the original
Speediance field names and values. The CLI interprets, reshapes, renames, computes,
and fabricates nothing — consumers decide shape, derivation, and storage.

- Output is `{training_id, info, detail}` where info = the raw cttTrainingInfo data
  (incl. completionRate) and detail = the raw cttTrainingInfoDetail data. Both are
  carried as json.RawMessage so any field — modeled or not, present today or added
  by a future app update — flows straight through (the lossy-typed-struct pattern
  is exactly what caused the issue #23 bug).
- Removed: the --telemetry flag, the reps_detail reshape + snake_case renames, the
  synthesized per-set weight, weight_source/derived_avg, and weight_avg_per_handle.
  No flag "unlocks" data — the endpoints return it, so the CLI returns it.
- Never fabricates: a field/array Speediance omits is omitted; a session with no
  detail emits "detail": null. Both endpoints are fetched, so completionRate is no
  longer lost.
- Real per-rep weights remain available verbatim in trainingInfoDetail.weights[]
  (already per-attachment) — consumers average if they want a single number.

api.FetchSessionDetail replaces FetchDetail/PopulateDetail; the typed session
output (SetData/SetOut/ExerciseOut/Session/RepDetail/Scores, AddDetailSets,
SessionOutput, and their helpers) is deleted. Net diff is ~415 lines smaller.

Tests: the genuine captured 940759 response is kept as the fixture; new guards
assert the raw keys pass through unrenamed (leftBreakTimes, forceControlScore,
bilateralBalanceScore, amplitudeStableScore, leftWatts/rightWatts, weights, …), the
sparse set emits only what was returned, nil payloads serialize as null, no
derived/renamed field appears, and the --telemetry flag no longer exists. workouts
is unchanged. Docs (README/SKILL/AGENTS/GOAL §9.3) updated.

BREAKING CHANGE: session --json now emits {training_id, info, detail} with verbatim
Speediance field names. The previous {training_id, completion_rate, exercises[...]}
shape, the derived weight/weight_source fields, and the --telemetry flag are removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
@stozo04 stozo04 changed the title fix: report actual per-set weight + expose per-rep telemetry in session (#23) refactor!: faithful, verbatim session passthrough (supersedes telemetry design) (#23) Jun 19, 2026
…ee/rowing)

The tool now resolves a session's type ITSELF, so a calling agent never needs to
know what the client did. "Client completed a workout — give me the data" just
works whether it was a program/Coach session, free weights, or a rowing/ski free
session.

- session <id> is autonomous: it probes the program namespace
  (cttTrainingInfo[Detail]) and falls back to the free namespace
  (freeTraining[Detail], discovered by exploring the live API), then emits a
  uniform {training_id, kind, info, detail} with kind ∈ {program, free, ""}.
  info/detail remain the VERBATIM Speediance payloads — no derivation.
- New `today [--date today|yesterday|YYYY-MM-DD]` command: the one-shot,
  agent-friendly entry point. It lists the day's sessions and returns each fully
  resolved as an array of the same shape. Because the list carries the
  authoritative numeric `type` alongside the trainingId, dispatch is collision-free.
- A trainingId collides across namespaces (the same number is unrelated sessions
  in program vs free), so `--free` / `--program` strict overrides force a namespace
  when an id is ambiguous; they are selection flags only (no content reshaping).
- workouts rows gain a `kind` field (mapped from the numeric type) for filtering.

Free lifts (incl. rowing/ski) expose session-level totals in info
(totalCapacity/totalEnergy/totalDistance/trainingCount) and an empty detail —
that is all Speediance records for them; there is no per-rep breakdown.

internal/api: ResolveSession + DispatchMode (Auto/FreeFirst/Program/FreeOnly),
program/free probes, FetchDay, fetchRecords refactor. internal/workout:
SessionType + KindForType, kind on Summary and SessionDetail. Tests cover
auto-detect free, today across mixed types, the collision overrides, mutually
exclusive flags, no-data-anywhere, and that program passthrough stays verbatim.
Docs (README/SKILL/AGENTS/GOAL §6,§9.3) updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
@stozo04 stozo04 changed the title refactor!: faithful, verbatim session passthrough (supersedes telemetry design) (#23) feat!: autonomous, faithful session detail (program/free/rowing) + today one-shot (#23) Jun 19, 2026
stozo04 and others added 3 commits June 19, 2026 10:27
…y|null)

Answering a consumer-agent question (advertised==actual): document that info is
object|null and detail is array|null, both never normalized, so consumers treat
null and [] identically as "no rows". Behavior is unchanged and already covered by
TestSessionAutoDetectsFree (detail []), TestSessionNoDataAnywhere /
TestSessionDetailNilPayloadsAreNull (null), and TestSessionFaithfulPassthroughJSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…ll detail

Discovered while verifying a real "Aerobic Rowing" session (2073939): guided
free-namespace sessions (numeric type 2/7 — guided cardio like Aerobic Rowing,
and guided programs) are served by freeTraining[Detail], and unlike a freestyle
Free Lift their freeTrainingDetail is POPULATED — Aerobic Rowing carries 10
per-interval finishedReps (distance/pace/spm/time/maxHeartRate) plus per-stroke
left/rightMinRopeLengths traces (~241 samples/interval).

The autonomous resolver already delivered these (free fallback), but the digest
mislabeled them:
- KindForType: map every positive non-5 type to "free" (was only 1), so
  `workouts`/`today` label guided rowing/programs "free" instead of "" — matching
  what `session`/`today` resolve by endpoint. type 5 stays "program"; 0/absent "".
- modeForType: non-5 types now dispatch free-first (efficient + collision-safe);
  type 5 stays program-first. Both still fall back, so a misclassification resolves.

kind="free" therefore means the free *namespace*, not "freestyle": it spans a
freestyle Free Lift (no info.name, detail []) AND a guided session (info.name,
often a populated detail). Docs (SKILL/AGENTS) updated to say so and to stop
implying free ⇒ empty detail. Tests: TestKindForType, a guided-rowing fixture in
dispatchServer (type 7, populated freeTrainingDetail), TestSessionGuidedRowingHasDetail,
and today now resolves the rowing session with its per-interval data in one call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…owing split)

Consumer-agent flagged that a rowing interval's `pace` field is an instantaneous
sample, not distance/time. Per the faithful philosophy the CLI emits it verbatim
and never "corrects" it — document that Speediance fields aren't guaranteed
self-consistent and consumers should derive (split = distance/time).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
@stozo04 stozo04 merged commit 3b68166 into main Jun 19, 2026
2 checks passed
@stozo04 stozo04 deleted the fix/issue-23-session-actual-weight-telemetry branch June 19, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session --json reports planned weight, not actual — expose real per-set/per-rep telemetry (capacity + trainingInfoDetail)

1 participant