# Debug Journal — physique Telegram live flow

Started: 2026-07-17T23:45:00+09:00
Goal: Determine why the isolated physique coach does not progress the live Telegram check-in and restore only the profile-scoped flow.

## Environment snapshot (Phase 0)

- Runtime: CPython Hermes gateway plus profile-local Python wizard CLI.
- Profile: `/home/cube/.hermes/profiles/physique-coach`.
- Candidate gateway PIDs observed: `1603841`, `1605513`.
- References read: debugging Python runtime, debugging setup/investigation, programming Python README.

## Hypotheses

1. [PENDING] Telegram never delivers anonymous-topic text to the profile bot — distinguishing evidence: raw profile gateway input/log contains no matching update after the user message — if true, fix is: delivery boundary.
2. [PENDING] The profile gateway receives the update but rejects it at owner/topic/sender validation — distinguishing evidence: received update plus a profile routing denial — if true, fix is: binding normalization.
3. [PENDING] The profile gateway receives and authorizes the update but the wizard command / subprocess fails silently — distinguishing evidence: accepted routing plus an exception or no draft mutation — if true, fix is: handler contract.
4. [PENDING] A different default Hermes process is polling or consuming the dedicated bot queue — distinguishing evidence: dedicated-token update is seen outside the profile / conflicting Telegram getUpdates process — if true, fix is: poller isolation.

5. [PENDING] Telegram supplies a synthetic `from_user` together with `sender_chat` for an anonymous supergroup-admin post, causing the profile code to trust the synthetic id and reject the otherwise valid target-topic answer — distinguishing evidence: a fake-user + exact-sender-chat update fails current ingress test and succeeds when sender-chat takes priority — if true, fix is: sender precedence.

## Failed hypothesis round counter

- Round 1: pending.

## Artifacts to revert

- [ ] `.omo/debug/physique-telegram-live-flow.debug-journal.md` — diagnostic journal; retain as internal evidence only if needed, otherwise remove at cleanup.
- [ ] `tests/gateway/test_telegram_physique_checkin.py` — regression test for the anonymous fake-user payload; retain as the permanent test.
- [ ] `gateway/platforms/telegram.py` — sender identity precedence fix; retain as the permanent fix.

## Findings

### 2026-07-17T23:46:00+09:00 — live process and profile state

- Source: `/proc/1605513/environ`, profile `gateway_state.json`, profile wizard binding/draft files.
- Value: `HERMES_HOME=/home/cube/.hermes/profiles/physique-coach`; `telegram.state=connected`; active binding `step=bodyweight`, `awaiting_text=true`; corresponding draft `answers={}`, `version=0`.
- Interpretation: the isolated profile is alive and a bodyweight response did not advance the actual draft.
- Refutes/Confirms: refutes H4; supports H1 or H2.

### 2026-07-17T23:47:00+09:00 — Telegram transport health

- Source: Telegram `getWebhookInfo` called with the profile-local credential, response redacted to non-secret fields.
- Value: `{"ok": true, "url_configured": false, "pending_update_count": 0, "last_error_date": null, "last_error_message": null}`.
- Interpretation: the dedicated bot has no webhook conflict or Telegram delivery error and its active poller has no stranded updates.
- Refutes/Confirms: refutes H4.

### 2026-07-17T23:48:00+09:00 — exact anonymous forum update simulation

- Source: real `python-telegram-bot` `Update.de_json` with `sender_chat=-1004460844240`, `message_thread_id=148`, and body text; existing profile adapter tests.
- Value: `{'effective_message': True, 'text_filter_matches': True, 'from_user_is_none': True, 'sender_chat_id': -1004460844240, 'chat_id': -1004460844240, 'topic_id': 148}`; `2 passed in 0.17s` for anonymous ingress and persisted active-draft continuation.
- Interpretation: if the Telegram API supplies an anonymous topic text update, the running adapter filter and profile boundary accept it and advance to the next prompt. The observed unchanged live draft means the live user text did not arrive as such an update, or Telegram suppressed it before the gateway.
- Refutes/Confirms: refutes H2 and H3 for the exact supported anonymous shape; supports H1.

### 2026-07-17T23:49:00+09:00 — root-cause boundary

- Source: profile gateway log and Telegram transport state.
- Value: profile log records the exact-topic `/start` at `23:37:14` but contains no non-command ingress/transition; live draft remains unmodified.
- Interpretation: Telegram commands are reaching the bot while ordinary anonymous text is not. BotFather Group Privacy is controlled by Telegram and cannot be inspected or switched through the Bot API. The required external state change is disabling Group Privacy for the dedicated Choi bot; merely sending `/start` does not change that setting.
- Refutes/Confirms: confirms H1 to the available external-runtime evidence limit.

### 2026-07-17T23:50:00+09:00 — Bot privacy state corrected

- Source: Telegram `getMe` called with the profile-local credential, response reduced to public capability fields.
- Value: `{"bot_id": 8523040466, "can_join_groups": true, "can_read_all_group_messages": true}`.
- Interpretation: the Bot API defines `can_read_all_group_messages=true` as disabled privacy mode. H1 is refuted; no BotFather change, re-add, or propagation wait is warranted.
- Refutes/Confirms: refutes H1.

### 2026-07-17T23:51:00+09:00 — anonymous sender semantic mismatch

- Source: Telegram Bot API Message documentation and `TelegramAdapter._physique_text_owner_id`.
- Value: official Message semantics state that a message sent on behalf of a chat can include `sender_chat` and a fake `from` user in a non-channel chat; current code returns any `from_user.id` before checking `sender_chat`.
- Interpretation: an anonymous administrator message can be delivered correctly yet be rejected as the synthetic id rather than mapped to the sole allowed profile owner. This exactly fits the live symptoms: a command is observed, typed answer does not advance, and no generic/default routing should occur.
- Refutes/Confirms: supports H5.

## Root cause (confirmed 2026-07-17T23:53:00+09:00)

- Mechanism: An anonymous administrator post in a supergroup can contain both the exact `sender_chat` and a synthetic `from_user`. The profile adapter selected the synthetic user id before considering the opt-in exact sender-chat boundary. The bridge therefore rejected the answer because that id was not the configured owner, leaving the draft at bodyweight without a reply.
- Evidence: `tests/gateway/test_telegram_physique_checkin.py::TestPhysiqueTelegramAdapterIngress::test_anonymous_typed_value_uses_sender_chat_when_telegram_supplies_a_fake_user` failed with actual `handle_text('70.2', 'telegram-fake-user', 'chat-1', 'topic-1')`; Telegram Bot API Message documentation describes this synthetic `from` behavior.
- Toggle proof: Before the sender-chat precedence change the regression test sends the fake id and fails; after the change it maps the identical update to `owner-1` and passes.
- Fix scope: `gateway/platforms/telegram.py` plus one regression test.

### Red phase (2026-07-17T23:52:00+09:00)

- Test: `tests/gateway/test_telegram_physique_checkin.py::TestPhysiqueTelegramAdapterIngress::test_anonymous_typed_value_uses_sender_chat_when_telegram_supplies_a_fake_user`
- Command: `./venv/bin/python -m pytest -q tests/gateway/test_telegram_physique_checkin.py::TestPhysiqueTelegramAdapterIngress::test_anonymous_typed_value_uses_sender_chat_when_telegram_supplies_a_fake_user`
- Output: `FAILED ... Expected: handle_text('70.2', 'owner-1', 'chat-1', 'topic-1'); Actual: handle_text('70.2', 'telegram-fake-user', 'chat-1', 'topic-1')`.

### Green phase (2026-07-17T23:53:00+09:00)

- Fix: `gateway/platforms/telegram.py::_physique_text_owner_id` — exact configured anonymous sender-chat now takes precedence over Telegram's fake user only within the existing profile/chat/topic boundary.
- Test: the regression test now passes.
- Full affected suite: `22 passed in 0.28s`.

### Manual QA — live Telegram profile bridge (2026-07-17T23:55:00+09:00)

- Scenario: replay the already user-supplied bodyweight value through the corrected exact profile/chat/topic bridge, then send and bind the generated next prompt through the dedicated bot.
- Observed output: `{"accepted": true, "next_step_is_sleep": true, "delivered_to_target_topic": true, "prompt_message_id_bound": true}`.
- State check: profile unit is `active/running`; current draft reports `step=sleep_duration`, `version=1`, `awaiting_text=true`, with a bound prompt message id.
- Expected output: bodyweight advances exactly once to the sleep-duration prompt in topic `148`.
- Fix verified: yes.

## Second root cause and final verification (2026-07-18T00:03:00+09:00)

- Corrected the earlier live-QA conclusion: replaying `69.7` through the bridge proved the domain transition, but did not prove that a delayed Telegram update would remain outside generic ingress.
- Source: persisted binding state and profile gateway log. The active `sleep_duration` binding expired at `2026-07-17T23:51:29+09:00`; the user's exact `7` arrived at `23:52:42+09:00` and was then batched by generic Hermes.
- Mechanism: `BindingStore.load` intentionally discards expired bindings. The adapter treated the resulting `None` from `handle_text` as ordinary chat text and entered generic batching.
- Permanent fix: for only the exact configured owner/chat/topic boundary, a scalar numeric answer without an active binding is consumed before generic ingress and opens the profile's morning check-in launcher. The profile binding lifetime was also raised from 30 minutes to 12 hours.
- Regression coverage: added a real binding-store expiry → fresh bridge → Telegram adapter ingress test. A generic enqueue never occurs and the recovery card is sent. Full affected suite: `24 passed in 0.27s` using the project virtual environment.
- Deployment: restarted only `physique-coach-gateway.service`; it is `active/running` with profile-only `HERMES_HOME`.
- Recovery: verified the existing morning draft was at `sleep_duration`, version 1, with the persisted bodyweight. Re-established its profile-scoped binding, applied the user's already-supplied `7` exactly once, sent the resulting sleep-quality 1–5 keyboard into topic `148`, and bound that prompt message. Final persisted state: `step=sleep_quality`, `awaiting_text=false`, bound prompt present.

## Final fix

- `gateway/platforms/telegram.py`: exact anonymous sender-chat identity precedence plus expiry-safe numeric recovery before generic ingress.
- `/home/cube/.hermes/profiles/physique-coach/config.yaml`: profile-only check-in expiry increased to 12 hours.
- `tests/gateway/test_telegram_physique_checkin.py`: fake-user, expired numeric, and real expired-binding ingress regressions.

## Save acknowledgement investigation (2026-07-18T00:06:00+09:00)

## Profile inference and post-save coaching investigation (2026-07-18)

### Hypotheses

1. [CONFIRMED] The dedicated profile has no selected model or Codex credentials, so a saved check-in can only render the deterministic completion card.
2. [CONFIRMED] Hermes does not support a live shared Codex credential pool between profiles: every `HERMES_HOME` owns its own `auth.json`; the supported adoption path is an explicit, one-time Codex CLI credential import into that profile.
3. [PENDING] Once the profile owns its model configuration and credential session, a direct profile-local inference probe can return a short response without creating a Telegram message or touching the generic ingress route.
4. [PENDING] Post-save feedback can be called only after a canonical `SAVED` transition and only inside the already exact-bound owner/chat/topic callback path, while provider failures leave the stored event and completion card intact.

### Evidence and planned scope

- The default Hermes profile selects `provider=openai-codex`, `model=gpt-5.3-codex-spark`, and the Codex Responses endpoint.
- The physique-coach profile contains neither a `model` block nor `auth.json`.
- `agent/credential_pool.py::_seed_from_singletons` explicitly rejects automatic `~/.codex/auth.json` reuse at pool-load time to avoid OAuth refresh-token races. `hermes_cli.auth::_login_openai_codex` provides the deliberate one-time import path instead.
- Any authorization material will remain in the profile's protected auth store; it will not be printed, copied into source/config text, or sent to Telegram.
- Planned permanent source scope: profile-gated post-save feedback only. The generic Telegram router, default Hermes profile, existing chat/session memory, and all Telegram target boundaries remain unchanged.

### Resolution and verification

- Profile configuration now selects `openai-codex` with `gpt-5.3-codex-spark`; a native, explicit Codex CLI credential adoption created only the profile's protected `auth.json`.
- Direct profile-local inference probe: provider `openai-codex`, model `gpt-5.3-codex-spark`, response `OK`, usage `96` tokens. The probe does not instantiate a Telegram adapter or send a Telegram message.
- The post-save path is feature-flagged by `physique_checkin.coaching_feedback_enabled`, runs only after the exact private callback returns `SAVED`, retrieves only the finalized same-owner/topic snapshot, and edits the same completion card. Unsafe/safety-stop records receive no model feedback.
- Provider failures are caught at the private feedback boundary: the canonical event and static completion confirmation remain intact, and no generic Telegram event is queued.
- Regression verification: `26 passed` for the Telegram physique suite; `5 passed` for the inline-card scheduler suite.
- Live profile service restarted successfully and is active. A profile-local feedback-function probe returned nonempty bounded text (219 characters) without a Telegram send.

### Environment snapshot

- Runtime: profile-scoped CPython gateway, launched by `physique-coach-gateway.service` with `HERMES_HOME=/home/cube/.hermes/profiles/physique-coach`.
- Service state before changes: `ActiveState=active`, `MainPID=1624543`.
- References read: debugging Python runtime; setup, investigate, fix, QA, cleanup methodology; programming Python guidance.

### Hypotheses

1. [PENDING] Telegram did not deliver the Save callback — distinguishing evidence: no finalized event/draft mutation — if true, fix is: transport recovery.
2. [PENDING] Save callback reached the wizard and persisted exactly once, but the bridge renders the unchanged summary prompt rather than a completion confirmation — distinguishing evidence: finalized draft/event plus successful callback result whose prompt remains `summary` — if true, fix is: saved confirmation.
3. [PENDING] The bridge generated a confirmation but Telegram edit delivery failed silently — distinguishing evidence: confirmation reply exists while `edit_message_text` raises or API rejects it — if true, fix is: delivery observability.

### Artifacts to revert

- [ ] `tests/gateway/test_telegram_physique_checkin.py` — permanent regression test for Save confirmation; retain.
- [ ] `gateway/platforms/physique_checkin.py` — permanent profile-gated bridge fix; retain.

### 2026-07-18T00:06:00+09:00 — live callback and persistence evidence

- Source: `/home/cube/.hermes/profiles/physique-coach/data/wizard/drafts/3080ea3d9a0d0516d47222b67087a089.json`, `events.jsonl`, and profile bindings.
- Value: draft `version=9`, `step="summary"`, `finalized_event_id="wizard_0fb1166aa7f8d0d13f251ebd"`; bindings `step="summary"`, `version=9`, `message_id="175"`; exactly one matching canonical event.
- Interpretation: the Telegram callback reached the profile wizard and finalized exactly once at `2026-07-18T00:04:26.804537+09:00`; it did not enter generic text ingress.
- Refutes/Confirms: refutes H1; confirms the persistence half of H2.

### 2026-07-18T00:06:00+09:00 — rendering mechanism

- Source: `gateway/platforms/physique_checkin.py:_advance` and `gateway/platforms/physique_checkin_prompts.py:build_wizard_prompt`.
- Value: a `WizardStatus.SAVED` result retained `binding.step="summary"` and returned the ordinary summary prompt, whose visible text is `"오늘 체크인을 저장할까?"` with Save buttons. The adapter then acknowledged only `"✓"`.
- Interpretation: the event was saved, but the same card was rendered as another Save card instead of a completion confirmation; there is no evidence of an outbound API failure.
- Refutes/Confirms: confirms H2; refutes H3.

## Root cause (confirmed 2026-07-18T00:06:00+09:00)

- Mechanism: `PhysiqueCheckinBridge._advance` treated `SAVED` like an ordinary step transition. Because finalized sessions retain the `summary` step, it generated the original Save prompt again and gave Telegram only a generic tick toast, despite the canonical event having been written.
- Evidence: live finalized draft and one matching event; deterministic adapter E2E test reproduced the visible `answer(text='✓')` rather than a completion acknowledgement.
- Toggle proof: before the `SAVED` branch, the E2E assertion expected `answer(text='체크인을 저장했어.')` but observed `answer(text='✓')`; with the branch, the exact same fixture returns a completion notice and no-button confirmation card.
- Fix scope: `gateway/platforms/physique_checkin.py` plus one adapter E2E regression.

### Red phase (2026-07-18T00:06:00+09:00)

- Test: `tests/gateway/test_telegram_physique_checkin.py::TestPhysiqueTelegramAdapterIngress::test_save_callback_records_once_and_replaces_summary_with_confirmation`
- Command: `venv/bin/pytest -q tests/gateway/test_telegram_physique_checkin.py::TestPhysiqueTelegramAdapterIngress::test_save_callback_records_once_and_replaces_summary_with_confirmation`
- Output: `Expected: answer(text='체크인을 저장했어.'); Actual: answer(text='✓')`.

### Green phase (2026-07-18T00:07:00+09:00)

- Fix: `gateway/platforms/physique_checkin.py:_advance` emits a no-button `✅ 오늘 체크인을 저장했어.` prompt and a visible callback notice only when the profile wizard reports `SAVED`.
- Test: the E2E adapter callback now passes and asserts: generic ingress count 0, exactly one canonical event before and after a replay, and a rendered confirmation.
- Full affected suites: `25 passed in 0.28s`; profile wizard domain suite: `23 passed in 0.10s`.

### Manual QA — live profile (2026-07-18T00:08:00+09:00)

- Scenario: deployed the profile-only bridge change, then edited the already-finalized Telegram summary card into the completion confirmation without invoking Save again.
- Observed output: `{"telegram_edit_ok": true, "canonical_event_count_before": 1, "canonical_event_count_after": 1}` and `{"draft_finalized": true, "matching_events": 1, "event_count": 1}`.
- Expected output: visible completion acknowledgement with no duplicate canonical record.
- Fix verified: yes.

## Daily launcher date label (2026-07-18T00:10:00+09:00)

- Scope: add only a visible KST date and Korean weekday to the profile-gated daily launcher. No correction workflow, coach setting mutation, or default Hermes routing change is authorized.
- Artifacts to retain: `gateway/platforms/telegram.py` (profile-gated launcher label) and `tests/gateway/test_telegram_physique_checkin.py` (fixed-clock regression).

### Red → green

- Red test: `test_launcher_shows_kst_date_and_korean_weekday_in_its_topic_card` failed because the sent text was only `오늘 기록을 시작하거나 이어갈 수 있어.` and lacked `2026년 7월 18일 (토)`.
- Fix: `TelegramAdapter.send_physique_checkin_launcher` converts the current UTC instant to `Asia/Seoul`, maps Python weekday to Korean, and prepends the date label. The code remains reachable only through the existing profile-gated inline-card capability.
- Green: `25 passed in 0.30s` for the full affected adapter suite.

### Manual QA — live profile

- Deployed by restarting only `physique-coach-gateway.service` (`active`, PID `1633853`).
- Invoked the real profile adapter's launcher send path with its dedicated Telegram bot. Telegram accepted message `176`; canonical event count was unchanged: `1 → 1`.
- The live card uses the deployed source path; the fixed-clock regression locks the exact visible text `2026년 7월 18일 (토)`.

## Morning scheduler investigation (2026-07-18T00:20:00+09:00)

### Hypotheses

1. [PENDING] The profile's 08:11 KST check-in job is declarative/paused but was never installed in the active scheduler — evidence: active cron store lacks the job or marks it disabled — if true, fix is: profile install.
2. [PENDING] The job is active but timezone/next-run resolution is wrong — evidence: active scheduler job has a non-KST timezone or missed/incorrect next execution — if true, fix is: timezone binding.
3. [PENDING] The job executed but its inline-card delivery failed or was suppressed by an idempotency claim — evidence: run history/claim state shows execution without a successful dedicated delivery — if true, fix is: delivery recovery.
4. [PENDING] The card the user just received is the manual QA launcher rather than a scheduler run — evidence: Telegram message id matches the manual send and no scheduler run exists at 08:11 — if true, fix is: no scheduler bug; remove manual QA from future verification.

### Artifacts to revert

- [ ] No files or runtime configuration modified during investigation. Any future scheduler test/fix will be recorded before editing.

### 2026-07-18T00:58:49+09:00 — live scheduler state

- Source: profile clock, `cron/jobs.json`, `hermes cron list --all`, profile cron output, and `data/inline-card-launch-claims/`.
- Value: local clock `2026-07-18T00:58:49+09:00`; morning job is `enabled=true`, `state="scheduled"`, schedule `11 8 * * *`, `next_run_at="2026-07-18T08:11:00+09:00"`; the only prior claim is `2026-07-17.claim` written at `23:21:29` and the only morning job output is `23:21:30`, `Status: launch card sent`.
- Interpretation: 08:11 on July 18 has not occurred yet. The user-visible card sent at the current time was the explicit manual QA launcher (message 176), not an automatic scheduler delivery. No July 18 scheduler run or delivery failure exists to repair.
- Refutes/Confirms: confirms H4; refutes H1, H2, and H3 for the July 18 scheduled occurrence because it is still future.

### Scheduler delivery path verification

- Source: `cron/scheduler.py:2070-2096`, `cron/physique_inline_card.py:60-115`, and existing `tests/cron/test_physique_inline_card.py`.
- Value: the exact `inline_card="physique-checkin-morning"` job bypasses generic local stdout delivery and schedules `adapter.send_inline_card` on the live gateway loop; `deliver="local"` is therefore intentional and does not select Jarvis/default Telegram delivery.
- Interpretation: the active profile job is wired to the dedicated adapter, and the future 08:11 run will use the profile-only card path.

### Resolution

- No code or scheduler mutation is safe or necessary: changing a correctly future-dated job would create a new timing risk. Future verification will not use a manual card send; scheduler execution will be verified from its run output and claim state only.
