I read this as a constrained production-debugging and minimal-repair task - I’ll trace the live gateway’s launch/config/package/state contract, prove the root cause with a deterministic regression test, then patch and verify without relaunching the controller or interacting with Telegram.


The live process/package trace has narrowed this to a launch-contract defect rather than Telegram transport: the gateway is healthy and polling, but the successfully launched controller is not part of the service package and left no runtime trigger visible to PID 3836302. I’m now locking down whether the missing handoff is a controller command bug, a profile/config mismatch, or an omitted candidate member before editing.


Root cause is confirmed: launch revalidated the f9a461 source manifest and clean profile, then started whatever wheel the systemd unit already referenced. That unit loaded the older dd1711/b486 cleanup wheel; the f9a461 candidate wheel is ae7052 and contains the controller, while the deployed wheel does not. More importantly, the launch contract had no deployed-candidate check and could return `AWAITING_HUMAN_TELEGRAM_ACTION` after starting an unbound runtime. I’m adding only that fail-closed binding check and its regression coverage; changing the controller invalidates f9a461, so verification will stop before any second launch.


The regression is green and the repair is now fail-closed before `systemctl start`: a stale or differently sourced Hermes wheel cannot consume an arm receipt. I’m doing final type/package verification against both the new candidate and the still-running f9 process; I will not install, restart, re-arm, or launch because the controller digest has changed.


I read this as a constrained production-debugging and minimal-repair task - I traced the live launch binding, added a fail-closed regression repair, and stopped before any second launch.

## Root cause

The controller validated candidate `f9a461...` but started an unrelated deployed wheel:

- Required candidate wheel: `ae705212...`
- Service venv wheel from `direct_url.json`: `dd1711d3...`
- Live service: active, PID `3836302`, correct profile environment.
- Telegram connected successfully; no relevant journal error.
- Outbox ledger/emergency remained empty at SHA-256 `8df495...`.

Hypotheses discriminated:

1. **Telegram/service failure:** rejected; gateway connected and remained healthy.
2. **Wrong profile or inaccessible state:** rejected; `HERMES_HOME`, working directory, open descriptors, and state paths all target `dualcoachtest`.
3. **Candidate/deployment mismatch:** confirmed. The f9 controller never checked that systemd’s runtime wheel matched its sealed candidate before reporting launch success.
4. **Source/package equality:** confirmed mismatch; the deployed wheel lacks the f9 controller and entry point.

## Repair

Added a launch-time deployed-wheel binding check at:

- `gateway/platforms/dualcoach_tasks21_25_controller.py:438`
- Fail-closed reason at line 555: `deployed_candidate_drift`
- Service start remains after the check at line 557.

Regression test:

- `tests/gateway/test_dualcoach_tasks21_25_controller.py:257`
- Demonstrated failure against the pre-repair controller, then passed after repair.

Verification:

- Focused tests: `42 passed`
- Ruff: passed
- ty: passed
- Rebuilt wheel contains the repaired controller and entry point.
- Live binding probe correctly returns `False`.
- LSP was attempted but rejected sibling paths due the tool’s request-CWD restriction.
- No service lifecycle, Telegram, provider-auth, activation, commit, push, or release action was performed.

## Required next boundary

The repair changes the controller digest from sealed `041903c...` to `76212d9...`; therefore candidate `f9a461...` and its arm receipt cannot be reused.

Exact next action: stop the current service through its supported systemd lifecycle, build/seal a new candidate containing controller digest `76212d9...`, install that exact candidate wheel into the service venv, obtain newly candidate-bound provider evidence, and re-arm. Stop again before issuing the second launch.

Evidence pointers:

- Arm receipt: `/home/cube/.local/state/dualcoach-task26-controller/arm-f9a46172386333a0067f43695fb1429a043a606f04baceb06e8c59b90c36235c.json`
- f9 manifest: `/home/cube/projects/richard/traning coach/.omo/evidence/task26/dualcoach-provider-auth-candidate-eefe28e328dc2ff89767500795df58c53457b8d2bc553c6e8c977c275b17b302/candidate-manifest.json`
- Deployed provenance: `/home/cube/projects/richard/hermes-agent/.venv/lib/python3.12/site-packages/hermes_agent-0.17.0.dist-info/direct_url.json`


I read this as an authorized Task26 candidate rebind through re-arm only - I’ll stop and attest the clean profile, verify the focused repair, seal/install an exact current-source wheel, perform at most one zero-retry provider probe, re-arm, write a durable rebind receipt, and stop before launch or Telegram activity.


I read this as completing the bounded Task26 rebind - verify the repair, seal/install one exact candidate, perform one provider probe, re-arm, write durable evidence, and stop before launch or Telegram activity.


I read this as completing the bounded Task26 rebind - report the verified stop boundary and exact evidence.

PASS: repaired candidate is installed and freshly armed, but not launched.

- Service: `inactive/dead`, `MainPID=0`
- Wheel SHA-256: `e4bd655672edf148ee3026b44f78149402234ba5cc0be3db19b875cbd86659be`
- Full candidate digest: `50685ac2c063c5b18fa895cc1991e3473368d177c7309bf54af0374ea67ba1d1`
- Deployment binding probe: `True`
- Provider probe: one request, zero retries, `store=false`, ready
- Arm receipt: fresh 0600 receipt, `READY_FOR_CLEAN_WINDOW`
- Protected registry, bootstrap, and Telegram ledgers: byte-identical
- Verification: 42 tests passed; Ruff and Ty passed
- LSP unavailable because the tool rejected sibling-repository paths outside the request cwd

Durable PASS receipt:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-runtime-rebind-50685ac2c063c5b18fa895cc1991e3473368d177c7309bf54af0374ea67ba1d1/task26-runtime-rebind-receipt.json`

SHA-256: `ddbdbb9f363ca71893dca6fef5ee0646b2ea558af9ebcbdb59f07b50bb64b3e2`

No controller launch, service restart, Telegram action, `getUpdates`, customer activation, retry, commit, push, or release occurred.


I read this as diagnosing the fresh post-launch no-card condition - inspect the exact live lifecycle and sealed source, discriminate competing causes, then repair and stop only if orchestration is at fault.


I read this as reporting the verified post-launch diagnosis and repair boundary - provide one root cause, evidence, and the next safe step.

**Root cause:** the controller falsely equated gateway startup with owner-review readiness. It started systemd and unconditionally returned `AWAITING_HUMAN_TELEGRAM_ACTION`, but startup only recovers existing durable sessions. The profile contained:

- zero room-bootstrap sessions
- zero customer-bootstrap sessions
- zero publication records
- no committed `owner_review` projection

Therefore `_recover_room_bootstrap_waiting_states()` had nothing to pass to `recover_waiting_session()`, so no card could be published.

Rejected hypotheses:

- Service/Telegram failure: PID `3945420` was healthy and Telegram was connected.
- Candidate drift: deployed and sealed wheel SHA matched `6a3f5f04...`.
- Broken recovery of pending state: no pending state existed.
- Owner route mismatch: the owner route was configured, but publication never reached routing.

Repair:

- Arm and launch now fail closed with `owner_review_not_ready` unless exactly one durable `AWAITING_ACTIVATION` session is bound to a committed `owner_review` projection.
- Deterministic regression failed before repair and passes afterward.
- Verification: 44 tests passed; Ruff and Ty passed.
- LSP remained unavailable because the tool rejected sibling-repository paths.

The service was safely stopped and remains `inactive/dead`, `MainPID=0`. Protected state and sealed candidate/lifecycle evidence are unchanged.

Repair receipt:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-post-launch-owner-review-repair-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/repair-receipt.json`

SHA-256: `17fd0654060371b75e1673cb634ffbbbc3630c24913ab8377962b78bb0818200`

**Next safe boundary:** establish one owner-review-ready synthetic session through the supported journaled preparation/reconciliation path, then reseal and re-arm. No human action pointer is valid yet because neither a prepared invite nor an owner-review card exists. No relaunch, provider-auth request, Telegram action, registry mutation, commit, push, or release occurred.


I read this as preparing and sealing exactly one recoverable owner-review state without launching - first verify the canonical API can satisfy the zero-registry constraint, then execute only if that path is genuinely supported.


I read this as executing preparation only if the canonical API can preserve zero registry growth - it cannot, so I stopped before mutation.

**Exact blocker:** the requested state is impossible under the current constraints.

The canonical production path requires:

1. `prepare_rehearsal_customer_invite()` creates a `PREPARED` session.
2. The synthetic customer claims it through a private Telegram `/start rc1_<token>` action.
3. `TelegramCustomerBootstrapRegistration.handoff_rehearsal_customer()` calls `admin.register_customer()`, necessarily increasing registry count from 0 to 1, although the customer remains disabled.
4. A durable consent callback advances it to `AWAITING_ACTIVATION`.
5. Onboarding answers/attestation or supported reconciliation creates `owner_review`.

Additionally, runtime `_current_authority()` requires exactly one matching registry customer. An `AWAITING_ACTIVATION` session with registry count zero would fail production authority validation and would be fabricated, unrecoverable state.

Current state remains unchanged:

- Service: `inactive/dead`, `MainPID=0`
- Registry customers: 0
- Room-bootstrap sessions: 0
- Customer-bootstrap sessions: 0
- Publication records: 0

**Supported first human action, after machine preparation is authorized:**

- Actor/chat: synthetic customer `8527916639`, private DM `8527916639`
- Bot: `@dual_coach_pilot_test_bot`
- Action: open the candidate-generated link
  `https://t.me/dual_coach_pilot_test_bot?start=rc1_<22-char-token>`
  and press **Start**, sending `/start rc1_<token>`

No token or link currently exists because generating one would mutate the bootstrap ledger, and the instruction required stopping before mutation when human input is necessary.

No profile, registry, evidence, source, candidate, provider, arm, or historical artifact was changed. No build, install, provider request, re-arm, launch, Telegram action, `getUpdates`, activation, commit, push, or release occurred.


I read this as preparing exactly one canonical invite - revalidate the empty stopped baseline, create or reuse one valid `PREPARED` session, seal its secret action pointer privately, and stop before claim.


I read this as preparing one canonical invite and stopping at the first human boundary - completed without claim or launch.

Prepared via `RoomBootstrapStore.prepare_rehearsal_customer_invite()` exactly once.

- Session: `cb_6S6RABpDgZ165V02A7qEzw`
- State: `PREPARED`, generation 1
- Bot: `@dual_coach_pilot_test_bot`
- Expected actor/chat: `8527916639` / private DM `8527916639`
- Expiry: `2026-08-14T16:47:00.861257Z`
- Token SHA-256: `44423d0047f02902401d1747289615cf97e12e86ec1747618207b2cdcf04d80f`
- Process-boundary reload: PASS
- Registry customers, activations, deliveries, and publications: all zero
- Service: `inactive/dead`, `MainPID=0`
- Ledger and receipt modes: `0600`

Private receipt containing the exact secret link:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-canonical-invite-preparation-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/preparation-receipt.json`

SHA-256: `71896a386ab2e25fefa5653af9d59436d61183db1081444b6d7bbded21908d85`

**Exact human action:** while signed into synthetic Telegram account `8527916639`, securely read `.human_action.customer_link` from that 0600 receipt, open it, and press **Start exactly once** in the private chat with `@dual_coach_pilot_test_bot`.

Stopped before claim, registry handoff, launch, consent, onboarding, owner review, provider request, reseal, re-arm, `getUpdates`, commit, push, or release.


I read this as opening only the legitimate claim-listener window - validate the invite, start the production service once if still valid, establish a durable event-driven watcher, and return the private action pointer.


I read this as opening the supported bootstrap-listener window - the service and event-driven claim watcher are now ready.

Validated before startup:

- Session `cb_6S6RABpDgZ165V02A7qEzw`
- State `PREPARED`, generation 1
- Candidate-bound and unexpired
- Registry customers, activations, deliveries, publications: zero

Listener boundary:

- Service started exactly once through systemd
- Service: `active/running`, PID `3948382`
- Telegram: connected
- Profile: `/home/cube/.hermes/profiles/dualcoachtest`
- Runtime wheel equals sealed wheel: `6a3f5f04a2982658f1c334273b05ae958b0353470997fc8df143c5b1fe12cbeb`
- Event-driven inotify watcher: active, PID `3948496`
- Invite expiry: `2026-08-14T16:47:00.861257Z`

Listener receipt:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-canonical-invite-preparation-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/listener-boundary-receipt.json`

SHA-256: `046070d00fa85771ba5e6d633546e1433e5c4dafce2a07908fc7d0d23097d06d`

**Exact human action:** using Telegram account `8527916639`, securely open `.human_action.customer_link` from the private 0600 preparation receipt and press **Start exactly once** in the private chat with `@dual_coach_pilot_test_bot`.

The watcher will write `claim-transition-receipt.json` when the canonical session leaves `PREPARED`. No controller launch, provider request, manual `getUpdates`, Telegram send/click, activation, commit, push, or release occurred.


I read this as atomically superseding the unclaimed expired invite - verify the empty boundary, use the store’s locked prepare path to expire-and-append once, retain the healthy listener, and arm one replacement watcher.


I read this as canonically superseding the expired invite and reopening the human claim boundary - completed.

Atomic production operation:

- Old session `cb_6S6RABpDgZ165V02A7qEzw`: `EXPIRED`, generation 2
- Expired token is rejected by production lookup
- Replacement session: `cb_9yTz0oNwdU8s8hradru8BA`
- Replacement state: `PREPARED`, generation 1
- Expiry: `2026-08-14T17:18:04.512968Z`
- New token SHA-256: `4c5dc4819f37d5ae30f9eb50574d24d01812e4922830cb90d5decbdc355aad1`

The existing service remained healthy and was not restarted:

- PID `3948382`
- Telegram connected
- Runtime wheel matches `6a3f5f04a2982658f1c334273b05ae958b0353470997fc8df143c5b1fe12cbeb`

Watcher accounting:

- Old PID `3948496`: exited with status 3 after recording no claim; unit reset and unloaded
- New event-driven watcher PID: `3954026`
- Watcher state: `active/running`

All remain zero: registry customers, activation, delivery, publication, and owner-review state.

Private 0600 receipts:

- Expiry/supersession: `expiry-supersession-receipt.json`
  SHA-256 `361d093f4b5c49cf73d2f77c2835292bd35910c88a08b3202b06c247cf2458c1`
- Replacement preparation: `replacement-preparation-receipt.json`
  SHA-256 `7f829687b4ffbe73881d5b7ebd79a0fb5d2cfe2de56aa0004b7db3a866bf87a0`
- Listener boundary: `replacement-listener-receipt.json`
  SHA-256 `f1c1dc93d428410bae553371e67762585956f04edbddc65a2119d95a43e8e1a7`

Directory:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-canonical-invite-supersession-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/`

**Exact human action:** using Telegram account `8527916639`, securely open `.human_action.customer_link` from the private `replacement-preparation-receipt.json` and press **Start exactly once** in the private chat with `@dual_coach_pilot_test_bot`.

No controller launch, provider request, manual `getUpdates`, Telegram send/click, activation, commit, push, or release occurred.


I read this as performing one more canonical locked supersession - determine the production TTL ceiling, retain the healthy listener, replace the expired session once, and arm one watcher without source changes.


I read this as superseding only after canonical expiry - the runtime clock proves the session is still valid, so I made no mutation.

**Blocker:** session `cb_9yTz0oNwdU8s8hradru8BA` has not expired.

- Current UTC: `2026-08-14T16:52:33.095165Z`
- Expiry: `2026-08-14T17:18:04.512968Z`
- Remaining: about 25m 31s
- State: `PREPARED`, generation 1
- Watcher PID `3954026`: active, not stale
- Production TTL is fixed at 30 minutes; no configurable longer TTL exists.
- The canonical prepare API rejects supersession while another live `PREPARED` session exists.

No third invite or receipt was fabricated. The service remains healthy on PID `3948382`, Telegram connected, with the required wheel.

The currently valid private link can be printed locally with:

```bash
jq -r '.human_action.customer_link' '/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-canonical-invite-supersession-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/replacement-preparation-receipt.json'
```

Safe action: using Telegram account `8527916639`, open that link and press **Start exactly once** in the private chat with `@dual_coach_pilot_test_bot`.

No state, watcher, service, evidence, or source was changed.


I read this as closing the failed rehearsal window - canonically expire the ledger entry, retire both watcher and gateway units, verify the zero-state boundary, and seal a blocked audit receipt.


I read this as fail-closed cleanup only - completed through production APIs and systemd.

**PASS cleanup**

- Both sessions: `EXPIRED`, generation 2
- Both invite tokens rejected
- Live `PREPARED` sessions: 0
- Registry customers, activations, deliveries, publications, owner reviews: all 0
- Watcher PID `3954026`: exited status 3, stopped/reset/unloaded
- Watcher unit: `not-found`, PID 0
- Gateway PID `3948382`: stopped/reset
- Gateway: `inactive/dead`, PID 0, result `success`
- Third invite created: no

Private audit receipt:

`/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-expired-human-window-cleanup-5e6f2f2a01321ac89c2950e511e7346d23070c51146c14c22c04db54034b28f1/cleanup-blocked-audit-receipt.json`

- Mode: `0600`
- SHA-256: `08fe1b6556e991c8f90fb4204fd63abe23d83917529c08e21b6da6d41930f414`

The receipt records one verification-wrapper exit `1`: canonical expiry had already committed, but the wrapper incorrectly expected `expire_unbound()` to return only changed sessions rather than the complete ledger. Process-boundary verification then passed.

**Sole remaining human action:** authorize a fresh canonical invite window only when synthetic actor `8527916639` is ready to open it immediately. No valid Telegram link or Start action currently exists.

No controller launch, provider request, manual update consumption, Telegram action, activation, source edit, commit, push, or release occurred.