Skip to content

Supervisor shell helpers materialize the entire child stdout/stderr in memory with no cap: peak RSS scales with child output volume and sums across concurrent calls #339

Description

@schickling-assistant

Summary

The supervisor's shell-out helpers capture the entire stdout/stderr of every child in memory, with no size cap, despite their doc comments saying "bounded output capture":

  • src/run.rs output_with_input_timeout_observed (rev b8a3b24a68e5b18521a63262629eb25a8dea5dfc, lines 214–217): after the child exits, both tempfiles are rewound and fully read into fresh Vec<u8> buffers via read_to_end.
  • src/ding/mod.rs lines 431–436: same pattern in its own copy of output_with_timeout.

The tempfile redirection itself is sound (it prevents an escaped descendant from blocking cleanup). The unbounded cost comes entirely from materializing the whole captured output on the heap for every single command invocation.

Impact

Peak RSS of the supervisor process scales 1:1 with child output volume, and concurrent invocations (one reconcile pass fanning out over many tasks) sum. During bursts of churn — mass task restarts where each restart shells out to helpers that can emit large output — this produces transient multi-GiB RSS spikes even though steady-state supervisor RSS is tens of MiB. Under timeout storms there is a second, independent hazard in the same functions: each timed-out child gets a detached thread::spawn(move || { let _ = child.wait(); }) reaper thread (run.rs line 95, ding/mod.rs line 424) with no bound on how many accumulate.

Minimal reproduction

A standalone harness containing a verbatim copy of output_with_input_timeout_observed (plus its two private helpers) at rev b8a3b24, instrumented with /proc/self/status RSS measurement:

https://github.com/schickling-repros/2026-08-st2-unbounded-child-output-capture

Single command: cargo run --release (Linux, needs dd; three pinned crates). Each part runs in its own subprocess because VmHWM is a process-lifetime high-water mark. Measured output (Linux x86_64, rustc 1.97.0):

Part 1 — single call, growing child stdout volume
   child MiB     RSS before       peak RSS
   ---------     ----------       --------
          16         2.4MiB        18.4MiB
          64         2.4MiB        66.4MiB
         256         2.4MiB       258.4MiB
        1024         2.4MiB      1026.4MiB

Part 2 — 8 concurrent captures x 128 MiB each (= 1024 MiB total in flight)
  captured 1024 MiB total; RSS before 2.4 MiB -> peak 895.4 MiB (delta 893.0 MiB)

Part 3 — control: identical children, read-back capped at 1 MiB
  4 x 256 MiB children; RSS before 2.4 MiB -> peak 3.4 MiB (delta 1.0 MiB)

Part 1 shows peak RSS = baseline + full captured volume for every call. Part 2 shows concurrent calls accumulating to roughly the sum of their outputs. Part 3 shows the control: identical children read back through a 1 MiB-capped reader keep RSS flat, i.e. the tempfile design already supports bounded capture with no behavioral loss beyond the cap itself.

Expected

Capture should be bounded/streaming as the doc comment promises: peak RSS stays roughly flat regardless of child output volume. A small fixed ceiling (ring buffer or Read::take on the rewound tempfile) would preserve all currently consumed behavior while making the memory bound explicit; oversized output could be truncated or spilled to the tempfile path that already exists.

Actual

Every command invocation allocates the full child output in RSS; concurrent invocations stack.

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions