@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
@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
RunChildErrorwith acode: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 thestdoutandstderrcaptured so far.OwnedCommandErroron thecommandsubpath:INVALID_EXECUTABLE,INVALID_COMMAND,SPAWN_FAILED,OUTPUT_LIMIT,MEMORY_LIMIT,PROCESS_INSPECTION,TEARDOWN_INCOMPLETE, withremainingProcesses.
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,mockVerdictfor 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.