return True
        if re.search(r"(?m)^<details\b|^|^<summary\b|^", content):
            return True
        if "$$" in content:
            return True
        return False

    def _rich_eligible(self, content: str) -> bool:
        """Capability/content eligibility for rich, ignoring ``expect_edits``.

        Shared core of :meth:`_should_attempt_rich` minus the per-call
        ``expect_edits`` metadata gate.  The rich EDIT-finalize path
        (:meth:`_try_edit_rich`) needs this: a streamed preview is sent with
        ``expect_edits=True`` to stay on the editable path mid-stream, but the
        FINAL edit should still upgrade to rich when the content warrants it.
        """
        return bool(
            getattr(self, "_rich_messages_enabled", True)
            and not getattr(self, "_rich_send_disabled", False)
            and content
            and content.strip()
            and self._needs_rich_rendering(content)
            and not self._has_telegram_desktop_details_math_crash_shape(content)
            and self._content_fits_rich_limits(content)
            and self._bot_supports_rich()
        )

    def _should_attempt_rich(
        self, content: str, metadata: Optional[Dict[str, Any]] = None
    ) -> bool:
        return bool(
            not (metadata or {}).get("expect_edits")
            and self._rich_eligible(content)
        )

    def prefers_fresh_final_streaming(
        self, content: str, metadata: Optional[Dict[str, Any]] = None
    ) -> bool:
        """Whether to replace a streamed preview with a fresh rich final.

        Disabled for Telegram. The fresh-final path briefly shows two copies of
        the final answer, then deletes the streaming preview after the rich send
        succeeds — it looks like duplicate delivery at the end of every streamed
        turn (the reason #46206 reverted it).  Rich finalize is instead handled
        by editing the existing preview in place via Bot API 10.1's
        ``editMessageText`` ``rich_message`` parameter (see
        :meth:`_try_edit_rich`), so no fresh re-send / delete is needed.
        """
        return False

    def streaming_overflow_limit(self) -> Optional[int]:
        """Allow the stream consumer to accumulate up to the rich-message cap
        before splitting, so a reply that fits one ``sendRichMessage`` /
        ``sendRichMessageDraft`` isn't fragmented at the 4,096 MarkdownV2 limit.

        Gated on the same rich capability as the send path (minus the
        content-length check — raising that cap is the whole point): rich not
        latched off and the bot exposes an async ``do_api_request``.  Returns
        ``None`` (→ legacy 4,096 limit) when rich isn't available, so non-rich
        streams split exactly as before.
        """
        if (
            getattr(self, "_rich_messages_enabled", True)
            and not getattr(self, "_rich_send_disabled", False)
            and self._bot_supports_rich()
        ):
            return self.RICH_MESSAGE_MAX_CHARS
        return None

    def _rich_message_payload(
        self, content: str, *, skip_entity_detection: bool = False
    ) -> Dict[str, Any]:
        """Build the ``InputRichMessage`` object from RAW markdown.

        Never pass ``format_message(content)`` here — that converts to
        MarkdownV2 and would escape/destroy rich syntax like table pipes.
        """
        payload: Dict[str, Any] = {"markdown": content}
        if skip_entity_detection:
            payload["skip_entity_detection"] = True
        return payload

    def _is_rich_capability_error(self, exc: Exception) -> bool:
        """True ⇒ the rich endpoint itself is unavailable (old PTB/server).

        These latch rich off for the rest of the adapter's life — retrying is
        pointless and would cost a failed roundtrip on every send. Per-message
        rejections (BadRequest from a parser/limit issue) are NOT capability
        errors: the next message may be fine.
        """
        name = exc.__class__.__name__.lower()
        if name in {"endpointnotfound", "invalidtoken"}:
            return True
        if isinstance(exc, (AttributeError, TypeError, NotImplementedError)):
            return True
        if getattr(exc, "error_code", None) == 404:
            return True
        s = str(exc).lower()
        if ("method" in s or "endpoint" in s) and (
            "not found" in s or "does not exist" in s
        ):
            return True
        return "no such method" in s

    def _is_rich_fallback_error(self, exc: Exception) -> bool:
        """True ⇒ permanent/capability error ⇒ safe to fall back to legacy.

        Conservative on purpose: only clearly-permanent failures (BadRequest,
        capability errors, unknown/unsupported endpoint) qualify. Everything
        else is treated as transient — the rich request may have reached
        Telegram, so we must NOT legacy-resend and risk a duplicate.
        """
        if self._is_bad_request_error(exc):
            return True
        if self._is_rich_capability_error(exc):
            return True
        s = str(exc).lower()
        return "unsupported" in s or "not implemented" in s

    def _compute_single_send_routing(
        self,
        chat_id: str,
        reply_to: Optional[str],
        metadata: Optional[Dict[str, Any]],
        thread_id: Optional[str],
    ) -> Optional[tuple]:
        """Routing for a single (rich) send — mirrors send()'s index-0 block.

        Returns ``(reply_to_id, thread_kwargs)``, or ``None`` to signal "skip
        rich, let the legacy path handle it" — used for the DM-topic fail-loud
        case so the legacy path stays the single source of the refuse result.
        """
        metadata_reply_to = self._metadata_reply_to_message_id(metadata)
        private_dm_topic_send = self._is_private_dm_topic_send(chat_id, thread_id, metadata)
        dm_topic_reply_to_off = (
            private_dm_topic_send
            and self._reply_to_mode == "off"
            and bool(metadata and metadata.get("telegram_dm_topic_reply_fallback"))
        )
        reply_to_source = reply_to or (
            str(metadata_reply_to)
            if private_dm_topic_send and metadata_reply_to is not None
            else None
        )
        if private_dm_topic_send:
            should_thread = reply_to_source is not None and self._reply_to_mode != "off"
        else:
            should_thread = self._should_thread_reply(reply_to_source, 0)
        reply_to_id = int(reply_to_source) if should_thread and reply_to_source else None
        if private_dm_topic_send and reply_to_id is None and not dm_topic_reply_to_off:
            # Refusing to send outside the requested DM topic — defer to the
            # legacy path, which returns the canonical fail-loud SendResult.
            return None
        thread_kwargs = self._thread_kwargs_for_send(
            chat_id,
            thread_id,
            metadata,
            reply_to_message_id=reply_to_id,
            reply_to_mode=self._reply_to_mode,
        )
        return reply_to_id, thread_kwargs

    async def _try_send_rich(
        self,
        chat_id: str,
        content: str,
        reply_to: Optional[str],
        metadata: Optional[Dict[str, Any]],
    ) -> Optional[SendResult]:
        """Attempt a single ``sendRichMessage`` send.

        Returns a :class:`SendResult` (success, or a transient failure that the
        caller must NOT legacy-resend), or ``None`` to signal "fall back to the
        legacy MarkdownV2 path" (permanent/capability error or DM-topic skip).
        """
        thread_id = self._metadata_thread_id(metadata)
        routing = self._compute_single_send_routing(chat_id, reply_to, metadata, thread_id)
        if routing is None:
            return None
        reply_to_id, thread_kwargs = routing

        payload: Dict[str, Any] = {
            "chat_id": int(chat_id),
            "rich_message": self._rich_message_payload(content),
        }
        # Only forward non-None routing keys: when direct_messages_topic_id is
        # present _thread_kwargs_for_send pairs it with message_thread_id=None,
        # which must not be sent as a stray field on the raw endpoint.
        payload.update({k: v for k, v in thread_kwargs.items() if v is not None})
        payload.update(self._notification_kwargs(metadata))
        if getattr(self, "_disable_link_previews", False):
            payload["link_preview_options"] = {"is_disabled": True}
        if reply_to_id is not None:
            # Spec: sendRichMessage takes reply_parameters (ReplyParameters
            # object), NOT the legacy reply_to_message_id scalar. Unknown
            # params are silently ignored by the Bot API, so the scalar would
            # quietly drop the reply anchor instead of erroring.
            payload["reply_parameters"] = {"message_id": reply_to_id}

        try:
            # Take the raw Bot API result (dict under real PTB). Passing
            # return_type=Message would make PTB deserialize a Bot API 10.1
            # response shape it does not fully model yet; a post-delivery parse
            # error must not be mistaken for a sendable failure.
            msg = await self._bot.do_api_request(
                "sendRichMessage", api_kwargs=payload
            )
        except Exception as exc:
            if self._is_rich_fallback_error(exc):
                if self._is_rich_capability_error(exc):
                    # Endpoint missing (old PTB/server) — latch rich off so
                    # every later send doesn't pay a doomed extra roundtrip.
                    self._rich_send_disabled = True
                logger.debug(
                    "[%s] sendRichMessage rejected (%s) — falling back to MarkdownV2",
                    self.name, exc,
                )
                return None
            # Transient / network / unknown: the request may have reached
            # Telegram. Do NOT legacy-resend (duplicate risk); surface a
            # failure with retry semantics mirroring the legacy send() except.
            err_str = str(exc).lower()
            try:
                from telegram.error import TimedOut as _TimedOut
            except (ImportError, AttributeError):
                _TimedOut = None
            is_timeout = (_TimedOut and isinstance(exc, _TimedOut)) or "timed out" in err_str
            is_connect_timeout = self._looks_like_connect_timeout(exc)
            logger.warning(
                "[%s] sendRichMessage transient failure (no legacy resend): %s",
                self.name, exc,
            )
            return SendResult(
                success=False,
                error=str(exc),
                retryable=(is_connect_timeout or not is_timeout),
            )

        message_id = None
        if isinstance(msg, dict):
            message_id = msg.get("message_id")
            if message_id is None:
                message_id = (msg.get("result") or {}).get("message_id")
        else:
            message_id = getattr(msg, "message_id", None)
        if message_id is not None:
            # Telegram won't echo rich content in reply_to_message, so remember
            # what we sent — replies to this message resolve via this index.
            try:
                from gateway import rich_sent_store
                rich_sent_store.record(str(chat_id), str(message_id), content)
            except Exception:
                pass
        return SendResult(
            success=True,
            message_id=str(message_id) if message_id is not None else None,
        )

    async def _try_edit_rich(
        self,
        chat_id: str,
        message_id: str,
        content: str,
    ) -> Optional[SendResult]:
        """Edit an existing message in place as a rich message (Bot API 10.1).

        Uses ``editMessageText`` with the ``rich_message`` parameter so a
        streamed preview can finalize as rich (tables/task lists/details/math)
        WITHOUT a fresh send + delete — no duplicate preview.  Mirrors
        :meth:`_try_send_rich`'s error contract:

        - success → ``SendResult(success=True, message_id=...)``
        - permanent / capability error → ``None`` (caller falls back to the
          legacy MarkdownV2 edit; capability errors latch rich off)
        - transient / unknown → ``SendResult(success=False)`` with retry
          semantics (the message may already be edited; do NOT legacy-resend)
        """
        payload: Dict[str, Any] = {
            "chat_id": int(chat_id),
            "message_id": int(message_id),

[13166 more lines in file. Use offset=1530 to continue.]


        if payload.get("status") == "card":
            grounding_input = self._adaptive_grounding_input(payload)
            if grounding_input is not None:
                current_supplier = lambda: self._adaptive_grounding_input(payload)
                self._coaching_current_input_supplier = (
                    "adaptive_operator",
                    current_supplier,
                )
                if not self._coaching_authority_valid(
                    "adaptive_operator",
                    grounding_input,
                    current_supplier,
                ):
                    self._coaching_current_input_supplier = None
                    await self._send_adaptive_operator_notice(
                        message,
                        "요청은 저장됐지만 검토 카드를 안전하게 갱신하지 못했습니다. "
                        "아래 적응형 영양 검토 메뉴에서 최신 카드를 다시 열어주세요.",
                    )
                    return
                coached_text = await TelegramAdapter._adaptive_coach_card_text(
                    self,
                    canonical_text,
                    payload,
                    grounding_input,
                )
                if coached_text is not None:
                    text = coached_text
                else:
                    self._coaching_current_input_supplier = None
                    coach_failed = True
                    text = "Coach 권한을 생성하지 못해 승인할 수 없습니다."
        buttons = [] if coach_failed else payload.get("buttons")
        markup = None
        if isinstance(buttons, list):
            rows = []
            for item in buttons:
                if isinstance(item, dict) and item.get("callback_data"):
                    label = str(item.get("label", item.get("customer_key", "선택")))
                    rows.append([InlineKeyboardButton(label[:64], callback_data=str(item["callback_data"]))])
                elif isinstance(item, str):
                    action = item.rsplit(":", 1)[-1]
                    rows.append([InlineKeyboardButton(action, callback_data=item)])
            if rows:
                markup = InlineKeyboardMarkup(rows)
        if (
            payload.get("status") == "card"
            and (
                grounding_input is None
                or not self._coaching_authority_valid(
                    "adaptive_operator",
                    grounding_input,
                    current_supplier,
                )
            )
        ):
            await self._send_adaptive_operator_notice(
                message,
                "요청은 저장됐지만 검토 카드를 안전하게 갱신하지 못했습니다. "
                "아래 적응형 영양 검토 메뉴에서 최신 카드를 다시 열어주세요.",
            )
            return
        if payload.get("status") == "card":
            replied = getattr(message, "reply_to_message", None)
            editor = getattr(replied, "edit_text", None)
            if callable(editor):
                await self._send_adaptive_operator_notice(
                    message,
                    "운영자 메모를 반영했습니다.",
                )
                await editor(text=text, reply_markup=markup)
                return
        sent = await self._send_adaptive_operator_notice(
            message,
            text,
            reply_markup=markup,
        )
        if payload.get("status") == "menu" and markup is not None:
            published_message_id = getattr(sent, "message_id", None)
            if isinstance(published_message_id, bool) or not isinstance(
                published_message_id, (str, int)
            ):
                raise RuntimeError("adaptive review menu publication receipt is invalid")
            service = getattr(self, "_adaptive_operator_service", None)
            rebind = getattr(service, "_rebind_card_button_sessions", None)
            if not callable(rebind):
                raise RuntimeError("adaptive review menu session rebind is unavailable")
            rebind(payload, str(published_message_id))

    async def _reserve_adaptive_review_update(self, update: object, message: object) -> bool:
        """Reserve the configured review chat/topic before every generic route."""
        control = getattr(self, "_diagnostic_control_service", None)
        host = getattr(self, "_diagnostic_isolation_host", None)
        if control is not None or host is not None:
            chat_id = getattr(getattr(message, "chat", None), "id", None)
            topic_id = getattr(message, "message_thread_id", None)
            user_id = getattr(getattr(message, "from_user", None), "id", None)
            if all(isinstance(value, int) and not isinstance(value, bool) for value in (chat_id, topic_id, user_id)):
                if control is not None and control.matches_space(chat_id=chat_id, topic_id=topic_id):
                    try:
                        control.authenticate(user_id=user_id, chat_id=chat_id, topic_id=topic_id)
                    except Exception:
                        await self._send_adaptive_operator_result(
                            message,
                            {"text": "격리 진단 제어 권한이 없습니다."},
                        )
                    return True
                if (
                    host is not None
                    and host.reserves_space(chat_id=chat_id, topic_id=topic_id)
                ):
                    try:
                        host.authorize_route(user_id=user_id, chat_id=chat_id, topic_id=topic_id)
                    except Exception:
                        await self._send_adaptive_operator_result(
                            message,
                            {"text": "격리 진단 역할 권한이 없습니다."},
                        )
                    return True
        configured = self._nutrition_operator_address()
        if configured is None:
            return False
        address = self._nutrition_address(message, message)
        if not self._is_adaptive_review_space(address):
            return False
        dual_service = getattr(self, "_dual_coach_review_service", None)
        if dual_service is None:
            nutrition = getattr(self, "_nutrition_coaching", None) or self._get_nutrition_coaching()
            if nutrition is not None:
                try:
                    from gateway.platforms.nutrition_coaching import DualCoachReviewService

                    dual_service = DualCoachReviewService(
                        nutrition,
                        getattr(self._adaptive_nutrition_config, "review_operator", None),
                    )
                    self._dual_coach_review_service = dual_service
                except Exception:
                    dual_service = None
        if dual_service is not None and dual_service.accepts(address) is True:
            await self._drain_dual_coach_review_cards()
        service = getattr(self, "_adaptive_operator_service", None)
        if service is None:
            nutrition = self._get_nutrition_coaching()
            service = getattr(self, "_adaptive_operator_service", None)
            if service is None and nutrition is not None:
                try:
                    from gateway.platforms.nutrition_coaching import AdaptiveOperatorService

                    service = AdaptiveOperatorService(
                        nutrition,
                        review_operator=getattr(self._adaptive_nutrition_config, "review_operator", None),
                        profile_root=getattr(nutrition, "profile_root", None),
                        schedule_confirm_handler=(
                            getattr(nutrition, "schedule_confirm_handler", None)
                            if getattr(
                                self._adaptive_nutrition_config,
                                "schedule_confirm_enabled",
                                False,
                            ) is True
                            else None
                        ),
                        schedule_confirm_enabled=getattr(
                            self._adaptive_nutrition_config,
                            "schedule_confirm_enabled",
                            False,
                        ),
                    )
                    self._adaptive_operator_service = service
                except Exception:
                    service = None
        if service is None:
            await self._send_adaptive_operator_result(
                message,
                {"text": "이 공간에서는 적응형 영양 검토를 사용할 수 없습니다."},
            )
            return True
        accepted = False
        accepts = getattr(service, "accepts", None)
        if callable(accepts):
            try:
                accepted = accepts(address) is True
            except Exception:
                accepted = False
        reply_to = getattr(message, "reply_to_message", None)
        if accepted and reply_to is None:
            keyboard_chat = f"{address.chat_id}:{address.topic_id}"
            shown = getattr(self, "_adaptive_operator_keyboard_chats", set())
            if keyboard_chat not in shown:
                await message.reply_text(
                    "적응형 영양 검토 메뉴를 열려면 아래 버튼을 사용하세요.",
                    reply_markup=self._adaptive_operator_reply_markup(),
                )
                shown.add(keyboard_chat)
                self._adaptive_operator_keyboard_chats = shown
        raw_text = getattr(message, "text", None)
        if not isinstance(raw_text, str):
            result = {
                "status": "rejected",
                "text": "운영자 메모는 텍스트 답장으로만 입력해 주세요.",
            }
        else:
            origin_message_id = getattr(reply_to, "message_id", None)
            result = service.handle_text(
                address,
                message_id=origin_message_id or getattr(message, "message_id", ""),
                text=raw_text,
                chat_id=getattr(getattr(message, "chat", None), "id", ""),
                topic_id=getattr(message, "message_thread_id", 59) or 59,
            )
        await self._send_adaptive_operator_result(message, result)
        return True
    @staticmethod
    def _adaptive_operator_reply_markup():
        """Persistent host-owned opener for the adaptive review surface."""
        rows = [["적응형 영양 검토"]]
        try:
            return ReplyKeyboardMarkup(rows, resize_keyboard=True, is_persistent=True)
        except TypeError:
            try:
                return ReplyKeyboardMarkup(rows, resize_keyboard=True)
            except TypeError:
                return rows
    async def _send_nutrition_topic(
        self,
        *,
        chat_id: object,
        topic_id: object,
        **kwargs: object,
    ) -> object:
        if str(topic_id) == "0":
            if self._bot is None:
                raise RuntimeError("Not connected")
            return await self._bot.send_message(chat_id=chat_id, **kwargs)
        thread_kwargs = self._thread_kwargs_for_send(
            str(chat_id),
            str(topic_id),
            {"thread_id": str(topic_id)},
        )
        if thread_kwargs.get("message_thread_id") is None:
            raise RuntimeError("strict topic delivery requires message_thread_id")
        return await self._send_message_strict_topic(
            chat_id=chat_id,
            **kwargs,
            **thread_kwargs,
        )

    @staticmethod
    def _customer_checkin_reply_markup():
        rows = [["오늘 체크인", "오늘 체크인 수정"]]
        try:
            return ReplyKeyboardMarkup(rows, resize_keyboard=True, is_persistent=True)
        except TypeError:
            return ReplyKeyboardMarkup(rows, resize_keyboard=True)

    async def _ensure_customer_checkin_keyboard(self, address: object) -> None:
        chat_id = str(getattr(address, "chat_id", "") or "")
        topic_id = str(getattr(address, "topic_id", "") or "")
        key = (chat_id, topic_id)
        initialized = getattr(self, "_customer_checkin_keyboard_topics", None)

[8986 more lines in file. Use offset=5710 to continue.]


**Identifying key reads and coordinator methods**

bash

rg -n '_nutrition_customer_card_prompt|_send_nutrition_customer_card|plain.*checkin|"checkin"|customer_start_command' /home/cube/projects/richard/hermes-agent/tests/gateway --glob '*.py' | head -250

cd /home/cube/projects/richard/hermes-agent && git diff -- gateway/platforms/telegram.py gateway/platforms/nutrition_coaching.py gateway/platforms/nutrition_coaching_config.py | rg -n -C 5 '_send_nutrition_customer_card|_nutrition_customer_card_prompt|customer_service_controls|customer_start_callback|checkin|customer_transport_allowed' | head -400


            "scheduled_delivery_routes",
            "cron_routes",
            "owner_deliveries",
        )
        generic_names = (
            "generic_reserved_routes",
            "generic_routes",
            "reserved_routes",
            "reserved_spaces",
            "operator_routes",
            "platform_reserved_routes",
        )
        for name in owner_names:
            if name in extra:
                owner_scheduled.append(extra[name])
        for name in generic_names:
            if name in extra:
                generic.append(extra[name])
        return tuple(owner_scheduled), tuple(generic)

    def _validate_adaptive_review_space_config(self) -> None:
        """Validate review ingress before Telegram startup or customer loading."""
        adaptive = getattr(self, "_adaptive_nutrition_config", None)
        review = getattr(adaptive, "review_operator", None)
        if review is None:
            return
        from gateway.platforms.nutrition_coaching import (
            validate_review_space_disjoint,
        )

        if not callable(validate_review_space_disjoint):
            raise RuntimeError("adaptive review-space validator is unavailable")
        owner_scheduled, generic = self._adaptive_reserved_route_categories()
        validate_review_space_disjoint(
            review,
            owner_scheduled_routes=owner_scheduled,
            generic_reserved_routes=generic,
        )
    def bootstrap_diagnostic_isolation(
        self,
        *,
        spec,
        authority,
        owner_user_id: int,
        review_chat_id: int,
        review_topic_id: int,
    ):
        """Create the dormant Topic-59 diagnostic control boundary."""
        if (
            getattr(self, "_nutrition_coaching", None) is not None
            or getattr(self, "_adaptive_operator_service", None) is not None
        ):
            raise RuntimeError("diagnostic host cannot share the production coordinator")
        from gateway.platforms.diagnostic_isolation import bootstrap_diagnostic_isolation

        controller = bootstrap_diagnostic_isolation(
            spec=spec,
            authority=authority,
            owner_user_id=owner_user_id,
            review_chat_id=review_chat_id,
            review_topic_id=review_topic_id,
            adapter=self,
        )
        self._diagnostic_isolation_fenced = True
        self._diagnostic_isolation_controller = controller
        self._diagnostic_control_service = controller.control
        self._diagnostic_isolation_host = None
        return controller
    def _get_nutrition_coaching(self):
        """Load the customer coordinator only for an explicit private registry."""
        if getattr(self, "_diagnostic_isolation_fenced", False):
            return None

        config = getattr(self, "_nutrition_coaching_config", None)
        if config is None:
            return None
        if getattr(self, "_adaptive_nutrition_error", None):
            self._nutrition_coaching_error = self._adaptive_nutrition_error
            return None
        if getattr(self, "_nutrition_coaching_error", None):
            return None
        existing = getattr(self, "_nutrition_coaching", None)
        if existing is not None:
            service = getattr(self, "_adaptive_operator_service", None)
            review_operator = getattr(
                getattr(self, "_adaptive_nutrition_config", None),
                "review_operator",
                None,
            )
            if service is None and review_operator is not None:
                try:
                    from gateway.platforms.nutrition_coaching import AdaptiveOperatorService

                    self._adaptive_operator_service = AdaptiveOperatorService(
                        existing,
                        review_operator=review_operator,
                        profile_root=getattr(existing, "profile_root", None),
                        schedule_confirm_handler=(
                            getattr(existing, "schedule_confirm_handler", None)
                            if getattr(
                                getattr(self, "_adaptive_nutrition_config", None),
                                "schedule_confirm_enabled",
                                False,
                            ) is True
                            else None
                        ),
                        schedule_confirm_enabled=getattr(
                            getattr(self, "_adaptive_nutrition_config", None),
                            "schedule_confirm_enabled",
                            False,
                        ),
                    )
                    from gateway.platforms.nutrition_coaching import DualCoachReviewService

                    self._dual_coach_review_service = DualCoachReviewService(existing, review_operator)
                except Exception:
                    self._adaptive_operator_service = None
        if existing is not None:
            refresh = getattr(existing, "refresh_live_registry", None)
            if callable(refresh) and not refresh():
                self._nutrition_live_state_error = (
                    getattr(existing, "_live_registry_error", None)
                    or "live customer registry is unavailable"
                )
                return None
            self._nutrition_live_state_error = None
            return existing
        try:
            profile_root, configured_registry_path = self._configured_nutrition_registry(
                config,
                getattr(self, "config", None),
            )
            package_root = profile_root / "workspace" / "checkin_cli"
            adaptive_config = getattr(self, "_adaptive_nutrition_config", None)
            raw_adaptive = (
                self.config.extra.get("adaptive_nutrition")
                if isinstance(getattr(self.config, "extra", None), dict)
                else None
            )
            if isinstance(raw_adaptive, dict) and raw_adaptive.get("enabled") is True and (
                adaptive_config is None or getattr(adaptive_config, "review_operator", None) is None
            ):
                raise ValueError("adaptive review_operator full triple is required")
            if package_root.is_symlink():
                raise ValueError("profile customer package symlink is not allowed")
            if package_root.is_dir():
                package_text = str(package_root)
                if package_text not in sys.path:
                    sys.path.insert(0, package_text)
            from gateway.platforms.nutrition_coaching import (
                AdaptiveOperatorService,
                DualCoachReviewService,
                NutritionCoachingCoordinator,
                TelegramCustomerTransport,
                load_committed_customer_registry,
            )

            registry, registry_path = load_committed_customer_registry(profile_root)
            try:
                canonical_registry_path = _Path(registry_path).resolve()
            except (OSError, RuntimeError, TypeError) as exc:
                raise ValueError("canonical registry path is unavailable") from exc
            if canonical_registry_path != configured_registry_path:
                raise ValueError("configured registry path is not canonical")
            review_operator = getattr(
                getattr(self, "_adaptive_nutrition_config", None),
                "review_operator",
                None,
            )
            owner_scheduled, generic_reserved = self._adaptive_reserved_route_categories()
            self._nutrition_coaching = NutritionCoachingCoordinator(
                profile_root,
                registry,
                registry_path=canonical_registry_path,
                delivery_enabled=(
                    getattr(getattr(self, "_adaptive_nutrition_config", None), "delivery_enabled", False)
                ),
                review_operator=review_operator,
                owner_scheduled_routes=owner_scheduled,
                generic_reserved_routes=generic_reserved,
            )
            transport = TelegramCustomerTransport(self, self._nutrition_coaching)
            setter = getattr(self._nutrition_coaching, "set_customer_transport", None)
            if callable(setter):
                setter(transport)
            else:
                setattr(self._nutrition_coaching, "customer_transport", transport)
            self._adaptive_operator_service = (
                AdaptiveOperatorService(
                    self._nutrition_coaching,
                    review_operator=review_operator,
                    profile_root=profile_root,
                    schedule_confirm_handler=(
                        getattr(self._nutrition_coaching, "schedule_confirm_handler", None)
                        if getattr(
                            getattr(self, "_adaptive_nutrition_config", None),
                            "schedule_confirm_enabled",
                            False,
                        ) is True
                        else None
                    ),
                    schedule_confirm_enabled=getattr(
                        getattr(self, "_adaptive_nutrition_config", None),
                        "schedule_confirm_enabled",
                        False,
                    ),
                )
                if review_operator is not None
                else None
            )
            self._dual_coach_review_service = (
                DualCoachReviewService(self._nutrition_coaching, review_operator)
                if review_operator is not None
                else None
            )
            self._nutrition_live_state_error = None
        except Exception as exc:
            diagnostic = (
                "nutrition_coaching initialization failed: "
                f"{type(exc).__name__}: {exc}. "
                f"Check registry_path={config.registry_path!s}, the profile workspace/checkin_cli package, "
                "and enabled customer consent/address records."
            )
            self._nutrition_coaching_error = diagnostic[:500]
            logger.error("[%s] %s", getattr(self, "name", "telegram"), self._nutrition_coaching_error)
            return None
        return self._nutrition_coaching
    async def _drain_dual_coach_review_cards(self, *, limit: int = 16) -> None:
        """Boundedly publish durable review rows to the configured operator Topic-59."""
        address_for = getattr(self, "_nutrition_operator_address", None)

[9916 more lines in file. Use offset=4780 to continue.]


        )

    async def _handle_nutrition_onboarding_text(
        self,
        update: object,
        message: object,
    ) -> bool:
        runtime = self._get_nutrition_onboarding_runtime()
        if runtime is None:
            return False
        return await runtime.handle_text(update, message)

    def _nutrition_diagnostic(self, default: str) -> str:
        return (
            getattr(self, "_nutrition_live_state_error", None)
            or getattr(self, "_nutrition_coaching_error", None)
            or default
        )

    def _nutrition_coaching_declared_enabled(self) -> bool:
        extra = getattr(getattr(self, "config", None), "extra", None)
        raw = extra.get("nutrition_coaching") if isinstance(extra, dict) else None
        return isinstance(raw, dict) and raw.get("enabled") is True

    def _nutrition_address(self, message_or_query: object, message: object | None = None):
        from gateway.platforms.nutrition_coaching import IncomingAddress

        target = message if message is not None else message_or_query
        chat = getattr(target, "chat", None)
        chat_type = str(getattr(chat, "type", "") or "").split(".")[-1].lower()
        if chat_type in {"private", "dm"}:
            topic_id = "0"
        else:
            try:
                topic_id = self._physique_thread_id(target)
            except (TypeError, ValueError):
                topic_id = ""
        return IncomingAddress(
            self._physique_owner_id(message_or_query),
            self._physique_chat_id(target),
            topic_id,
        )

    def _nutrition_operator_address(self):
        """Return the exact configured adaptive review ingress address."""
        from gateway.platforms.nutrition_coaching import IncomingAddress

        adaptive = getattr(self, "_adaptive_nutrition_config", None)
        review = getattr(adaptive, "review_operator", None)
        if review is None:
            return None
        try:
            if isinstance(review, dict):
                user_id = review.get("user_id")
                chat_id = review.get("chat_id")
                topic_id = review.get("topic_id")
            else:
                user_id = getattr(review, "user_id", None)
                chat_id = getattr(review, "chat_id", None)
                topic_id = getattr(review, "topic_id", None)
            user_id = str(user_id or "").strip()
            chat_id = str(chat_id or "").strip()
            topic_id = str(topic_id or "").strip()
            if not user_id or not chat_id or topic_id != "59":
                return None
            return IncomingAddress(user_id, chat_id, topic_id)
        except (AttributeError, TypeError, ValueError):
            return None

    def _nutrition_generation_card_route(self, coordinator: object):
        """Use the configured replacement route or the exact review ingress."""
        from gateway.platforms.nutrition_coaching import IncomingAddress

        route = getattr(
            getattr(self, "_nutrition_coaching_config", None),
            "operator_card_route",
            None,
        )
        if route is None:
            return (
                self._nutrition_coaching_review_address()
                or self._nutrition_operator_address()
            )
        owner = getattr(coordinator, "owner", None)
        try:
            user_id = str(getattr(route, "user_id", "") or "").strip()
            chat_id = str(getattr(route, "chat_id", "") or "").strip()
            raw_topic_id = getattr(route, "topic_id", None)
            topic_id = str(raw_topic_id).strip() if raw_topic_id is not None else ""
            version = getattr(route, "version", None)

[9386 more lines in file. Use offset=5310 to continue.]


    def _physique_markup(prompt: WizardPrompt):
        """Render only opaque button addresses; prompt text holds all labels."""
        if not prompt.buttons and not prompt.button_rows:
            return None
        if not TELEGRAM_AVAILABLE:
            return None
        from telegram import InlineKeyboardButton as Button
        from telegram import InlineKeyboardMarkup as Markup

        rows = prompt.button_rows or tuple((button,) for button in prompt.buttons)
        return Markup([
            [Button(label, callback_data=callback) for label, callback in row]
            for row in rows
        ])

    @staticmethod
    def _is_physique_recovery_command(text: str) -> bool:
        """Recognize only the value-free manual Start/Resume command."""
        return text.strip() == "체크인 시작"
    @staticmethod
    def _is_nutrition_customer_start_command(text: str) -> bool:
        """Recognize only the exact customer topic start aliases."""
        return " ".join(str(text).split()) in {"체크인 시작", "오늘 체크인"}

    @staticmethod
    def _is_physique_numeric_answer(text: str) -> bool:
        """Recognize a scalar answer that belongs to an expired private wizard."""
        return re.fullmatch(r"\d+(?:\.\d+)?", text.strip()) is not None

    async def _render_physique_callback_prompt(
        self,
        query: _NutritionDraftQuery,
        reply: WizardReply,
    ) -> None:
        """Edit the same topic message after a button transition."""
        if reply.prompt is None:
            return
        text = reply.prompt.text
        if reply.notice == "체크인을 저장했습니다.":
            callback = CallbackData.parse(str(getattr(query, "data", "")))
            bridge = self._get_physique_checkin()
            snapshot = (
                bridge.finalized_coaching_snapshot(callback.session_id)
                if callback is not None and bridge is not None
                else None
            )
            if snapshot is not None:
                # Telegram callbacks render only the finalized authority snapshot.
                # Provider-backed coaching belongs to the out-of-band worker seam.
                text = self._nutrition_daily_text(snapshot)
        try:
            await query.edit_message_text(
                text=text,
                reply_markup=self._physique_markup(reply.prompt),
            )
        except Exception:
            # The domain transition is already versioned. A Telegram edit
            # failure is safe: the user can use the current Start/Resume card.
            return
    @staticmethod
    def _nutrition_daily_text(
        snapshot: object,
        feedback: object | None = None,
    ) -> str:
        """Render the approved daily check-in copy from finalized facts."""

[7161 more lines in file. Use offset=7535 to continue.]


            or fields.get("epoch") != epoch.get("epoch")
        ):
            return False
        return (
            epoch.get("activation") is True
            and epoch.get("delivery") is True
        )

    def coaching_processing_allowed(self, customer_key: str) -> bool:
        """Revalidate registered-customer authority for one coaching stage.

        This gate intentionally does not inspect a destination or grant
        delivery authority.  It is called immediately before each stage and
        before publication by the Telegram adapter.
        """
        if not isinstance(customer_key, str) or not customer_key.strip():
            return False
        if not self._ensure_live_registry():
            return False
        if self.customer_service_paused(customer_key):
            return False
        resolved = self._by_key.get(customer_key)
        if resolved is None:
            return False
        customer = getattr(resolved, "customer", None)
        spec = getattr(customer, "spec", None)
        if getattr(spec, "enabled", False) is not True:
            return False
        if self._consent_error(customer) is not None:
            return False
        return not self._customer_has_safety_hold(resolved)
    def customer_transport_allowed(
        self,
        customer_key: str,
        destination: object | None = None,
        *,
        kst_date: date | datetime | None = None,
        adaptive: bool = False,
    ) -> bool:
        """Revalidate customer authority immediately before customer transport."""
        if not self._ensure_live_registry():
            return False
        if self.customer_service_paused(customer_key):
            return False
        resolved = self._by_key.get(customer_key)
        if resolved is None or not bool(getattr(resolved.customer.spec, "enabled", False)):
            return False
        if self._consent_error(resolved.customer) is not None:
            return False
        window_allowed = (
            self._adaptive_plan_window_allows(resolved.customer, kst_date=kst_date)
            if adaptive
            else self._customer_plan_window_allows(resolved.customer, kst_date=kst_date)
        )
        if not window_allowed:
            return False
        canonical = resolved.customer.spec.telegram
        if destination is not None:
            destination_key = tuple(
                str(getattr(destination, field, "") or "")
                for field in ("user_id", "chat_id", "topic_id")
            )
            if destination_key != tuple(str(value) for value in canonical.key):
                return False
        return not self._customer_has_safety_hold(resolved)

    def validate_delivery_transport(
        self,
        draft_id: str,
        owner: IncomingAddress,
        destination: object | None = None,
        text: str | None = None,
        *,
        kst_date: date | datetime | None = None,
    ) -> bool:
        """Atomically consume one pending reservation immediately before transport."""
        if not self._ensure_live_registry() or owner.key != self._registry.owner.key:
            return False
        with self._delivery_lock():
            if not self._ensure_live_registry():
                return False
            try:
                drafts = self._read_drafts()
                deliveries = self._read_deliveries()
                generations = self._read_draft_generations()
            except DraftLedgerError:
                return False
            record = drafts.get(draft_id)
            if (
                not isinstance(record, dict)
                or self._record_eligibility_error(record, owner) is not None
            ):
                return False
            selection = self._draft_selection_from_record(record, owner)
            if selection is None:
                return False
            customer_spec = getattr(selection.customer, "spec", None)
            if (
                customer_spec is None
                or self._selection_eligibility_error(selection) is not None
                or record.get("status") != "approved"
                or self._approved_record_error(draft_id, selection, record) is not None
            ):
                return False
            customer_key = str(getattr(customer_spec, "customer_key", ""))
            record_text = str(record.get("text", "")).strip()
            if (
                not isinstance(text, str)
                or text.strip() != record_text
                or telegram_utf16_length(record_text)
                > TELEGRAM_SINGLE_MESSAGE_LIMIT_UTF16
            ):
                return False
            canonical_destination = customer_spec.telegram
            supplied_destination = destination if destination is not None else canonical_destination
            supplied_key = tuple(
                str(getattr(supplied_destination, field, "") or "")
                for field in ("user_id", "chat_id", "topic_id")
            )
            canonical_key = tuple(
                str(getattr(canonical_destination, field, "") or "")
                for field in ("user_id", "chat_id", "topic_id")
            )
            if supplied_key != canonical_key:
                return False
            history = generations.get(draft_id)
            if history is None:
                return False
            generation = history[-1]
            if generation.state is not DraftGenerationState.DELIVERY_PENDING:
                return False
            delivery = deliveries.get(self._delivery_key(draft_id, record_text))
            revision = self._draft_revision(record_text)
            if (
                not isinstance(delivery, dict)
                or delivery.get("status") != "pending"
                or delivery.get("customer_key") != customer_key
                or delivery.get("session_id") != str(record.get("session_id", ""))
                or delivery.get("text") != record_text
                or delivery.get("revision") != revision
                or delivery.get("approved_revision") != record.get("approved_revision")
                or delivery.get("approved_event_id") != record.get("approved_event_id")
                or delivery.get("checkin_revision") != generation.checkin_revision
                or delivery.get("canonical_payload_digest") != revision
                or delivery.get("provider_chat_id") != canonical_key[1]
                or delivery.get("provider_topic_id") != canonical_key[2]
                or delivery.get("idempotency_key")
                != self._delivery_idempotency_key(
                    customer_key,
                    generation.checkin_revision,
                    revision,
                )
                or not self.customer_transport_allowed(
                    customer_key,
                    canonical_destination,
                    kst_date=kst_date,
                )
            ):
                return False
            consumed = dict(delivery)
            consumed["status"] = "unknown_provider_outcome"
            next_deliveries = dict(deliveries)
            next_deliveries[self._delivery_key(draft_id, record_text)] = consumed
            try:
                self._write_json_private(
                    self._delivery_ledger_path(),
                    next_deliveries,
                )
            except (OSError, ValueError):
                return False
            return True

    def customer(self, customer_key: str) -> CustomerRuntime | None:
        if not self._ensure_live_registry():
            return None
        resolved = self._by_key.get(customer_key)
        return resolved.customer if resolved is not None else None
    def event_source(self, customer_key: str) -> object | None:
        """Return the live profile EventStore for one enabled customer."""
        return getattr(self, "_event_sources", {}).get(customer_key)
    def event_store(self, customer_key: str) -> object | None:
        """Compatibility name for the canonical per-customer EventStore."""
        return self.event_source(customer_key)

    def resolve(self, address: IncomingAddress) -> ResolvedCustomer | None:
        if not self._ensure_live_registry():
            return None
        return self._routes.get(address.key)

    def resolve_stopped(self, address: IncomingAddress) -> str | None:
        if not self._ensure_live_registry():
            return None
        return self._stopped_routes.get(address.key)

    @staticmethod
    def _customer_pause_callback(customer_key: object, action: str) -> str:
        if action not in {"p", "r"}:
            raise ValueError("unsupported customer pause action")
        key = str(customer_key or "").strip()
        if not key:
            raise ValueError("customer key is required")
        token = hashlib.sha256(
            f"customer-pause\0{key}".encode("utf-8")
        ).hexdigest()[:24]
        callback = f"{_CUSTOMER_PAUSE_CALLBACK_PREFIX}:{token}:{action}"
        if len(callback.encode("utf-8")) > 64:
            raise ValueError("customer pause callback exceeds Telegram limit")
        return callback

    def customer_service_paused(self, customer_key: str) -> bool:
        if self._service_state_error is not None:
            return True
        try:
            return self._service_state_store.is_paused(customer_key)
        except CustomerServiceStateError as exc:
            self._service_state_error = str(exc)
            return True

    def customer_service_controls(
        self,
        address: IncomingAddress,
    ) -> CustomerOnboardingCard | None:
        customer = self._registered_customer(address)
        spec = getattr(customer, "spec", None)
        if spec is None or getattr(spec, "enabled", False) is not True:
            return None
        customer_key = str(getattr(spec, "customer_key", "") or "")
        return self.customer_service_controls_for_customer(customer_key)

    def customer_service_controls_for_customer(

[18349 more lines in file. Use offset=2750 to continue.]


/home/cube/projects/richard/hermes-agent/tests/gateway/test_telegram_physique_checkin.py:332:        prompt = TelegramAdapter._nutrition_customer_card_prompt(


18--from typing import TYPE_CHECKING, Any, Callable
19-+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Literal, TypeGuard, cast
20- from zoneinfo import ZoneInfo
21-
22- logger = logging.getLogger(__name__)
23: from gateway.platforms.physique_checkin import CallbackData, PhysiqueCheckinBridge, WizardPrompt, WizardReply
24: from gateway.platforms.physique_checkin_config import PhysiqueCheckinConfig
25-+from gateway.platforms.nutrition_service_state import (
26-+    CustomerServiceStateError,
27-+    CustomerServiceStateStore,
28-+)
29-+from gateway.platforms.nutrition_coaching_proposal import (
--
36-+)
37-+from gateway.platforms.nutrition_coaching_proposal_validation import (
38-+    CoachProposalAttemptDiagnostic,
39-+)
40- try:
41:     from checkin_cli.operator_console import (
42-         TELEGRAM_SINGLE_MESSAGE_LIMIT_UTF16,
43-@@ -54,6 +70,7 @@ try:
44-         MealSlot,
45-         build_snapshot,
46-         canonical_event_digest,
--
70-     compile_meal_plan = None
71-     digest = None
72-     load_approved_adaptive_artifacts = None
73-@@ -104,40 +124,56 @@ except ImportError:
74- try:
75:     from checkin_cli.adaptive_nutrition import (
76-         CooldownResult,
77-+        DailyNutritionPlan,
78-         DailyNutritionTarget,
79-         WeeklyCarbCycle,
80-+        WeeklyNutritionPlan,
--
84-+    DailyNutritionPlan = None
85-     DailyNutritionTarget = None
86-     WeeklyCarbCycle = None
87-+    WeeklyNutritionPlan = None
88- try:
89:     from checkin_cli.customer_admin import (
90-+        approve_dual_coach_risk_policy,
91-         load_approved_adaptive_registration_inputs,
92-         reconcile_adaptive_nutrition_journals,
93-         profile_authority_lock,
94-+        validate_adaptive_registration_reapproval,
95-         validate_review_space_disjoint,
96-     )
97:     from checkin_cli.customer_coaching import (
98-         AdaptiveRegistrationInputs,
99-         CustomerTrainingScheduleEntry,
100-+        TelegramAddress,
101-     )
102- except ImportError:
--
109-     AdaptiveRegistrationInputs = None
110-     CustomerTrainingScheduleEntry = None
111-+    TelegramAddress = None
112-
113- if TYPE_CHECKING:
114:     from checkin_cli.customer_coaching import CustomerRegistry, CustomerRuntime
115:     from checkin_cli.customer_grounding import CustomerSnapshot
116- _KST = ZoneInfo("Asia/Seoul")
117--_PRIVATE_TRAINER_SELECTION_PREFIX = "pt1"
118--_PRIVATE_TRAINER_SELECTION_TOKEN: re.Pattern[str] = re.compile(r"^[a-f0-9]{24}$")
119- _CUSTOMER_START_CALLBACK_PREFIX = "cs1"
120- _CUSTOMER_START_CALLBACK_TOKEN: re.Pattern[str] = re.compile(r"^[a-f0-9]{24}$")
--
154-         "reason_category_ids": facts.reason_category_ids,
155-+        "current_targets": current_targets,
156-         "target_macros": target_macros,
157-         "carb_category_targets": tuple(carb_targets),
158-         "safety_held": facts.safety_held,
159:@@ -423,27 +465,6 @@ def parse_customer_start_callback(value: object) -> str | None:
160-     return token if _CUSTOMER_START_CALLBACK_TOKEN.fullmatch(token) else None
161-
162-
163--def trainer_private_selection_callback(trainer_user_id: object, customer_key: object) -> str:
164--    """Return a stable, opaque Telegram callback for one trainer/customer pair."""
--
228-     transport_required: bool = False
229-+    coach_review: CoachReview | None = None
230-+    coach_review_state: str = ""
231-+    generation: int | None = None
232-+    generation_record_digest: str | None = None
233:+    generation_checkin_revision: str | None = None
234-+    generation_draft_revision: str | None = None
235-
236--class DraftLedgerError(ValueError):
237--    """Raised when the durable draft ledger cannot be trusted for a write."""
238--
--
240--        detail = str(detail)
241--        super().__init__(detail)
242--        self.detail = detail
243--def _resolve_committed_registry_path(profile_root: Path) -> Path:
244--    """Resolve the profile package's canonical registry path without revalidating it."""
245:-    from checkin_cli.customer_admin import _resolve_profile_root, _resolve_registry_path
246--
247--    root = _resolve_profile_root(Path(profile_root))
248--    return _resolve_registry_path(root)
249--
250-+@dataclass(frozen=True, slots=True)
--
280-+_DRAFT_GENERATION_AUTHORITY_SCHEMA = "draft-generation-authority-v2"
281-+_DRAFT_GENERATION_ROOT_SCHEMA = "draft-generation-root-v1"
282-+_DRAFT_GENERATION_CARD_STATES = frozenset(
283-+    {"claimed", "published", "not_sent", "unknown_provider_outcome"}
284-+)
285:+_CHECKIN_FINALIZATION_JOURNAL_SCHEMA = "checkin-finalization-journal-v1"
286-+_CHECKIN_SESSION_ID = re.compile(r"^[a-f0-9]{32}$")
287-+_DRAFT_GENERATION_MAX_ATTEMPTS = 2
288-+_DRAFT_GENERATION_LEASE = timedelta(minutes=5)
289-+_DRAFT_GENERATION_DIGEST = re.compile(r"[0-9a-f]{64}")
290-+_DRAFT_GENERATION_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}")
--
294-+    {
295-+        "schema_version",
296-+        "generation",
297-+        "state",
298-+        "customer_key",
299:+        "checkin_event_id",
300:+        "checkin_revision",
301-+        "draft_revision",
302-+        "model_contract_version",
303-+        "provider_contract_version",
304-+        "delivery_provider_contract_version",
305-+        "actor",
--
359-+    DraftGenerationState.SENT_AUDITED: frozenset(),
360-+}
361-
362--def load_committed_customer_registry(profile_root: Path) -> tuple[CustomerRegistry, Path]:
363--    """Load the profile registry through its committed-activation runtime gate."""
364:-    from checkin_cli.customer_admin import load_runtime_customer_registry
365-
366--    root = Path(profile_root)
367--    registry = load_runtime_customer_registry(root)
368--    return registry, _resolve_committed_registry_path(root)
369-+class DraftGenerationTransitionError(ValueError):
--
436-+@dataclass(frozen=True, slots=True)
437-+class ProviderGenerationAudit:
438-+    """Privacy-safe provider metadata retained without request or response text."""
439-
440--    def _configure_registry(self, registry: CustomerRegistry) -> None:
441:-        from checkin_cli.wizard import WizardService
442-+    status: str
443-+    finish_reason: str
444-+    incomplete_reason: str | None
445-+    input_tokens: int
446-+    output_tokens: int
--
1398-+    token: str
1399-+    worker_id: str
1400-+    attempt: int
1401-+    generation: int
1402-+    record_digest: str
1403:+    checkin_revision: str
1404-+    lease_expires_at: datetime
1405-+
1406-+
1407-+@dataclass(frozen=True, slots=True)
1408-+class DraftGenerationCardClaim:
--
1410-+
1411-+    card_id: str
1412-+    token: str
1413-+    generation: int
1414-+    record_digest: str
1415:+    checkin_revision: str
1416-+    draft_revision: str | None
1417-+    route: IncomingAddress
1418-+    payload_digest: str
1419-+    claim_id: str
1420-+    message_id: str | None
--
1426-+
1427-+    schema_version: str
1428-+    generation: int
1429-+    state: DraftGenerationState
1430-+    customer_key: str
1431:+    checkin_event_id: str
1432:+    checkin_revision: str
1433-+    draft_revision: str | None
1434-+    model_contract_version: str
1435-+    provider_contract_version: str
1436-+    delivery_provider_contract_version: str
1437-+    actor: str
--
1463-+    @classmethod
1464-+    def create_pending(
1465-+        cls,
1466-+        *,
1467-+        customer_key: str,
1468:+        checkin_event_id: str,
1469:+        checkin_revision: str,
1470-+        model_contract_version: str,
1471-+        provider_contract_version: str,
1472-+        actor: str,
1473-+        authority_digest: str,
1474-+        occurred_at: datetime,
--
1479-+        timestamp = _draft_generation_timestamp(occurred_at)
1480-+        return cls._create(
1481-+            generation=1,
1482-+            state=DraftGenerationState.GENERATION_PENDING,
1483-+            customer_key=customer_key,
1484:+            checkin_event_id=checkin_event_id,
1485:+            checkin_revision=checkin_revision,
1486-+            draft_revision=None,
1487-+            model_contract_version=model_contract_version,
1488-+            provider_contract_version=provider_contract_version,
1489-+            delivery_provider_contract_version=_DRAFT_DELIVERY_PROVIDER_CONTRACT,
1490-+            actor=actor,
--
1506-+        cls,
1507-+        *,
1508-+        generation: int,
1509-+        state: DraftGenerationState,
1510-+        customer_key: str,
1511:+        checkin_event_id: str,
1512:+        checkin_revision: str,
1513-+        draft_revision: str | None,
1514-+        model_contract_version: str,
1515-+        provider_contract_version: str,
1516-+        delivery_provider_contract_version: str,
1517-+        actor: str,
--
1528-+        delivery_provider_receipt: str | None,
1529-+    ) -> DraftGenerationRecord:
1530-+        idempotency_key = cls._idempotency_key(
1531-+            state,
1532-+            customer_key,
1533:+            checkin_event_id,
1534:+            checkin_revision,
1535-+            draft_revision,
1536-+        )
1537-+        unsigned: dict[str, object] = {
1538-+            "schema_version": _DRAFT_GENERATION_SCHEMA,
1539-+            "generation": generation,
1540-+            "state": state.value,
1541-+            "customer_key": customer_key,
1542:+            "checkin_event_id": checkin_event_id,
1543:+            "checkin_revision": checkin_revision,
1544-+            "draft_revision": draft_revision,
1545-+            "model_contract_version": model_contract_version,
1546-+            "provider_contract_version": provider_contract_version,
1547-+            "delivery_provider_contract_version": delivery_provider_contract_version,
1548-+            "actor": actor,
--
1562-+        return cls(
1563-+            schema_version=_DRAFT_GENERATION_SCHEMA,
1564-+            generation=generation,
1565-+            state=state,
1566-+            customer_key=customer_key,
1567:+            checkin_event_id=checkin_event_id,
1568:+            checkin_revision=checkin_revision,
1569-+            draft_revision=draft_revision,
1570-+            model_contract_version=model_contract_version,
1571-+            provider_contract_version=provider_contract_version,
1572-+            delivery_provider_contract_version=delivery_provider_contract_version,
1573-+            actor=actor,
--
1588-+
1589-+    @staticmethod
1590-+    def _idempotency_key(
1591-+        state: DraftGenerationState,
1592-+        customer_key: str,
1593:+        checkin_event_id: str,
1594:+        checkin_revision: str,
1595-+        draft_revision: str | None,
1596-+    ) -> str:
1597-+        payload: dict[str, object] = {
1598-+            "domain": "nutrition-generation-v1",
1599-+            "customer_key": customer_key,
1600:+            "checkin_event_id": checkin_event_id,
1601:+            "checkin_revision": checkin_revision,
1602-+        }
1603-+        if state in {
1604-+            DraftGenerationState.DELIVERY_PENDING,
1605-+            DraftGenerationState.DELIVERED,
1606-+            DraftGenerationState.SENT_AUDITED,
--
1615-+        if type(self.state) is not DraftGenerationState:
1616-+            raise DraftGenerationTransitionError("generation state is invalid")
1617-+        if (
1618-+            type(self.customer_key) is not str
1619-+            or not 1 <= len(self.customer_key) <= 128
1620:+            or type(self.checkin_event_id) is not str
1621:+            or not 1 <= len(self.checkin_event_id) <= 128
1622:+            or _DRAFT_GENERATION_DIGEST.fullmatch(self.checkin_revision) is None
1623-+            or _DRAFT_GENERATION_IDENTIFIER.fullmatch(self.model_contract_version)
1624-+            is None
1625-+            or _DRAFT_GENERATION_IDENTIFIER.fullmatch(
1626-+                self.provider_contract_version
1627-+            )
--
1743-+                "delivery provider receipt is premature"
1744-+            )
1745-+        if self.idempotency_key != self._idempotency_key(
1746-+            self.state,
1747-+            self.customer_key,
1748:+            self.checkin_event_id,
1749:+            self.checkin_revision,
1750-+            self.draft_revision,
1751-+        ):
1752-+            raise DraftGenerationTransitionError(
1753-+                "generation idempotency key is invalid"
1754-+            )
--
1762-+        self,
1763-+        target: DraftGenerationState,
1764-+        *,
1765-+        expected_generation: int,
1766-+        expected_record_digest: str,
1767:+        expected_checkin_event_id: str,
1768:+        expected_checkin_revision: str,
1769-+        expected_draft_revision: str | None,
1770-+        actor: str,
1771-+        authority_digest: str,
1772-+        occurred_at: datetime,
1773-+        draft_revision: str | None = None,
--
1782-+            raise DraftGenerationTransitionError("generation terminal repair is invalid")
1783-+        if type(expected_generation) is not int or expected_generation != self.generation:
1784-+            raise DraftGenerationTransitionError("generation CAS value is stale")
1785-+        if expected_record_digest != self.record_digest:
1786-+            raise DraftGenerationTransitionError("generation record is stale")
1787:+        if expected_checkin_event_id != self.checkin_event_id:
1788-+            raise DraftGenerationTransitionError("generation check-in event is stale")
1789:+        if expected_checkin_revision != self.checkin_revision:
1790-+            raise DraftGenerationTransitionError(
1791-+                "generation check-in revision is stale"
1792-+            )
1793-+        if expected_draft_revision != self.draft_revision:
1794-+            raise DraftGenerationTransitionError("generation draft revision is stale")
--
1889-+            )
1890-+        return self._create(
1891-+            generation=self.generation + 1,
1892-+            state=target,
1893-+            customer_key=self.customer_key,
1894:+            checkin_event_id=self.checkin_event_id,
1895:+            checkin_revision=self.checkin_revision,
1896-+            draft_revision=next_draft_revision,
1897-+            model_contract_version=self.model_contract_version,
1898-+            provider_contract_version=self.provider_contract_version,
1899-+            delivery_provider_contract_version=self.delivery_provider_contract_version,
1900-+            actor=actor,
--
1919-+        payload: dict[str, object] = {
1920-+            "schema_version": self.schema_version,
1921-+            "generation": self.generation,
1922-+            "state": self.state.value,
1923-+            "customer_key": self.customer_key,
1924:+            "checkin_event_id": self.checkin_event_id,
1925:+            "checkin_revision": self.checkin_revision,
1926-+            "draft_revision": self.draft_revision,
1927-+            "model_contract_version": self.model_contract_version,
1928-+            "provider_contract_version": self.provider_contract_version,
1929-+            "delivery_provider_contract_version": self.delivery_provider_contract_version,
1930-+            "actor": self.actor,
--
1967-+            generation=_draft_generation_required_int(payload, "generation"),
1968-+            state=state,
1969-+            customer_key=_draft_generation_required_string(
1970-+                payload, "customer_key"
1971-+            ),
1972:+            checkin_event_id=_draft_generation_required_string(
1973:+                payload, "checkin_event_id"
1974-+            ),
1975:+            checkin_revision=_draft_generation_required_string(
1976:+                payload, "checkin_revision"
1977-+            ),
1978-+            draft_revision=_draft_generation_optional_string(
1979-+                payload, "draft_revision"
1980-+            ),
1981-+            model_contract_version=_draft_generation_required_string(
--
2046-+            raise DraftGenerationTransitionError(
2047-+                "generation history transition is invalid"
2048-+            )
2049-+        if (
2050-+            successor.customer_key != predecessor.customer_key
2051:+            or successor.checkin_event_id != predecessor.checkin_event_id
2052:+            or successor.checkin_revision != predecessor.checkin_revision
2053-+            or successor.model_contract_version
2054-+            != predecessor.model_contract_version
2055-+            or successor.provider_contract_version
2056-+            != predecessor.provider_contract_version
2057-+            or successor.delivery_provider_contract_version
--
2113-+        detail = str(detail)
2114-+        super().__init__(detail)
2115-+        self.detail = detail
2116-+def _resolve_committed_registry_path(profile_root: Path) -> Path:
2117-+    """Resolve the profile package's canonical registry path without revalidating it."""


**Inspecting test line 332 for button error**

rg -n 'def customer_start_callback|class CustomerOnboardingCard|buttons:' /home/cube/projects/richard/hermes-agent/gateway/platforms/nutrition_coaching.py | head -80; rg -n 'class WizardPrompt' /home/cube/projects/richard/hermes-agent/gateway/platforms/physique_checkin.py

cd /home/cube/projects/richard/hermes-agent && python - <<'PY'
import ast
p='gateway/platforms/telegram.py'
t=ast.parse(open(p).read(),p)
for n in ast.walk(t):
 if isinstance(n,(ast.FunctionDef,ast.AsyncFunctionDef)) and n.name in {'_handle_text_message','_send_nutrition_customer_card','_nutrition_customer_card_prompt','_physique_markup'}:
  print(n.name,n.lineno,n.end_lineno)
PY
# Identify live process mapped source and hashes
readlink -f /proc/343199/cwd; readlink -f /proc/343199/exe; sha256sum gateway/platforms/telegram.py gateway/platforms/nutrition_coaching.py


            for prompt in prompts.values()
        )
        assert "kg 단위 숫자" in prompts["bodyweight"].text
        assert "kcal 단위 숫자" in prompts["calories"].text
        assert "순서는 탄수화물 단백질 지방" in prompts["macros"].text
        assert "Meal 1" in prompts["meals"].text
        assert "간식" in prompts["meals"].text
        assert "L 단위 숫자" in prompts["water"].text
        assert "시간 단위 숫자" in prompts["sleep_duration"].text
        assert "1은 매우 나쁨" in prompts["sleep_quality"].text
        assert "가스 또는 복부 팽만" in prompts["digestion"].text
        assert "1은 매우 나쁨" in prompts["condition"].text
        assert "각각 1부터 5까지" in prompts["appetite_stress"].text

        training = build_wizard_prompt(
            "training_summary",
            False,
            callback,
            flow="nutrition_daily",
        )
        note = build_wizard_prompt(
            "optional_note",
            False,
            callback,
            flow="nutrition_daily",
        )
        summary = build_wizard_prompt(
            "summary",
            False,
            callback,
            flow="nutrition_daily",
        )

        assert tuple(label for label, _ in training.buttons) == ("운동 내용 입력",)
        assert tuple(label for label, _ in note.buttons) == ("없음", "직접 입력")
        assert tuple(label for label, _ in summary.buttons) == (
            "체크인 저장",
            "입력 수정",
            "나중에 저장",
        )

    def test_customer_checkin_card_explains_start_and_submission_without_help(self):
        coordinator = SimpleNamespace(
            customer_start_callback=lambda customer_key: f"start:{customer_key}",
            customer_service_controls_for_customer=lambda _customer_key: None,
            customer=lambda _customer_key: SimpleNamespace(
                spec=SimpleNamespace(
                    plan=SimpleNamespace(starts_on=date(2026, 7, 31))
                )
            ),
        )

        prompt = TelegramAdapter._nutrition_customer_card_prompt(
            coordinator,
            "client_001",
            date(2026, 7, 31),
        )

        assert prompt is not None
        assert prompt.text == (
            "2026년 7월 31일 · D+1\n"
            "이 고객 체크인 토픽의 오늘 기록입니다.\n"
            "아래 ‘오늘 체크인 시작’을 누르면 첫 질문이 열립니다.\n"
            "입력을 모두 마친 뒤 ‘체크인 저장’을 누르면 오늘 기록이 제출됩니다."
        )
        assert prompt.button_rows[0][0][0] == "오늘 체크인 시작"

    def test_summary_uses_three_compact_actions_and_a_flow_specific_two_column_edit_menu(self):
        # Given: a completed morning check-in waiting at its summary.
        callback = lambda action: f"callback:{action}"

        # When: the summary and its Edit tab are rendered.
        summary = build_wizard_prompt("summary", False, callback, flow="morning")
        edit_menu = build_wizard_prompt("edit_menu", False, callback, flow="morning")

        # Then: the summary is not an eleven-button field dump, and the menu
        # exposes only Korean morning fields in compact two-button rows.
        assert tuple(label for label, _ in summary.buttons) == ("저장", "수정", "임시저장")
        assert summary.button_rows == (summary.buttons,)
        assert edit_menu.text == "어느 항목을 수정할까요?"
        assert all("_" not in label for label, _ in edit_menu.buttons)
        assert tuple(label for label, _ in edit_menu.buttons) == (
            "체중", "수면 시간", "수면 질", "컨디션", "통증", "칼로리", "오늘 운동", "특이사항", "요약으로 돌아가기",
        )
        assert all(len(row) == 2 for row in edit_menu.button_rows[:-1])
        assert edit_menu.button_rows[-1][0][0] == "요약으로 돌아가기"

    def test_edit_tab_changes_the_same_binding_screen_and_field_callback_enters_correction(self, tmp_path):
        # Given: an exact owner/topic callback reaches a bound summary card.
        bridge, service = _bridge(tmp_path)
        launch = bridge.open_launcher("morning", message_id="44")
        assert launch.callback_data is not None
        bridge.handle_callback(launch.callback_data, "owner-1", "chat-1", "topic-1", "44")
        binding = bridge._bindings["0123456789abcdef0123456789abcdef"]
        binding.step = "summary"
        binding.version = 2
        binding.awaiting_text = False

        # When: the user opens Edit then selects the Korean bodyweight field.
        menu = bridge.handle_callback(

[3951 more lines in file. Use offset=380 to continue.]


        if self._service_state_error is not None:
            return True
        try:
            return self._service_state_store.is_paused(customer_key)
        except CustomerServiceStateError as exc:
            self._service_state_error = str(exc)
            return True

    def customer_service_controls(
        self,
        address: IncomingAddress,
    ) -> CustomerOnboardingCard | None:
        customer = self._registered_customer(address)
        spec = getattr(customer, "spec", None)
        if spec is None or getattr(spec, "enabled", False) is not True:
            return None
        customer_key = str(getattr(spec, "customer_key", "") or "")
        return self.customer_service_controls_for_customer(customer_key)

    def customer_service_controls_for_customer(
        self,
        customer_key: str,
    ) -> CustomerOnboardingCard | None:
        if not self._ensure_live_registry():
            return None
        resolved = self._by_key.get(customer_key)
        if resolved is None:
            return None
        paused = self.customer_service_paused(customer_key)
        label, action = (
            ("코칭 다시 시작", "r")
            if paused
            else ("코칭 일시중지", "p")
        )
        text = (
            "코칭이 일시중지되어 있습니다."
            if paused
            else "현재 코칭이 진행 중입니다."
        )
        return CustomerOnboardingCard(
            text,
            ((label, self._customer_pause_callback(customer_key, action)),),
        )

    def handle_customer_pause_callback(
        self,
        address: IncomingAddress,
        callback_data: str,
    ) -> CustomerTransition:
        match = _CUSTOMER_PAUSE_CALLBACK.fullmatch(callback_data)
        resolved = self.resolve(address)
        spec = getattr(getattr(resolved, "customer", None), "spec", None)
        if match is None or spec is None:
            return CustomerTransition(
                WizardReply(
                    True,
                    False,
                    "이 코칭 상태 버튼은 현재 사용할 수 없습니다.",
                    None,
                    None,
                )
            )
        action = match.group("action")
        expected = self._customer_pause_callback(
            getattr(spec, "customer_key", None),
            action,
        )
        if not hmac.compare_digest(expected, callback_data):
            return CustomerTransition(
                WizardReply(
                    True,
                    False,
                    "코칭 상태가 변경되었습니다. 고객 메뉴를 다시 열어주세요.",
                    None,
                    None,
                )
            )
        return self._set_customer_paused(
            address,
            paused=action == "p",
        )

    def _set_customer_paused(
        self,
        address: IncomingAddress,
        *,
        paused: bool,
    ) -> CustomerTransition:
        resolved = self.resolve(address)
        spec = getattr(getattr(resolved, "customer", None), "spec", None)
        customer_key = str(getattr(spec, "customer_key", "") or "")
        if not customer_key:
            return CustomerTransition(
                WizardReply(
                    True,
                    False,
                    "이 공간에서는 코칭 상태를 변경할 수 없습니다.",
                    None,
                    None,
                )
            )
        audit_date = self._transport_kst_date()
        if not isinstance(audit_date, date):
            return CustomerTransition(
                WizardReply(
                    True,
                    False,
                    "코칭 상태를 안전하게 저장하지 못했습니다. 새 처리는 중단됩니다.",
                    None,
                    None,
                )
            )
        try:
            self._service_state_store.set_paused(
                customer_key,

[18254 more lines in file. Use offset=2845 to continue.]


445:def customer_start_callback(customer_key: object) -> str:
538:class CustomerOnboardingCard:
540:    buttons: tuple[tuple[str, str], ...] = ()
3087:    def customer_start_callback(self, customer_key: object) -> str:
10473:        buttons: list[dict[str, str]] = []
10664:        buttons: list[object] = []
10665:        for item in raw_buttons:
10931:        for item in buttons:
11240:        buttons: list[dict[str, str]] = []

Command exited with code 1


_nutrition_customer_card_prompt 5739 5780
_send_nutrition_customer_card 5781 5832
_physique_markup 7470 7483
_handle_text_message 13585 13738
/home/cube/.hermes/profiles/dualcoachtest
/home/cube/miniconda3/bin/python3.12
b41060dea28eb3bbb83217068f5218d5df2c879dba00e73c9ca4e577a6049dad  gateway/platforms/telegram.py
07f868adf9fe0565e6eac89fd76b09601adea315e2a122a09ee748614d912c2f  gateway/platforms/nutrition_coaching.py


**Diagnosing button_rows structure and locking issues**

**Inspecting recovery writes and handle_text logic**

rg -n '^    def handle_text\(' /home/cube/projects/richard/hermes-agent/gateway/platforms/nutrition_coaching.py; rg -n 'def _render_nutrition_text' /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram.py; rg -n 'def _registered_customer|def customer_start_callback' /home/cube/projects/richard/hermes-agent/gateway/platforms/nutrition_coaching.py | head -40

rg -n -C 8 'handler_exception' /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram.py /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_polling_receipts.py


3193:    def handle_text(self, address: IncomingAddress, text: str) -> CustomerTransition:
12627:    def handle_text(
6091:    async def _render_nutrition_text(
445:def customer_start_callback(customer_key: object) -> str:
2887:    def _registered_customer(self, address: IncomingAddress) -> object | None:
3087:    def customer_start_callback(self, customer_key: object) -> str:
9078:    def _registered_customer(self, customer_key: object) -> CustomerRuntime:
17320:    def _registered_customer_runtime(self) -> object:


                WizardReply(
                    True,
                    False,
                    "서비스 중단 요청을 안전하게 저장하지 못했습니다. 새 처리는 중단됩니다.",
                    None,
                    None,
                )
            )
        try:
            withdraw_customer(
                self._profile_root,
                customer_key,
                kst_date=audit_date,
            )
        except (CustomerAdminError, OSError, TypeError, ValueError):
            return CustomerTransition(
                WizardReply(
                    True,
                    False,
                    "서비스 중단 요청을 안전하게 저장하지 못했습니다. 새 처리는 중단됩니다.",
                    None,
                    None,
                )
            )
        self._ensure_live_registry()
        return CustomerTransition(
            WizardReply(
                True,
                True,
                (
                    "서비스 중단과 정보 처리 동의 철회가 완료되었습니다. "
                    "새 체크인과 코칭 전달은 즉시 중단됩니다."
                ),
                None,
                None,
            )
        )
    def customer_start_callback(self, customer_key: object) -> str:
        """Return the stable launcher callback for one live customer identity."""
        return customer_start_callback(customer_key)

    def resolve_customer_start(
        self,
        address: IncomingAddress,
        callback_data: object,
    ) -> ResolvedCustomer | None:
        """Resolve a customer launcher only through the live exact Telegram route."""
        if not self._ensure_live_registry():
            return None
        token = parse_customer_start_callback(callback_data)
        if token is None:
            return None
        expected = f"{_CUSTOMER_START_CALLBACK_PREFIX}:{token}"
        for resolved in self._by_key.values():
            spec = resolved.customer.spec
            if not bool(getattr(spec, "enabled", False)):
                continue
            telegram = getattr(spec, "telegram", None)
            if telegram is None or address.key != tuple(telegram.key):
                continue
            try:
                candidate = customer_start_callback(spec.customer_key)
            except ValueError:
                continue
            if candidate == expected:
                return resolved
        return None

    def current_kst_date(self) -> date:
        """Return the coordinator's current KST date provider value."""
        return self._transport_kst_date() or _current_kst_date()

    def owns_space(self, chat_id: str, topic_id: str) -> bool:
        if not self._ensure_live_registry():
            return False
        return (chat_id, topic_id) in self._spaces

    def owns_owner_space(self, chat_id: str, topic_id: str) -> bool:
        if not self._ensure_live_registry():
            return False
        return (str(chat_id), str(topic_id)) == self.owner_space

    def is_owner(self, address: IncomingAddress) -> bool:
        if not self._ensure_live_registry():
            return False
        return address.key == self._registry.owner.key

    @staticmethod
    def _terminal_launcher_reply(
        resolved: ResolvedCustomer,
        opening: WizardReply,
    ) -> WizardReply:
        """Turn a completed same-day launcher into a terminal, data-free notice."""
        callback_data = opening.callback_data
        callback = (
            CallbackData.parse(callback_data)
            if isinstance(callback_data, str)
            else None
        )
        if callback is None:
            return opening
        bridge = resolved.bridge
        safety_getter = getattr(bridge, "finalized_safety_snapshot", None)
        try:
            safety = safety_getter(callback.session_id) if callable(safety_getter) else None
        except Exception:
            safety = None
        if isinstance(safety, Mapping):
            notice = "오늘 체크인은 안전 기록으로 저장되어 코칭이 보류되었습니다."
            return WizardReply(True, True, notice, WizardPrompt(f"✅ {notice}"), None)

[17939 more lines in file. Use offset=3160 to continue.]


            identifier = "safety_hold"
        elif "adher" in text or "순응" in text:
            identifier = "adherence"
        elif "cooldown" in text or "대기" in text:
            identifier = "cooldown"
        elif "sample" in text or "자료" in text or "데이터" in text:
            identifier = "insufficient_data"
        elif "trend" in text or "추세" in text or "rate" in text:
            identifier = "trend"
        else:
            identifier = "review"
        if identifier not in ids:
            ids.append(identifier)
    return tuple(ids[:8])

def customer_start_callback(customer_key: object) -> str:
    """Return a stable opaque Telegram callback for one customer topic."""
    key = str(customer_key or "").strip()
    if not key:
        raise ValueError("customer key is required")
    token = hashlib.sha256(f"customer-start\0{key}".encode("utf-8")).hexdigest()[:24]
    value = f"{_CUSTOMER_START_CALLBACK_PREFIX}:{token}"
    if len(value.encode("utf-8")) > 64:
        raise ValueError("customer start callback exceeds Telegram's 64-byte limit")
    return value

def parse_customer_start_callback(value: object) -> str | None:
    """Extract an opaque customer-start token without accepting customer data."""
    if not isinstance(value, str):
        return None
    parts = value.split(":")
    if len(parts) != 2 or parts[0] != _CUSTOMER_START_CALLBACK_PREFIX:
        return None
    token = parts[1]
    return token if _CUSTOMER_START_CALLBACK_TOKEN.fullmatch(token) else None

def _current_kst_date() -> date:
    return datetime.now(_KST).date()

def _coerce_kst_date(value: object) -> date | None:
    if isinstance(value, datetime):
        if value.tzinfo is not None:
            return value.astimezone(_KST).date()
        return value.date()
    return value if isinstance(value, date) else None

def _plan_window_start(customer: object) -> date | None:
    spec = getattr(customer, "spec", None)
    plan = getattr(spec, "plan", None)
    starts_on = getattr(plan, "starts_on", None)
    if isinstance(starts_on, str):
        try:
            starts_on = date.fromisoformat(starts_on)
        except ValueError:
            return None
    return _coerce_kst_date(starts_on)

@dataclass(frozen=True, slots=True)
class IncomingAddress:
    user_id: str
    chat_id: str
    topic_id: str

    @property
    def key(self) -> tuple[str, str, str]:
        return (self.user_id, self.chat_id, self.topic_id)

@dataclass(frozen=True, slots=True)
class CallbackInput:
    data: str
    address: IncomingAddress
    message_id: str

@dataclass(frozen=True, slots=True)
class _CheckinFinalizationEntry:
    customer_key: str
    session_id: str
    state: Literal["prepared", "committed"]

@dataclass(frozen=True, slots=True)
class CompletionNotice:
    customer_key: str
    display_name: str
    kst_day: str
    request_token: str | None
    safety_held: bool = False
    safety_event: object | None = None
    hold_reasons: tuple[str, ...] = ()
    referral_guidance: str = ""
    role: str = "customer"

@dataclass(frozen=True, slots=True)
class CustomerTransition:
    reply: WizardReply
    completion: CompletionNotice | None = None

@dataclass(frozen=True, slots=True)
class CustomerOnboardingCard:
    text: str
    buttons: tuple[tuple[str, str], ...] = ()

@dataclass(frozen=True, slots=True)
class ResolvedCustomer:
    customer: CustomerRuntime
    bridge: PhysiqueCheckinBridge

@dataclass(frozen=True, slots=True)
class DraftSelection:
    customer: CustomerRuntime
    snapshot: CustomerSnapshot
    session_id: str | None = None

[20544 more lines in file. Use offset=555 to continue.]