diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index 9fac95b9c..37603c262 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -27,4 +27,14 @@ describe("sanitizeUserFacingText", () => { const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}'; expect(sanitizeUserFacingText(raw)).toBe("LLM error server_error: Something exploded"); }); + + it("collapses consecutive duplicate paragraphs", () => { + const text = "Hello there!\n\nHello there!"; + expect(sanitizeUserFacingText(text)).toBe("Hello there!"); + }); + + it("does not collapse distinct paragraphs", () => { + const text = "Hello there!\n\nDifferent line."; + expect(sanitizeUserFacingText(text)).toBe(text); + }); }); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index fdfed02a2..b47938c23 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -77,6 +77,29 @@ function stripFinalTagsFromText(text: string): string { return text.replace(FINAL_TAG_RE, ""); } +function collapseConsecutiveDuplicateBlocks(text: string): string { + const trimmed = text.trim(); + if (!trimmed) return text; + const blocks = trimmed.split(/\n{2,}/); + if (blocks.length < 2) return text; + + const normalizeBlock = (value: string) => value.trim().replace(/\s+/g, " "); + const result: string[] = []; + let lastNormalized: string | null = null; + + for (const block of blocks) { + const normalized = normalizeBlock(block); + if (lastNormalized && normalized === lastNormalized) { + continue; + } + result.push(block.trim()); + lastNormalized = normalized; + } + + if (result.length === blocks.length) return text; + return result.join("\n\n"); +} + function isLikelyHttpErrorText(raw: string): boolean { const match = raw.match(HTTP_STATUS_PREFIX_RE); if (!match) return false; @@ -321,7 +344,7 @@ export function sanitizeUserFacingText(text: string): string { return formatRawAssistantErrorForUi(trimmed); } - return stripped; + return collapseConsecutiveDuplicateBlocks(stripped); } export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean { diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index c89e0d699..fdf40be61 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -14,6 +14,7 @@ import { writeConfigFile, } from "../config/config.js"; import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; +import { logAcceptedEnvOption } from "../infra/env.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js"; import { onHeartbeatEvent } from "../infra/heartbeat-events.js"; @@ -149,6 +150,14 @@ export async function startGatewayServer( ): Promise { // Ensure all default port derivations (browser/canvas) see the actual runtime port. process.env.CLAWDBOT_GATEWAY_PORT = String(port); + logAcceptedEnvOption({ + key: "CLAWDBOT_RAW_STREAM", + description: "raw stream logging enabled", + }); + logAcceptedEnvOption({ + key: "CLAWDBOT_RAW_STREAM_PATH", + description: "raw stream log path override", + }); let configSnapshot = await readConfigFileSnapshot(); if (configSnapshot.legacyIssues.length > 0) { diff --git a/src/infra/env.ts b/src/infra/env.ts index 49839fcfe..2139c65a7 100644 --- a/src/infra/env.ts +++ b/src/infra/env.ts @@ -1,5 +1,32 @@ +import { createSubsystemLogger } from "../logging/subsystem.js"; import { parseBooleanValue } from "../utils/boolean.js"; +const log = createSubsystemLogger("env"); +const loggedEnv = new Set(); + +type AcceptedEnvOption = { + key: string; + description: string; + value?: string; + redact?: boolean; +}; + +function formatEnvValue(value: string, redact?: boolean): string { + if (redact) return ""; + const singleLine = value.replace(/\s+/g, " ").trim(); + if (singleLine.length <= 160) return singleLine; + return `${singleLine.slice(0, 160)}…`; +} + +export function logAcceptedEnvOption(option: AcceptedEnvOption): void { + if (process.env.VITEST || process.env.NODE_ENV === "test") return; + if (loggedEnv.has(option.key)) return; + const rawValue = option.value ?? process.env[option.key]; + if (!rawValue || !rawValue.trim()) return; + loggedEnv.add(option.key); + log.info(`env: ${option.key}=${formatEnvValue(rawValue, option.redact)} (${option.description})`); +} + export function normalizeZaiEnv(): void { if (!process.env.ZAI_API_KEY?.trim() && process.env.Z_AI_API_KEY?.trim()) { process.env.ZAI_API_KEY = process.env.Z_AI_API_KEY;