openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,446 lines • 55.1 kB
JavaScript
import { c as extractErrorCode, i as isBlockedObjectKey } from "./errno-jiy5vtf-.mjs";
import { D as asOptionalRecord, _ as normalizeLowercaseStringOrEmpty, b as normalizeOptionalString, k as isRecord, w as resolveGlobalSingleton } from "./number-coercion-PJEgwXYS.mjs";
import { c as expandHomePrefix } from "./utils-Bv29YgC5.mjs";
import { D as getSealedRuntimeSecureTempRoot, a as resolveFileLogRedactOptions, d as loggingState, i as redactToolPayloadText, n as redactSecrets, nt as expectDefined, o as serializeRedactedFileLogRecord, r as redactSensitiveText, t as redactLogRecordForTransport, tt as truncateUtf16Safe, u as readLoggingConfig } from "./redact-DO6ydQMZ.mjs";
import { a as areDiagnosticsEnabledForProcess, d as isValidDiagnosticTraceFlags, f as isValidDiagnosticTraceId, i as stripAnsi, l as getActiveDiagnosticTraceContext, o as emitDiagnosticEvent, p as hasInternalDiagnosticEventInterest, s as emitDiagnosticEventWithTrustedTraceContext, u as isValidDiagnosticSpanId } from "./signal-exit-barrier-CXSq-V21.mjs";
import fsSync from "node:fs";
import path from "node:path";
import os from "node:os";
import { createHash } from "node:crypto";
import { appendRegularFile, appendRegularFileSync } from "@openclaw/fs-safe/advanced";
import fs from "node:fs/promises";
import util from "node:util";
import { Chalk } from "chalk";
import { Logger } from "tslog";
/** Clear the active progress line when it is attached to a TTY stream. */
function clearActiveProgressLine() {}
//#endregion
//#region src/global-state.ts
let globalVerbose = false;
function isVerbose() {
return globalVerbose;
}
//#endregion
//#region packages/terminal-core/src/restore.ts
const RESET_SEQUENCE = "\x1B[0m\x1B[?25h\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?2004l\x1B[<u\x1B[>4;0m";
function reportRestoreFailure(scope, err, reason) {
const suffix = reason ? ` (${reason})` : "";
const message = `[terminal] restore ${scope} failed${suffix}: ${String(err)}`;
try {
process.stderr.write(`${message}\n`);
} catch (writeErr) {
console.error(`[terminal] restore reporting failed${suffix}: ${String(writeErr)}`);
}
}
function restoreTerminalState(reason, options = {}) {
const resumeStdin = options.resumeStdinIfPaused ?? options.resumeStdin ?? false;
const resetStream = options.resetStream ?? process.stdout;
const restore = (scope, action) => {
try {
action();
} catch (err) {
reportRestoreFailure(scope, err, reason);
}
};
restore("progress line", clearActiveProgressLine);
const stdin = process.stdin;
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
restore("raw mode", () => stdin.setRawMode(false));
if (resumeStdin && typeof stdin.isPaused === "function" && stdin.isPaused()) restore("stdin resume", () => stdin.resume());
}
if (resetStream.isTTY) restore("terminal reset", () => resetStream.write(RESET_SEQUENCE));
}
//#endregion
//#region src/runtime.ts
function shouldEmitRuntimeLog(env = process.env) {
if (env.VITEST !== "true") return true;
if (env.OPENCLAW_TEST_RUNTIME_LOG === "1") return true;
return typeof console.log.mock === "object";
}
function shouldEmitRuntimeStdout(env = process.env) {
if (env.VITEST !== "true") return true;
if (env.OPENCLAW_TEST_RUNTIME_LOG === "1") return true;
return typeof process.stdout.write.mock === "object";
}
function isPipeClosedError(err) {
const code = err?.code;
return code === "EPIPE" || code === "EIO";
}
function writeStdout(value) {
if (!shouldEmitRuntimeStdout()) return;
const line = value.endsWith("\n") ? value : `${value}\n`;
try {
process.stdout.write(line);
} catch (err) {
if (isPipeClosedError(err)) return;
throw err;
}
}
function createRuntimeIo() {
return {
log: (...args) => {
if (!shouldEmitRuntimeLog()) return;
console.log(...args);
},
error: (...args) => {
console.error(...args);
},
writeStdout,
writeJson: (value, space = 2) => {
writeStdout(JSON.stringify(value, void 0, space > 0 ? space : void 0));
}
};
}
/** Keep terminal reset bytes off stdout when the invocation owns machine-readable output. */
function restoreRuntimeTerminalState(reason, options = {}) {
const resetStream = options.resetStream ?? (loggingState.forceConsoleToStderr ? process.stderr : void 0);
restoreTerminalState(reason, {
...options,
...resetStream ? { resetStream } : {}
});
}
const defaultRuntime = {
...createRuntimeIo(),
exit: (code, opts) => {
restoreRuntimeTerminalState("runtime exit", {
resumeStdinIfPaused: false,
...opts?.resetStream ? { resetStream: opts.resetStream } : {}
});
process.exit(code);
throw new Error("unreachable");
}
};
//#endregion
//#region src/logging/timestamps.ts
const validTimeZoneCache = /* @__PURE__ */ new Map();
const timestampFormatterCache = /* @__PURE__ */ new Map();
let hostTimeZone;
let lastTimestampParts;
function isValidTimeZone(tz) {
const cached = validTimeZoneCache.get(tz);
if (cached !== void 0) return cached;
let valid;
try {
new Intl.DateTimeFormat("en", { timeZone: tz }).format();
valid = true;
} catch {
valid = false;
}
validTimeZoneCache.set(tz, valid);
return valid;
}
function resolveEffectiveTimeZone(timeZone) {
const explicit = timeZone ?? process.env.TZ;
return explicit && isValidTimeZone(explicit) ? explicit : hostTimeZone ??= Intl.DateTimeFormat().resolvedOptions().timeZone;
}
function formatOffset(offsetRaw) {
return offsetRaw === "GMT" ? "+00:00" : offsetRaw.slice(3);
}
function getTimestampParts(date, timeZone) {
const effectiveTimeZone = resolveEffectiveTimeZone(timeZone);
const second = Math.floor(date.getTime() / 1e3);
if (lastTimestampParts?.second === second && lastTimestampParts.timeZone === effectiveTimeZone) return lastTimestampParts.parts;
let fmt = timestampFormatterCache.get(effectiveTimeZone);
if (!fmt) {
fmt = new Intl.DateTimeFormat("en", {
timeZone: effectiveTimeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
fractionalSecondDigits: 3,
timeZoneName: "longOffset"
});
timestampFormatterCache.set(effectiveTimeZone, fmt);
}
const parts = {};
for (const part of fmt.formatToParts(date)) parts[part.type] = part.value;
lastTimestampParts = {
second,
timeZone: effectiveTimeZone,
parts
};
return parts;
}
function formatTimestamp(date, options) {
const style = options?.style ?? "medium";
const parts = getTimestampParts(date, options?.timeZone);
const offset = formatOffset(parts.timeZoneName ?? "GMT");
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, "0");
switch (style) {
case "short": return `${parts.hour}:${parts.minute}:${parts.second}${offset}`;
case "medium": return `${parts.hour}:${parts.minute}:${parts.second}.${milliseconds}${offset}`;
case "long": return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${milliseconds}${offset}`;
}
throw new Error("Unsupported timestamp style");
}
//#endregion
//#region src/logging/json-console-line.ts
function formatJsonConsoleLine(params) {
const envelope = {
...params.meta,
time: formatTimestamp(/* @__PURE__ */ new Date(), { style: "long" }),
level: params.level,
...params.subsystem ? { subsystem: params.subsystem } : {},
message: params.message
};
return JSON.stringify(redactLogRecordForTransport(envelope, { format: "console" }));
}
/** Formats diagnostics that must bypass console capture without bypassing JSON console style. */
function formatConsoleDiagnosticLine(params) {
return (loggingState.overrideSettings?.consoleStyle ?? readLoggingConfig()?.consoleStyle) === "json" ? formatJsonConsoleLine(params) : params.message;
}
//#endregion
//#region src/logging/levels.ts
const ALLOWED_LOG_LEVELS = [
"silent",
"fatal",
"error",
"warn",
"info",
"debug",
"trace"
];
const MIN_LEVEL_BY_LOG_LEVEL = {
trace: 1,
debug: 2,
info: 3,
warn: 4,
error: 5,
fatal: 6,
silent: Number.POSITIVE_INFINITY
};
function tryParseLogLevel(level) {
if (typeof level !== "string") return;
const candidate = level.trim();
return ALLOWED_LOG_LEVELS.includes(candidate) ? candidate : void 0;
}
function normalizeLogLevel(level, fallback = "info") {
return tryParseLogLevel(level) ?? fallback;
}
function levelToMinLevel(level) {
return MIN_LEVEL_BY_LOG_LEVEL[level];
}
function isLogLevelEnabled(level, minimum) {
return level !== "silent" && minimum !== "silent" && levelToMinLevel(level) >= levelToMinLevel(minimum);
}
//#endregion
//#region src/logging/env-log-level.ts
/** Resolves OPENCLAW_LOG_LEVEL once per value, warning only when the invalid value changes. */
function resolveEnvLogLevelOverride() {
const trimmed = normalizeOptionalString(process.env.OPENCLAW_LOG_LEVEL) ?? "";
if (!trimmed) {
loggingState.invalidEnvLogLevelValue = null;
return;
}
const parsed = tryParseLogLevel(trimmed);
if (parsed) {
loggingState.invalidEnvLogLevelValue = null;
return parsed;
}
if (loggingState.invalidEnvLogLevelValue !== trimmed) {
loggingState.invalidEnvLogLevelValue = trimmed;
const message = `[openclaw] Ignoring invalid OPENCLAW_LOG_LEVEL="${redactToolPayloadText(trimmed)}" (allowed: ${ALLOWED_LOG_LEVELS.join("|")}).`;
process.stderr.write(`${formatConsoleDiagnosticLine({
level: "warn",
message
})}\n`);
}
}
//#endregion
//#region src/infra/tmp-openclaw-dir.ts
/** Preferred shared OpenClaw temp root on POSIX systems when ownership and permissions are safe. */
const DEFAULT_POSIX_TMP_ROOT = "/tmp/openclaw";
let resolveSecureTempRootRuntime;
function loadResolveSecureTempRoot() {
if (resolveSecureTempRootRuntime) return resolveSecureTempRootRuntime;
const injected = getSealedRuntimeSecureTempRoot();
if (injected) {
resolveSecureTempRootRuntime = injected;
return injected;
}
if (typeof SEALED_RUNTIME_BUILD === "boolean" && SEALED_RUNTIME_BUILD) throw new Error("sealed temp-root runtime was not registered before use");
if (typeof process.getBuiltinModule !== "function") throw new Error("Node module loading is unavailable for secure temp-root resolution");
resolveSecureTempRootRuntime = process.getBuiltinModule("module").createRequire(import.meta.url)("@openclaw/fs-safe/secure-temp-root").resolveSecureTempRoot;
return resolveSecureTempRootRuntime;
}
/** Resolves a safe OpenClaw temp root, falling back to user-scoped os.tmpdir paths when needed. */
function resolvePreferredOpenClawTmpDir(options = {}) {
return loadResolveSecureTempRoot()({
...options,
preferredDir: options.preferredDir ?? "/tmp/openclaw",
fallbackPrefix: "openclaw",
warningPrefix: "[openclaw]",
unsafeFallbackLabel: "OpenClaw temp dir",
skipPreferredOnWindows: true
});
}
//#endregion
//#region src/logging/log-file-shared.ts
const LOG_PREFIX = "openclaw";
const LOG_SUFFIX = ".log";
function canUseNodeFs() {
try {
return process.getBuiltinModule?.("fs") !== void 0;
} catch {
return false;
}
}
function formatLocalDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}
//#endregion
//#region src/logging/log-file-path.ts
const ROLLING_LOG_FILE_RE = /^(openclaw(?:-[a-z0-9-]+)?)-(\d{4}-\d{2}-\d{2})\.log$/u;
const MAX_LOG_PROFILE_SEGMENT_LENGTH = 220;
function encodeLogProfileSegment(profile) {
let encoded = "";
for (const char of profile) if (/^[a-z0-9]$/u.test(char)) encoded += char;
else if (char === "-") encoded += "--";
else if (char === "_") encoded += "-0";
else if (/^[A-Z]$/u.test(char)) encoded += `-1${char.toLowerCase()}`;
else encoded += `-2${char.codePointAt(0)?.toString(16) ?? "0"}-`;
return encoded;
}
function resolveLogProfileSegment(env) {
const profile = env.OPENCLAW_PROFILE?.trim();
if (!profile || profile.toLowerCase() === "default") return null;
const encoded = encodeLogProfileSegment(profile);
if (encoded.length <= MAX_LOG_PROFILE_SEGMENT_LENGTH) return encoded;
return `-3${createHash("sha256").update(profile).digest("hex")}`;
}
/** Resolves today's default rolling log path for the active CLI profile. */
function resolveDefaultRollingLogFile(options) {
const date = options?.date ?? /* @__PURE__ */ new Date();
const env = options?.env ?? process.env;
const logDir = options?.logDir ?? (canUseNodeFs() ? resolvePreferredOpenClawTmpDir() : "/tmp/openclaw");
const profileSegment = resolveLogProfileSegment(env);
const profileSuffix = profileSegment ? `-${profileSegment}` : "";
return path.join(logDir, `${LOG_PREFIX}${profileSuffix}-${formatLocalDate(date)}${LOG_SUFFIX}`);
}
/** Returns whether a configured path had the legacy default rolling filename shape. */
function isLegacyRollingLogFilePath(file) {
const base = path.basename(file);
return base === `openclaw-YYYY-MM-DD.log` || ROLLING_LOG_FILE_RE.exec(base)?.[1] === "openclaw";
}
/** Advances a rolling log path to the requested date while preserving its profile family. */
function resolveRollingLogFilePathForDate(file, date) {
const match = ROLLING_LOG_FILE_RE.exec(path.basename(file));
if (!match) return isLegacyRollingLogFilePath(file) ? path.join(path.dirname(file), `${LOG_PREFIX}-${formatLocalDate(date)}${LOG_SUFFIX}`) : file;
return path.join(path.dirname(file), `${match[1]}-${formatLocalDate(date)}${LOG_SUFFIX}`);
}
//#endregion
//#region src/logging/logger-file-message.ts
const MAX_FILE_LOG_MESSAGE_CHARS = 4096;
function clampMessage(text) {
return text.length > MAX_FILE_LOG_MESSAGE_CHARS ? `${truncateUtf16Safe(text, MAX_FILE_LOG_MESSAGE_CHARS)}...(truncated)` : text;
}
function stringifyFileLogMessagePart(value, json) {
if (json) return JSON.stringify(value);
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
if (isRecord(value) && typeof value.message === "string") return value.message;
}
function buildFileLogMessage(record, parts) {
const text = [];
const spans = [];
let length = 0;
for (const { key, json, primitiveText } of parts) {
const value = record[key];
const part = primitiveText ?? stringifyFileLogMessagePart(value, json);
if (!part?.trim()) continue;
if (text.length > 0) length += 1;
spans.push({
key,
json,
messageField: !json && isRecord(value),
start: length,
...primitiveText === void 0 ? {} : { primitiveLength: primitiveText.length }
});
text.push(part);
length += part.length;
}
if (text.length === 0) return;
const joined = text.join(" ");
const prefix = truncateUtf16Safe(joined, MAX_FILE_LOG_MESSAGE_CHARS);
const textValue = clampMessage(joined);
return {
text: textValue,
contentLength: prefix.length,
finish: (value) => value === textValue ? value : clampMessage(value),
parts: spans
};
}
//#endregion
//#region src/logging/logger-file-transport.ts
const DEFAULT_MAX_QUEUED_RECORDS = 4096;
const MAX_APPEND_BATCH_BYTES = 65536;
const MAX_ROTATED_LOG_FILES = 5;
const MAX_TRACKED_APPEND_FAILURE_FILES = 64;
let queue = [];
let queueStart = 0;
let activeBatch = null;
let activeIndex = 0;
let droppedCount = 0;
let droppedTarget = null;
let maxQueuedRecords = DEFAULT_MAX_QUEUED_RECORDS;
let scheduledFlush = null;
let flushPromise = null;
let drainGeneration = 0;
let processExiting = false;
let processHooksInstalled = false;
let appendFile = appendRegularFile;
const warnedRotationFiles = /* @__PURE__ */ new Map();
const warnedAppendFiles = /* @__PURE__ */ new Set();
let appendFailureTrackingSaturated = false;
function rotatedLogPath(file, index) {
const ext = path.extname(file);
return `${file.slice(0, file.length - ext.length)}.${index}${ext}`;
}
/**
* Creates a log directory and its missing parents with a bounded number of mkdir calls.
* Node's recursive mkdirSync retries forever when a parent exists but refuses new
* children (a deleted directory that is still mounted, such as a stale systemd
* PrivateTmp), which would block the event loop inside the logger.
*/
function ensureLogDirectorySync(dir, createParents = true) {
try {
fsSync.mkdirSync(dir);
} catch (error) {
const code = extractErrorCode(error);
if (code === "EEXIST" && fsSync.statSync(dir).isDirectory()) return;
const parent = path.dirname(dir);
if (!createParents || code !== "ENOENT" || parent === dir) throw error;
ensureLogDirectorySync(parent);
ensureLogDirectorySync(dir, false);
}
}
function rotateLogFile(file) {
try {
ensureLogDirectorySync(path.dirname(file));
fsSync.rmSync(rotatedLogPath(file, MAX_ROTATED_LOG_FILES), { force: true });
for (let index = 4; index >= 1; index -= 1) {
const from = rotatedLogPath(file, index);
if (fsSync.existsSync(from)) fsSync.renameSync(from, rotatedLogPath(file, index + 1));
}
if (fsSync.existsSync(file)) fsSync.renameSync(file, rotatedLogPath(file, 1));
return true;
} catch {
return false;
}
}
async function getCurrentLogFileBytes(file) {
try {
return (await fs.stat(file)).size;
} catch {
return 0;
}
}
function getCurrentLogFileBytesSync(file) {
try {
return fsSync.statSync(file).size;
} catch {
return 0;
}
}
function buildDroppedMarker(target, count) {
const date = /* @__PURE__ */ new Date();
const message = `[openclaw] file log queue overflow; dropped ${count} oldest record${count === 1 ? "" : "s"}`;
const record = {
0: message,
_meta: {
date,
hostname: target.hostname,
logLevelName: "WARN",
name: "openclaw"
},
time: formatTimestamp(date, { style: "long" }),
hostname: target.hostname,
message,
dropped: count
};
return {
...target,
payload: `${serializeRedactedFileLogRecord(record)}\n`
};
}
function writeFileTransportWarning(message, synchronous) {
try {
const line = `${formatConsoleDiagnosticLine({
level: "warn",
message: redactSensitiveText(message)
})}\n`;
if (synchronous) fsSync.writeSync(process.stderr.fd, line);
else process.stderr.write(line);
return true;
} catch {
return false;
}
}
function warnAboutRotationFailure(entry, synchronous) {
if (warnedRotationFiles.get(entry.file) === entry.maxFileBytes) return;
warnedRotationFiles.set(entry.file, entry.maxFileBytes);
writeFileTransportWarning(`[openclaw] log file rotation failed; continuing writes file=${entry.file} maxFileBytes=${entry.maxFileBytes}`, synchronous);
}
function warnAboutAppendFailure(entry, synchronous) {
if (warnedAppendFiles.has(entry.file)) return;
const saturated = warnedAppendFiles.size >= MAX_TRACKED_APPEND_FAILURE_FILES;
if (saturated && appendFailureTrackingSaturated) return;
if (!writeFileTransportWarning(saturated ? "[openclaw] log file append failure diagnostics saturated; suppressing new file targets" : `[openclaw] log file append failed; records dropped; check that the path is a writable regular file; file=${entry.file}`, synchronous)) return;
if (saturated) appendFailureTrackingSaturated = true;
else warnedAppendFiles.add(entry.file);
}
function clearAppendFailure(entry) {
if (warnedAppendFiles.delete(entry.file)) appendFailureTrackingSaturated = false;
}
function claimQueuedEntries() {
const entries = queueStart === 0 ? queue : [...queue.slice(queueStart), ...queue.slice(0, queueStart)];
queue = [];
queueStart = 0;
if (droppedCount > 0 && droppedTarget) entries.unshift(buildDroppedMarker(droppedTarget, droppedCount));
droppedCount = 0;
droppedTarget = null;
return entries;
}
function prepareWrite(entry, cursor, synchronous, entries, index) {
let payloadBytes = Buffer.byteLength(entry.payload, "utf8");
if (cursor.bytes > 0 && cursor.bytes + payloadBytes > entry.maxFileBytes) {
if (rotateLogFile(entry.file)) {
cursor.bytes = 0;
warnedRotationFiles.delete(entry.file);
} else warnAboutRotationFailure(entry, synchronous);
}
let nextIndex = index + 1;
if (synchronous) return {
payload: entry.payload,
payloadBytes,
nextIndex
};
const payloads = [entry.payload];
for (; nextIndex < entries.length; nextIndex += 1) {
const next = entries[nextIndex];
if (!next || next.file !== entry.file || next.maxFileBytes !== entry.maxFileBytes) break;
const nextBytes = Buffer.byteLength(next.payload, "utf8");
if (payloadBytes + nextBytes > MAX_APPEND_BATCH_BYTES || cursor.bytes + payloadBytes + nextBytes > entry.maxFileBytes) break;
payloads.push(next.payload);
payloadBytes += nextBytes;
}
return {
payload: payloads.join(""),
payloadBytes,
nextIndex
};
}
async function writeEntries(entries, generation) {
const cursors = /* @__PURE__ */ new Map();
for (let index = 0; index < entries.length;) {
if (generation !== drainGeneration || processExiting) return;
const entry = entries[index];
if (!entry) return;
let cursor = cursors.get(entry.file);
if (!cursor) {
cursor = { bytes: await getCurrentLogFileBytes(entry.file) };
if (generation !== drainGeneration || processExiting) return;
cursors.set(entry.file, cursor);
}
const batch = prepareWrite(entry, cursor, false, entries, index);
activeIndex = batch.nextIndex;
try {
await appendFile({
filePath: entry.file,
content: batch.payload
});
cursor.bytes += batch.payloadBytes;
clearAppendFailure(entry);
} catch {
warnAboutAppendFailure(entry, false);
} finally {
for (const written of entries.slice(index, batch.nextIndex)) written.payload = "";
index = batch.nextIndex;
}
}
}
function writeEntriesSync(entries) {
const cursors = /* @__PURE__ */ new Map();
for (let index = 0; index < entries.length;) {
const entry = entries[index];
if (!entry) return;
let cursor = cursors.get(entry.file);
if (!cursor) {
cursor = { bytes: getCurrentLogFileBytesSync(entry.file) };
cursors.set(entry.file, cursor);
}
const batch = prepareWrite(entry, cursor, true, entries, index);
try {
appendRegularFileSync({
filePath: entry.file,
content: batch.payload
});
cursor.bytes += batch.payloadBytes;
clearAppendFailure(entry);
} catch {
warnAboutAppendFailure(entry, true);
}
entry.payload = "";
index = batch.nextIndex;
}
}
async function runFlushLoop() {
const generation = drainGeneration;
for (;;) {
if (generation !== drainGeneration || processExiting) return;
const entries = claimQueuedEntries();
if (entries.length === 0) return;
activeBatch = entries;
activeIndex = 0;
await writeEntries(entries, generation);
if (generation !== drainGeneration) return;
activeBatch = null;
activeIndex = 0;
}
}
function startFlush() {
if (flushPromise || processExiting) return;
const running = runFlushLoop().catch(() => void 0);
flushPromise = running;
running.then(() => {
if (flushPromise === running) flushPromise = null;
if (queue.length > 0 || droppedCount > 0) scheduleFlush();
});
}
function scheduleFlush() {
if (scheduledFlush || flushPromise || processExiting) return;
scheduledFlush = setImmediate(() => {
scheduledFlush = null;
startFlush();
});
}
function handleProcessBeforeExit() {
flushFileLogQueue();
}
function handleProcessExit() {
processExiting = true;
drainFileLogQueueSync();
}
function installProcessHooks() {
if (processHooksInstalled) return;
processHooksInstalled = true;
process.on("beforeExit", handleProcessBeforeExit);
process.on("exit", handleProcessExit);
}
function removeProcessHooks() {
if (!processHooksInstalled) return;
process.removeListener("beforeExit", handleProcessBeforeExit);
process.removeListener("exit", handleProcessExit);
processHooksInstalled = false;
}
/** Enqueues one serialized record without waiting for filesystem I/O. */
function enqueueFileLog(entry) {
if (processExiting) {
writeEntriesSync([entry]);
return;
}
installProcessHooks();
if (queue.length >= maxQueuedRecords) {
const dropped = queue[queueStart];
if (dropped) {
dropped.payload = "";
droppedTarget ??= dropped;
droppedCount += 1;
}
queue[queueStart] = entry;
queueStart = (queueStart + 1) % queue.length;
} else queue.push(entry);
scheduleFlush();
}
/** Waits until every record currently queued for the async transport has settled. */
async function flushFileLogQueue() {
for (;;) {
if (scheduledFlush) {
clearImmediate(scheduledFlush);
scheduledFlush = null;
}
if (!flushPromise && (queue.length > 0 || droppedCount > 0)) startFlush();
const running = flushPromise;
if (!running) return;
await running;
}
}
/** Synchronously rescues pending records for process.exit() and crash-adjacent paths. */
function drainFileLogQueueSync() {
if (scheduledFlush) {
clearImmediate(scheduledFlush);
scheduledFlush = null;
}
drainGeneration += 1;
const entries = activeBatch ? activeBatch.slice(activeIndex) : [];
activeBatch = null;
activeIndex = 0;
entries.push(...claimQueuedEntries());
writeEntriesSync(entries);
}
function setFileLogQueueMaxRecordsForTests(value) {
maxQueuedRecords = Math.max(1, value ?? DEFAULT_MAX_QUEUED_RECORDS);
}
function setFileLogAppenderForTests(value) {
appendFile = value ?? appendRegularFile;
}
function resetFileLogTransportForTests() {
drainFileLogQueueSync();
removeProcessHooks();
processExiting = false;
appendFile = appendRegularFile;
maxQueuedRecords = DEFAULT_MAX_QUEUED_RECORDS;
warnedRotationFiles.clear();
warnedAppendFiles.clear();
appendFailureTrackingSaturated = false;
}
const fileLogTransport = resolveGlobalSingleton(Symbol.for("openclaw.fileLogTransport"), () => {
if (process.env.VITEST !== "true") installProcessHooks();
return {
drainSync: drainFileLogQueueSync,
enqueue: enqueueFileLog,
flush: flushFileLogQueue,
resetForTests: resetFileLogTransportForTests,
setAppenderForTests: setFileLogAppenderForTests,
setMaxQueuedRecordsForTests: setFileLogQueueMaxRecordsForTests
};
});
//#endregion
//#region src/logging/logger.ts
const DEFAULT_LOG_FILE = `${DEFAULT_POSIX_TMP_ROOT}/openclaw.log`;
const MAX_LOG_AGE_MS = 864e5;
const DEFAULT_MAX_LOG_FILE_BYTES = 104857600;
let cachedHostname = null;
const MAX_DIAGNOSTIC_LOG_BINDINGS_JSON_CHARS = 8192;
const MAX_DIAGNOSTIC_LOG_MESSAGE_CHARS = 4096;
const MAX_DIAGNOSTIC_LOG_ATTRIBUTE_COUNT = 32;
const MAX_DIAGNOSTIC_LOG_ATTRIBUTE_VALUE_CHARS = 2048;
const MAX_DIAGNOSTIC_LOG_NAME_CHARS = 120;
const MAX_FILE_LOG_CONTEXT_VALUE_CHARS = 512;
const DIAGNOSTIC_LOG_ATTRIBUTE_KEY_RE = /^[A-Za-z0-9_.:-]{1,64}$/u;
function clampLogText(value, maxChars) {
return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value;
}
function sanitizeDiagnosticLogText(value, maxChars) {
return clampLogText(redactSensitiveText(clampLogText(value, maxChars)), maxChars);
}
function normalizeDiagnosticLogName(value) {
if (!value || value.trim().startsWith("{")) return;
const sanitized = sanitizeDiagnosticLogText(value.trim(), MAX_DIAGNOSTIC_LOG_NAME_CHARS);
return DIAGNOSTIC_LOG_ATTRIBUTE_KEY_RE.test(sanitized) ? sanitized : void 0;
}
function assignDiagnosticLogAttribute(attributes, state, key, value) {
if (state.count >= MAX_DIAGNOSTIC_LOG_ATTRIBUTE_COUNT) return;
const normalizedKey = key.trim();
if (isBlockedObjectKey(normalizedKey)) return;
if (redactSensitiveText(normalizedKey) !== normalizedKey) return;
if (!DIAGNOSTIC_LOG_ATTRIBUTE_KEY_RE.test(normalizedKey)) return;
let attribute;
if (typeof value === "string") attribute = sanitizeDiagnosticLogText(value, MAX_DIAGNOSTIC_LOG_ATTRIBUTE_VALUE_CHARS);
else if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) attribute = value;
else return;
attributes[normalizedKey] = attribute;
state.count += 1;
}
function addDiagnosticLogAttributesFrom(attributes, state, source) {
if (!source) return;
for (const key in source) {
if (state.count >= MAX_DIAGNOSTIC_LOG_ATTRIBUTE_COUNT) break;
if (!Object.hasOwn(source, key) || key === "trace") continue;
assignDiagnosticLogAttribute(attributes, state, key, source[key]);
}
}
function isPlainLogRecordObject(value) {
if (!isRecord(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function normalizeTraceContext(value) {
const candidate = asOptionalRecord(value);
if (!candidate || !isValidDiagnosticTraceId(candidate.traceId)) return;
if (candidate.spanId !== void 0 && !isValidDiagnosticSpanId(candidate.spanId)) return;
if (candidate.parentSpanId !== void 0 && !isValidDiagnosticSpanId(candidate.parentSpanId)) return;
if (candidate.traceFlags !== void 0 && !isValidDiagnosticTraceFlags(candidate.traceFlags)) return;
return {
traceId: candidate.traceId,
...candidate.spanId ? { spanId: candidate.spanId } : {},
...candidate.parentSpanId ? { parentSpanId: candidate.parentSpanId } : {},
...candidate.traceFlags ? { traceFlags: candidate.traceFlags } : {}
};
}
function extractTraceContext(value) {
return normalizeTraceContext(value) ?? normalizeTraceContext(asOptionalRecord(value)?.trace);
}
function getSortedNumericLogEntries(logObj) {
return Object.entries(logObj).filter(([key]) => /^\d+$/.test(key)).toSorted((a, b) => Number(a[0]) - Number(b[0]));
}
function normalizeFileLogContextValue(value) {
if (typeof value === "string") {
const normalized = value.trim();
return normalized ? clampLogText(normalized, MAX_FILE_LOG_CONTEXT_VALUE_CHARS) : void 0;
}
if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return String(value);
}
function readFirstContextString(sources, keys) {
for (const source of sources) {
if (!source) continue;
for (const key of keys) {
const value = normalizeFileLogContextValue(source[key]);
if (value) return value;
}
}
}
function resolveLogHostname() {
if (cachedHostname) return cachedHostname;
const hostname = os.hostname().trim();
if (!hostname) return "unknown";
cachedHostname = hostname;
return hostname;
}
function withResolvedLogMetaHostname(meta, hostname) {
return isRecord(meta) ? {
...meta,
hostname
} : meta;
}
function extractLogBindingPrefix(numericArgs) {
if (typeof numericArgs[0] === "string" && numericArgs[0].length <= MAX_DIAGNOSTIC_LOG_BINDINGS_JSON_CHARS && numericArgs[0].trim().startsWith("{")) try {
const parsed = JSON.parse(numericArgs[0]);
if (isRecord(parsed)) return {
bindings: parsed,
args: numericArgs.slice(1)
};
} catch {}
return { args: numericArgs };
}
function resolveLogTraceContext(bindings, numericArgs) {
for (const value of [bindings, ...numericArgs]) {
const trace = extractTraceContext(value);
if (trace) return {
trace,
trustedTraceContext: false
};
}
const activeTrace = getActiveDiagnosticTraceContext();
return activeTrace ? {
trace: activeTrace,
trustedTraceContext: true
} : { trustedTraceContext: false };
}
function prepareFileLogRecord(logObj) {
const entries = getSortedNumericLogEntries(logObj);
const { bindings, args } = extractLogBindingPrefix(entries.map(([, value]) => value));
const { trace } = resolveLogTraceContext(bindings, args);
const structuredArg = isPlainLogRecordObject(args[0]) ? args[0] : void 0;
const sources = [
structuredArg,
bindings,
logObj
];
const metadataCount = structuredArg && typeof structuredArg.message !== "string" ? 1 : 0;
const messageParts = entries.slice(entries.length - args.length + metadataCount).map(([key, value]) => {
const part = {
key,
json: value != null && ![
"string",
"number",
"boolean",
"bigint"
].includes(typeof value) && !(value instanceof Error) && !(isPlainLogRecordObject(value) && typeof value.message === "string")
};
if (typeof value === "number" && !Number.isFinite(value)) part.primitiveText = String(value);
return part;
});
const agentId = readFirstContextString(sources, ["agent_id", "agentId"]);
const sessionId = readFirstContextString(sources, [
"session_id",
"sessionId",
"sessionKey"
]);
const channel = readFirstContextString(sources, ["channel", "messageProvider"]);
return {
fields: {
hostname: resolveLogHostname(),
...agentId ? { agent_id: agentId } : {},
...sessionId ? { session_id: sessionId } : {},
...channel ? { channel } : {},
...trace
},
messageParts
};
}
function buildDiagnosticLogRecord(logObj) {
const meta = logObj["_meta"];
const { bindings, args: numericArgs } = extractLogBindingPrefix(getSortedNumericLogEntries(logObj).map(([, value]) => value));
const { trace, trustedTraceContext } = resolveLogTraceContext(bindings, numericArgs);
const structuredArg = numericArgs[0];
const structuredBindings = isPlainLogRecordObject(structuredArg) ? structuredArg : void 0;
if (structuredBindings) numericArgs.shift();
let message = "";
if (numericArgs.length > 0 && typeof numericArgs[numericArgs.length - 1] === "string") message = sanitizeDiagnosticLogText(String(numericArgs.pop()), MAX_DIAGNOSTIC_LOG_MESSAGE_CHARS);
else if (numericArgs.length === 1 && (typeof numericArgs[0] === "number" || typeof numericArgs[0] === "boolean")) {
message = String(numericArgs[0]);
numericArgs.length = 0;
}
if (!message) message = "log";
const attributes = Object.create(null);
const attributeState = { count: 0 };
addDiagnosticLogAttributesFrom(attributes, attributeState, bindings);
addDiagnosticLogAttributesFrom(attributes, attributeState, structuredBindings);
const code = {};
if (meta?.path?.fileLine) {
const line = Number(meta.path.fileLine);
if (Number.isFinite(line)) code.line = line;
}
if (meta?.path?.method) code.functionName = sanitizeDiagnosticLogText(meta.path.method, MAX_DIAGNOSTIC_LOG_NAME_CHARS);
const loggerName = normalizeDiagnosticLogName(meta?.name);
const loggerParents = meta?.parentNames?.map(normalizeDiagnosticLogName).filter((name) => Boolean(name));
return {
event: {
type: "log.record",
level: meta?.logLevelName ?? "INFO",
message,
...loggerName ? { loggerName } : {},
...loggerParents?.length ? { loggerParents } : {},
...Object.keys(attributes).length > 0 ? { attributes } : {},
...Object.keys(code).length > 0 ? { code } : {},
...trace ? { trace } : {}
},
trustedTraceContext
};
}
function attachDiagnosticEventTransport(logger) {
logger.attachTransport({
format: () => "",
write: (logObj) => {
if (!areDiagnosticsEnabledForProcess() || !hasInternalDiagnosticEventInterest("log.record")) return;
try {
const record = buildDiagnosticLogRecord(redactSecrets(logObj));
(record.trustedTraceContext ? emitDiagnosticEventWithTrustedTraceContext : emitDiagnosticEvent)(record.event);
} catch {}
}
});
}
function canUseSilentVitestFileLogFastPath(envLevel) {
return process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG !== "1" && !envLevel && !loggingState.overrideSettings;
}
function resolveDefaultActiveLogFile() {
if (process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG === "1") return path.join(process.cwd(), ".artifacts", "test-logs", `${LOG_PREFIX}-vitest-${process.pid}-${formatLocalDate(/* @__PURE__ */ new Date())}${LOG_SUFFIX}`);
return resolveDefaultRollingLogFile();
}
function resolveSettings() {
if (!canUseNodeFs()) return {
level: "silent",
file: DEFAULT_LOG_FILE,
maxFileBytes: DEFAULT_MAX_LOG_FILE_BYTES,
rolling: false
};
const envLevel = resolveEnvLogLevelOverride();
if (canUseSilentVitestFileLogFastPath(envLevel)) return {
level: "silent",
file: resolveDefaultRollingLogFile(),
maxFileBytes: DEFAULT_MAX_LOG_FILE_BYTES,
rolling: true
};
const cfg = loggingState.overrideSettings ?? readLoggingConfig();
const defaultLevel = process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG !== "1" ? "silent" : "info";
const fromConfig = normalizeLogLevel(cfg?.level, defaultLevel);
const level = envLevel ?? fromConfig;
const rolling = cfg?.file ? isLegacyRollingLogFilePath(cfg.file) : true;
return {
level,
file: resolveActiveLogFileWithMode(cfg?.file ?? resolveDefaultActiveLogFile(), rolling),
maxFileBytes: resolveMaxLogFileBytes(cfg?.maxFileBytes),
rolling
};
}
function getRuntimeSettings() {
const settings = loggingState.cachedSettings ?? resolveSettings();
loggingState.cachedSettings = settings;
return settings;
}
function isFileLogLevelEnabled(level) {
return isLogLevelEnabled(level, getRuntimeSettings().level);
}
function inheritLogLevel(logger, getLevel) {
let resolveLevel = getLevel;
Object.defineProperty(logger.settings, "minLevel", {
configurable: true,
enumerable: true,
get: () => resolveLevel(),
set: (level) => {
resolveLevel = () => level;
}
});
}
var RuntimeLogger = class extends Logger {
getSubLogger(settings, logObj) {
const minLevel = settings?.minLevel ?? this.settings.minLevel;
const child = super.getSubLogger({
...settings,
minLevel: minLevel === Infinity ? levelToMinLevel("fatal") : minLevel
}, logObj);
if (settings?.minLevel == null) inheritLogLevel(child, () => this.settings.minLevel);
else if (minLevel === Infinity) child.settings.minLevel = minLevel;
return child;
}
};
function buildLogger() {
const logger = new RuntimeLogger({
name: "openclaw",
mask: { keys: [] },
meta: { property: "_meta" },
minLevel: levelToMinLevel("fatal"),
type: "hidden"
});
inheritLogLevel(logger, () => levelToMinLevel(getRuntimeSettings().level));
let activeFile;
logger.attachTransport({
format: () => "",
write: (logObj) => {
try {
const settings = getRuntimeSettings();
if (settings.level === "silent") return;
const nextActiveFile = resolveActiveLogFileWithMode(settings.file, settings.rolling);
if (nextActiveFile !== activeFile) {
activeFile = nextActiveFile;
try {
ensureLogDirectorySync(path.dirname(activeFile));
if (settings.rolling) pruneOldRollingLogs(path.dirname(activeFile));
} catch {}
}
const time = formatTimestamp(logObj.date ?? /* @__PURE__ */ new Date(), { style: "long" });
const { fields, messageParts } = prepareFileLogRecord(logObj);
const line = serializeRedactedFileLogRecord({
...logObj,
_meta: withResolvedLogMetaHostname(logObj["_meta"], fields.hostname),
time,
...fields
}, {
deriveMessage: (materialized) => buildFileLogMessage(materialized, messageParts),
decodedOptions: resolveFileLogRedactOptions()
});
fileLogTransport.enqueue({
file: activeFile,
hostname: fields.hostname,
maxFileBytes: settings.maxFileBytes,
payload: `${line}\n`
});
} catch {}
}
});
attachDiagnosticEventTransport(logger);
return logger;
}
function resolveMaxLogFileBytes(raw) {
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw);
return DEFAULT_MAX_LOG_FILE_BYTES;
}
function getLogger() {
const cachedLogger = loggingState.cachedLogger;
if (cachedLogger) return cachedLogger;
getRuntimeSettings();
const logger = buildLogger();
loggingState.cachedLogger = logger;
return logger;
}
function getChildLogger(bindings, opts) {
const base = getLogger();
const name = bindings ? JSON.stringify(bindings) : void 0;
return base.getSubLogger({
name,
prefix: bindings ? [name ?? ""] : [],
...opts?.level ? { minLevel: levelToMinLevel(opts.level) } : {}
});
}
function resolveActiveLogFileWithMode(file, rolling) {
const expandedFile = expandHomePrefix(file);
return rolling ? resolveRollingLogFilePathForDate(expandedFile, /* @__PURE__ */ new Date()) : expandedFile;
}
function pruneOldRollingLogs(dir) {
try {
const entries = fsSync.readdirSync(dir, { withFileTypes: true });
const cutoff = Date.now() - MAX_LOG_AGE_MS;
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith(`openclaw-`) || !entry.name.endsWith(".log")) continue;
const fullPath = path.join(dir, entry.name);
try {
if (fsSync.statSync(fullPath).mtimeMs < cutoff) fsSync.rmSync(fullPath, { force: true });
} catch {}
}
} catch {}
}
//#endregion
//#region src/logging/console.ts
function normalizeConsoleLevel(level) {
if (isVerbose()) return "debug";
if (!level && process.env.VITEST === "true" && process.env.OPENCLAW_TEST_CONSOLE !== "1") return "silent";
return normalizeLogLevel(level, "info");
}
function normalizeConsoleStyle(style) {
if (style === "compact" || style === "json" || style === "pretty") return style;
if (!process.stdout.isTTY) return "compact";
return "pretty";
}
function resolveConsoleSettings() {
const envLevel = resolveEnvLogLevelOverride();
if (process.env.VITEST === "true" && process.env.OPENCLAW_TEST_CONSOLE !== "1" && !isVerbose() && !envLevel && !loggingState.overrideSettings) return {
level: "silent",
style: normalizeConsoleStyle(void 0)
};
const cfg = loggingState.overrideSettings ?? readLoggingConfig();
return {
level: envLevel ?? normalizeConsoleLevel(cfg?.consoleLevel),
style: normalizeConsoleStyle(cfg?.consoleStyle)
};
}
function getConsoleSettings() {
const cached = loggingState.cachedConsoleSettings;
if (cached) return cached;
const settings = resolveConsoleSettings();
loggingState.cachedConsoleSettings = settings;
return settings;
}
function shouldLogSubsystemToConsole(subsystem) {
const filter = loggingState.consoleSubsystemFilter;
if (!filter || filter.length === 0) return true;
const normalizedSubsystem = normalizeOptionalString(subsystem);
if (!normalizedSubsystem) return false;
return filter.some((prefix) => normalizedSubsystem === prefix || normalizedSubsystem.startsWith(`${prefix}/`));
}
const SUPPRESSED_CONSOLE_PREFIXES = [
"Closing session:",
"Opening session:",
"Removing old closed session:",
"Session already closed",
"Session already open"
];
`${process.release.name}${process.pid}`;
function shouldSuppressConsoleMessage(message) {
return SUPPRESSED_CONSOLE_PREFIXES.some((prefix) => message.startsWith(prefix));
}
function isEpipeError(err) {
const code = err?.code;
return code === "EPIPE" || code === "EIO";
}
function formatConsoleTimestamp(style) {
const now = /* @__PURE__ */ new Date();
if (style === "pretty") return formatTimestamp(now, { style: "short" }).replace(/[+-]\d{2}:\d{2}$/, "");
return formatTimestamp(now, { style: "long" });
}
function captureConsoleTraceStack(message, caller) {
const trace = new Error(message);
trace.name = "Trace";
Error.captureStackTrace(trace, caller);
return trace.stack === void 0 ? `Trace: ${message}` : typeof trace.stack === "string" ? trace.stack : util.format(trace.stack);
}
function hasTimestampPrefix(value) {
return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)/.test(value);
}
function writeFormattedConsoleOutput(params) {
const consoleStyle = getConsoleSettings().style;
const trimmed = consoleStyle !== "json" && loggingState.consoleTimestampPrefix ? stripAnsi(params.formatted).trimStart() : "";
const timestamp = trimmed && !hasTimestampPrefix(trimmed) ? formatConsoleTimestamp(consoleStyle) : "";
const stack = params.level === "trace" && params.caller ? captureConsoleTraceStack(params.formatted, params.caller) : void 0;
try {
const rendered = consoleStyle === "json" ? formatJsonConsoleLine({
level: params.level,
message: stripAnsi(params.formatted),
meta: stack === void 0 ? void 0 : { stack: stripAnsi(stack) }
}) : redactSensitiveText(stack ?? params.formatted);
const line = timestamp ? `${timestamp} ${rendered}` : rendered;
if (loggingState.forceConsoleToStderr) process.stderr.write(`${line}\n`);
else if (consoleStyle !== "json" && !timestamp && params.args.length === 0 && stack === void 0) params.write.apply(console, params.args);
else params.write.call(console, line);
} catch (err) {
if (isEpipeError(err)) return;
throw err;
}
}
/** Writes a root logger line to the pre-capture console sink without re-entering file capture. */
function writeRootConsoleLine(method, line) {
const rawConsole = loggingState.rawConsole;
if (!rawConsole) return false;
if (shouldSuppressConsoleMessage(line)) return true;
writeFormattedConsoleOutput({
level: method === "error" ? "error" : "info",
args: [line],
formatted: line,
write: rawConsole[method]
});
return true;
}
(() => {
try {
const inspect = process.getBuiltinModule?.("util").inspect;
return typeof inspect === "function" ? inspect : null;
} catch {
return null;
}
})();
function isRichConsoleEnv() {
const term = normalizeLowercaseStringOrEmpty(process.env.TERM);
if (process.env.COLORTERM || process.env.TERM_PROGRAM) return true;
return term.length > 0 && term !== "dumb";
}
const consoleColors = [];
function getColorForConsole() {
const level = typeof process.env.FORCE_COLOR === "string" && process.env.FORCE_COLOR.trim().length > 0 && process.env.FORCE_COLOR.trim() !== "0" || !process.env.NO_COLOR && (process.stdout.isTTY || process.stderr.isTTY || isRichConsoleEnv()) ? 1 : 0;
return consoleColors[level] ??= new Chalk({ level });
}
const SUBSYSTEM_COLORS = [
"cyan",
"green",
"yellow",
"blue",
"magenta",
"red"
];
const SUBSYSTEM_COLOR_OVERRIDES = /* @__PURE__ */ new Map([["gmail-watcher", "blue"]]);
const SUBSYSTEM_PREFIXES_TO_DROP = /* @__PURE__ */ new Set([
"gateway",
"channels",
"providers"
]);
const CHANNEL_SUBSYSTEM_PREFIXES = /* @__PURE__ */ new Set([
"clickclack",
"discord",
"feishu",
"googlechat",
"imessage",
"irc",
"line",
"matrix",
"mattermost",
"msteams",
"nextcloud-talk",
"nostr",
"openclaw-weixin",
"qqbot",
"signal",
"slack",
"synology-chat",
"telegram",
"tlon",
"twitch",
"webchat",
"wecom",
"whatsapp",
"yuanbao",
"zalo",
"zalouser"
]);
function pickSubsystemColor(subsystem) {
const override = SUBSYSTEM_COLOR_OVERRIDES.get(subsystem);
if (override) return override;
let hash = 0;
for (let i = 0; i < subsystem.length; i += 1) hash = hash * 31 + subsystem.charCodeAt(i) | 0;
const idx = Math.abs(hash) % SUBSYSTEM_COLORS.length;
return expectDefined(SUBSYSTEM_COLORS[idx], "subsystem colors entry at idx");
}
function formatSubsystemForConsole(subsystem) {
const parts = subsystem.split("/").filter(Boolean);
const original = parts.join("/") || subsystem;
while (parts[0] !== void 0 && SUBSYSTEM_PREFIXES_TO_DROP.has(parts[0])) parts.shift();
const first = parts.at(0);
if (first === void 0) return original;
if (CHANNEL_SUBSYSTEM_PREFIXES.has(normalizeLowercaseStringOrEmpty(first))) return first;
return parts.slice(-2).join("/");
}
function stripRedundantSubsystemPrefixForConsole(message, displaySubsystem) {
if (!displaySubsystem) return message;
if (message.startsWith("[")) {
const closeIdx = message.indexOf("]");
if (closeIdx > 1) {
const bracketTag = message.slice(1, closeIdx);
if (normalizeLowercaseStringOrEmpty(bracketTag) === normalizeLowercaseStringOrEmpty(displaySubsystem)) return message.slice(closeIdx + 1).replace(/^ */, "");
}
}
const prefix = message.slice(0, displaySubsystem.length);
if (normalizeLowercaseStringOrEmpty(prefix) !== normalizeLowercaseStringOrEmpty(displaySubsystem)) return message;
const next = message.slice(displaySubsystem.length, displaySubsystem.length + 1);
if (next !== ":" && next !== " ") return message;
return message.slice(displaySubsystem.length).replace(/^ *:? */, "");
}
function createConsoleLineFormatter(subsystem) {
const displaySubsystem = formatSubsystemForConsole(subsystem);
const prefix = `[${displaySubsystem}]`;
const prefixColor = pickSubsystemColor(displaySubsystem);
return (level, message, style) => {
const color = getColorForConsole();
const levelColor = level === "error" || level === "fatal" ? color.red : level === "warn" ? color.yellow : level === "debug" || level === "trace" ? color.gray : color.cyan;
const displayMessage = stripRedundantSubsystemPrefixForConsole(redactSensitiveText(message), displaySubsystem);
const time = style === "pretty" || loggingState.consoleTimestampPrefix ? color.gray(formatConsoleTimestamp(style)) : "";
const prefixToken = color[prefixColor](prefix);
return `${time ? `${time} ${prefixToken}` : prefixToken} ${levelColor(displayMessage)}`;
};
}
function writeConsoleLine(level, line, opts = {}) {
const sanitized = process.platform === "win32" && process.env.GITHUB_ACTIONS === "true" ? line.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "?").replace(/[\uD800-\uDFFF]/g, "?") : line;
const redacted = opts.redacted ? sanitized : redactSensitiveText(sanitized);
const sink = loggingState.rawConsole ?? console;
if (loggingState.forceConsoleToStderr || level === "error" || level === "fatal") (sink.error ?? console.error)(redacted);
else if (level === "warn") (sink.warn ?? console.warn)(redacted);
else (sink.log ?? console.log)(redacted);
}
function shouldSuppressProbeConsoleLine(params) {
if (isVerbose()) return false;
if (params.level === "error" || params.level === "fatal") return false;
const message = typeof params.message === "string" ? params.message : "";
if (!params.suppressProbeMessages) return false;
if ((typeof params.meta?.runId === "string" ? params.meta.runId : typeof params.meta?.sessionId === "string" ? params.meta.sessionId : void 0)?.startsWith("probe-")) return true;
return /(sessionId|runId)=probe-/.test(message);
}
const CONSOLE_META_LEVELS = /* @__PURE__ */ new Set([
"warn",
"error",
"fatal"
]);
const CONSOLE_META_MAX_CHARS = 2048;
function formatConsoleMetaValue(value) {
if (value === void 0) return;
if (typeof value === "string") return /\s|=/.test(value) || value.length === 0 ? JSON.stringify(value) : value;
if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
return JSON.stringify(value);
}
/** Keeps an Error's message, which a JSON round-trip would otherwise reduce to `{}`. */
function prepareConsoleMetaRecord(meta) {
let prepared;
for (const [key, value] of Object.entries(meta)) if (value instanceof Error) {
prepared ??= { ...meta };
prepared[key] = value.message;
}
return prepared ?? meta;
}
/**
* Renders structured fields as one compact `key=value` tail so warn/error/fatal
* records keep their diagnostics in plain-text sinks such as journald, which only
* see the console line. The JSON console style and the file sink carry the same
* fields natively, so only plain styles call this.
*
* Fields pass through the shared console transport redactor before flattening, so
* key-aware protection such as `apiToken` survives the loss of structure and the
* length cap can only clip text that is already masked. That redactor also owns
* circular references, bigints, and values with no JSON form, and returns parsed
* data, so rendering a value here cannot re-enter a stateful `toJSON`.
*/
function formatConsoleMeta(meta) {
let redacted;
try {
redacted = redactLogRecordForTransport(prepareConsoleMetaRecord(meta), { format: "console" });
} catch {
return "";
}
const parts = [];
for (const [key, value] of Object.entries(redacted)) {
const rendered = formatConsoleMetaValue(value);
if (rendered !== void 0) parts.push(`${key}=${rendered}`);
}
const joined = parts.join(" ");
return joined.length > CONSOLE_META_MAX_CHARS ? `${joined.slice(0, CONSOLE_META_MAX_CHARS)}...(truncated)` : joined;
}
function logToFile(fileLogger, level, message, meta) {
if (level === "silent") return;
if (meta && Object.keys(meta).length > 0) fileLogger[level](meta, message);
else fileLogger[level](message);
}
function createSubsystemLogger(subsystem) {
const resolvedSubsystem = normalizeOptionalString(subsystem) ?? "unknown";
const suppressProbeMessages = resolvedSubsystem === "agent/embedded" || resolvedSubsystem.startsWith("agent/embedded/") || resolvedSubsystem === "model-fallback" || resolvedSubsystem.startsWith("model-fallback/");
let fileChild;
let fileChildWithoutStack;
let formatConsoleLine;
const getFileLogger = (level) => {
fileChild ??= getChildLogger({ subsystem: resolvedSubsystem });
if (level === "error" || level === "fatal" || areDiagnosticsEnabledForProcess() && hasInternalDiagnosticEventInterest("log.record")) return fileChild;
if (!fileChildWithoutStack) {
fileChildWithoutStack = fileChild.getSubLogger({ stack: { capture: "off" } });
fileChildWithoutStack.settings.parentNames = fileChild.settings.parentNames;
}
return fileChildWithoutStack;
};
const emitLog = (level, message, meta) => {
const consoleSettings = getConsoleSettings();
const consoleEnabled = isLogLevelEnabled(level, consoleSettings.level) && shouldLogSubsystemToConsole(resolvedSubsystem);
const fileEnabled = isFileLogLevelEnabled(level);
if (!consoleEnabled && !fileEnabled) return;
let consoleMessageOverride;
let fileMeta = meta;
if (meta && Object.keys(meta).length > 0) {
const { consoleMessage, ...rest } = meta;
if (typeof consoleMessage === "string") consoleMessageOverride = consoleMessage;
fileMeta = Object.keys(rest).length > 0 ? rest : void 0;
}
if (fileEnabled) logToFile(getFileLogger(level), level, message, fileMeta);
if (!consoleEnabled) return;
const consoleMeta = consoleSettings.style !== "json" && consoleMessageOverride === void 0 && fileMeta && CONSOLE_META_LEVELS.has(level) ? formatConsoleMeta(fileMeta) : "";
const consoleMessage = consoleMessageOverride ?? (consoleMeta ? `${message} ${consoleMeta}` : message);
if (shouldSuppressProbeConsoleLine({
level,
suppressProbeMessages,
message: consoleMessage,
meta: fileMeta
})) return;
writeConsoleLine(level, consoleSettings.style === "json" ? formatJsonConsoleLine({
level,
subsystem: resolvedSubsystem,
message,
meta: fileMeta
}) : (formatConsoleLine ??= createConsoleLineFormatter(resolvedSubsystem))(level, consoleMessage, consoleSettings.style), { redacted: true });
};
return {
subsystem: resolvedSubsystem,
isEnabled(level, target = "any") {
const isConsoleEnabled = isLogLevelEnabled(level, getConsoleSettings().level) && shouldLogSubsystemToConsole(resolvedSubsystem);
const isFileEnabled = isFileLogLevelEnabled(level);
if (target === "console") return isConsoleEnabled;
if (target === "file") return isFileEnabled;
return isConsoleEnabled || isFileEnabled;
},
trace(message, meta) {
emitLog("trace", message, meta);
},
debug(message, meta) {
emitLog("debug", message, meta);
},
info(message, meta) {
emitLog("info", message, meta);
},
warn(message, meta) {
emitLog("warn", message, meta);
},
error(message, meta) {
emitLog("error", message, meta);
},
fatal(message, meta) {
emitLog("fatal", message, meta);
},
raw(message) {
if (isFileLogLevelEnabled("info")) logToFile(getFileLogger("info"), "info", message, { raw: true });
const consoleSettings = getConsoleSettings();
if (isLogLevelEnabled("info", consoleSettings.level) && shouldLogSubsystemToConsole(resolvedSubsystem)) {
if (shouldSuppressProbeConsoleLine({
level: "info",
suppressProbeMessages,
message
})) return;
writeConsoleLine("info", consoleSettings.style === "json" ? formatJsonConsoleLine({
level: "info",
subsystem: resolvedSubsystem,
message
}) : message, { redacted: consoleSettings.style === "json" });
}
},
child(name) {
return createSubsystemLogger(`${resolvedSubsystem}/${name}`);
}
};
}
//#endregion
export { resolvePreferredOpenClawTmpDir as a, getLogger as i, writeRootConsoleLine as n, defaultRuntime as o, getChildLogger as r, isVerbose as s, createSubsystemLogger as t };