From 8f797f213eeeca42be8b626ade0f5eeb8fc81d17 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Wed, 14 Jan 2026 17:55:06 -0800 Subject: [PATCH 1/2] Logging: tolerate EIO console writes --- CHANGELOG.md | 1 + src/logging/console-capture.test.ts | 85 +++++++++++++++++++++++++++++ src/logging/console.ts | 3 +- 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/logging/console-capture.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c9b180d..23833b6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06. - Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06. - Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight. +- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06. - Sandbox: restore `docker.binds` config validation for custom bind mounts. (#873) — thanks @akonyer. - Sandbox: preserve configured PATH for `docker exec` so custom tools remain available. (#873) — thanks @akonyer. - Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr. diff --git a/src/logging/console-capture.test.ts b/src/logging/console-capture.test.ts new file mode 100644 index 000000000..623587604 --- /dev/null +++ b/src/logging/console-capture.test.ts @@ -0,0 +1,85 @@ +import crypto from "node:crypto"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + enableConsoleCapture, + resetLogger, + routeLogsToStderr, + setLoggerOverride, +} from "../logging.js"; +import { loggingState } from "./state.js"; + +type ConsoleSnapshot = { + log: typeof console.log; + info: typeof console.info; + warn: typeof console.warn; + error: typeof console.error; + debug: typeof console.debug; + trace: typeof console.trace; +}; + +let snapshot: ConsoleSnapshot; + +beforeEach(() => { + snapshot = { + log: console.log, + info: console.info, + warn: console.warn, + error: console.error, + debug: console.debug, + trace: console.trace, + }; + loggingState.consolePatched = false; + loggingState.forceConsoleToStderr = false; + loggingState.rawConsole = null; + resetLogger(); +}); + +afterEach(() => { + console.log = snapshot.log; + console.info = snapshot.info; + console.warn = snapshot.warn; + console.error = snapshot.error; + console.debug = snapshot.debug; + console.trace = snapshot.trace; + loggingState.consolePatched = false; + loggingState.forceConsoleToStderr = false; + loggingState.rawConsole = null; + resetLogger(); + setLoggerOverride(null); + vi.restoreAllMocks(); +}); + +describe("enableConsoleCapture", () => { + it("swallows EIO from stderr writes", () => { + setLoggerOverride({ level: "info", file: tempLogPath() }); + vi.spyOn(process.stderr, "write").mockImplementation(() => { + throw eioError(); + }); + routeLogsToStderr(); + enableConsoleCapture(); + expect(() => console.log("hello")).not.toThrow(); + }); + + it("swallows EIO from original console writes", () => { + setLoggerOverride({ level: "info", file: tempLogPath() }); + console.log = () => { + throw eioError(); + }; + enableConsoleCapture(); + expect(() => console.log("hello")).not.toThrow(); + }); +}); + +function tempLogPath() { + return path.join(os.tmpdir(), `clawdbot-log-${crypto.randomUUID()}.log`); +} + +function eioError() { + const err = new Error("EIO") as NodeJS.ErrnoException; + err.code = "EIO"; + return err; +} diff --git a/src/logging/console.ts b/src/logging/console.ts index 3d7c6ca00..88adbb1b1 100644 --- a/src/logging/console.ts +++ b/src/logging/console.ts @@ -89,7 +89,8 @@ function shouldSuppressConsoleMessage(message: string): boolean { } function isEpipeError(err: unknown): boolean { - return Boolean((err as { code?: string })?.code === "EPIPE"); + const code = (err as { code?: string })?.code; + return code === "EPIPE" || code === "EIO"; } /** From 5c52dbf66122185786f57ae3d80ae0d421407a56 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 03:22:54 +0000 Subject: [PATCH 2/2] style: oxfmt fixes (#925) (thanks @grp06) --- src/commands/agents.providers.ts | 6 +++++- src/config/config.sandbox-docker.test.ts | 5 +---- src/gateway/hooks.ts | 3 +-- src/plugins/types.ts | 4 +--- src/utils/message-channel.ts | 4 +++- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/commands/agents.providers.ts b/src/commands/agents.providers.ts index bc1c85ba9..e5e5cbce3 100644 --- a/src/commands/agents.providers.ts +++ b/src/commands/agents.providers.ts @@ -1,5 +1,9 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; -import { getChannelPlugin, listChannelPlugins, normalizeChannelId } from "../channels/plugins/index.js"; +import { + getChannelPlugin, + listChannelPlugins, + normalizeChannelId, +} from "../channels/plugins/index.js"; import type { ChannelId } from "../channels/plugins/types.js"; import type { ClawdbotConfig } from "../config/config.js"; import type { AgentBinding } from "../config/types.js"; diff --git a/src/config/config.sandbox-docker.test.ts b/src/config/config.sandbox-docker.test.ts index d16304a72..2bc0ff43e 100644 --- a/src/config/config.sandbox-docker.test.ts +++ b/src/config/config.sandbox-docker.test.ts @@ -9,10 +9,7 @@ describe("sandbox docker config", () => { defaults: { sandbox: { docker: { - binds: [ - "/var/run/docker.sock:/var/run/docker.sock", - "/home/user/source:/source:rw", - ], + binds: ["/var/run/docker.sock:/var/run/docker.sock", "/home/user/source:/source:rw"], }, }, }, diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index e64f587ea..6065d121d 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -141,8 +141,7 @@ const listHookChannelValues = () => ["last", ...listChannelPlugins().map((plugin export type HookMessageChannel = ChannelId | "last"; const getHookChannelSet = () => new Set(listHookChannelValues()); -export const getHookChannelError = () => - `channel must be ${listHookChannelValues().join("|")}`; +export const getHookChannelError = () => `channel must be ${listHookChannelValues().join("|")}`; export function resolveHookChannel(raw: unknown): HookMessageChannel | null { if (raw === undefined) return "last"; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index c70863689..c4fe25842 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -112,9 +112,7 @@ export type ClawdbotPluginApi = { tool: AnyAgentTool | ClawdbotPluginToolFactory, opts?: { name?: string; names?: string[] }, ) => void; - registerChannel: ( - registration: ClawdbotPluginChannelRegistration | ChannelPlugin, - ) => void; + registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void; registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void; registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void; registerService: (service: ClawdbotPluginService) => void; diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts index bae71560a..704ea0fd1 100644 --- a/src/utils/message-channel.ts +++ b/src/utils/message-channel.ts @@ -86,7 +86,9 @@ export const listGatewayAgentChannelAliases = (): string[] => export type GatewayAgentChannelHint = GatewayMessageChannel | "last"; export const listGatewayAgentChannelValues = (): string[] => - Array.from(new Set([...listGatewayMessageChannels(), "last", ...listGatewayAgentChannelAliases()])); + Array.from( + new Set([...listGatewayMessageChannels(), "last", ...listGatewayAgentChannelAliases()]), + ); export function isGatewayMessageChannel(value: string): value is GatewayMessageChannel { return listGatewayMessageChannels().includes(value as GatewayMessageChannel);