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

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 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) serializes each argument, and bufferOutput (defined in src/output-sink.ts lines 32-35) stores the formatted string—always with a trailing newline—in an in-memory array:

// 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 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 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 lines 99-114 registers handlers on both process.on("beforeExit") and process.on("exit"):

// 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.

Complete Buffer and Flush Example

// 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 — Core implementation of bufferOutput, flushSink, markHardStop, and installLifecycleFlush
  • src/run.tsconsole.log override and explicit flushSink invocation at script completion
  • src/index.ts — CLI-side sink initialization via options.cliLog || createBufferedLog()
  • src/format.tsformatCliLogValue 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.

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 →