I read this as a read-only invariant audit of the restart timeout - I’ll inspect the recorded evidence and lifecycle implementation, then rank five falsifiable assumptions with exact source locations and a decisive focused query.


I found a stronger runtime discriminator than the timeout alone: the replacement did launch and reached the same fake API (calls 67-70), but stalled before `deleteWebhook/getUpdates`. I’m tracing that startup interval now, especially the persisted-state recovery invoked between membership validation and polling.


I read this as a read-only invariant audit of the restart timeout - I inspected the lifecycle evidence and traced replacement startup through recovery and polling.

## Key finding

The replacement child **did launch and contact the same fake API**. Calls 67-70 are its startup:

- `getMe`
- `getMe`
- `getChatMember`
- `sendChatAction`

It then stopped before command registration, `deleteWebhook`, or `getUpdates` (`.omo/evidence/v1.1.1-stability/disposable-e2e/fake-api-calls.jsonl:67-70`).

More importantly, `child_adapter_stopped` exists without `child_adapter_ready` (`process-events.jsonl:5`; ready is emitted only at `scripts/dualcoach_v111_disposable_process.py:174-184`). This strongly indicates `adapter.connect()` returned false or raised, and the child exited while the harness continued waiting solely for `getUpdates`.

## Five load-bearing assumptions

### 1. The replacement is the intended child with the same profile/config/runtime identity

**Evidence**

- Every launch receives the same `profile` object and explicit child environment: `scripts/run_dualcoach_v111_disposable_e2e.py:124-173`.
- The child loads preflight from the explicit `--profile` path and overlays the supplied API URL: `scripts/dualcoach_v111_disposable_process.py:143-163`.
- Calls 67-70 prove the replacement reached the existing loopback Bot API.
- The prepared profile is created once at `scripts/run_dualcoach_v111_disposable_e2e.py:653-678`.

**Smallest falsification query**

Run only child startup against the retained pre-update-4 fixture and print:

- PID/argv
- `preflight.redacted_identity`
- `HERMES_HOME`
- `DUALCOACH_PROFILE_PACKAGE`
- effective `config.extra["base_url"]`

Compare these with the preceding child.

**Observable**

- **True:** identical profile path, config SHA-256, credential identity, package path, and API port; calls continue at sequence 67.
- **False:** differing hash/path/port, or no calls on the existing fake API.

---

### 2. “Waiting for polling” also detects child startup failure

This assumption is already contradicted by the evidence.

**Evidence**

- `_start_and_wait_for_poll` waits only on an API action future: `scripts/run_dualcoach_v111_disposable_e2e.py:224-246`.
- It never races that future against `child.wait()` or `child.poll()`.
- Child stdout and stderr are discarded: `scripts/run_dualcoach_v111_disposable_e2e.py:172-173`.
- `child_adapter_ready` is written only after successful `adapter.connect()`: `scripts/dualcoach_v111_disposable_process.py:174-181`.
- Current evidence has `child_adapter_stopped` but no replacement-ready event: `process-events.jsonl:5`.

**Smallest falsification query**

A focused harness test where `_start_child` returns a process that exits nonzero before `getUpdates`. Await `_start_and_wait_for_poll`.

**Observable**

- **True:** it immediately reports the child return code/startup error.
- **False/current behavior:** it waits 20 seconds and reports `timed out waiting for ... polling`.

---

### 3. The fake Telegram server preserves pending update 4 and action state across children

**Evidence**

- One `FakeTelegramApi` is created and started for the whole lifecycle: `scripts/run_dualcoach_v111_disposable_e2e.py:653,678`.
- Engine updates and actions are instance-owned lists: `scripts/dualcoach_v111_fake_telegram_engine.py:51-53`.
- `getUpdates` removes only IDs lower than the requested offset: `dualcoach_v111_fake_telegram_engine.py:328-339`.
- Observed offsets progress across earlier children as `[0,2,0,3,0,4]`: `.omo/evidence/v1.1.1-stability/disposable-e2e/offsets.json:1`.
- Calls 67-70 continue the prior action sequence rather than restarting at 1.

**Smallest falsification query**

Snapshot `next_update_id`, pending update IDs, server identity/port, and action count immediately before and after replacement launch.

**Observable**

- **True:** same engine/port, update 4 remains queued, action count advances from 66 to 70.
- **False:** a new engine/port, empty queue, reset sequence, or update 4 missing.

---

### 4. The update-4 child was fully reaped and released all process-scoped locks

**Evidence**

- The harness waits for the exact fault exit before setting `child = None`: `scripts/run_dualcoach_v111_disposable_e2e.py:797-804`.
- The fault uses `os._exit(73)`: `scripts/dualcoach_v111_disposable_process.py:54-61,101-118`.
- Final cleanup reports every child reaped: `.omo/evidence/v1.1.1-stability/disposable-e2e/cleanup.json:1`.
- Task26 update processing holds a shared `flock` through the update: `gateway/platforms/task26_runtime_authority.py:138-159`; process exit should release it.

**Smallest falsification query**

Immediately after `_await_exit`, assert:

1. `child.poll() == 73`
2. `/proc/<pid>` is absent
3. a fresh process can acquire the authority lock non-blockingly.

**Observable**

- **True:** PID absent and lock acquisition succeeds.
- **False:** surviving PID or `LOCK_NB` returns `EWOULDBLOCK`.

---

### 5. Recovery of the response-before-receipt state completes before polling starts

This is the leading violated production invariant.

**Evidence**

- Startup explicitly runs all nutrition recovery **before** `deleteWebhook` and `start_polling`: `gateway/platforms/telegram.py:3054-3057,3108-3128`.
- Calls 67-70 correspond to initialization and staff-readiness validation. No later command or polling call appears.
- Update 4 faults immediately before `GatewayOnboardingPublicationOutbox.record_receipt`: `scripts/dualcoach_v111_disposable_process.py:101-118`.
- Recovery only commits `RECEIPTED` outbox rows: `telegram_nutrition_onboarding_runtime_publication_transport.py:45-58`.
- Replay of an existing `DISPATCHING` publication reconstructs authority via `outbox.get`; an authority mismatch can raise before the branch marks it uncertain: `telegram_nutrition_onboarding_runtime_publication_transport.py:322-352`.
- `recover_waiting_session` invokes publication recovery and then republishes: `telegram_nutrition_onboarding_runtime.py:182-218`.
- Existing focused coverage tests an idempotent edit and receipt-gate advancement separately, but not this durable `DISPATCHING` startup path: `tests/gateway/test_telegram_publication_recovery.py:105,217`.

**Smallest falsification query**

Create the exact durable state produced after update 4:

- domain publication `PREPARED`
- outbox row `DISPATCHING`
- workflow cursor 2
- no Telegram receipt

Then instantiate a fresh runtime and call `recover_waiting_session` under a bounded timeout, recording the exception and resulting outbox/domain states.

**Observable**

- **True:** recovery returns, publication becomes safely `UNCERTAIN` or reconciled, and startup reaches `deleteWebhook/getUpdates`.
- **False/likely:** `outbox.get` raises an authority mismatch or another recovery exception; `adapter.connect()` returns false, producing `child_adapter_stopped` without ready/polling.

## Most decisive query

Race the replacement child’s exit against its first `getUpdates`, while preserving bounded stderr:

```text
FIRST_COMPLETED(child.wait(), normal_restart_getUpdates_future)
```

Run only through the update-4 restart boundary, not the full E2E.

Predicted result: **child exit wins**, with the traceback rooted in response-before-receipt startup recovery. That directly distinguishes production recovery failure from fake-server, reaping, configuration, or polling-signal problems.

## Minimum likely fix seam

`gateway/platforms/telegram_nutrition_onboarding_runtime_publication_transport.py:322-352` — the prepared-publication replay path. It should handle an existing `DISPATCHING` authority deterministically without allowing reconstructed render-authority mismatch to abort adapter startup.

Separately, the harness should eventually race child exit against polling, but that is observability; it does not fix the likely recovery defect.

No files were edited and no full E2E was run.