From d66a05dc418e59a2f6e1a25eb752c6ed017138bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 04:30:22 +0000 Subject: [PATCH] RPC: route logs to stderr to keep stdout JSON clean --- src/logging.ts | 17 ++++++++++++++++- src/rpc/loop.ts | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/logging.ts b/src/logging.ts index 8cb44be01..8ae9e6e12 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -44,6 +44,7 @@ let cachedLogger: TsLogger | null = null; let cachedSettings: ResolvedSettings | null = null; let overrideSettings: LoggerSettings | null = null; let consolePatched = false; +let forceConsoleToStderr = false; function normalizeLevel(level?: string): Level { if (isVerbose()) return "trace"; @@ -183,6 +184,12 @@ export function resetLogger() { overrideSettings = null; } +// Route all console output (including tslog console writes) to stderr. +// This keeps stdout clean for RPC/JSON modes. +export function routeLogsToStderr(): void { + forceConsoleToStderr = true; +} + /** * Route console.* calls through pino while still emitting to stdout/stderr. * This keeps user-facing output unchanged but guarantees every console call is captured in log files. @@ -224,7 +231,15 @@ export function enableConsoleCapture(): void { } catch { // never block console output on logging failures } - orig.apply(console, args as []); + if (forceConsoleToStderr) { + const target = + level === "error" || level === "fatal" || level === "warn" + ? process.stderr + : process.stderr; // in RPC/JSON mode, keep stdout clean + target.write(`${formatted}\n`); + } else { + orig.apply(console, args as []); + } }; console.log = forward("info", original.log); diff --git a/src/rpc/loop.ts b/src/rpc/loop.ts index 300a430fa..e1b1f186c 100644 --- a/src/rpc/loop.ts +++ b/src/rpc/loop.ts @@ -15,6 +15,7 @@ import { listSystemPresence, updateSystemPresence, } from "../infra/system-presence.js"; +import { routeLogsToStderr } from "../logging.js"; import { setHeartbeatsEnabled } from "../provider-web.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -28,6 +29,9 @@ export async function runRpcLoop(io: { input: Readable; output: Writable; }): Promise { + // Keep stdout reserved for RPC JSON replies; send all other logs to stderr. + routeLogsToStderr(); + const rl = createInterface({ input: io.input, crlfDelay: Infinity }); const respond = (obj: unknown) => {