# How the Output Sink Buffer and Flush Works for console.log in ego-lite

> Understand how ego-lite buffers and flushes console.log output, preventing duplicate errors and adding update notices. Learn about normal script completion and hard-stop error handling.

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

---

**The `console.log` calls in ego-lite are intercepted and buffered, then flushed either when the script completes normally or when a hard-stop error occurs, with special handling to suppress duplicate errors and append optional update notices.**

The `citrolabs/ego-lite` repository implements a custom **output sink** that controls how agent scripts emit text. Because ego-lite agents have only one output channel—`console.log`—the framework wraps this method to ensure clean, predictable output even when errors occur.

## Buffering Stage: Intercepting console.log

Every `console.log` call in an ego-lite script is redirected through the output sink's buffering system. In [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) lines 43-45, the framework overrides the global `console.log` implementation to route arguments through `formatCliLogValue` and into `bufferOutput`.

The `formatCliLogValue` function (from [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts)) serializes each argument, and `bufferOutput` (defined in [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts) lines 32-35) stores the formatted string—always with a trailing newline—in an in-memory array:

```typescript
// What happens internally for each console.log call
console.log('Step 1: Opening page');
// → bufferOutput('Step 1: Opening page\n');
console.log({ status: 'ok', value: 42 });
// → bufferOutput("{ status: 'ok', value: 42 }\n");

```

This buffering allows the sink to control exactly when and how output reaches the terminal or underlying writable stream.

## Hard-Stop Handling: Suppressing Noisy Errors

When a **hard-stop error** occurs—typically an `EgoError` indicating the user needs to fix their script—the sink prevents repetitive log spam. The `markHardStop` function in [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts) lines 46-55 records the first hard-stop message it receives and ignores any subsequent duplicates.

This design ensures that if multiple errors cascade, the user sees only the single, actionable guidance message rather than a wall of repeated failures.

## Flushing Stage: Controlled Output Emission

The `flushSink` function (called from [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) lines 27-30) determines what actually gets written based on the run outcome:

| Scenario | Behavior |
|----------|----------|
| **Normal completion** | All buffered chunks written in order, followed by optional notice trailer |
| **Hard-stop occurred** | Buffer discarded; only the hard-stop message emitted (unless already propagating) |
| **Uncaught error** | Same flush logic applies via lifecycle handlers |

The flush also appends any **update-notice trailer** set via `setNoticeTrailer`, ensuring version notifications always appear after the script's own output.

## Automatic Lifecycle Flushing

To guarantee output emission even on unexpected termination, `installLifecycleFlush` in [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts) lines 99-114 registers handlers on both `process.on("beforeExit")` and `process.on("exit")`:

```typescript
// This ensures flushing occurs for both SDK and CLI execution paths
process.on('beforeExit', flushSink);
process.on('exit', flushSink);

```

These handlers cover cases where the script exits without explicitly calling `flushSink`, such as when running through the CLI entry point in [`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts).

## Complete Buffer and Flush Example

```typescript
// Typical ego-lite script execution flow
import { setNoticeTrailer } from 'ego-lite';

console.log('Launching browser...');
console.log('Navigating to https://example.com');
// Both lines buffered, not yet visible

// Set an update notice (appears after all script output)
setNoticeTrailer('⚡ ego-lite update available: https://github.com/citrolabs/ego-lite/releases/latest');

// On successful completion: buffer flushed, notice appended
// Output:
// Launching browser...
// Navigating to https://example.com
// ⚡ ego-lite update available: https://github.com/citrolabs/ego-lite/releases/latest

// Hard-stop scenario:
throw new EgoError('You must call `await page.waitForSelector()` first');
// → markHardStop records this message
// → flushSink discards buffered logs, emits only:
// You must call `await page.waitForSelector()` first

```

## Key Source Files

- **[`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts)** — Core implementation of `bufferOutput`, `flushSink`, `markHardStop`, and `installLifecycleFlush`
- **[`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts)** — `console.log` override and explicit `flushSink` invocation at script completion
- **[`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts)** — CLI-side sink initialization via `options.cliLog || createBufferedLog()`
- **[`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts)** — `formatCliLogValue` for argument serialization

## Summary

- **Buffering**: All `console.log` calls are intercepted, formatted, and stored in an array via `bufferOutput`.
- **Hard-stop protection**: `markHardStop` deduplicates critical errors so only the first guidance message surfaces.
- **Flushing**: `flushSink` either emits buffered content normally or discards it for hard-stop scenarios, with automatic lifecycle handlers ensuring execution.

## Frequently Asked Questions

### What happens to console.log output if a hard-stop error occurs?

The buffered output is discarded entirely. Only the hard-stop message recorded by `markHardStop` is emitted, unless that same error is already propagating through another channel. This prevents confusing the user with partial logs mixed with error guidance.

### How does the update notice always appear last?

The `setNoticeTrailer` function stores a string that `flushSink` appends after all buffered content, regardless of whether the script succeeded or failed. This guarantees version notifications appear after any script-generated output.

### Why does ego-lite need to override console.log?

According to the `citrolabs/ego-lite` source code, agents have exactly one output channel. The custom sink enables the framework to buffer output until script completion, suppress duplicate hard-stop errors, and append trailers—all while maintaining a simple `console.log` interface for users.