Understanding the bufferOutput Mechanism in Ego-Lite: When and How It Flushes

The bufferOutput mechanism in ego-lite captures all console.log output into an in-memory buffer and flushes it exactly once per heredoc run during process teardown, discarding the buffer and emitting only the hard-stop error message if a critical failure occurs.

The citrolabs/ego-lite repository implements a specialized output buffering system to manage agent logging during browser automation runs. Instead of writing directly to stdout, the bufferOutput mechanism intercepts all console.log calls and stores them in a controlled sink, enabling clean error handling and atomic output delivery. This architecture ensures that regular logs accumulate safely while providing immediate, uncluttered feedback when scripts encounter hard-stop failures.

How the bufferOutput Mechanism Works

The Output Sink Architecture

In src/output-sink.ts, ego-lite defines an internal output sink that redirects the agent’s console.log calls away from the process stdout. Each log fragment is pushed into an in-memory array (buffer: string[]) via the bufferOutput() function. This decouples log generation from log delivery, allowing the runtime to manipulate or discard output based on the execution outcome.

Internal State Tracking

The sink maintains three distinct states to manage the output lifecycle:

  • buffer – An array of formatted log lines accumulated during execution. The bufferOutput() function pushes each chunk into this array (lines 33-35 in src/output-sink.ts).
  • hardStopMessage – Stores the first hard-stop error message when an EgoError aborts the script. Set via markHardStop() (lines 51-55), this message supersedes the entire buffer if triggered.
  • noticeTrailer – An optional footer appended after the final output, regardless of success or failure. Configured via setNoticeTrailer() (lines 42-44).

When Is the Buffer Flushed?

The Flush Lifecycle via flushSink

The buffer flushes exactly once per heredoc run through the flushSink(stream, thrown) function. This method evaluates the execution context to determine output behavior:

  • Normal completion: The buffered log lines write to the provided writable stream in chronological order.
  • Hard-stop occurred: The entire buffer drops, and only the stored hardStopMessage emits (or the uncaught error itself if the script terminated unexpectedly).
  • Notice trailer: After either path, noticeTrailer appends if present.

Automatic Process Teardown Handling

To guarantee the sink drains without explicit CLI wrapper calls, installLifecycleFlush(stream) registers process event listeners at lines 108-114 of src/output-sink.ts. This utility attaches to process.on('beforeExit') for clean exits and process.on('exit') for uncaught async rejections, ensuring flushSink executes exactly once during termination.

Implementation in the Runtime

During run initialization, executionContext() in src/run.ts overwrites the global console.log to route formatted output into the buffer. All subsequent logging accumulates silently until the process teardown triggers the automatic flush.

Practical Code Examples

The following patterns demonstrate how to interact with the buffering system directly:

// 1️⃣ Replace console.log with the buffered version (run.ts)
import { bufferOutput } from "./output-sink.js";

console.log = (...args) => {
  bufferOutput(`${args.map(formatCliLogValue).join(" ")}\n`);
};
// 2️⃣ Mark a hard-stop from anywhere in the runtime
import { markHardStop } from "./output-sink.js";

if (isHardStopError(err)) {
  // Only the first hard-stop message survives; buffer is discarded
  markHardStop(err.message);
}
// 3️⃣ Append an out-of-band notice (e.g., async version check)
import { setNoticeTrailer } from "./output-sink.js";

setNoticeTrailer("⚡ ego-lite update available – run `ego-browser --doctor`");
// 4️⃣ Register the automatic flush (once per process)
import { installLifecycleFlush } from "./output-sink.js";

installLifecycleFlush(process.stdout);

Summary

  • The bufferOutput mechanism redirects console.log to an in-memory array in src/output-sink.ts, preventing immediate stdout writes during agent execution.
  • Flush timing occurs exactly once per run via flushSink, triggered automatically by installLifecycleFlush during process beforeExit or exit events.
  • Hard-stop handling discards the entire buffer and emits only the error message when markHardStop() records a critical failure.
  • Notice trailers append after both success and error outputs through setNoticeTrailer(), supporting auxiliary messaging like update notifications.

Frequently Asked Questions

What triggers the bufferOutput mechanism to flush in ego-lite?

The buffer flushes automatically when the Node.js process terminates. The installLifecycleFlush() function registers listeners for process.on('beforeExit') and process.on('exit') events, ensuring flushSink() runs exactly once to emit either the buffered logs or a hard-stop error message.

Does the bufferOutput mechanism preserve logs when a script fails?

It depends on the failure type. During a hard-stop (critical EgoError), the buffer discards all accumulated logs and only emits the hardStopMessage set via markHardStop(). For normal completions or non-critical errors, the full buffer writes to stdout.

Use the setNoticeTrailer() function imported from src/output-sink.ts. This stores a message that flushSink() appends after the main output (whether buffered logs or a hard-stop error), making it ideal for version warnings or diagnostic hints.

Where is the console.log override initialized in the ego-lite codebase?

The override occurs in src/run.ts within the executionContext() function. This module replaces the global console.log implementation with a wrapper that calls bufferOutput(), ensuring all agent logging routes through the centralized sink before the automatic flush occurs.

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 →