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

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. 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. 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 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 (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:

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:

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.
  • 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 and invoke it between test cases. This wipes the buffer, hard‑stop message, and trailer so subsequent executions start with a clean sink.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →