# How the Ego‑Lite cliLog Output Sink Buffers and Flushes Agent Console Output

> Discover how Ego-Lite cliLog output sink buffers and flushes agent console logs at process exit. Learn about error handling and update notices.

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

---

**The Ego‑Lite cliLog output sink intercepts every agent `console.log` call, accumulates formatted lines in an in‑memory `buffer` array, and flushes them in a single pass at process exit, applying special handling for hard‑stop errors and update‑notice trailers.**

In the `citrolabs/ego-lite` repository, agent scripts do not write directly to the host’s stdout. Instead, the runtime funnels all console output through a dedicated 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). This design guarantees that the cliLog output sink buffers and flushes agent console output exactly once per run, keeping logs clean and deterministic.

## Buffering Console Output with bufferOutput

Each time an agent invokes `console.log`, the runtime replaces the native method with a wrapper that calls `bufferOutput(chunk)` from [`output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/output-sink.ts). According to the source code, this function pushes the already‑formatted line—trailing newline included—onto an internal `buffer` array (lines 32‑35). No bytes reach the terminal during this phase; everything stays in memory until the final flush.

## Detecting Hard‑Stop Errors via markHardStop

When a permission‑related `EgoError` triggers a hard stop, the runtime calls `markHardStop(message)` (lines 46‑55). The sink stores the first such message in `hardStopMessage` and silently ignores subsequent hard‑stop calls. This prevents the same guidance from flooding the console on every loop iteration.

## Appending an Update‑Notice Trailer with setNoticeTrailer

An asynchronous version‑checker in [`update-notice.ts`](https://github.com/citrolabs/ego-lite/blob/main/update-notice.ts) can call `setNoticeTrailer(line)` (lines 38‑44) to register a line that appears after all buffered logs. Because the trailer is held separately in `noticeTrailer`, it never races ahead of the agent’s own output and is only written during the final flush.

## Flushing the Buffer in a Single Pass

At the end of a run, the CLI invokes `flushSink(stream, thrown)` (lines 58‑88). The function follows a strict priority:

1. **Hard‑stop override** – If `hardStopMessage` is set, the entire buffer is discarded and only the hard‑stop message is written to the stream.
2. **Normal output** – Otherwise, every chunk in the `buffer` array is written verbatim via `stream.write`.
3. **Trailer append** – Finally, if a `noticeTrailer` exists, it is written after the main output.

## Automatic Process Lifecycle Hooks

When `ego-lite` is consumed as an SDK through `installEgoSdk`, the sink registers `beforeExit` and `exit` listeners via `installLifecycleFlush(stream)` (lines 99‑115). These hooks ensure that `flushSink` runs automatically even when the host process never calls the explicit CLI `execute()` wrapper.

The CLI entry point in [`package/ego-browser/src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/run.ts) (lines 9‑10) imports the same utilities and wires them into the global process, replacing `console.log` with the buffering wrapper and installing the lifecycle hook before the agent script begins.

## Resetting Sink State for Tests

In‑process test suites can call `resetSink()` (lines 92‑99) to clear the internal `buffer`, `hardStopMessage`, and `noticeTrailer` between runs. This guarantees isolation when multiple agent executions occur inside the same Node.js process.

## Code Examples

### Runtime Script Usage

The following pattern mirrors what the Ego‑Lite runtime does when intercepting an agent’s `console.log`:

```typescript
import { bufferOutput, setNoticeTrailer, markHardStop, flushSink } from './output-sink.js';
import { createWriteStream } from 'node:fs';

// Simulated console.log interception
function agentLog(...args) {
  const line = args.map(String).join(' ') + '\n';
  bufferOutput(line); // ← buffers the line
}

agentLog('Fetching page…');
agentLog('Page title:', 'Home');

// Hard‑stop scenario (as triggered from ego-errors.ts)
if (!hasPermission) {
  markHardStop('⚠️  You must claim the task space first.\n');
}

// Optional update‑notice (set by a background checker in update-notice.ts)
setNoticeTrailer('🚀 ego‑lite update available!');

// Single flush point at end of run
const out = createWriteStream(1); // stdout
flushSink(out, false);            // ← flushes buffer or hard‑stop message

```

### CLI Entry Point Wiring in run.ts

The CLI automatically hooks the sink via [`package/ego-browser/src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/run.ts):

```typescript
import { bufferOutput, flushSink, resetSink, installLifecycleFlush } from './output-sink.js';
import { createWriteStream } from 'node:fs';

// Register process exit hooks so flush runs automatically
installLifecycleFlush(createWriteStream(1));

// The runtime replaces console.log with a wrapper calling bufferOutput
globalThis.console.log = (...args) => bufferOutput(args.join(' ') + '\n');

// After the user script finishes, the lifecycle hook or explicit CLI call invokes flushSink once

```

## Summary

- **`bufferOutput`** stores every formatted `console.log` line in an in‑memory array inside [`package/ego-browser/src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/output-sink.ts).
- **`markHardStop`** captures the first hard‑stop guidance message and suppresses duplicates.
- **`setNoticeTrailer`** queues an update notice that always appears after the main logs.
- **`flushSink`** writes everything exactly once, discarding normal output if a hard stop occurred, then appending the optional trailer.
- **`installLifecycleFlush`** registers process `beforeExit` and `exit` hooks so flushing happens automatically in SDK mode.
- **`resetSink`** clears all state for isolated test runs.

## Frequently Asked Questions

### What happens to buffered output when a hard‑stop error occurs?

If `markHardStop` was called during the run, `flushSink` discards the entire buffer and writes only the stored `hardStopMessage` to the stream. This ensures the agent sees the critical guidance once, without being buried under earlier log lines.

### How does the update notice avoid appearing before agent logs?

`setNoticeTrailer` stores the notice line in a separate variable (`noticeTrailer`). During `flushSink`, the trailer is written only after the main buffer has been iterated, so it always appears at the end of the output.

### Can I use the output sink outside the CLI wrapper?

Yes. The `installEgoSdk` path calls `installLifecycleFlush`, which attaches `beforeExit` and `exit` listeners that invoke `flushSink` automatically. This lets any Node.js script benefit from the same buffering behavior without manually calling the CLI `execute()` function.

### How do tests prevent sink state from leaking between runs?

Tests can import `resetSink` from [`package/ego-browser/src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/output-sink.ts) and invoke it between test cases. This wipes the buffer, hard‑stop message, and trailer so subsequent executions start with a clean sink.