Shared control-session lifecycle

Pass-5 #6 requires a shared control client, including reply routing and pending-request teardown. The helper existed, but failure paths still had several holes:

The helper now validates decoded reply shape, owns wire IDs, serializes before registering waiters, handles child/stdin errors, and rejects sends after normal or signal exit. Completion and unresolved-request rejection use close, after stdio drains. The public exited promise still resolves the exit code (null for a signal); failed-spawn details are preserved in rejected sends.

Verification:

No emulator behavior changes. Callers still own deadlines and process cleanup; this helper does not impose a new timeout, truncate captured output, support arbitrary non-piped stdio, or guarantee completion when a child remains alive without replying. This is lifecycle coverage, not a full gameplay/browser sweep or closure of the wider review.