Merge pull request #925 from grp06/fix/console-eio
Logging: tolerate EIO console writes (AI)
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
- 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.
|
- 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.
|
- 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: 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.
|
- 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.
|
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
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 { ChannelId } from "../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { AgentBinding } from "../config/types.js";
|
import type { AgentBinding } from "../config/types.js";
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ describe("sandbox docker config", () => {
|
|||||||
defaults: {
|
defaults: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
docker: {
|
docker: {
|
||||||
binds: [
|
binds: ["/var/run/docker.sock:/var/run/docker.sock", "/home/user/source:/source:rw"],
|
||||||
"/var/run/docker.sock:/var/run/docker.sock",
|
|
||||||
"/home/user/source:/source:rw",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,8 +141,7 @@ const listHookChannelValues = () => ["last", ...listChannelPlugins().map((plugin
|
|||||||
export type HookMessageChannel = ChannelId | "last";
|
export type HookMessageChannel = ChannelId | "last";
|
||||||
|
|
||||||
const getHookChannelSet = () => new Set<string>(listHookChannelValues());
|
const getHookChannelSet = () => new Set<string>(listHookChannelValues());
|
||||||
export const getHookChannelError = () =>
|
export const getHookChannelError = () => `channel must be ${listHookChannelValues().join("|")}`;
|
||||||
`channel must be ${listHookChannelValues().join("|")}`;
|
|
||||||
|
|
||||||
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
|
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
|
||||||
if (raw === undefined) return "last";
|
if (raw === undefined) return "last";
|
||||||
|
|||||||
85
src/logging/console-capture.test.ts
Normal file
85
src/logging/console-capture.test.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -89,7 +89,8 @@ function shouldSuppressConsoleMessage(message: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isEpipeError(err: unknown): 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -112,9 +112,7 @@ export type ClawdbotPluginApi = {
|
|||||||
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
tool: AnyAgentTool | ClawdbotPluginToolFactory,
|
||||||
opts?: { name?: string; names?: string[] },
|
opts?: { name?: string; names?: string[] },
|
||||||
) => void;
|
) => void;
|
||||||
registerChannel: (
|
registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void;
|
||||||
registration: ClawdbotPluginChannelRegistration | ChannelPlugin,
|
|
||||||
) => void;
|
|
||||||
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
||||||
registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
registerCli: (registrar: ClawdbotPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
||||||
registerService: (service: ClawdbotPluginService) => void;
|
registerService: (service: ClawdbotPluginService) => void;
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ export const listGatewayAgentChannelAliases = (): string[] =>
|
|||||||
export type GatewayAgentChannelHint = GatewayMessageChannel | "last";
|
export type GatewayAgentChannelHint = GatewayMessageChannel | "last";
|
||||||
|
|
||||||
export const listGatewayAgentChannelValues = (): string[] =>
|
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 {
|
export function isGatewayMessageChannel(value: string): value is GatewayMessageChannel {
|
||||||
return listGatewayMessageChannels().includes(value as GatewayMessageChannel);
|
return listGatewayMessageChannels().includes(value as GatewayMessageChannel);
|
||||||
|
|||||||
Reference in New Issue
Block a user