---
slug: nutricoach-telegram-checkin-stepper
status: review-approved
intent: clear
review_required: true
plan_path: .omo/plans/nutricoach-telegram-checkin-stepper.md
plan_sha256: b9767068c8206febb16ff4716bd4e01a8faf23cd11dc15f362ec55e0e8b97353
review_round_id: nutricoach-stepper-20260831T151451Z
review_round_limit: 5
round_status: approved
pending-action: write and review .omo/plans/nutricoach-telegram-checkin-stepper.md
review:
  momus:
    status: approved
    workspace_root: /home/cube/projects/richard/traning coach
    runtime_home: null
    target: .omo/plans/nutricoach-telegram-checkin-stepper.md
    round_id: nutricoach-stepper-20260831T151451Z
    plan_sha256: b9767068c8206febb16ff4716bd4e01a8faf23cd11dc15f362ec55e0e8b97353
    launch_id: momus-20260831T151451301329679Z
    session: st_01a05863
    result: okay_no_blocking_contradiction
approach: Extend the existing nutrition_daily wizard with a confirmation-prefixed next-question card, truthful step progress, at most two secondary controls (`잘 모르겠어요`, valid-history-only `이전 답 수정`), durable draft preservation plus explicit resume when needed, and send/binding recovery; preserve legacy sessions and deploy only through a fresh sealed successor after installed-wheel and live-shaped rehearsal.
---

# Draft: nutricoach-telegram-checkin-stepper

## Components (topology ledger)
<!-- Lock the SHAPE before depth. One row per top-level component that can succeed or fail independently. -->
<!-- id | outcome (one line) | status: active|deferred | evidence path -->
1 | Transition card | Every accepted answer yields one new bottom-of-chat card that confirms receipt without echoing the value and shows the next question plus truthful progress | active | `gateway/platforms/physique_checkin.py:632-746`, `gateway/platforms/physique_checkin_prompts.py:51-173`
2 | Navigation and unknown state | Nutrition-daily supports previous-answer correction, durable leave-without-loss, explicit Resume when needed, and explicit unknown without fabricating measurements or losing answer history | active | `dualcoach/profile/checkin_cli/wizard.py:571-755`, `dualcoach/profile/checkin_cli/models.py:272-294`
3 | Telegram delivery recovery | State advance, Telegram send, and prompt binding remain replay-safe across send uncertainty, stale callbacks, duplicate updates, and restart | active | `gateway/platforms/telegram.py:10194-10229`, `gateway/platforms/physique_checkin_bindings.py:1-117`
4 | Session compatibility | Existing r70 drafts, especially the live calories-step session, resume at the exact step after upgrade with no schema rewrite or answer loss | active | production `wizard/drafts/235d1edf627bf3b9ba321caa32a198f0.json`, `wizard/telegram-bindings.json`
5 | Qualification and production | Tests, installed-wheel QA, sealed rehearsal, one-use live authority, rollback, and seven-day observer prove the successor on real operational surfaces | active | `scripts/prepare_nutricoach_v150_v15_preseal.py`, `scripts/rehearse_nutricoach_v150_v15.py`, `scripts/nutricoach_v150_seven_day_observer.py`

## Open assumptions (announced defaults)
<!-- Record any default you adopt instead of asking, so the user can veto it at the gate. -->
<!-- assumption | adopted default | rationale | reversible? -->
Surface | Apply the redesigned card only to `nutrition_daily`; morning/workout/trainer flows keep current copy and behavior | The reported drop-off and chosen option concern the customer nutrition check-in | yes
Buttons | Show `잘 모르겠어요` and show `이전 답 수정` only when history exists; remove `나중에 계속` from nutrition-daily cards | The user selected button option 2; leaving Telegram requires no state transition and the durable draft remains | yes
Privacy | Confirmation says only `답변이 저장됐어요`; it never repeats weight, calories, macros, meals, health text, or other customer values | The chat is private but health-data minimization is already a project invariant | yes
Rendering | Send one new question card after each accepted answer; do not edit/delete earlier Telegram or customer messages | New cards stay at the bottom where customers notice them and avoid edit-in-place ordering problems | yes
Dependencies | Add no web app, database, external dependency, analytics, or paid service | Existing Telegram/Python surfaces already supply the needed controls | yes
Compatibility | Preserve `WizardSession` schema and treat old drafts as valid without migration | Current durable state already contains sufficient step/history/version data | yes
Release | Build and rehearse while the customer may continue, but execute the live switch only after the active session is complete or exact-step successor resume has passed preflight | Prevents a mid-answer customer disruption | yes
Testing | TDD for behavior changes, plus installed-wheel/manual Telegram-shaped QA and sealed production gates | Required by project test and deployment discipline | yes
Generation | Use fresh generation `r71`; preflight must prove every r71 candidate/preseal/authority/execution/runtime/observer root absent before creation | r70 is current and consumed; one-use roots must never be replayed | no after sealing

## Findings (cited - path:lines)
- The chosen UX is partly present already: `_with_navigation` renders `진행 position/total`, `이전`, and `나중에 계속`, but no accepted-answer confirmation and no general unknown control (`gateway/platforms/physique_checkin.py:704-746`).
- Accepted typed answers advance and persist the domain session before Telegram renders the next prompt (`gateway/platforms/physique_checkin.py:359-389,632-675`).
- `_render_physique_text_prompt` binds the new message only after Telegram send returns, but currently swallows every send exception and leaves no explicit recovery receipt (`gateway/platforms/telegram.py:10194-10229`).
- Previous navigation already truncates future history and recomputes dependent branches; defer increments the version and keeps the durable draft (`dualcoach/profile/checkin_cli/wizard.py:716-755`).
- The contract model permits nullable measurements, but nutrition serialization currently indexes calories/macros and converts them unconditionally, so unknown support needs an explicit typed storage-to-event contract rather than a UI-only button (`dualcoach/profile/checkin_cli/models.py:272-294`, `dualcoach/profile/checkin_cli/wizard.py:1145-1182`).
- Existing gateway UX tests exercise Korean units, invalid-field recovery, full completion, previous/defer/replay boundaries, and installed-wheel smoke paths (`tests/gateway/test_customer_nutrition_ux.py`, `tests/gateway/test_telegram_physique_checkin.py`, `dualcoach/profile/tests/test_customer_nutrition_wizard.py`).
- The nutrition-daily launcher enters through `gateway/platforms/nutrition_coaching.py`, while typed/callback transitions stay inside `PhysiqueCheckinBridge`; cron cards are not this customer-launch path and remain out of scope.
- The implementation must preserve the opaque `pc1:` callback grammar, Telegram's 64-byte callback limit, exact owner/chat/topic/message checks, binding version checks, and `WizardStatus` meanings (`gateway/platforms/physique_checkin.py:24-122,301-389,632-675`).
- Existing regression anchors include completed-day reopen without duplication, append-only correction, repeated correction accumulation, compact nutrition edit menu, same-binding edit transitions, callback opacity/length, and launcher round-trip tests.
- Production r70 currently has one unfinished `nutrition_daily` draft at `calories`, version 1, with `bodyweight` as the only answer and prompt message 101 bound; completion ledger remains empty.
- A direct live read at `2026-08-31T22:48:16+09:00` reconfirmed that draft; an explore-agent claim that no draft existed was rejected as stale/incomplete evidence.
- The current immutable deployment chain is `seal_nutricoach_v150_candidate.py` -> `verify_nutricoach_v150_candidate.py` -> `prepare_nutricoach_v150_v15_preseal.py` -> `verify_nutricoach_v150_preseal_v15.py` -> exact sealed tests -> `rehearse_nutricoach_v150_v15.py` -> one-use `execute_nutricoach_v150_sealed_live.py`.
- Current production evidence roots are `.strict-runtime/81a7a06e-v150`, `live-transaction-preseal-v15-runtime-authority-r70`, `live-executions-v15/nutricoach-v150-v15-runtime-authority-r70`, `live-authorization-v15/nutricoach-v150-v15-runtime-authority-r70`, and `observer-r70`; a successor must use fresh generation-specific roots and never replay r70 authority.
- Observer source is split across `nutricoach_v150_observer_contract.py`, `nutricoach_v150_observer_runtime.py`, `nutricoach_v150_observer_final.py`, and `nutricoach_v150_seven_day_observer.py`; rebinding must update candidate/runtime/execution/authorization/output plus the systemd checksum and writable output root.
- Telegram cannot participate in an atomic transaction and `sendMessage` has no idempotency key or reliable bot-readable message history; the achievable guarantee is at-most-once automatic send attempt plus explicit recovery, not exactly-once delivery.
- Domain state and Telegram binding state are separate durable files. A crash after wizard CAS commit but before prompt receipt can leave the domain cursor ahead of the visible UI, so the binding layer needs a content-free durable projection record.
- The projection record belongs at the gateway edge, not inside canonical health-domain events: it may contain ingress update/message identity, source and target cursor, phase, attempt generation, receipt message ID, and expiry, but no raw answer, callback payload, or customer-value hash.
- File locks must protect only local state transitions and must never be held during Telegram I/O; existing private-mode, temporary-file, `fsync`, `os.replace`, and no-follow patterns remain mandatory.

## Decisions (with rationale)
- Use option 2 selected by the user: confirmation plus a new next-question card.
- Use button option 2 selected by the user: `잘 모르겠어요` plus valid-history-only `이전 답 수정`; no per-question `나중에 계속` button.
- Do not claim that opening a Telegram chat emits an automatic resume event. The existing card remains answerable until expiry; explicit launcher/Resume republishes the authoritative cursor afterward or after delivery uncertainty.
- Treat the next card, not a separate acknowledgement message, as the receipt surface to avoid doubling message volume.
- Keep progress machine-derived from durable step history and flow steps; never hardcode display numbers in Telegram copy.
- Reuse opaque versioned callbacks and the existing previous transition instead of introducing parallel navigation state; retain the domain defer capability for compatibility but do not render it on nutrition-daily cards.
- Keep `checkin_cli.wizard` authoritative for answers, cursor versions, branches, finalization, and canonical events; gateway binding/projection state is repairable UI state only.
- Add an additive binding schema that reads legacy v1 and writes v2. Preserve the existing `pc1:` grammar so live r70 cards remain actionable.
- Never automatically resend after any durable Telegram send failure or uncertainty. Require explicit customer Resume; a subsequent valid text/callback may reconcile the cursor without inventing a message ID.
- Prefer one new next-question card whose header confirms the prior answer. Do not edit/delete the old card as part of the normal path; this removes a second Telegram operation and still matches the user-selected option 2.
- On rollback, refuse an old runtime while any v2 projection is nonterminal or delivery-uncertain; use the successor in recovery-only mode and fix forward instead.
- Add `state_schema_version=2` and `unknown_steps` to the private wizard session. Known, unknown, and unanswered are mutually exclusive; legacy sessions default to `unknown_steps=()`.
- Offer `잘 모르겠어요` for every nutrition-daily field except `optional_note`. Unknown counts as completed progress, stores no value in `answers`, and maps to omitted/`None` `ContractCheckin` fields.
- Preserve unknown explicitly only in the private finalized session. Canonical events remain schema-compatible and represent it as missing evidence; no sentinel text enters events, reports, callbacks, bindings, logs, or customer copy.
- Unknown measurements never become zero, normal, rest, adherence, or safety evidence. Existing projections must continue to classify them as missing.
- Progress is completed known fields plus completed unknown fields plus optional-note `없음`, over the 12 base fields plus active adaptive follow-ups.
- Existing r70 calories-step session loads with `unknown_steps=()`, keeps session/customer/day/version/step/binding/expiry unchanged, and first serializes v2 only after a successful mutation.
- Target a fresh `r71` successor. If any r71 one-use root exists at execution preflight, stop rather than reuse or overwrite it.

## Scope IN
- Nutrition-daily accepted-answer confirmation, next-card progress and navigation copy.
- Unknown answer semantics for fields where a missing value can be represented without fabricating data.
- Previous answer correction, durable save when leaving, explicit Resume after expiry or uncertain delivery, stale/replay/duplicate protection, send uncertainty, restart recovery, and expiry.
- Legacy r70 draft compatibility and live calories-step preflight.
- Related profile/gateway tests, installed-wheel customer simulation, fresh sealed successor, production verification, and observer rebinding.

## Scope OUT (Must NOT have)
- No Telegram Mini App or browser UI.
- No redesign of onboarding, morning, workout, trainer, weekly reporting, or operator review surfaces except shared helpers strictly required by nutrition-daily.
- No echo of customer-entered values in confirmation copy.
- No customer message sent by the agent during implementation or qualification.
- No new external service, dependency, analytics, payment/account action, commit, push, PR, or merge.

## Open questions
- None. The user selected option 2 and the remaining storage, privacy, rollout, budget, stack, scale, audience, and test-strategy choices have evidence-backed defaults above.

## Approval gate
status: awaiting-approval
approach: Extend only `nutrition_daily` with one value-free confirmation-plus-next-question card, at most two secondary controls (`잘 모르겠어요`, valid-history-only `이전 답 수정`), durable draft preservation without a `나중에 계속` button, explicit exact-cursor Resume after expiry or uncertain delivery, schema-v2 private unknown metadata, gateway-owned durable publication projection, v1-compatible exact-session preservation, and a fresh r71 sealed production upgrade with observer rebinding.
next workflow action: Execute the approved plan in a separate worker session with `/ulw-execute nutricoach-telegram-checkin-stepper`.
<!-- When exploration is exhausted and unknowns are answered, set status: awaiting-approval. -->
<!-- That durable record is the loop guard: on a later turn read it and resume at the gate instead of re-running exploration. -->

## Review convergence ledger
- Round 1 receipt: `st_01a05860`, plan SHA-256 `ce838f230c7af6d1c38201654316be5f119b43c8e9067459b4f083feee86b5a9`.
- Accepted blocker 1 (`explicit_requirement_or_accepted_decision`): bind every implementation reference and command to the actual source workspace `/home/cube/projects/richard/.worktrees/nutricoach-v150-combined`.
- Accepted blocker 2 (`explicit_requirement_or_accepted_decision`): give F1-F4 exact native tool invocations, steps, evidence paths, and machine-verifiable APPROVE/blocker_count 0 conditions.
- Non-blocking notes: none.
- Ledger frozen after round 1. Later rounds may verify these two blockers, regressions introduced by their fixes, and only new findings that pass blocker eligibility.
- Round 2 initialized: fixed implementation workspace binding and executable F1-F4 invocation contracts; pending fresh Momus receipt.
- Round 2 approved: Momus receipt `st_01a05863` returned `[OKAY]`; live plan SHA-256 matched `b9767068c8206febb16ff4716bd4e01a8faf23cd11dc15f362ec55e0e8b97353`; no blocking contradiction remained.
