tractor/ai/prompt-io/claude/20260601T231429Z_0e3e008b_p...

5.7 KiB
Raw Blame History

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 trios check=True-driven cancel cascade.
  2. ALWAYS isolate the parent controlling-tty so a spawned child cant 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 its 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-termd line at EOF. - async with stream: ensures aclose at EOF/cancel (mirrors trios 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.