fix(logging): decouple file logs from console verbose

This commit is contained in:
Peter Steinberger
2026-01-03 12:32:14 +00:00
parent e52bdaa2a2
commit bb54e60179
18 changed files with 105 additions and 67 deletions

View File

@@ -15,6 +15,7 @@
- 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.
- Thinking: default to low for reasoning-capable models when no /think or config default is set.
- Logging: decouple file log levels from console verbosity; verbose-only details are captured when `logging.level` is debug/trace.
### Docs
- Skills: add Sheets/Docs examples to gog skill (#128) — thanks @mbelinky.

View File

@@ -23,12 +23,25 @@ Clawdis uses a file logger backed by `tslog` (`src/logging.ts`).
The file format is one JSON object per line.
**Verbose vs. log levels**
- **File logs** are controlled exclusively by `logging.level`.
- `--verbose` only affects **console verbosity** (and WS log style); it does **not**
raise the file log level.
- To capture verbose-only details in file logs, set `logging.level` to `debug` or
`trace`.
## Console capture
The CLI entrypoint enables console capture (`src/index.ts` calls `enableConsoleCapture()`).
That means every `console.log/info/warn/error/debug/trace` is also written into the file logs,
while still behaving normally on stdout/stderr.
You can tune console verbosity independently via:
- `logging.consoleLevel` (default `info`)
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
## Gateway WebSocket logs
The gateway prints WebSocket protocol logs in two modes:
@@ -80,7 +93,7 @@ Behavior:
- **Sub-loggers by subsystem** (auto prefix + structured field `{ subsystem }`)
- **`logRaw()`** for QR/UX output (no prefix, no formatting)
- **Console styles** (e.g. `pretty | compact | json`)
- **Console log level** separate from file log level (file keeps full detail)
- **Console log level** separate from file log level (file keeps full detail when `logging.level` is set to `debug`/`trace`)
- **WhatsApp message bodies** are logged at `debug` (use `--verbose` to see them)
This keeps existing file logs stable while making interactive output scannable.

View File

@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import type { ClawdisConfig } from "../config/config.js";
import { isVerbose, logVerbose } from "../globals.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { applyTemplate, type MsgContext } from "./templating.js";
@@ -36,7 +36,7 @@ export async function transcribeInboundAudio(
);
await fs.writeFile(tmpPath, buffer);
mediaPath = tmpPath;
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`Downloaded audio for transcription (${(buffer.length / (1024 * 1024)).toFixed(2)}MB) -> ${tmpPath}`,
);
@@ -48,7 +48,7 @@ export async function transcribeInboundAudio(
const argv = transcriber.command.map((part) =>
applyTemplate(part, templCtx),
);
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(`Transcribing audio via command: ${argv.join(" ")}`);
}
const { stdout } = await runExec(argv[0], argv.slice(1), {

View File

@@ -20,7 +20,7 @@ import type {
} from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, isVerbose, logVerbose, warn } from "../globals.js";
import { danger, logVerbose, shouldLogVerbose, warn } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { detectMime } from "../media/mime.js";
import { saveMediaBuffer } from "../media/store.js";
@@ -139,7 +139,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
const groupDmChannels = dmConfig?.groupChannels;
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`discord: config dm=${dmEnabled ? "on" : "off"} allowFrom=${summarizeAllowList(allowFrom)} groupDm=${groupDmEnabled ? "on" : "off"} groupDmChannels=${summarizeAllowList(groupDmChannels)} guilds=${summarizeGuilds(guildEntries)} historyLimit=${historyLimit} mediaMaxMb=${Math.round(mediaMaxBytes / (1024 * 1024))}`,
);
@@ -191,7 +191,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const wasMentioned =
!isDirectMessage && Boolean(botId && message.mentions.has(botId));
const baseText = resolveDiscordMessageText(message);
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`discord: inbound id=${message.id} guild=${message.guild?.id ?? "dm"} channel=${message.channelId} mention=${wasMentioned ? "yes" : "no"} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"} content=${baseText ? "yes" : "no"}`,
);
@@ -414,7 +414,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
});
}
if (isVerbose()) {
if (shouldLogVerbose()) {
const preview = combinedBody.slice(0, 200).replace(/\n/g, "\\n");
logVerbose(
`discord inbound: channel=${message.channelId} from=${ctxPayload.From} preview="${preview}"`,
@@ -485,7 +485,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
textLimit,
});
didSendReply = true;
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`discord: delivered ${replies.length} reply${replies.length === 1 ? "" : "ies"} to ${replyTarget}`,
);
@@ -524,7 +524,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
logVerbose("discord: drop slash (dms disabled)");
return;
}
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`discord: slash inbound guild=${interaction.guildId ?? "dm"} channel=${interaction.channelId} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"}`,
);

View File

@@ -83,7 +83,7 @@ import {
sendMessageDiscord,
} from "../discord/index.js";
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
import { isVerbose } from "../globals.js";
import { isVerbose, shouldLogVerbose } from "../globals.js";
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
import {
monitorIMessageProvider,
@@ -2084,7 +2084,7 @@ export async function startGatewayServer(
lastError: null,
};
const task = monitorWebProvider(
isVerbose(),
shouldLogVerbose(),
undefined,
true,
undefined,
@@ -2137,7 +2137,7 @@ export async function startGatewayServer(
running: false,
lastError: "disabled",
};
if (isVerbose()) {
if (shouldLogVerbose()) {
logTelegram.debug(
"telegram provider disabled (telegram.enabled=false)",
);
@@ -2154,7 +2154,7 @@ export async function startGatewayServer(
lastError: "not configured",
};
// keep quiet by default; this is a normal state
if (isVerbose()) {
if (shouldLogVerbose()) {
logTelegram.debug(
"telegram provider not configured (no TELEGRAM_BOT_TOKEN)",
);
@@ -2171,7 +2171,7 @@ export async function startGatewayServer(
const username = probe.ok ? probe.bot?.username?.trim() : null;
if (username) telegramBotLabel = ` (@${username})`;
} catch (err) {
if (isVerbose()) {
if (shouldLogVerbose()) {
logTelegram.debug(`bot probe failed: ${String(err)}`);
}
}
@@ -2240,7 +2240,7 @@ export async function startGatewayServer(
running: false,
lastError: "disabled",
};
if (isVerbose()) {
if (shouldLogVerbose()) {
logDiscord.debug("discord provider disabled (discord.enabled=false)");
}
return;
@@ -2254,7 +2254,7 @@ export async function startGatewayServer(
lastError: "not configured",
};
// keep quiet by default; this is a normal state
if (isVerbose()) {
if (shouldLogVerbose()) {
logDiscord.debug(
"discord provider not configured (no DISCORD_BOT_TOKEN)",
);
@@ -2267,7 +2267,7 @@ export async function startGatewayServer(
const username = probe.ok ? probe.bot?.username?.trim() : null;
if (username) discordBotLabel = ` (@${username})`;
} catch (err) {
if (isVerbose()) {
if (shouldLogVerbose()) {
logDiscord.debug(`bot probe failed: ${String(err)}`);
}
}
@@ -2335,7 +2335,7 @@ export async function startGatewayServer(
lastError: "not configured",
};
// keep quiet by default; this is a normal state
if (isVerbose()) {
if (shouldLogVerbose()) {
logSignal.debug("signal provider not configured (no signal config)");
}
return;
@@ -2346,7 +2346,7 @@ export async function startGatewayServer(
running: false,
lastError: "disabled",
};
if (isVerbose()) {
if (shouldLogVerbose()) {
logSignal.debug("signal provider disabled (signal.enabled=false)");
}
return;
@@ -2367,7 +2367,7 @@ export async function startGatewayServer(
lastError: "not configured",
};
// keep quiet by default; this is a normal state
if (isVerbose()) {
if (shouldLogVerbose()) {
logSignal.debug(
"signal provider not configured (signal config present but missing required fields)",
);
@@ -2448,7 +2448,7 @@ export async function startGatewayServer(
lastError: "not configured",
};
// keep quiet by default; this is a normal state
if (isVerbose()) {
if (shouldLogVerbose()) {
logIMessage.debug(
"imessage provider not configured (no imessage config)",
);
@@ -2461,7 +2461,7 @@ export async function startGatewayServer(
running: false,
lastError: "disabled",
};
if (isVerbose()) {
if (shouldLogVerbose()) {
logIMessage.debug(
"imessage provider disabled (imessage.enabled=false)",
);
@@ -4725,7 +4725,10 @@ export async function startGatewayServer(
if (configured) {
thinkingLevel = configured;
} else {
const { provider, model } = resolveSessionModelRef(cfg, entry);
const { provider, model } = resolveSessionModelRef(
cfg,
entry,
);
const catalog = await loadGatewayModelCatalog();
thinkingLevel = resolveThinkingDefault({
cfg,
@@ -6629,7 +6632,7 @@ export async function startGatewayServer(
const { token } = resolveTelegramToken(cfg);
const result = await sendMessageTelegram(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
verbose: shouldLogVerbose(),
token: token || undefined,
});
const payload = {
@@ -6707,7 +6710,7 @@ export async function startGatewayServer(
} else {
const result = await sendMessageWhatsApp(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
verbose: shouldLogVerbose(),
});
const payload = {
runId: idem,

View File

@@ -1,5 +1,5 @@
import chalk from "chalk";
import { getLogger } from "./logging.js";
import { getLogger, isFileLogLevelEnabled } from "./logging.js";
let globalVerbose = false;
let globalYes = false;
@@ -12,14 +12,24 @@ export function isVerbose() {
return globalVerbose;
}
export function shouldLogVerbose() {
return globalVerbose || isFileLogLevelEnabled("debug");
}
export function logVerbose(message: string) {
if (!globalVerbose) return;
console.log(chalk.gray(message));
if (!shouldLogVerbose()) return;
try {
getLogger().debug({ message }, "verbose");
} catch {
// ignore logger failures to avoid breaking verbose printing
}
if (!globalVerbose) return;
console.log(chalk.gray(message));
}
export function logVerboseConsole(message: string) {
if (!globalVerbose) return;
console.log(chalk.gray(message));
}
export function setYes(v: boolean) {

View File

@@ -4,7 +4,7 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, isVerbose, logVerbose } from "../globals.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { mediaKindFromMime } from "../media/constants.js";
import type { RuntimeEnv } from "../runtime.js";
import { createIMessageRpcClient } from "./client.js";
@@ -252,7 +252,7 @@ export async function monitorIMessageProvider(
}
}
if (isVerbose()) {
if (shouldLogVerbose()) {
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
logVerbose(
`imessage inbound: chatId=${chatId ?? "unknown"} from=${ctxPayload.From} len=${body.length} preview="${preview}"`,

View File

@@ -1,5 +1,11 @@
import net from "node:net";
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
import {
danger,
info,
logVerbose,
shouldLogVerbose,
warn,
} from "../globals.js";
import { logDebug } from "../logger.js";
import { runExec } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
@@ -95,7 +101,7 @@ export async function handlePortError(
runtime.exit(1);
}
runtime.error(danger(`${context} failed: ${String(err)}`));
if (isVerbose()) {
if (shouldLogVerbose()) {
const stdout = (err as { stdout?: string })?.stdout;
const stderr = (err as { stderr?: string })?.stderr;
if (stdout?.trim()) logDebug(`stdout: ${stdout.trim()}`);

View File

@@ -1,7 +1,13 @@
import { existsSync } from "node:fs";
import chalk from "chalk";
import { promptYesNo } from "../cli/prompt.js";
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
import {
danger,
info,
logVerbose,
shouldLogVerbose,
warn,
} from "../globals.js";
import { runExec } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { ensureBinary } from "./binaries.js";
@@ -173,7 +179,7 @@ export async function ensureFunnel(
"Tip: Funnel is optional for CLAWDIS. You can keep running the web gateway without it: `pnpm clawdis gateway`",
),
);
if (isVerbose()) {
if (shouldLogVerbose()) {
if (stdout.trim()) runtime.error(chalk.gray(`stdout: ${stdout.trim()}`));
if (stderr.trim()) runtime.error(chalk.gray(`stderr: ${stderr.trim()}`));
runtime.error(err as Error);

View File

@@ -1,11 +1,4 @@
import {
danger,
info,
isVerbose,
logVerbose,
success,
warn,
} from "./globals.js";
import { danger, info, logVerboseConsole, success, warn } from "./globals.js";
import { createSubsystemLogger, getLogger } from "./logging.js";
import { defaultRuntime, type RuntimeEnv } from "./runtime.js";
@@ -67,5 +60,5 @@ export function logError(
export function logDebug(message: string) {
// Always emit to file logger (level-filtered); console only when verbose.
getLogger().debug(message);
if (isVerbose()) logVerbose(message);
logVerboseConsole(message);
}

View File

@@ -65,7 +65,6 @@ let rawConsole: {
} | null = null;
function normalizeLevel(level?: string): Level {
if (isVerbose()) return "trace";
const candidate = level ?? "info";
return ALLOWED_LEVELS.includes(candidate as Level)
? (candidate as Level)
@@ -112,6 +111,12 @@ function levelToMinLevel(level: Level): number {
return map[level];
}
export function isFileLogLevelEnabled(level: LogLevel): boolean {
const settings = cachedSettings ?? resolveSettings();
if (!cachedSettings) cachedSettings = settings;
return levelToMinLevel(level) <= levelToMinLevel(settings.level);
}
function normalizeConsoleLevel(level?: string): Level {
if (isVerbose()) return "debug";
const candidate = level ?? "info";

View File

@@ -1,7 +1,7 @@
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import { danger, isVerbose } from "../globals.js";
import { danger, shouldLogVerbose } from "../globals.js";
import { logDebug, logError } from "../logger.js";
const execFileAsync = promisify(execFile);
@@ -22,13 +22,13 @@ export async function runExec(
};
try {
const { stdout, stderr } = await execFileAsync(command, args, options);
if (isVerbose()) {
if (shouldLogVerbose()) {
if (stdout.trim()) logDebug(stdout.trim());
if (stderr.trim()) logError(stderr.trim());
}
return { stdout, stderr };
} catch (err) {
if (isVerbose()) {
if (shouldLogVerbose()) {
logError(danger(`Command failed: ${command} ${args.join(" ")}`));
}
throw err;

View File

@@ -4,7 +4,7 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, isVerbose, logVerbose } from "../globals.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { mediaKindFromMime } from "../media/constants.js";
import { saveMediaBuffer } from "../media/store.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -369,7 +369,7 @@ export async function monitorSignalProvider(
});
}
if (isVerbose()) {
if (shouldLogVerbose()) {
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
logVerbose(
`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`,

View File

@@ -12,7 +12,7 @@ import type { ReplyPayload } from "../auto-reply/types.js";
import type { ReplyToMode } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, isVerbose, logVerbose } from "../globals.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { formatErrorMessage } from "../infra/errors.js";
import { getChildLogger } from "../logging.js";
import { mediaKindFromMime } from "../media/constants.js";
@@ -173,7 +173,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
MediaUrl: media?.path,
};
if (replyTarget && isVerbose()) {
if (replyTarget && shouldLogVerbose()) {
const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120);
logVerbose(
`telegram reply-context: replyToId=${replyTarget.id} replyToSender=${replyTarget.sender} replyToBody="${preview}"`,
@@ -192,7 +192,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
});
}
if (isVerbose()) {
if (shouldLogVerbose()) {
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
logVerbose(
`telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length} preview="${preview}"`,

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isVerbose, logVerbose } from "./globals.js";
import { logVerbose, shouldLogVerbose } from "./globals.js";
export async function ensureDir(dir: string) {
await fs.promises.mkdir(dir, { recursive: true });
@@ -79,7 +79,7 @@ export function jidToE164(jid: string): string | null {
const phone = JSON.parse(data);
if (phone) return `+${phone}`;
} catch {
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`LID mapping not found for ${lid}; skipping inbound message`,
);

View File

@@ -21,7 +21,7 @@ import {
saveSessionStore,
updateLastRoute,
} from "../config/sessions.js";
import { isVerbose, logVerbose } from "../globals.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { createSubsystemLogger, getChildLogger } from "../logging.js";
@@ -325,7 +325,7 @@ export async function runWebHeartbeatOnce(opts: {
},
"heartbeat skipped",
);
if (isVerbose()) {
if (shouldLogVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (empty reply)");
}
emitHeartbeatEvent({ status: "ok-empty", to });
@@ -352,7 +352,7 @@ export async function runWebHeartbeatOnce(opts: {
{ to, reason: "heartbeat-token", rawLength: replyPayload.text?.length },
"heartbeat skipped",
);
if (isVerbose()) {
if (shouldLogVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (HEARTBEAT_OK)");
}
emitHeartbeatEvent({ status: "ok-token", to });
@@ -593,7 +593,7 @@ async function deliverWebReply(params: {
index === 0 ? remainingText.shift() || undefined : undefined;
try {
const media = await loadWebMedia(mediaUrl, maxMediaBytes);
if (isVerbose()) {
if (shouldLogVerbose()) {
logVerbose(
`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`,
);
@@ -1015,7 +1015,7 @@ export async function monitorWebProvider(
whatsappInboundLog.info(
`Inbound message ${fromDisplay} -> ${msg.to} (${msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
);
if (isVerbose()) {
if (shouldLogVerbose()) {
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
}
@@ -1268,7 +1268,7 @@ export async function monitorWebProvider(
whatsappOutboundLog.info(
`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`,
);
if (isVerbose()) {
if (shouldLogVerbose()) {
const preview =
replyPayload.text != null
? elide(replyPayload.text, 400)

View File

@@ -13,7 +13,7 @@ import {
} from "@whiskeysockets/baileys";
import { loadConfig } from "../config/config.js";
import { isVerbose, logVerbose } from "../globals.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { saveMediaBuffer } from "../media/store.js";
import {
@@ -87,7 +87,8 @@ export async function monitorWebInbox(options: {
try {
// Advertise that the gateway is online right after connecting.
await sock.sendPresenceUpdate("available");
if (isVerbose()) logVerbose("Sent global 'available' presence on connect");
if (shouldLogVerbose())
logVerbose("Sent global 'available' presence on connect");
} catch (err) {
logVerbose(
`Failed to send 'available' presence on connect: ${String(err)}`,
@@ -189,7 +190,7 @@ export async function monitorWebInbox(options: {
await sock.readMessages([
{ remoteJid, id, participant, fromMe: false },
]);
if (isVerbose()) {
if (shouldLogVerbose()) {
const suffix = participant ? ` (participant ${participant})` : "";
logVerbose(
`Marked message ${id} as read for ${remoteJid}${suffix}`,
@@ -198,7 +199,7 @@ export async function monitorWebInbox(options: {
} catch (err) {
logVerbose(`Failed to mark message ${id} read: ${String(err)}`);
}
} else if (id && isSelfChat && isVerbose()) {
} else if (id && isSelfChat && shouldLogVerbose()) {
// Self-chat mode: never auto-send read receipts (blue ticks) on behalf of the owner.
logVerbose(`Self-chat mode: skipping read receipt for ${id}`);
}

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { isVerbose, logVerbose } from "../globals.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import {
type MediaKind,
maxBytesForKind,
@@ -26,7 +26,7 @@ export async function loadWebMedia(
const optimizeAndClampImage = async (buffer: Buffer, cap: number) => {
const originalSize = buffer.length;
const optimized = await optimizeImageToJpeg(buffer, cap);
if (optimized.optimizedSize < originalSize && isVerbose()) {
if (optimized.optimizedSize < originalSize && shouldLogVerbose()) {
logVerbose(
`Optimized media from ${(originalSize / (1024 * 1024)).toFixed(2)}MB to ${(optimized.optimizedSize / (1024 * 1024)).toFixed(2)}MB (side≤${optimized.resizeSide}px, q=${optimized.quality})`,
);