Skip to main content
@obversa/core runs one child process 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. Workflows run child processes all the time, a git call, a model CLI, a test command, and a hang carries no error; this package makes every one of them end with a result. The engine command runner, the runtime’s Git and command sites and the Git memory adapter run their children through it.

Install

Included in @obversa/obversa.

Run one

Run a child with a deadline and an output cap, and read what it wrote:
examples/run-child.ts
Output
runChild(options) needs executable, timeoutMs and maxOutputBytes. It returns exitCode (a number, or null when a signal stopped the child), stdout and stderr as bytes, and the timedOut and aborted flags. All of it comes from facts the helper recorded, never from whichever callback fired first. stdout and stderr hold what arrived before the child exited and during a short drain after it; a timed-out child’s result still carries what it wrote before the deadline, up to the cap. When the deadline passes, the child is stopped and the result says so, even when the stopped child reports no exit code. 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. Children the helper started are stopped when your process exits or is interrupted: on exit they get a terminate signal, and on an interrupt the helper stops its children and re-raises the signal. Your own handler for a signal takes precedence.

Find an executable

resolveCommandExecutable from @obversa/core/command reads your PATH when the file runs, so a copied example doesn’t carry a machine-specific path:
examples/teams/threshold-panel.ts (excerpt)

Run an owned command

runOwnedCommand from @obversa/core/command runs a command-line tool to a deadline with output and memory bounds and returns a typed result, and stopOwnedProcessTree stops what it started. Both accept an ownerId, a sha256: digest the command passes to its children as OBVERSA_RUN_OWNER. Nested commands keep an inherited marker, and it wins over a supplied ownerId; only the outer command that supplied the id sweeps that owner’s marked processes during cleanup. commandCleanupCapability() says how the platform cleans up: on Linux inherited-owner, processes carrying the marker are found through /proc; elsewhere observed-processes, cleanup follows the observed tree, and a helper that starts a new session before it’s seen can escape. inspectOwnerMarkedProcesses(ownerId) lists marked processes on Linux without stopping them and returns an empty list elsewhere, which proves nothing. Owner markers coordinate cleanup; they aren’t a security boundary. ownedCommandIdentity, attemptEnvironment, retryAfterHeaderToMs, scrubCapture, redactEnvValues and redactSecrets are the helpers the engine plugins share.

Read Claude’s stream

@obversa/core/claude-stream-json exports mapMessage, newAccumulator and the Accumulator type for reading Claude stream messages. @obversa/core/claude-tools exports claudeToolOptions, the tool configuration the Claude adapters share: it keeps only requested Read, Grep and Glob tools in read mode, no tools in none mode, removes permission rules for withheld tools, and refuses malformed rules and custom tools it can’t bound with an EngineError of kind invalid-config.

Options

timeoutMs and killGraceMs

timeoutMs is the deadline; at it the child is signalled. killGraceMs is how long the child has to stop before it is killed, and a child that still doesn’t stop makes the call throw TEARDOWN_INCOMPLETE.

detached

By default only the child is signalled, so a process the child started and left behind isn’t stopped, and the child sits in your process group so a terminal interrupt reaches it. detached: true gives the child its own group and stops the whole group at the deadline.

The rest

Errors

  • RunChildError with a code: INVALID_OPTIONS, SPAWN_FAILED (the executable can’t start), OUTPUT_LIMIT (the child wrote past the cap), TEARDOWN_INCOMPLETE (the child didn’t stop within the grace). It carries the stdout and stderr captured so far.
  • OwnedCommandError on the command subpath: INVALID_EXECUTABLE, INVALID_COMMAND, SPAWN_FAILED, OUTPUT_LIMIT, MEMORY_LIMIT, PROCESS_INSPECTION, TEARDOWN_INCOMPLETE, with remainingProcesses.

API

  • runChild(options): one child to a deadline. Run one.
  • @obversa/core/command: runOwnedCommand, stopOwnedProcessTree, ownedCommandIdentity, resolveCommandExecutable, commandCleanupCapability, inspectOwnerMarkedProcesses, inspectOwnedProcessTree, inspectAttemptMarkedProcesses, inspectPipeHoldingProcesses, capturePipeOwnerProbe, measureOwnedProcessMemory, readAttemptMarkerProcessIds, parseWindowsProcessRows, DEFAULT_OWNED_COMMAND_LIMITS, OwnedCommandError. Run an owned command.
  • @obversa/core/claude-stream-json: mapMessage, newAccumulator. @obversa/core/claude-tools: claudeToolOptions. Read Claude’s stream.
  • @obversa/core/testing: MockEngine, MockResponder, mockVerdict for scripted engine results. The adapter conformance kits are in @obversa/api/testing.

Next steps

  • Safe node attempts: how an engine turn is bounded and cleaned up on top of this.
  • API: the engine contract the command runner serves.