---
model: claude-opus-4-7[1m]
service: claude
session: trio-0.33-subproc-supervisor-retroactive
timestamp: 2026-06-01T23:14:29Z
git_ref: 0e3e008b
scope: code
substantive: true
raw_file: 20260601T231429Z_0e3e008b_prompt_io.raw.md
---

## Prompt

**RETROACTIVE LOG** — original session prompts not
preserved; reconstructed from the staged work product.

The work designs a `trio.Nursery.start()`-style wrapper
around `trio.run_process()` for SC-friendly subprocess
supervision. From the resulting code shape, the
prompting intent was:

1. Surface rc!=0 `CalledProcessError` DETERMINISTICALLY,
   without the nursery-eg-wrapping that complicates
   `collapse_eg()` usage and races the relay reader on
   trio's `check=True`-driven cancel cascade.
2. ALWAYS isolate the parent controlling-tty so a
   spawned child can't emit terminal control-seqs onto
   the launching tty (clobbering scrollback). Default
   `stdin=DEVNULL`; default `stdout=DEVNULL` unless
   explicitly relayed/overridden; distinguish "caller
   passed nothing" from "caller passed `None` for
   inherit".
3. Optional live per-line relay of child std-streams to
   the `tractor` log — STREAMED (not
   buffered-until-exit) so long-lived daemon output is
   visible during the run. Pick a custom log level that
   shows at usual `info`/`devx` console levels but is
   separately filterable.
4. Concurrent pipe-drain reader MANDATORY when piping
   without `capture_*` — without it the child blocks on
   `write()` once the OS pipe buffer fills (~64KiB),
   causing deadlocks on output bursts.
5. Non-blocking `tn.start()` semantics: hand the live
   `trio.Process` to the parent immediately;
   supervise/relay run to completion in the supervisor
   coro.
6. Hermetic `trio`-only unit tests (no actor-runtime)
   covering each of: per-line relay, tty isolation,
   no-deadlock on >64KiB unnewlined output, CPE
   rebuild w/ stderr relay, CPE rebuild on the silent
   drain+capture path.

## Response summary

Adds `tractor/trionics/_subproc.py` (296 LOC) +
`tests/trionics/test_subproc.py` (230 LOC) + a
re-export in `tractor/trionics/__init__.py`.

**`supervise_run_process()`** (public, re-exported)
- `check=False` is forced to `trio.run_process`; the
  rc-check runs in the supervisor coro AFTER `own_tn`
  unwinds (both the child AND the relay readers have
  hit EOF + fully drained). A BARE
  `subprocess.CalledProcessError` is rebuilt + raised
  from there, with `.stderr` bytes passed in the
  constructor AND attached as an `add_note()`'d
  `|_.stderr:` block for legible teardown logs.
- `stdin=DEVNULL` always. `stdout` default chosen via a
  `_UNSET` sentinel: `relay_stdout=True` → PIPE,
  explicit `stdout=...` → as given, else `DEVNULL`.
  `stderr` defaults to PIPE whenever we relay OR need
  the CPE note (when `check=True`), else `DEVNULL`.
- `relay_level='io'` (custom level 21; sorts just
  above stdlib `INFO`=20 so it shows at usual
  `info`/`devx` levels and stays separately
  filterable). `runtime`=15 would silently filter at
  default levels, so it's rejected as a default.
- `task_status.started(trio_proc)` delivers the live
  process immediately. The internal `own_tn`
  supervises `trio.run_process` + any relay readers to
  completion.
- `**run_process_kwargs` forward verbatim;
  `stdin/stdout/stderr/check` are MANAGED keys
  (override on conflict).
- Crash-handling deliberately NOT baked in — compose
  `maybe_open_crash_handler()` on top at the call-site.

**`_relay_stream_lines()`** (internal helper)
- Three modes (combinable): `emit`-only (live per-line
  relay), `accum`-only (silent drain+capture for a CPE
  note), or both (live relay AND capture).
- Per-line split handles cross-chunk residuals via a
  rolling `residual` bytes buffer; flushes any trailing
  un-newline-term'd line at EOF.
- `async with stream:` ensures aclose at EOF/cancel
  (mirrors trio's internal `_subprocess` drain idiom).

**`_add_stderr_note()`** (internal helper)
- `add_note()`s a `textwrap.indent(...)`'d
  `|_.stderr:` block onto a `CalledProcessError` for
  teardown logs.

**Tests** (5 hermetic, trio-only) — `_capture_relay`
fixture monkeypatches `_subproc.log.<level>` to a list:
- `test_stdout_relayed_per_line`: per-line stdout
  relay carries each `line=N` to the records.
- `test_parent_tty_isolated`: `readlink /proc/self/fd/0`
  and `fd/1` from the child show `pipe:` (fd1) +
  `/dev/null` (fd0); NO `/dev/pts/*`.
- `test_no_deadlock_on_big_unnewlined_output`: 200KiB
  of `x` with no newlines completes inside
  `fail_after(2)` — exercises the concurrent drain.
- `test_stderr_relay_and_cpe_rebuild`: rc=3 with
  `relay_stderr=True` raises bare CPE
  (via `collapse_eg()`) with `b'boom' in cpe.stderr`,
  the note attached, AND per-line live relay.
- `test_nonrelay_cpe_note`: rc=7 with no relay still
  produces CPE with `.stderr` + note via the silent
  drain+capture path.

## Files changed

- `tractor/trionics/_subproc.py` — NEW. Public
  `supervise_run_process()` + helpers
  `_relay_stream_lines()` / `_add_stderr_note()` + the
  `_UNSET` sentinel.
- `tests/trionics/test_subproc.py` — NEW. 5 hermetic
  trio-only tests + `_capture_relay` monkeypatch
  fixture.
- `tractor/trionics/__init__.py` — re-export
  `supervise_run_process`.

## Human edits

**RETROACTIVE**: this log is being written from the
staged diff, not from a live session. The code as
staged is the canonical artifact; any human edits the
user made during the originating design session are
already integrated and cannot be separated post-hoc.
The `.raw.md` sibling is a diff-pointer placeholder,
NOT a pre-edit transcript.

Future prompt-io entries for in-flight work should be
written DURING the design session per the skill
contract so the pre-edit `.raw.md` captures the
unedited model output for genuine provenance.
