# How Ego‑Lite Buffers and Flushes `console.log` Output

> Discover how ego-lite buffers and flushes console.log output efficiently. Learn how formatted lines are managed and atomically flushed on process exit.

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

---

**Ego‑Lite routes every agent‑visible `console.log` call through a dedicated output-sink module that buffers formatted lines during script execution, then atomically flushes them on process exit—discarding the buffer entirely if a hard‑stop error occurred.**

The ego‑lite runtime (from `citrolabs/ego-lite`) intercepts standard console output to give agents predictable, ordered logging with special handling for catastrophic failures. This design prevents partial or interleaved output when scripts are forcibly terminated.

## How the Output Sink Captures `console.log`

When a script starts, the runtime in [`run.ts`](https://github.com/citrolabs/ego-lite/blob/main/run.ts) replaces the global `console.log` with a wrapper that feeds into the output sink's buffer instead of writing directly to stdout.

```ts
// Excerpt from run.ts: console.log override
import { bufferOutput } from "./output-sink.js";
import { formatCliLogValue } from "./format.js";

console.log = (...args) => {
  // Each call is buffered instead of written directly
  bufferOutput(`${args.map(formatCliLogValue).join(" ")}\n`);
};

```

Each formatted string passes to `bufferOutput()`, which pushes it into an in‑memory array. No I/O occurs during script execution—the **buffer simply accumulates** strings.

## The `flushSink` Decision Logic

The `flushSink(stream, thrown)` function in [`output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/output-sink.ts) determines what actually gets written. It operates in two distinct modes:

### Hard‑Stop Mode: Discard Buffer

If `markHardStop(message)` was called during execution (typically from an unrecoverable agent error), the entire buffer is discarded. Only the hard‑stop message itself is written to the stream—once, cleanly.

### Normal Completion: Emit Buffered Lines

If no hard‑stop occurred, `flushSink` writes all buffered lines to the provided writable stream in capture order. This guarantees sequential output even if multiple async operations logged concurrently.

After either case, an **optional notice trailer** (set via `setNoticeTrailer`) is appended separately, ensuring out‑of-band update messages appear as a footer without corrupting the script's output structure.

## Lifecycle Integration for SDK Usage

When using ego‑lite as a library rather than through its CLI wrapper, [`index.ts`](https://github.com/citrolabs/ego-lite/blob/main/index.ts) invokes `installLifecycleFlush()` to register automatic flushing:

```ts
// Lifecycle hook installation (SDK path in index.ts)
import { installLifecycleFlush } from "./output-sink.js";

installLifecycleFlush(); // registers on process.beforeExit and process.exit

```

This ensures the sink flushes even if the consuming application doesn't explicitly call `flushSink`, matching the behavior of the managed CLI execution path.

## Manual Sink Control

For tests or advanced use cases, the sink exposes explicit control functions:

```ts
// Example: Manually flushing with a notice trailer
import { flushSink, setNoticeTrailer } from "./output-sink.js";

setNoticeTrailer("🚀 ego lite update available!");
flushSink(process.stdout, false); // writes buffer + trailer immediately

```

```ts
// Example: Resetting between test runs
import { resetSink } from "./output-sink.js";

resetSink(); // clears buffer, hard‑stop state, and trailer

```

## Source File Reference

| File | Role |
|------|------|
| [`package/ego-browser/src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/output-sink.ts) | Core buffer implementation, `flushSink`, `markHardStop`, `setNoticeTrailer`, lifecycle hooks |
| [`package/ego-browser/src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/run.ts) | CLI entry point; overrides `console.log`, orchestrates `resetSink`/`flushSink` around execution |
| [`package/ego-browser/src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts) | SDK entry point; creates default logger, invokes `installLifecycleFlush` for standalone usage |

## Summary

- **Buffering**: `console.log` is overridden to call `bufferOutput()`, storing formatted strings in memory
- **Flush decision**: `flushSink` chooses between discarding buffer (hard‑stop) or emitting all lines (normal completion)
- **Guaranteed footer**: Notice trailers are appended after the main output, never interleaved
- **Automatic cleanup**: Lifecycle hooks ensure flushing on process exit for both CLI and SDK usage
- **Testability**: `resetSink()` provides clean state isolation between runs

## Frequently Asked Questions

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

The entire buffered output is discarded. Only the hard‑stop message itself is written to stdout, ensuring agents see a single, clean error line rather than a partial execution log followed by a failure notice.

### Can I flush the output sink before the process exits?

Yes. Import `flushSink` from [`output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/output-sink.ts) and call it with a writable stream and a boolean indicating whether an error was thrown: `flushSink(process.stdout, false)`. This writes buffered content immediately and appends any configured notice trailer.

### Why does ego‑lite buffer `console.log` instead of writing directly?

Buffering enables atomic emission of all logs and supports the hard‑stop semantics. Without buffering, a fatal error could leave partial output visible; with buffering, the runtime can suppress all normal output and present only the error when necessary.

### How do I clear the sink state between test cases?

Call `resetSink()` from [`output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/output-sink.ts). This empties the buffer array, clears any stored hard‑stop message, and removes the notice trailer, providing isolated state for each test run.