I read this as a read-only system-boundary investigation - I’ll trace the successful and failing restart paths, rank three contract violations, and give one decisive runtime query for each.


The existing disposable evidence gives the required runtime boundary trace without rerunning services. I’m correlating its process events, API calls, offsets, and transcript against the harness and fake-server contracts now.


I read this as a read-only system-boundary investigation - I traced the historical runtime evidence and ranked the three contract violations.

## Successful vs failing restart

The same executable, profile, environment builder, token, and loopback URL are reused for every child at `scripts/run_dualcoach_v111_disposable_e2e.py:124-174`.

Observed API progression:

- Restart after update 2: sequences 24-37 reach `getUpdates`.
- Restart after update 3: sequences 44-57 reach `getUpdates`.
- Normal restart after update 4: sequences 67-70 reach `getMe`, `getChatMember`, and `sendChatAction`, then stop before recovery `sendMessage`, command registration, webhook deletion, or `getUpdates`.

Evidence: `.omo/evidence/v1.1.1-stability/disposable-e2e/fake-api-calls.jsonl:24-70`.

Therefore subprocess launch, loopback routing, token/config loading, and the basic membership probe succeeded. The divergence is inside adapter startup after `sendChatAction` and before polling begins. Offsets are not the initial blocker: observed offsets end at `[0,2,0,3,0,4]`, with no poll from the replacement child.

## Ranked boundary-contract violations

### 1. Critical: ownership of a child is lost when readiness times out

`_start_and_wait_for_poll()` spawns the child locally at `scripts/run_dualcoach_v111_disposable_e2e.py:238`, but does not return it until the readiness future resolves at lines 245-246. At the failing assignment on lines 808-815, timeout propagation leaves the caller’s `child` as `None`, and the replacement is never added to `children` at line 816.

Cleanup then:

- Treats `None` as already reaped at lines 209-213.
- Checks only previously registered children at lines 1049-1054.
- Can consequently emit `"child_processes_reaped": true` without owning or waiting for the timed-out replacement.

This makes the all-true cleanup receipt non-authoritative. No orphan remains now, but that does not prove the harness reaped it.

**Decisive query:** retain the `Popen` in `children` immediately after spawn, then on timeout record `pid`, `poll()`, and `waitpid(pid, WNOHANG)` before and after cleanup. A live PID after the reported cleanup confirms the violation.

### 2. High: the launch boundary discards the only evidence that distinguishes hang from startup failure

All child stdout and stderr are sent to `DEVNULL` at `scripts/run_dualcoach_v111_disposable_e2e.py:167-174`. Readiness waits only on the fake API future at lines 233-245 and never races that future against `child.wait()`.

Thus the harness cannot distinguish:

- blocked `sendChatAction` response,
- membership-journal lock,
- authority lease acquisition,
- recovery code deadlock,
- adapter exception or early exit.

The runtime trace narrows the stall to after API sequence 70, but `_record_action()` logs the request before the fake response is generated (`scripts/dualcoach_v111_fake_telegram_engine.py:210-274,293-326`), so even successful completion of `sendChatAction` is unproven.

**Decisive query:** capture bounded stderr and race `getUpdates` readiness against child exit; while still alive after sequence 70, take one `py-spy dump --pid <pid>`. The top stack decisively identifies whether it is awaiting HTTP response, blocked on a file lock/authority lease, or executing recovery.

### 3. Medium: “polling-ready” fires on request observation, not completed polling or adapter readiness

The harness subscribes to any `getUpdates` action at `scripts/run_dualcoach_v111_disposable_e2e.py:233-245`. The fake resolves that future in `_record_action()` at `scripts/dualcoach_v111_fake_telegram_engine.py:293-326`, before `_get_updates()` executes at lines 328-340 and before the HTTP reply is written.

The child’s actual readiness signal is emitted only after `adapter.connect()` returns at `scripts/dualcoach_v111_disposable_process.py:173-180`. Historical evidence proves these are different states: the update-3 and update-4 children reached polling and faulted, but never emitted `child_adapter_ready` (`process-events.jsonl:2-4`).

Consequently, successful prior restart paths were allowed to process and crash while startup was still incomplete. The failing normal restart is state-dependent startup recovery, so comparing it against those prematurely declared “ready” generations is not contract-equivalent.

**Decisive query:** timestamp three separate events: `getUpdates_request_received`, `getUpdates_response_written`, and `child_adapter_ready`. Require the intended readiness event explicitly. The current trace will show update processing can precede `child_adapter_ready`.

## Conclusion

The immediate stall is after the normal child’s membership-topic probe and before its first recovery publication/poll. Existing evidence cannot identify the exact internal await because the harness discards stderr and loses child ownership on timeout. Ports, launch command, environment reuse, and offsets are not supported as primary causes by the observed API trace.

Read-only investigation only; no files, services, or tests were changed or started.