refactor: split gateway server methods

This commit is contained in:
Peter Steinberger
2026-01-03 18:14:07 +01:00
parent 4a6b33d799
commit 73fa2e10bc
8 changed files with 3065 additions and 2860 deletions

View File

@@ -24,6 +24,8 @@
- Agent tools: scope the Discord tool to Discord surface runs.
- Agent tools: format verbose tool summaries without brackets, with unique emojis and `tool: detail` style.
- Gateway: split server helpers/tests into hooks/session-utils/ws-log/net modules for better isolation; add unit coverage for hooks/session utils/ws log.
- Gateway: extract WS method handling + HTTP/provider/constant helpers to shrink server wiring and improve testability.
- Onboarding: fix Control UI basePath usage when showing/opening gateway URLs.
- macOS Connections: move to sidebar + detail layout with structured sections and header actions.
- macOS onboarding: increase window height so the permissions page fits without scrolling.
- Thinking: default to low for reasoning-capable models when no /think or config default is set.

View File

@@ -0,0 +1,9 @@
export const MAX_PAYLOAD_BYTES = 512 * 1024; // cap incoming frame size
export const MAX_BUFFERED_BYTES = 1.5 * 1024 * 1024; // per-connection send buffer limit
export const MAX_CHAT_HISTORY_MESSAGES_BYTES = 6 * 1024 * 1024; // keep history responses comfortably under client WS limits
export const HANDSHAKE_TIMEOUT_MS = 10_000;
export const TICK_INTERVAL_MS = 30_000;
export const HEALTH_REFRESH_INTERVAL_MS = 60_000;
export const DEDUPE_TTL_MS = 5 * 60_000;
export const DEDUPE_MAX = 1000;

View File

@@ -1,21 +1,31 @@
import {
createServer as createHttpServer,
type IncomingMessage,
type Server as HttpServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import { type WebSocketServer } from "ws";
import type { WebSocketServer } from "ws";
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
import type { CanvasHostHandler } from "../canvas-host/server.js";
import { type HooksConfigResolved, extractHookToken, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody } from "./hooks.js";
import { applyHookMappings } from "./hooks-mapping.js";
import { handleControlUiHttpRequest } from "./control-ui.js";
import type { createSubsystemLogger } from "../logging.js";
import { handleControlUiHttpRequest } from "./control-ui.js";
import {
extractHookToken,
type HooksConfigResolved,
normalizeAgentPayload,
normalizeHookHeaders,
normalizeWakePayload,
readJsonBody,
} from "./hooks.js";
import { applyHookMappings } from "./hooks-mapping.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
type HookDispatchers = {
dispatchWakeHook: (value: { text: string; mode: "now" | "next-heartbeat" }) => void;
dispatchWakeHook: (value: {
text: string;
mode: "now" | "next-heartbeat";
}) => void;
dispatchAgentHook: (value: {
message: string;
name: string;
@@ -46,13 +56,22 @@ export type HooksRequestHandler = (
res: ServerResponse,
) => Promise<boolean>;
export function createHooksRequestHandler(opts: {
hooksConfig: HooksConfigResolved | null;
bindHost: string;
port: number;
logHooks: SubsystemLogger;
} & HookDispatchers): HooksRequestHandler {
const { hooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts;
export function createHooksRequestHandler(
opts: {
hooksConfig: HooksConfigResolved | null;
bindHost: string;
port: number;
logHooks: SubsystemLogger;
} & HookDispatchers,
): HooksRequestHandler {
const {
hooksConfig,
bindHost,
port,
logHooks,
dispatchAgentHook,
dispatchWakeHook,
} = opts;
return async (req, res) => {
if (!hooksConfig) return false;
const url = new URL(req.url ?? "/", `http://${bindHost}:${port}`);
@@ -97,7 +116,9 @@ export function createHooksRequestHandler(opts: {
const headers = normalizeHookHeaders(req);
if (subPath === "wake") {
const normalized = normalizeWakePayload(payload as Record<string, unknown>);
const normalized = normalizeWakePayload(
payload as Record<string, unknown>,
);
if (!normalized.ok) {
sendJson(res, 400, { ok: false, error: normalized.error });
return true;
@@ -108,7 +129,9 @@ export function createHooksRequestHandler(opts: {
}
if (subPath === "agent") {
const normalized = normalizeAgentPayload(payload as Record<string, unknown>);
const normalized = normalizeAgentPayload(
payload as Record<string, unknown>,
);
if (!normalized.ok) {
sendJson(res, 400, { ok: false, error: normalized.error });
return true;
@@ -178,8 +201,12 @@ export function createGatewayHttpServer(opts: {
controlUiBasePath: string;
handleHooksRequest: HooksRequestHandler;
}): HttpServer {
const { canvasHost, controlUiEnabled, controlUiBasePath, handleHooksRequest } =
opts;
const {
canvasHost,
controlUiEnabled,
controlUiBasePath,
handleHooksRequest,
} = opts;
const httpServer: HttpServer = createHttpServer((req, res) => {
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return;

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
import type { ClawdisConfig } from "../config/config.js";
import { shouldLogVerbose } from "../globals.js";
import type { createSubsystemLogger } from "../logging.js";
import type { RuntimeEnv } from "../runtime.js";
import { monitorDiscordProvider } from "../discord/index.js";
import { probeDiscord } from "../discord/probe.js";
import { shouldLogVerbose } from "../globals.js";
import { monitorIMessageProvider } from "../imessage/index.js";
import type { createSubsystemLogger } from "../logging.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import type { RuntimeEnv } from "../runtime.js";
import { monitorSignalProvider } from "../signal/index.js";
import { resolveTelegramToken } from "../telegram/token.js";
import { monitorTelegramProvider } from "../telegram/monitor.js";
import { probeTelegram } from "../telegram/probe.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import { readWebSelfId } from "../web/session.js";
import { resolveTelegramToken } from "../telegram/token.js";
import type { WebProviderStatus } from "../web/auto-reply.js";
import { readWebSelfId } from "../web/session.js";
import { formatError } from "./server-utils.js";
export type TelegramRuntimeStatus = {
@@ -245,7 +245,9 @@ export function createProviderManager(
lastError: "disabled",
};
if (shouldLogVerbose()) {
logTelegram.debug("telegram provider disabled (telegram.enabled=false)");
logTelegram.debug(
"telegram provider disabled (telegram.enabled=false)",
);
}
return;
}

View File

@@ -5,7 +5,9 @@ import { formatError, normalizeVoiceWakeTriggers } from "./server-utils.js";
describe("normalizeVoiceWakeTriggers", () => {
test("returns defaults when input is empty", () => {
expect(normalizeVoiceWakeTriggers([])).toEqual(defaultVoiceWakeTriggers());
expect(normalizeVoiceWakeTriggers(null)).toEqual(defaultVoiceWakeTriggers());
expect(normalizeVoiceWakeTriggers(null)).toEqual(
defaultVoiceWakeTriggers(),
);
});
test("trims and limits entries", () => {
@@ -20,8 +22,10 @@ describe("formatError", () => {
});
test("handles status/code", () => {
expect(formatError({ status: 500, code: "EPIPE" })).toBe("500 EPIPE");
expect(formatError({ status: 404 })).toBe("404");
expect(formatError({ code: "ENOENT" })).toBe("ENOENT");
expect(formatError({ status: 500, code: "EPIPE" })).toBe(
"status=500 code=EPIPE",
);
expect(formatError({ status: 404 })).toBe("status=404 code=unknown");
expect(formatError({ code: "ENOENT" })).toBe("status=unknown code=ENOENT");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -504,7 +504,7 @@ export async function runOnboardingWizard(
const links = resolveControlUiLinks({
bind,
port,
basePath: config.gateway?.controlUi?.basePath,
basePath: baseConfig.gateway?.controlUi?.basePath,
});
const tokenParam =
authMode === "token" && gatewayToken
@@ -530,7 +530,7 @@ export async function runOnboardingWizard(
const links = resolveControlUiLinks({
bind,
port,
basePath: config.gateway?.controlUi?.basePath,
basePath: baseConfig.gateway?.controlUi?.basePath,
});
const tokenParam =
authMode === "token" && gatewayToken