# Buffered Output Sink in Ego-Lite: How It Prevents Duplicate Error Noise

> Discover the buffered output sink in ego-lite. Learn how it captures and flushes console logs to prevent duplicate error noise, ensuring clean, deduplicated messages, especially during hard-stop errors.

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

---

**The buffered output sink in ego-lite captures all `console.log` output in memory and flushes it selectively at process exit, ensuring agents see only clean, deduplicated messages—especially critical when hard-stop errors would otherwise spam duplicate lines on every loop iteration.**

Ego-lite executes agent scripts inside isolated, short-lived Node processes where the agent reads output exclusively through `console.log`. Because user-controlled code can trigger repeated hard-stop errors (such as those produced by `buildEgoError`), the framework needs a mechanism to prevent noisy, duplicated error streams from overwhelming the agent's view. The buffered output sink, implemented in [`package/ego-browser/src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/output-sink.ts), solves this by intercepting all log lines and applying intelligent filtering before final emission.

## What the Buffered Output Sink Records

The sink maintains three distinct categories of output in internal state:

- **`buffer`** – An array storing every regular `console.log` line captured during script execution.
- **Hard-stop message** – The first error message captured by `markHardStop`, stored once and never overwritten.
- **Notice trailer** – An optional footer added via `setNoticeTrailer` that appears at the end of all output.

This separation allows the sink to make context-aware decisions during the final flush.

## How `flushSink` Decides What to Emit

When the Node process prepares to exit, `flushSink` evaluates the captured state and applies one of three behaviors:

| Scenario | Flush Behavior |
|----------|---------------|
| **Hard-stop was recorded** | Discard the entire `buffer` array and emit **only** the stored hard-stop message (single occurrence). |
| **No hard-stop occurred** | Flush all buffered lines in original order, preserving complete script output. |
| **Either case** | Append the notice trailer if `setNoticeTrailer` was called. |

This logic prevents the agent from seeing a polluted stream where hard-stop errors repeat on every loop iteration while user code continues executing.

## Lifecycle Integration and Process Isolation

The sink integrates with Node's process lifecycle through `installLifecycleFlush`, which registers handlers on `beforeExit` and `exit` events:

```typescript
import {
  bufferOutput,
  markHardStop,
  setNoticeTrailer,
  installLifecycleFlush,
} from "./output-sink";

// 1. Register automatic flushing at process exit
installLifecycleFlush(process.stdout);

// 2. Normal logging captures to buffer instead of immediate stdout
console.log("Starting task…");
bufferOutput("Starting task…\n");

// 3. Record hard-stop once if encountered
try {
  // Operation that may trigger hard-stop
} catch (e) {
  if (isHardStop(e)) {
    markHardStop(e.message); // First occurrence only
  }
}

// 4. Optional footer appears at end of all output
setNoticeTrailer("🚀 Ego-lite update available – run `npm install -g ego-lite`");

// 5. Automatic flush occurs on process exit; manual flush available for tests
// flushSink(process.stdout, /*thrown=*/ false);

```

Because ego-lite spawns a fresh Node process for each heredoc execution, the sink's state is automatically isolated per run. The `resetSink` function exists solely for in-process test scenarios where multiple sink operations occur within the same process lifetime.

## Source Code Locations

As implemented in **citrolabs/ego-lite**, the buffered output sink spans these key files:

- [`package/ego-browser/src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/output-sink.ts) – Core implementation including `bufferOutput`, `markHardStop`, `setNoticeTrailer`, `flushSink`, and `installLifecycleFlush`.
- `package/ego-browser/src/output-sink.test.mjs` – Unit tests verifying buffering mechanics, hard-stop deduplication, and trailer attachment.
- `package/ego-browser/src/run-output-sink.test.mjs` – Integration tests validating sink behavior through the CLI runner.
- [`package/ego-browser/src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts) – Entry point that defaults to the buffered sink when no custom CLI log sink is provided.

## Summary

- The **buffered output sink** intercepts all `console.log` calls to prevent immediate stdout emission.
- It **deduplicates hard-stop errors** by capturing only the first occurrence and suppressing subsequent repetitions.
- **`flushSink`** applies context-aware filtering: full buffer on success, single hard-stop message on error.
- **Lifecycle hooks** guarantee output emission even on abrupt process termination.
- Process-per-run isolation eliminates state leakage; `resetSink` supports test environments.

## Frequently Asked Questions

### What problem does the buffered output sink solve?

Without buffering, a hard-stop error triggered inside a user-controlled loop would print the same error message on every iteration, creating an unreadable stream of duplicates mixed with normal logs. The sink captures these once and presents a clean, single message to the agent.

### When does the buffered output actually reach stdout?

Emission occurs during Node's `beforeExit` or `exit` events via `installLifecycleFlush`, or manually through `flushSink`. This deferred write enables the final filtering logic that separates successful output from hard-stop scenarios.

### Can I use the buffered output sink outside of hard-stop handling?

Yes. The sink's architecture supports general output capture and transformation. The notice trailer mechanism (`setNoticeTrailer`) demonstrates this extensibility—allowing framework-level messages to appear consistently at the end of any script run, regardless of execution outcome.

### How does ego-lite ensure sink state doesn't leak between script runs?

Each heredoc execution launches a completely new Node process, which instantiates fresh sink state. The `resetSink` utility exists only for internal testing where multiple sink lifecycles occur within one process.