# Understanding the bufferOutput Mechanism in Ego-Lite: When and How It Flushes

> Discover the bufferOutput mechanism in ego-lite. Learn how it captures console.log output and when it flushes during process teardown for efficient logging.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-07-27

---

**The bufferOutput mechanism in ego-lite captures all `console.log` output into an in-memory buffer and flushes it exactly once per heredoc run during process teardown, discarding the buffer and emitting only the hard-stop error message if a critical failure occurs.**

The **citrolabs/ego-lite** repository implements a specialized output buffering system to manage agent logging during browser automation runs. Instead of writing directly to stdout, the **bufferOutput mechanism** intercepts all `console.log` calls and stores them in a controlled sink, enabling clean error handling and atomic output delivery. This architecture ensures that regular logs accumulate safely while providing immediate, uncluttered feedback when scripts encounter hard-stop failures.

## How the bufferOutput Mechanism Works

### The Output Sink Architecture

In [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts), ego-lite defines an internal output sink that redirects the agent’s `console.log` calls away from the process stdout. Each log fragment is pushed into an in-memory array (`buffer: string[]`) via the `bufferOutput()` function. This decouples log generation from log delivery, allowing the runtime to manipulate or discard output based on the execution outcome.

### Internal State Tracking

The sink maintains three distinct states to manage the output lifecycle:

- **`buffer`** – An array of formatted log lines accumulated during execution. The `bufferOutput()` function pushes each chunk into this array (lines 33-35 in [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts)).
- **`hardStopMessage`** – Stores the first hard-stop error message when an `EgoError` aborts the script. Set via `markHardStop()` (lines 51-55), this message supersedes the entire buffer if triggered.
- **`noticeTrailer`** – An optional footer appended after the final output, regardless of success or failure. Configured via `setNoticeTrailer()` (lines 42-44).

## When Is the Buffer Flushed?

### The Flush Lifecycle via flushSink

The buffer flushes **exactly once per heredoc run** through the `flushSink(stream, thrown)` function. This method evaluates the execution context to determine output behavior:

- **Normal completion**: The buffered log lines write to the provided writable stream in chronological order.
- **Hard-stop occurred**: The entire buffer drops, and only the stored `hardStopMessage` emits (or the uncaught error itself if the script terminated unexpectedly).
- **Notice trailer**: After either path, `noticeTrailer` appends if present.

### Automatic Process Teardown Handling

To guarantee the sink drains without explicit CLI wrapper calls, `installLifecycleFlush(stream)` registers process event listeners at lines 108-114 of [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts). This utility attaches to `process.on('beforeExit')` for clean exits and `process.on('exit')` for uncaught async rejections, ensuring `flushSink` executes exactly once during termination.

## Implementation in the Runtime

During run initialization, `executionContext()` in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) overwrites the global `console.log` to route formatted output into the buffer. All subsequent logging accumulates silently until the process teardown triggers the automatic flush.

## Practical Code Examples

The following patterns demonstrate how to interact with the buffering system directly:

```typescript
// 1️⃣ Replace console.log with the buffered version (run.ts)
import { bufferOutput } from "./output-sink.js";

console.log = (...args) => {
  bufferOutput(`${args.map(formatCliLogValue).join(" ")}\n`);
};

```

```typescript
// 2️⃣ Mark a hard-stop from anywhere in the runtime
import { markHardStop } from "./output-sink.js";

if (isHardStopError(err)) {
  // Only the first hard-stop message survives; buffer is discarded
  markHardStop(err.message);
}

```

```typescript
// 3️⃣ Append an out-of-band notice (e.g., async version check)
import { setNoticeTrailer } from "./output-sink.js";

setNoticeTrailer("⚡ ego-lite update available – run `ego-browser --doctor`");

```

```typescript
// 4️⃣ Register the automatic flush (once per process)
import { installLifecycleFlush } from "./output-sink.js";

installLifecycleFlush(process.stdout);

```

## Summary

- The **bufferOutput mechanism** redirects `console.log` to an in-memory array in [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts), preventing immediate stdout writes during agent execution.
- **Flush timing** occurs exactly once per run via `flushSink`, triggered automatically by `installLifecycleFlush` during process `beforeExit` or `exit` events.
- **Hard-stop handling** discards the entire buffer and emits only the error message when `markHardStop()` records a critical failure.
- **Notice trailers** append after both success and error outputs through `setNoticeTrailer()`, supporting auxiliary messaging like update notifications.

## Frequently Asked Questions

### What triggers the bufferOutput mechanism to flush in ego-lite?

The buffer flushes automatically when the Node.js process terminates. The `installLifecycleFlush()` function registers listeners for `process.on('beforeExit')` and `process.on('exit')` events, ensuring `flushSink()` runs exactly once to emit either the buffered logs or a hard-stop error message.

### Does the bufferOutput mechanism preserve logs when a script fails?

It depends on the failure type. During a **hard-stop** (critical `EgoError`), the buffer discards all accumulated logs and only emits the `hardStopMessage` set via `markHardStop()`. For normal completions or non-critical errors, the full buffer writes to stdout.

### How can I add a custom footer to ego-lite output regardless of success or failure?

Use the `setNoticeTrailer()` function imported from [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts). This stores a message that `flushSink()` appends after the main output (whether buffered logs or a hard-stop error), making it ideal for version warnings or diagnostic hints.

### Where is the console.log override initialized in the ego-lite codebase?

The override occurs in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) within the `executionContext()` function. This module replaces the global `console.log` implementation with a wrapper that calls `bufferOutput()`, ensuring all agent logging routes through the centralized sink before the automatic flush occurs.