I read this as a static incident-path investigation - I’ll trace the live Telegram check-in flow, activation refresh semantics, and pre-send failure surface, then rank hypotheses against the reported `handler_exception` without making changes or live calls.


The ingress is split between generic Telegram exception fencing and a customer-specific nutrition route, not the legacy owner bridge. I’m narrowing on the exact coordinator refresh call and the statements executed before the local send exception handler.


The current evidence rules out “restart required”: the already-instantiated coordinator fingerprints and reloads the committed registry on every ingress. The incident marker is intentionally content-free, so the exact exception is not logged; the strongest static gap is the first-time reply-keyboard send, which occurs before the card send’s catch.


I read this as a static incident-path investigation - I traced the current source and correlated it with the committed activation and `handler_exception` evidence without edits or live calls.

## Conclusion

- **Activation does not require a gateway restart.** An existing coordinator calls `refresh_live_registry()` on every `_get_nutrition_coaching()` ingress and rebuilds routes when the registry inode/size/mtime/ctime changes.
- The committed activation is present and valid-looking: the registry is enabled and the activation journal is `committed`.
- Plain ASCII **`checkin` is not an accepted launcher**. Only normalized Korean `체크인 시작` and `오늘 체크인` launch the customer card.
- The strongest `handler_exception` candidate on the accepted launcher path is the **first reply-keyboard publication**, because it occurs immediately before—and outside—the card send’s `try/except`.
- Separately, the current profile lacks `data/owner-actions/customer-service-state.json`. That makes the transport gate fail closed as “paused,” preventing card publication cleanly. It explains “no card,” but by itself should not produce `handler_exception`.

## Exact accepted text

Customer launcher recognition normalizes all whitespace with `" ".join(text.split())`, then accepts exactly:

- `체크인 시작`
- `오늘 체크인`

Evidence: `gateway/platforms/telegram.py:7489-7492`.

Therefore leading/trailing whitespace, repeated spaces, tabs, and newlines between the Korean words are accepted. These are not accepted:

- `checkin`
- `check-in`
- `체크인`
- case or punctuation variants

Correction aliases are handled separately:

- `오늘 체크인 수정`
- `체크인 수정`

Evidence: `gateway/platforms/nutrition_coaching.py:3193-3208`.

## Exact live accepted-launcher path

1. `_handle_text_message` extracts the effective message and runs onboarding/bootstrap/review reservations first:
   `gateway/platforms/telegram.py:13592-13612`.

2. `_get_nutrition_coaching()` retrieves the existing coordinator:
   `gateway/platforms/telegram.py:13613`.

3. Because the coordinator already exists, `_get_nutrition_coaching()` calls `refresh_live_registry()` before returning it:
   `gateway/platforms/telegram.py:4667-4676`.

4. `refresh_live_registry()` delegates to `_ensure_live_registry()`:
   `gateway/platforms/nutrition_coaching.py:2312-2314`.

5. `_ensure_live_registry()`:
   - acquires the profile authority lock;
   - reloads via `load_committed_customer_registry`;
   - fingerprints the registry;
   - calls `_configure_registry()` if its generation changed.
   Evidence: `gateway/platforms/nutrition_coaching.py:2274-2310`.

6. `_configure_registry()` reconstructs enabled customer routes, registered `WizardService`, `PhysiqueCheckinBridge`, bindings, `_routes`, `_by_key`, event sources, and owned spaces:
   `gateway/platforms/nutrition_coaching.py:2240-2269`.

7. Telegram builds the exact address. A private DM is forced to topic `"0"`:
   `gateway/platforms/telegram.py:5244-5261`.

8. `nutrition.resolve(address)` refreshes again and performs an exact `(user_id, chat_id, topic_id)` lookup:
   `gateway/platforms/nutrition_coaching.py:2704-2707`; called at `gateway/platforms/telegram.py:13628`.

9. For an accepted alias, `_send_nutrition_customer_card()` is called:
   `gateway/platforms/telegram.py:13629-13636`.

10. It resolves the route again and obtains the customer key:
    `gateway/platforms/telegram.py:5790-5799`.

11. The transport gate calls `customer_transport_allowed(...)`:
    `gateway/platforms/telegram.py:5800-5808`.

12. That gate refreshes again, checks operational pause, enabled status, consent, 28-day plan window, exact destination, and safety hold:
    `gateway/platforms/nutrition_coaching.py:2551-2584`.

13. The card prompt builds:
    - opaque stable customer-start callback;
    - program-day label;
    - pause/resume control;
    - inline “오늘 체크인 시작” button.
    Evidence: `gateway/platforms/telegram.py:5739-5780`; callback generation at `gateway/platforms/nutrition_coaching.py:445-455`.

14. Before the card itself, `_ensure_customer_checkin_keyboard()` publishes a persistent reply keyboard containing:
    - `오늘 체크인`
    - `오늘 체크인 수정`
    Evidence: `gateway/platforms/telegram.py:5698-5721`, called at `gateway/platforms/telegram.py:5817`.

15. Only after that does the card send enter a catch-all `try/except`:
    `gateway/platforms/telegram.py:5818-5831`.

## Activation refresh behavior

No restart is intended.

The coordinator stores a four-part registry fingerprint at construction and compares it on every refresh: inode, size, mtime_ns, ctime_ns. An atomic registry replacement therefore triggers reconfiguration. Evidence:

- initialization: `gateway/platforms/nutrition_coaching.py:2143-2152`
- fingerprint: `gateway/platforms/nutrition_coaching.py:2182-2193`
- reload/reconfigure: `gateway/platforms/nutrition_coaching.py:2274-2310`
- ingress refresh: `gateway/platforms/telegram.py:4667-4676`

The profile loader also revalidates committed activation receipts on every load:

- `checkin_cli/customer_admin.py:1982-1989`
- receipt validation: `checkin_cli/customer_admin.py:1790-1914`
- enabled-customer validation: `checkin_cli/customer_admin.py:1917-1926`

Current state supports a successful committed activation:

- registry `enabled: true`, written `2026-08-17 08:29:36 KST`;
- journal `state: committed`, `recovery_required: false`;
- transaction `4308149d8c04435583b708fea73a7a3a`;
- incident update `629525116` failed at `08:47:23 KST`;
- gateway had been running since `01:41:24 KST`.

Thus the registry changed while the gateway was running, precisely the scenario the live refresh handles.

## Unhandled exception candidates before the card-send catch

From `_send_nutrition_customer_card()` entry through line 5817:

1. **Authority-lock entry/exit failure** during any refresh. The `with profile_authority_lock(...)` is outside `_ensure_live_registry()`’s inner `try`:
   `gateway/platforms/nutrition_coaching.py:2279-2281`.
   Registry loading and reconfiguration exceptions inside the block are converted to `False`, but context-manager failures can escape.

2. **Fallback transport-gate call failure.** The first call catches only `TypeError`, then invokes the compatibility signature without protection:
   `gateway/platforms/telegram.py:5803-5806`.

3. **Unexpected operational-state-store exception.** `customer_service_paused()` catches only `CustomerServiceStateError`; non-contract exceptions could escape:
   `gateway/platforms/nutrition_coaching.py:2731-2740`.

4. **Current-date provider exception.** `current_date()` is invoked without a surrounding catch at the card layer:
   `gateway/platforms/telegram.py:5809-5813`.
   The normal coordinator implementation internally falls back, making this low probability.

5. **Program-day customer lookup exception.** `_nutrition_program_day_label()` calls `coordinator.customer()` before its arithmetic-only `try`:
   `gateway/platforms/telegram.py:5729-5734`.
   That lookup refreshes the registry again.

6. **Service-controls lookup exception.** `customer_service_controls_for_customer()` is wholly outside the prompt builder’s catch:
   `gateway/platforms/telegram.py:5760-5769`.
   It refreshes again and reads the pause ledger.

7. **Malformed control-button iteration.** `tuple(getattr(controls, "buttons", ()) or ())` can raise `TypeError`:
   `gateway/platforms/telegram.py:5770`.

8. **Reply-keyboard construction failure.** The persistent constructor catches `TypeError`, but the compatibility constructor can still throw:
   `gateway/platforms/telegram.py:5698-5703`.

9. **Reply-keyboard transport failure—the primary gap.** Any Telegram/network/runtime exception from `_send_nutrition_topic()` escapes because line 5817 precedes the catch beginning at 5818:
   `gateway/platforms/telegram.py:5715-5720`, `5817-5818`.
   For topic `"0"`, this directly awaits `bot.send_message`; a disconnected bot raises `RuntimeError`, and all provider exceptions propagate: `gateway/platforms/telegram.py:5680-5684`.

10. **Post-send set mutation** at `initialized.add(key)`:
    `gateway/platforms/telegram.py:5721`.
    The key is constructed from strings, so this is effectively negligible.

The outer PTB handler discards exception detail and records only `handler_exception`:
`gateway/platforms/telegram.py:11241-11254`. That is why the logs cannot identify which candidate fired.

## Ranked incident hypotheses

1. **First-time reply-keyboard send escaped before the card catch.**
   Activation introduced a new route, so its `(chat_id, topic_id)` was absent from the process-local initialized set. The first accepted launcher necessarily executes line 5817. Any provider/network error becomes exactly the observed `handler_exception`, while the gateway remains running.

2. **The customer sent literal ASCII `checkin`, not an accepted launcher alias.**
   It would bypass the card/keyboard path, enter `nutrition.handle_text()`, receive the “submission only” rejection, and then use `_render_nutrition_text()`’s uncaught send at `gateway/platforms/telegram.py:6128-6134`. A send failure there also becomes `handler_exception`. This hypothesis rises to first if `'checkin'` is the exact submitted text rather than shorthand in the incident description.

3. **Profile-module/provenance contamination during refresh.**
   The adapter only inserts the profile package path into `sys.path`; already imported `checkin_cli` modules remain cached: `gateway/platforms/telegram.py:4693-4705`. Earlier logs contain “nutrition onboarding module is outside configured profile,” so cross-profile import history is a real environmental concern. Most refresh failures are converted to a clean `None`, however, making it less consistent with `handler_exception` unless the authority-lock boundary itself failed.

4. **Missing operational pause ledger blocked transport.**
   `data/owner-actions/customer-service-state.json` is currently absent. `is_paused()` raises when missing (`gateway/platforms/nutrition_service_state.py:72-74`), and `customer_service_paused()` fails closed. This should return `False` from the transport gate without an exception, so it explains silent non-publication but not the receipt marker.

5. **Card prompt/control construction raised unexpectedly.**
   The unguarded controls lookup and iteration are genuine exception surfaces but use trusted coordinator types, making them less likely than the publication gap.

## Best test seam

Use `TelegramAdapter._handle_text_message()` with a real temporary `NutritionCoachingCoordinator` and an injected async `_send_nutrition_topic`:

1. Construct the coordinator from a disabled registry.
2. Atomically replace it with a valid committed enabled registry.
3. Submit both accepted aliases and assert live route refresh without recreating the adapter.
4. Assert exactly two publications on first launch: reply keyboard, then inline card.
5. Inject a sentinel exception on the first publication and assert that it identifies the pre-catch keyboard seam.
6. Parameterize ASCII `checkin` as a negative launcher case.

The existing ingress tests around `tests/gateway/test_telegram_physique_checkin.py:2474-2523` are the closest location, but they do not currently combine live registry replacement, the real transport gate, and first keyboard publication.