147 lines
5.7 KiB
Markdown
147 lines
5.7 KiB
Markdown
|
|
---
|
||
|
|
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.
|