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

> Learn how Freebuff's terminal command broker detects parent process termination via stdin EOF events, ensuring clean exits and classified failures. Understand the mechanism.

- Repository: [Codebuff/freebuff](https://github.com/CodebuffAI/freebuff)
- Tags: internals
- Published: 2026-09-01

---

**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`](https://github.com/CodebuffAI/freebuff/blob/main/cli/src/utils/terminal-command-broker.ts), the `serveTerminalCommandBroker()` function starts the broker and immediately begins listening on `process.stdin`:

```typescript
// 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:

```typescript
// 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:

```typescript
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`:

```typescript
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

```typescript
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`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/run.ts) at line 308 receives broker errors and transforms them into user-facing diagnostics:

```typescript
// 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:

```typescript
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`](https://github.com/CodebuffAI/freebuff/blob/main/cli/src/utils/terminal-command-broker.ts) | Core broker implementation including `serveTerminalCommandBroker()`, `classifyTerminalBrokerFailure()`, and `reportTerminalBrokerFailure()` |
| [`sdk/src/tools/run-terminal-command.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/tools/run-terminal-command.ts) | `TerminalCommandBroker` interface definition |
| [`sdk/src/run.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/run.ts) | High-level `run()` API that bridges broker errors to SDK consumers |
| [`cli/src/entry.ts`](https://github.com/CodebuffAI/freebuff/blob/main/cli/src/entry.ts) | CLI entry point with broker mode detection |
| [`cli/src/utils/__tests__/terminal-command-broker.test.ts`](https://github.com/CodebuffAI/freebuff/blob/main/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.