How Freebuff Detects and Handles Parent Process Termination in Its Terminal Command Broker

Freebuff's terminal command broker detects parent process termination by monitoring its stdin pipe for EOF events, which fire when the parent CLI exits and closes the pipe, then classifies the failure and exits cleanly with code 1.

The Freebuff CLI spawns a terminal command broker as a child process to execute long-running operations like code generation and test execution. This broker must shut down automatically when the parent CLI terminates unexpectedly—whether from a user abort, terminal close, or crash—to prevent orphaned processes. The detection mechanism relies on UNIX pipe semantics and is implemented in the CodebuffAI/freebuff repository.

Pipe-Based Detection Mechanism

The broker does not use OS-specific process monitoring APIs. Instead, it exploits a fundamental property of pipes: when the writing process (the parent CLI) exits, the reading process (the broker) receives an end-of-file (EOF) on its standard input stream.

The Stdin Monitor Loop

In cli/src/utils/terminal-command-broker.ts, the serveTerminalCommandBroker() function starts the broker and immediately begins listening on process.stdin:

// From cli/src/utils/terminal-command-broker.ts
process.stdin.on('end', () => {
  handleBrokerFailure({
    stage: 'stdio',
    error: new Error('stdin closed'),
  });
});

The stdin stream carries JSON-encoded BrokerProtocol messages during normal operation. When the parent terminates, the pipe closes and the 'end' event fires without warning.

Keep-Alive Heartbeat

The parent CLI maintains pipe liveness through periodic writes. In createTerminalCommandBroker() at line 355, the parent establishes a heartbeat that writes small keep-alive tokens:

// Parent-side keep-alive mechanism
const heartbeat = setInterval(() => {
  broker.stdin.write(JSON.stringify({ type: 'ping' }) + '\n');
}, 1000);

This serves dual purposes: it prevents the pipe from timing out due to inactivity, and it ensures that any parent crash manifests as an observable pipe closure rather than a stalled connection.

Failure Classification and Reporting

Once EOF is detected, the broker executes a structured failure handling pipeline defined in TerminalBrokerFailureStage.

Stage Enumeration

The TerminalBrokerFailureStage type at line 27 categorizes where failures originate:

type TerminalBrokerFailureStage = 
  | 'stdio'      // stdin/stdout pipe issues
  | 'protocol'   // JSON parsing or message validation
  | 'execution'  // Command runtime errors
  | 'internal';  // Unexpected exceptions

Parent termination always maps to the 'stdio' stage.

Error Classification Function

The classifyTerminalBrokerFailure() function at line 52 inspects the error and stage to produce a TerminalBrokerFailureCode:

function classifyTerminalBrokerFailure(
  stage: TerminalBrokerFailureStage,
  error: Error
): TerminalBrokerFailureCode {
  if (stage === 'stdio' && error.message.includes('stdin closed')) {
    return 'ParentProcessTerminated';
  }
  // ... other classifications
}

This classification distinguishes parent termination from other stdio failures like protocol corruption or premature stream closure.

Telemetry and Exit Sequence

The reportTerminalBrokerFailure() function at line 74 performs three operations:

  1. Log the failure with structured telemetry including stage, code, and timestamp
  2. Emit a final BrokerProtocol message to any listening parent (best-effort)
  3. Call process.exit(1) to terminate immediately
function reportTerminalBrokerFailure(params: {
  stage: TerminalBrokerFailureStage;
  failureCode: TerminalBrokerFailureCode;
}): never {
  telemetry.record({
    event: 'broker_failure',
    stage: params.stage,
    code: params.failureCode,
  });
  process.exit(1);
}

The never return type ensures TypeScript recognizes this path as terminal.

Error Propagation to SDK Callers

Beneath the broker, the Freebuff SDK surfaces these failures through its public API. The run() function in sdk/src/run.ts at line 308 receives broker errors and transforms them into user-facing diagnostics:

// sdk/src/run.ts
const result = await broker.run(command);
if (result.failureCode === 'ParentProcessTerminated') {
  throw new RunError('Parent process terminated – command cancelled');
}

This propagation chain ensures that tools consuming the Freebuff SDK receive actionable error information rather than opaque process crashes.

Implementation Example

To configure a broker with custom parent termination handling:

import { createTerminalCommandBroker } from './utils/terminal-command-broker';

const broker = createTerminalCommandBroker({
  reportFailure: ({ stage, failureCode }) => {
    if (failureCode === 'ParentProcessTerminated') {
      console.error('CLI disconnected — shutting down cleanly');
    }
  },
});

await run({
  command: ['pytest', '-v'],
  terminalCommandBroker: broker,
});

Key Source Files

File Purpose
cli/src/utils/terminal-command-broker.ts Core broker implementation including serveTerminalCommandBroker(), classifyTerminalBrokerFailure(), and reportTerminalBrokerFailure()
sdk/src/tools/run-terminal-command.ts TerminalCommandBroker interface definition
sdk/src/run.ts High-level run() API that bridges broker errors to SDK consumers
cli/src/entry.ts CLI entry point with broker mode detection
cli/src/utils/__tests__/terminal-command-broker.test.ts Test coverage for parent termination scenarios

Summary

  • Detection method: EOF on process.stdin when parent closes the pipe
  • Classification: TerminalBrokerFailureStage = 'stdio' with TerminalBrokerFailureCode = 'ParentProcessTerminated'
  • Cleanup action: Telemetry logging followed by process.exit(1)
  • SDK exposure: run() transforms broker failures into RunError exceptions
  • OS portability: Relies on standard pipe semantics, no platform-specific APIs required

Frequently Asked Questions

What triggers the parent process termination detection?

The broker detects parent termination when its process.stdin emits an 'end' event. This occurs because the parent CLI holds the write end of the pipe; when the parent exits, the operating system closes all its file descriptors, causing the read end in the broker to signal EOF.

Can the broker distinguish parent termination from other stdio errors?

Yes. The classifyTerminalBrokerFailure() function specifically checks for the 'stdin closed' error message combined with the 'stdio' stage to return ParentProcessTerminated. Other stdio failures produce different codes such as ProtocolCorruption or PrematureClose.

What happens if the parent crashes without closing stdin properly?

Even in crash scenarios, the operating system reclaims the parent's file descriptors. The kernel closes the write end of the pipe, which still triggers EOF on the broker's read end. This makes the mechanism robust against SIGKILL, SIGSEGV, and other abnormal terminations.

How does this compare to explicit heartbeat protocols?

Freebuff uses an implicit heartbeat through normal pipe operation rather than explicit health-check messages. The periodic keep-alive writes prevent false positives from idle timeouts, but the actual termination detection requires zero additional network or IPC overhead—just standard stream events.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →