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

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 replaces the global console.log with a wrapper that feeds into the output sink's buffer instead of writing directly to stdout.

// 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 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 invokes installLifecycleFlush() to register automatic flushing:

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

// 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
// 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 Core buffer implementation, flushSink, markHardStop, setNoticeTrailer, lifecycle hooks
package/ego-browser/src/run.ts CLI entry point; overrides console.log, orchestrates resetSink/flushSink around execution
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 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. This empties the buffer array, clears any stored hard‑stop message, and removes the notice trailer, providing isolated state for each test run.

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 →