feat(gateway): enrich agent WS logs

main
Peter Steinberger 2025-12-20 14:54:38 +00:00
parent f508fd3fa2
commit d95c09d94a
2 changed files with 86 additions and 11 deletions

View File

@ -418,6 +418,73 @@ function formatForLog(value: unknown): string {
} }
} }
function compactPreview(input: string, maxLen = 160): string {
const oneLine = input.replace(/\s+/g, " ").trim();
if (oneLine.length <= maxLen) return oneLine;
return `${oneLine.slice(0, Math.max(0, maxLen - 1))}`;
}
function summarizeAgentEventForWsLog(
payload: unknown,
): Record<string, unknown> {
if (!payload || typeof payload !== "object") return {};
const rec = payload as Record<string, unknown>;
const runId = typeof rec.runId === "string" ? rec.runId : undefined;
const stream = typeof rec.stream === "string" ? rec.stream : undefined;
const seq = typeof rec.seq === "number" ? rec.seq : undefined;
const data =
rec.data && typeof rec.data === "object"
? (rec.data as Record<string, unknown>)
: undefined;
const extra: Record<string, unknown> = {};
if (runId) extra.run = shortId(runId);
if (stream) extra.stream = stream;
if (seq !== undefined) extra.aseq = seq;
if (!data) return extra;
if (stream === "assistant") {
const text = typeof data.text === "string" ? data.text : undefined;
if (text?.trim()) extra.text = compactPreview(text);
const mediaUrls = Array.isArray(data.mediaUrls)
? data.mediaUrls
: undefined;
if (mediaUrls && mediaUrls.length > 0) extra.media = mediaUrls.length;
return extra;
}
if (stream === "tool") {
const phase = typeof data.phase === "string" ? data.phase : undefined;
const name = typeof data.name === "string" ? data.name : undefined;
if (phase || name) extra.tool = `${phase ?? "?"}:${name ?? "?"}`;
const toolCallId =
typeof data.toolCallId === "string" ? data.toolCallId : undefined;
if (toolCallId) extra.call = shortId(toolCallId);
const meta = typeof data.meta === "string" ? data.meta : undefined;
if (meta?.trim()) extra.meta = meta;
if (typeof data.isError === "boolean") extra.err = data.isError;
return extra;
}
if (stream === "job") {
const state = typeof data.state === "string" ? data.state : undefined;
if (state) extra.state = state;
if (data.to === null) extra.to = null;
else if (typeof data.to === "string") extra.to = data.to;
if (typeof data.durationMs === "number")
extra.ms = Math.round(data.durationMs);
if (typeof data.aborted === "boolean") extra.aborted = data.aborted;
const error = typeof data.error === "string" ? data.error : undefined;
if (error?.trim()) extra.error = compactPreview(error, 120);
return extra;
}
const reason = typeof data.reason === "string" ? data.reason : undefined;
if (reason?.trim()) extra.reason = reason;
return extra;
}
function normalizeVoiceWakeTriggers(input: unknown): string[] { function normalizeVoiceWakeTriggers(input: unknown): string[] {
const raw = Array.isArray(input) ? input : []; const raw = Array.isArray(input) ? input : [];
const cleaned = raw const cleaned = raw
@ -1117,14 +1184,18 @@ export async function startGatewayServer(
seq: eventSeq, seq: eventSeq,
stateVersion: opts?.stateVersion, stateVersion: opts?.stateVersion,
}); });
logWs("out", "event", { const logMeta: Record<string, unknown> = {
event, event,
seq: eventSeq, seq: eventSeq,
clients: clients.size, clients: clients.size,
dropIfSlow: opts?.dropIfSlow, dropIfSlow: opts?.dropIfSlow,
presenceVersion: opts?.stateVersion?.presence, presenceVersion: opts?.stateVersion?.presence,
healthVersion: opts?.stateVersion?.health, healthVersion: opts?.stateVersion?.health,
}); };
if (event === "agent") {
Object.assign(logMeta, summarizeAgentEventForWsLog(payload));
}
logWs("out", "event", logMeta);
for (const c of clients) { for (const c of clients) {
const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES; const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES;
if (slow && opts?.dropIfSlow) continue; if (slow && opts?.dropIfSlow) continue;
@ -1802,8 +1873,7 @@ export async function startGatewayServer(
typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : "";
const mainKey = const mainKey =
(loadConfig().inbound?.session?.mainKey ?? "main").trim() || "main"; (loadConfig().inbound?.session?.mainKey ?? "main").trim() || "main";
const sessionKey = const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : mainKey;
sessionKeyRaw.length > 0 ? sessionKeyRaw : mainKey;
const { storePath, store, entry } = loadSessionEntry(sessionKey); const { storePath, store, entry } = loadSessionEntry(sessionKey);
const now = Date.now(); const now = Date.now();
const sessionId = entry?.sessionId ?? randomUUID(); const sessionId = entry?.sessionId ?? randomUUID();
@ -2061,6 +2131,8 @@ export async function startGatewayServer(
); );
} }
const tailnetDns = await resolveTailnetDnsHint();
try { try {
const sshPortEnv = process.env.CLAWDIS_SSH_PORT?.trim(); const sshPortEnv = process.env.CLAWDIS_SSH_PORT?.trim();
const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN; const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN;
@ -2069,8 +2141,6 @@ export async function startGatewayServer(
? sshPortParsed ? sshPortParsed
: undefined; : undefined;
const tailnetDns = await resolveTailnetDnsHint();
const bonjour = await startGatewayBonjourAdvertiser({ const bonjour = await startGatewayBonjourAdvertiser({
instanceName: formatBonjourInstanceName(machineDisplayName), instanceName: formatBonjourInstanceName(machineDisplayName),
gatewayPort: port, gatewayPort: port,

View File

@ -1738,11 +1738,16 @@ describe("web auto-reply", () => {
const resolver = vi const resolver = vi
.fn() .fn()
.mockImplementation(async (_ctx, opts?: { onToolResult?: Function }) => { .mockImplementation(
await opts?.onToolResult?.({ text: "[🛠️ tool1]" }); async (
await opts?.onToolResult?.({ text: "[🛠️ tool2]" }); _ctx,
return { text: "final" }; opts?: { onToolResult?: (r: { text: string }) => Promise<void> },
}); ) => {
await opts?.onToolResult?.({ text: "[🛠️ tool1]" });
await opts?.onToolResult?.({ text: "[🛠️ tool2]" });
return { text: "final" };
},
);
await monitorWebProvider(false, listenerFactory, false, resolver); await monitorWebProvider(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined(); expect(capturedOnMessage).toBeDefined();