5.7 KiB
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:
- Surface rc!=0
CalledProcessErrorDETERMINISTICALLY, without the nursery-eg-wrapping that complicatescollapse_eg()usage and races the relay reader on trio’scheck=True-driven cancel cascade. - 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; defaultstdout=DEVNULLunless explicitly relayed/overridden; distinguish “caller passed nothing” from “caller passedNonefor inherit”. - Optional live per-line relay of child std-streams to the
tractorlog — STREAMED (not buffered-until-exit) so long-lived daemon output is visible during the run. Pick a custom log level that shows at usualinfo/devxconsole levels but is separately filterable. - Concurrent pipe-drain reader MANDATORY when piping without
capture_*— without it the child blocks onwrite()once the OS pipe buffer fills (~64KiB), causing deadlocks on output bursts. - Non-blocking
tn.start()semantics: hand the livetrio.Processto the parent immediately; supervise/relay run to completion in the supervisor coro. - 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. Publicsupervise_run_process()+ helpers_relay_stream_lines()/_add_stderr_note()+ the_UNSETsentinel.tests/trionics/test_subproc.py— NEW. 5 hermetic trio-only tests +_capture_relaymonkeypatch fixture.tractor/trionics/__init__.py— re-exportsupervise_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.