Skip to main content
Every workflow runs child processes: a git call, a model CLI, a test command. Each is the one thing that can wait forever, and a hang carries no error. @obversa/process runs one child with a deadline and returns a result you can read whatever happened: it finished, it hung, it filled a pipe, or it ignored a signal.

What you get

  • A deadline that always means timeout. When the time passes, the child is stopped and the result says so, even when the stopped child reports no exit code. By default only the child is signalled, so a process the child started and left behind is not stopped. Pass detached: true to stop the child’s whole process group.
  • No pipe can stall it. Standard input is closed after the optional input, and both output streams are drained until they close or for a short grace after the child exits, under one combined byte cap. The exit is the result; the pipes are a bounded extra.
  • A result, not a race. The exit code, the output bytes and the timed-out and aborted flags come from facts the helper recorded, never from whichever callback fired first.

Run one

The call

runChild(options) takes executable, args, cwd, env, an optional stdin, timeoutMs, killGraceMs, maxOutputBytes, an optional AbortSignal, and inheritParentEnv (default true). It returns exitCode (a number, or null when a signal stopped the child), stdout and stderr as bytes, timedOut and aborted. It throws RunChildError with code SPAWN_FAILED when the executable cannot start, OUTPUT_LIMIT when the child writes past the cap, and TEARDOWN_INCOMPLETE when the child does not stop within the grace period after its deadline.

When your process dies

Children the helper started are stopped when your process exits or is interrupted, the way the signal-exit library does it: a terminate signal on exit, and on an interrupt the helper stops its children and re-raises the signal so your process ends as it would have. Your own handler for a signal takes precedence. A child sits in your process group by default so a terminal interrupt reaches it; detached: true gives it its own group.

Things that catch people out

  • Bytes written after the grace are not captured; the result holds what arrived before the child exited plus the short drain.
  • The output cap is combined across standard output and standard error.
  • The environment is merged over the parent’s unless you say otherwise.
  • A timed-out child still has output: what it wrote before the deadline is in the result, up to the cap.

Where it is used

The engine command runner, the runtime’s git and command sites, and the Git memory adapter run their children through this package, so a hang in any of them ends the same way, with a timeout you can read.