import path from "node:path"; import os, { homedir } from "node:os"; import json5 from "json5"; import { ApplicationCommandOptionType, ButtonStyle, ChannelType, PermissionFlagsBits, Routes, StickerFormatType } from "discord-api-types/v10"; import { Button, ChannelSelectMenu, ChannelType as ChannelType$1, Client, Command, CommandWithSubcommands, Container, MentionableSelectMenu, MessageCreateListener, MessageReactionAddListener, MessageReactionRemoveListener, MessageType, Modal, PresenceUpdateListener, ReadyListener, RoleSelectMenu, Row, Separator, StringSelectMenu, TextDisplay, UserSelectMenu, serializePayload } from "@buape/carbon"; import crypto, { X509Certificate, createHash, createHmac, randomBytes, randomUUID } from "node:crypto"; import { inspect, promisify } from "node:util"; import { execFile, execFileSync, spawn, spawnSync } from "node:child_process"; import * as fs$2 from "node:fs/promises"; import fs$1 from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { complete, completeSimple, streamSimple } from "@mariozechner/pi-ai"; import AjvPkg from "ajv"; import { ProxyAgent, fetch as fetch$1 } from "undici"; import { Buffer as Buffer$1 } from "node:buffer"; import { CURRENT_SESSION_VERSION, DefaultResourceLoader, SessionManager, SettingsManager, codingTools, createAgentSession, createEditTool, createReadTool, createWriteTool, estimateTokens, generateSummary, readTool } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { createServer } from "node:http"; import { spinner } from "@clack/prompts"; import WebSocket, { WebSocket as WebSocket$1 } from "ws"; import { EdgeTTS } from "node-edge-tts"; import { createJiti } from "jiti"; import readline from "node:readline"; import { API_CONSTANTS, Bot, GrammyError, InputFile, webhookCallback } from "grammy"; import { createOscProgressController, supportsOscProgress } from "osc-progress"; import { GatewayCloseCodes, GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway"; import { VoicePlugin } from "@buape/carbon/voice"; import { AudioPlayerStatus, EndBehaviorType, VoiceConnectionStatus, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel } from "@discordjs/voice"; import { HttpsProxyAgent } from "https-proxy-agent"; import { setTimeout as setTimeout$1 } from "node:timers/promises"; import { messagingApi } from "@line/bot-sdk"; import SlackBolt from "@slack/bolt"; import { run, sequentialize } from "@grammyjs/runner"; import { apiThrottler } from "@grammyjs/transformer-throttler"; import { EventEmitter } from "node:events"; //#region src/channels/plugins/message-action-names.ts const CHANNEL_MESSAGE_ACTION_NAMES = [ "send", "broadcast", "poll", "react", "reactions", "read", "edit", "unsend", "reply", "sendWithEffect", "renameGroup", "setGroupIcon", "addParticipant", "removeParticipant", "leaveGroup", "sendAttachment", "delete", "pin", "unpin", "list-pins", "permissions", "thread-create", "thread-list", "thread-reply", "search", "sticker", "sticker-search", "member-info", "role-info", "emoji-list", "emoji-upload", "sticker-upload", "role-add", -- "node.pair.list", "node.pair.approve", "node.pair.reject", "node.pair.verify", "device.pair.list", "device.pair.approve", "device.pair.reject", "device.pair.remove", "device.token.rotate", "device.token.revoke", "node.rename" ], [READ_SCOPE]: [ "health", "doctor.memory.status", "logs.tail", "channels.status", "status", "usage.status", "usage.cost", "tts.status", "tts.providers", "models.list", "tools.catalog", "agents.list", "agent.identity.get", "skills.status", "voicewake.get", "sessions.list", "sessions.preview", "sessions.resolve", "sessions.usage", "sessions.usage.timeseries", "sessions.usage.logs", "cron.list", "cron.status", "cron.runs", "system-presence", "last-heartbeat", "node.list", "node.describe", "chat.history", "config.get", "talk.config", "agents.files.list", "agents.files.get" ], [WRITE_SCOPE]: [ "send", "poll", "agent", "agent.wait", "wake", "talk.mode", "tts.enable", "tts.disable", "tts.convert", "tts.setProvider", "voicewake.set", "node.invoke", "chat.send", "chat.abort", "browser.request", "push.test" ], [ADMIN_SCOPE]: [ "channels.logout", "agents.create", "agents.update", "agents.delete", "skills.install", "skills.update", "cron.add", "cron.update", "cron.remove", "cron.run", "sessions.patch", "sessions.reset", "sessions.delete", "sessions.compact", "connect", "chat.inject", "web.login.start", "web.login.wait", "set-heartbeats", "system-event", "agents.files.set" ] }; const ADMIN_METHOD_PREFIXES = [ "exec.approvals.", "config.", "wizard.", "update." ]; const METHOD_SCOPE_BY_NAME = new Map(Object.entries(METHOD_SCOPE_GROUPS).flatMap(([scope, methods]) => methods.map((method) => [method, scope]))); function resolveScopedMethod(method) { const explicitScope = METHOD_SCOPE_BY_NAME.get(method); if (explicitScope) return explicitScope; if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) return ADMIN_SCOPE; } function resolveRequiredOperatorScopeForMethod(method) { return resolveScopedMethod(method); } function resolveLeastPrivilegeOperatorScopesForMethod(method) { const requiredScope = resolveRequiredOperatorScopeForMethod(method); if (requiredScope) return [requiredScope]; return []; -- * * Local providers (ollama, vllm) need a dummy API key to be registered. * Users often configure `agents.defaults.model.primary: "ollama/โ€ฆ"` but * forget to set `OLLAMA_API_KEY`, resulting in a confusing "Unknown model" * error. This detects known providers that require opt-in auth and adds * a hint. * * See: https://github.com/openclaw/openclaw/issues/17328 */ const LOCAL_PROVIDER_HINTS = { ollama: "Ollama requires authentication to be registered as a provider. Set OLLAMA_API_KEY=\"ollama-local\" (any value works) or run \"openclaw configure\". See: https://docs.openclaw.ai/providers/ollama", vllm: "vLLM requires authentication to be registered as a provider. Set VLLM_API_KEY (any value works) or run \"openclaw configure\". See: https://docs.openclaw.ai/providers/vllm" }; function buildUnknownModelError(provider, modelId) { const base = `Unknown model: ${provider}/${modelId}`; const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()]; return hint ? `${base}. ${hint}` : base; } //#endregion //#region src/tts/tts-core.ts const DEFAULT_ELEVENLABS_BASE_URL$1 = "https://api.elevenlabs.io"; const TEMP_FILE_CLEANUP_DELAY_MS = 300 * 1e3; function isValidVoiceId(voiceId) { return /^[a-zA-Z0-9]{10,40}$/.test(voiceId); } function normalizeElevenLabsBaseUrl(baseUrl) { const trimmed = baseUrl.trim(); if (!trimmed) return DEFAULT_ELEVENLABS_BASE_URL$1; return trimmed.replace(/\/+$/, ""); } function requireInRange(value, min, max, label) { if (!Number.isFinite(value) || value < min || value > max) throw new Error(`${label} must be between ${min} and ${max}`); } function assertElevenLabsVoiceSettings(settings) { requireInRange(settings.stability, 0, 1, "stability"); requireInRange(settings.similarityBoost, 0, 1, "similarityBoost"); requireInRange(settings.style, 0, 1, "style"); requireInRange(settings.speed, .5, 2, "speed"); } function normalizeLanguageCode(code) { const trimmed = code?.trim(); if (!trimmed) return; const normalized = trimmed.toLowerCase(); if (!/^[a-z]{2}$/.test(normalized)) throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); return normalized; } function normalizeApplyTextNormalization(mode) { const trimmed = mode?.trim(); if (!trimmed) return; const normalized = trimmed.toLowerCase(); if (normalized === "auto" || normalized === "on" || normalized === "off") return normalized; throw new Error("applyTextNormalization must be one of: auto, on, off"); } function normalizeSeed(seed) { if (seed == null) return; const next = Math.floor(seed); if (!Number.isFinite(next) || next < 0 || next > 4294967295) throw new Error("seed must be between 0 and 4294967295"); return next; } function parseBooleanValue(value) { const normalized = value.trim().toLowerCase(); if ([ "true", "1", "yes", "on" ].includes(normalized)) return true; if ([ "false", "0", "no", "off" ].includes(normalized)) return false; } function parseNumberValue(value) { const parsed = Number.parseFloat(value); return Number.isFinite(parsed) ? parsed : void 0; } function parseTtsDirectives(text, policy) { if (!policy.enabled) return { cleanedText: text, overrides: {}, warnings: [], hasDirective: false }; const overrides = {}; const warnings = []; let cleanedText = text; let hasDirective = false; cleanedText = cleanedText.replace(/\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi, (_match, inner) => { hasDirective = true; if (policy.allowText && overrides.ttsText == null) overrides.ttsText = inner.trim(); return ""; }); cleanedText = cleanedText.replace(/\[\[tts:([^\]]+)\]\]/gi, (_match, body) => { hasDirective = true; const tokens = body.split(/\s+/).filter(Boolean); for (const token of tokens) { const eqIndex = token.indexOf("="); if (eqIndex === -1) continue; const rawKey = token.slice(0, eqIndex).trim(); const rawValue = token.slice(eqIndex + 1).trim(); if (!rawKey || !rawValue) continue; const key = rawKey.toLowerCase(); try { switch (key) { case "provider": if (!policy.allowProvider) break; if (rawValue === "openai" || rawValue === "elevenlabs" || rawValue === "edge") overrides.provider = rawValue; else warnings.push(`unsupported provider "${rawValue}"`); break; case "voice": case "openai_voice": case "openaivoice": if (!policy.allowVoice) break; if (isValidOpenAIVoice(rawValue)) overrides.openai = { ...overrides.openai, voice: rawValue }; else warnings.push(`invalid OpenAI voice "${rawValue}"`); break; case "voiceid": case "voice_id": case "elevenlabs_voice": case "elevenlabsvoice": if (!policy.allowVoice) break; if (isValidVoiceId(rawValue)) overrides.elevenlabs = { ...overrides.elevenlabs, voiceId: rawValue }; else warnings.push(`invalid ElevenLabs voiceId "${rawValue}"`); break; case "model": case "modelid": case "model_id": case "elevenlabs_model": case "elevenlabsmodel": case "openai_model": case "openaimodel": if (!policy.allowModelId) break; if (isValidOpenAIModel(rawValue)) overrides.openai = { ...overrides.openai, model: rawValue }; else overrides.elevenlabs = { -- languageCode: normalizeLanguageCode(rawValue) }; break; case "seed": if (!policy.allowSeed) break; overrides.elevenlabs = { ...overrides.elevenlabs, seed: normalizeSeed(Number.parseInt(rawValue, 10)) }; break; default: break; } } catch (err) { warnings.push(err.message); } } return ""; }); return { cleanedText, ttsText: overrides.ttsText, hasDirective, overrides, warnings }; } const OPENAI_TTS_MODELS = [ "gpt-4o-mini-tts", "tts-1", "tts-1-hd" ]; /** * Custom OpenAI-compatible TTS endpoint. * When set, model/voice validation is relaxed to allow non-OpenAI models. * Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1 * * Note: Read at runtime (not module load) to support config.env loading. */ function getOpenAITtsBaseUrl() { return (process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1").replace(/\/+$/, ""); } function isCustomOpenAIEndpoint() { return getOpenAITtsBaseUrl() !== "https://api.openai.com/v1"; } const OPENAI_TTS_VOICES = [ "alloy", "ash", "ballad", "cedar", "coral", "echo", "fable", "juniper", "marin", "onyx", "nova", "sage", "shimmer", "verse" ]; function isValidOpenAIModel(model) { if (isCustomOpenAIEndpoint()) return true; return OPENAI_TTS_MODELS.includes(model); } function isValidOpenAIVoice(voice) { if (isCustomOpenAIEndpoint()) return true; return OPENAI_TTS_VOICES.includes(voice); } function resolveSummaryModelRef(cfg, config) { const defaultRef = resolveDefaultModelForAgent({ cfg }); const override = config.summaryModel?.trim(); if (!override) return { ref: defaultRef, source: "default" }; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: defaultRef.provider }); const resolved = resolveModelRefFromString({ -- function inferEdgeExtension(outputFormat) { const normalized = outputFormat.toLowerCase(); if (normalized.includes("webm")) return ".webm"; if (normalized.includes("ogg")) return ".ogg"; if (normalized.includes("opus")) return ".opus"; if (normalized.includes("wav") || normalized.includes("riff") || normalized.includes("pcm")) return ".wav"; return ".mp3"; } async function edgeTTS(params) { const { text, outputPath, config, timeoutMs } = params; await new EdgeTTS({ voice: config.voice, lang: config.lang, outputFormat: config.outputFormat, saveSubtitles: config.saveSubtitles, proxy: config.proxy, rate: config.rate, pitch: config.pitch, volume: config.volume, timeout: config.timeoutMs ?? timeoutMs }).ttsPromise(text, outputPath); } //#endregion //#region src/tts/tts.ts const DEFAULT_TIMEOUT_MS$1 = 3e4; const DEFAULT_TTS_MAX_LENGTH = 1500; const DEFAULT_TTS_SUMMARIZE = true; const DEFAULT_MAX_TEXT_LENGTH = 4096; const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io"; const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE"; const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"; const DEFAULT_OPENAI_MODEL = "gpt-4o-mini-tts"; const DEFAULT_OPENAI_VOICE = "alloy"; const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural"; const DEFAULT_EDGE_LANG = "en-US"; const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3"; const DEFAULT_ELEVENLABS_VOICE_SETTINGS = { stability: .5, similarityBoost: .75, style: 0, useSpeakerBoost: true, speed: 1 }; const TELEGRAM_OUTPUT = { openai: "opus", elevenlabs: "opus_48000_64", extension: ".opus", voiceCompatible: true }; const DEFAULT_OUTPUT = { openai: "mp3", elevenlabs: "mp3_44100_128", extension: ".mp3", voiceCompatible: false }; const TELEPHONY_OUTPUT = { openai: { format: "pcm", sampleRate: 24e3 }, elevenlabs: { format: "pcm_22050", sampleRate: 22050 } }; const TTS_AUTO_MODES = new Set([ "off", "always", "inbound", "tagged" ]); let lastTtsAttempt; function normalizeTtsAutoMode(value) { if (typeof value !== "string") return; const normalized = value.trim().toLowerCase(); if (TTS_AUTO_MODES.has(normalized)) return normalized; } function resolveModelOverridePolicy(overrides) { if (!(overrides?.enabled ?? true)) return { enabled: false, allowText: false, allowProvider: false, allowVoice: false, allowModelId: false, allowVoiceSettings: false, allowNormalization: false, allowSeed: false }; const allow = (value, defaultValue = true) => value ?? defaultValue; return { enabled: true, allowText: allow(overrides?.allowText), allowProvider: allow(overrides?.allowProvider, false), allowVoice: allow(overrides?.allowVoice), allowModelId: allow(overrides?.allowModelId), allowVoiceSettings: allow(overrides?.allowVoiceSettings), allowNormalization: allow(overrides?.allowNormalization), allowSeed: allow(overrides?.allowSeed) }; } function resolveTtsConfig(cfg) { const raw = cfg.messages?.tts ?? {}; const providerSource = raw.provider ? "config" : "default"; const edgeOutputFormat = raw.edge?.outputFormat?.trim(); return { auto: normalizeTtsAutoMode(raw.auto) ?? (raw.enabled ? "always" : "off"), mode: raw.mode ?? "final", provider: raw.provider ?? "edge", providerSource, summaryModel: raw.summaryModel?.trim() || void 0, modelOverrides: resolveModelOverridePolicy(raw.modelOverrides), elevenlabs: { apiKey: raw.elevenlabs?.apiKey, baseUrl: raw.elevenlabs?.baseUrl?.trim() || DEFAULT_ELEVENLABS_BASE_URL, voiceId: raw.elevenlabs?.voiceId ?? DEFAULT_ELEVENLABS_VOICE_ID, modelId: raw.elevenlabs?.modelId ?? DEFAULT_ELEVENLABS_MODEL_ID, seed: raw.elevenlabs?.seed, applyTextNormalization: raw.elevenlabs?.applyTextNormalization, languageCode: raw.elevenlabs?.languageCode, voiceSettings: { stability: raw.elevenlabs?.voiceSettings?.stability ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.stability, similarityBoost: raw.elevenlabs?.voiceSettings?.similarityBoost ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.similarityBoost, style: raw.elevenlabs?.voiceSettings?.style ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.style, useSpeakerBoost: raw.elevenlabs?.voiceSettings?.useSpeakerBoost ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.useSpeakerBoost, speed: raw.elevenlabs?.voiceSettings?.speed ?? DEFAULT_ELEVENLABS_VOICE_SETTINGS.speed } }, openai: { apiKey: raw.openai?.apiKey, model: raw.openai?.model ?? DEFAULT_OPENAI_MODEL, voice: raw.openai?.voice ?? DEFAULT_OPENAI_VOICE }, edge: { enabled: raw.edge?.enabled ?? true, voice: raw.edge?.voice?.trim() || DEFAULT_EDGE_VOICE, lang: raw.edge?.lang?.trim() || DEFAULT_EDGE_LANG, outputFormat: edgeOutputFormat || DEFAULT_EDGE_OUTPUT_FORMAT, outputFormatConfigured: Boolean(edgeOutputFormat), pitch: raw.edge?.pitch?.trim() || void 0, rate: raw.edge?.rate?.trim() || void 0, volume: raw.edge?.volume?.trim() || void 0, saveSubtitles: raw.edge?.saveSubtitles ?? false, proxy: raw.edge?.proxy?.trim() || void 0, timeoutMs: raw.edge?.timeoutMs }, prefsPath: raw.prefsPath, maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH, timeoutMs: raw.timeoutMs ?? DEFAULT_TIMEOUT_MS$1 }; } function resolveTtsPrefsPath(config) { if (config.prefsPath?.trim()) return resolveUserPath(config.prefsPath.trim()); const envPath = process.env.OPENCLAW_TTS_PREFS?.trim(); if (envPath) return resolveUserPath(envPath); return path.join(CONFIG_DIR, "settings", "tts.json"); } function resolveTtsAutoModeFromPrefs(prefs) { const auto = normalizeTtsAutoMode(prefs.tts?.auto); if (auto) return auto; if (typeof prefs.tts?.enabled === "boolean") return prefs.tts.enabled ? "always" : "off"; } function resolveTtsAutoMode(params) { const sessionAuto = normalizeTtsAutoMode(params.sessionAuto); if (sessionAuto) return sessionAuto; const prefsAuto = resolveTtsAutoModeFromPrefs(readPrefs(params.prefsPath)); if (prefsAuto) return prefsAuto; return params.config.auto; } function buildTtsSystemPromptHint(cfg) { const config = resolveTtsConfig(cfg); const prefsPath = resolveTtsPrefsPath(config); const autoMode = resolveTtsAutoMode({ config, prefsPath }); if (autoMode === "off") return; const maxLength = getTtsMaxLength(prefsPath); const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off"; return [ "Voice (TTS) is enabled.", autoMode === "inbound" ? "Only use TTS when the user's last message includes audio/voice." : autoMode === "tagged" ? "Only use TTS when you include [[tts]] or [[tts:text]] tags." : void 0, `Keep spoken text โ‰ค${maxLength} chars to avoid auto-summary (summary ${summarize}).`, "Use [[tts:...]] and optional [[tts:text]]...[[/tts:text]] to control voice/expressiveness." ].filter(Boolean).join("\n"); } function readPrefs(prefsPath) { try { if (!existsSync(prefsPath)) return {}; return JSON.parse(readFileSync(prefsPath, "utf8")); } catch { return {}; } } function atomicWriteFileSync(filePath, content) { const tmpPath = `${filePath}.tmp.${Date.now()}.${randomBytes(8).toString("hex")}`; writeFileSync(tmpPath, content, { mode: 384 }); try { renameSync(tmpPath, filePath); } catch (err) { try { unlinkSync(tmpPath); } catch {} throw err; } } function updatePrefs(prefsPath, update) { const prefs = readPrefs(prefsPath); update(prefs); mkdirSync(path.dirname(prefsPath), { recursive: true }); atomicWriteFileSync(prefsPath, JSON.stringify(prefs, null, 2)); } function isTtsEnabled(config, prefsPath, sessionAuto) { return resolveTtsAutoMode({ config, prefsPath, sessionAuto }) !== "off"; } function setTtsAutoMode(prefsPath, mode) { updatePrefs(prefsPath, (prefs) => { const next = { ...prefs.tts }; delete next.enabled; next.auto = mode; prefs.tts = next; }); } function setTtsEnabled(prefsPath, enabled) { setTtsAutoMode(prefsPath, enabled ? "always" : "off"); } function getTtsProvider(config, prefsPath) { const prefs = readPrefs(prefsPath); if (prefs.tts?.provider) return prefs.tts.provider; if (config.providerSource === "config") return config.provider; if (resolveTtsApiKey(config, "openai")) return "openai"; if (resolveTtsApiKey(config, "elevenlabs")) return "elevenlabs"; return "edge"; } function setTtsProvider(prefsPath, provider) { updatePrefs(prefsPath, (prefs) => { prefs.tts = { ...prefs.tts, provider }; }); } function getTtsMaxLength(prefsPath) { return readPrefs(prefsPath).tts?.maxLength ?? DEFAULT_TTS_MAX_LENGTH; } function setTtsMaxLength(prefsPath, maxLength) { updatePrefs(prefsPath, (prefs) => { prefs.tts = { ...prefs.tts, maxLength }; }); } function isSummarizationEnabled(prefsPath) { return readPrefs(prefsPath).tts?.summarize ?? DEFAULT_TTS_SUMMARIZE; } function setSummarizationEnabled(prefsPath, enabled) { updatePrefs(prefsPath, (prefs) => { prefs.tts = { ...prefs.tts, summarize: enabled }; }); } function getLastTtsAttempt() { return lastTtsAttempt; } function setLastTtsAttempt(entry) { lastTtsAttempt = entry; } function resolveOutputFormat(channelId) { if (channelId === "telegram") return TELEGRAM_OUTPUT; return DEFAULT_OUTPUT; } function resolveChannelId(channel) { return channel ? normalizeChannelId(channel) : null; } function resolveEdgeOutputFormat(config) { return config.edge.outputFormat; } function resolveTtsApiKey(config, provider) { if (provider === "elevenlabs") return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY; if (provider === "openai") return config.openai.apiKey || process.env.OPENAI_API_KEY; } const TTS_PROVIDERS = [ "openai", "elevenlabs", "edge" ]; function resolveTtsProviderOrder(primary) { return [primary, ...TTS_PROVIDERS.filter((provider) => provider !== primary)]; } function isTtsProviderConfigured(config, provider) { if (provider === "edge") return config.edge.enabled; return Boolean(resolveTtsApiKey(config, provider)); } function formatTtsProviderError(provider, err) { const error = err instanceof Error ? err : new Error(String(err)); if (error.name === "AbortError") return `${provider}: request timed out`; return `${provider}: ${error.message}`; } async function textToSpeech(params) { const config = resolveTtsConfig(params.cfg); const prefsPath = params.prefsPath ?? resolveTtsPrefsPath(config); const output = resolveOutputFormat(resolveChannelId(params.channel)); if (params.text.length > config.maxTextLength) return { success: false, error: `Text too long (${params.text.length} chars, max ${config.maxTextLength})` }; const userProvider = getTtsProvider(config, prefsPath); const providers = resolveTtsProviderOrder(params.overrides?.provider ?? userProvider); const errors = []; for (const provider of providers) { const providerStart = Date.now(); try { if (provider === "edge") { if (!config.edge.enabled) { errors.push("edge: disabled"); continue; } const tempRoot = resolvePreferredOpenClawTmpDir(); mkdirSync(tempRoot, { recursive: true, mode: 448 }); const tempDir = mkdtempSync(path.join(tempRoot, "tts-")); let edgeOutputFormat = resolveEdgeOutputFormat(config); const fallbackEdgeOutputFormat = edgeOutputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : void 0; const attemptEdgeTts = async (outputFormat) => { const extension = inferEdgeExtension(outputFormat); const audioPath = path.join(tempDir, `voice-${Date.now()}${extension}`); await edgeTTS({ text: params.text, outputPath: audioPath, config: { ...config.edge, outputFormat }, timeoutMs: config.timeoutMs }); return { audioPath, outputFormat }; }; let edgeResult; try { edgeResult = await attemptEdgeTts(edgeOutputFormat); } catch (err) { if (fallbackEdgeOutputFormat && fallbackEdgeOutputFormat !== edgeOutputFormat) { logVerbose(`TTS: Edge output ${edgeOutputFormat} failed; retrying with ${fallbackEdgeOutputFormat}.`); edgeOutputFormat = fallbackEdgeOutputFormat; try { edgeResult = await attemptEdgeTts(edgeOutputFormat); } catch (fallbackErr) { try { rmSync(tempDir, { recursive: true, force: true }); } catch {} throw fallbackErr; } } else { try { rmSync(tempDir, { recursive: true, force: true }); } catch {} throw err; } } scheduleCleanup(tempDir); const voiceCompatible = isVoiceCompatibleAudio({ fileName: edgeResult.audioPath }); return { -- timeoutMs: config.timeoutMs }); } else { const openaiModelOverride = params.overrides?.openai?.model; const openaiVoiceOverride = params.overrides?.openai?.voice; audioBuffer = await openaiTTS({ text: params.text, apiKey, model: openaiModelOverride ?? config.openai.model, voice: openaiVoiceOverride ?? config.openai.voice, responseFormat: output.openai, timeoutMs: config.timeoutMs }); } const latencyMs = Date.now() - providerStart; const tempRoot = resolvePreferredOpenClawTmpDir(); mkdirSync(tempRoot, { recursive: true, mode: 448 }); const tempDir = mkdtempSync(path.join(tempRoot, "tts-")); const audioPath = path.join(tempDir, `voice-${Date.now()}${output.extension}`); writeFileSync(audioPath, audioBuffer); scheduleCleanup(tempDir); return { success: true, audioPath, latencyMs, provider, outputFormat: provider === "openai" ? output.openai : output.elevenlabs, voiceCompatible: output.voiceCompatible }; } catch (err) { errors.push(formatTtsProviderError(provider, err)); } } return { success: false, error: `TTS conversion failed: ${errors.join("; ") || "no providers available"}` }; } async function textToSpeechTelephony(params) { const config = resolveTtsConfig(params.cfg); const prefsPath = params.prefsPath ?? resolveTtsPrefsPath(config); if (params.text.length > config.maxTextLength) return { success: false, error: `Text too long (${params.text.length} chars, max ${config.maxTextLength})` }; const providers = resolveTtsProviderOrder(getTtsProvider(config, prefsPath)); const errors = []; for (const provider of providers) { const providerStart = Date.now(); try { if (provider === "edge") { errors.push("edge: unsupported for telephony"); continue; } const apiKey = resolveTtsApiKey(config, provider); if (!apiKey) { errors.push(`${provider}: no API key`); continue; } if (provider === "elevenlabs") { const output = TELEPHONY_OUTPUT.elevenlabs; return { success: true, audioBuffer: await elevenLabsTTS({ text: params.text, apiKey, baseUrl: config.elevenlabs.baseUrl, voiceId: config.elevenlabs.voiceId, -- latencyMs: Date.now() - providerStart, provider, outputFormat: output.format, sampleRate: output.sampleRate }; } catch (err) { errors.push(formatTtsProviderError(provider, err)); } } return { success: false, error: `TTS conversion failed: ${errors.join("; ") || "no providers available"}` }; } async function maybeApplyTtsToPayload(params) { const config = resolveTtsConfig(params.cfg); const prefsPath = resolveTtsPrefsPath(config); const autoMode = resolveTtsAutoMode({ config, prefsPath, sessionAuto: params.ttsAuto }); if (autoMode === "off") return params.payload; const text = params.payload.text ?? ""; const directives = parseTtsDirectives(text, config.modelOverrides); if (directives.warnings.length > 0) logVerbose(`TTS: ignored directive overrides (${directives.warnings.join("; ")})`); const trimmedCleaned = directives.cleanedText.trim(); const visibleText = trimmedCleaned.length > 0 ? trimmedCleaned : ""; const ttsText = directives.ttsText?.trim() || visibleText; const nextPayload = visibleText === text.trim() ? params.payload : { ...params.payload, text: visibleText.length > 0 ? visibleText : void 0 }; if (autoMode === "tagged" && !directives.hasDirective) return nextPayload; if (autoMode === "inbound" && params.inboundAudio !== true) return nextPayload; if ((config.mode ?? "final") === "final" && params.kind && params.kind !== "final") return nextPayload; if (!ttsText.trim()) return nextPayload; if (params.payload.mediaUrl || (params.payload.mediaUrls?.length ?? 0) > 0) return nextPayload; if (text.includes("MEDIA:")) return nextPayload; if (ttsText.trim().length < 10) return nextPayload; const maxLength = getTtsMaxLength(prefsPath); let textForAudio = ttsText.trim(); let wasSummarized = false; if (textForAudio.length > maxLength) if (!isSummarizationEnabled(prefsPath)) { logVerbose(`TTS: truncating long text (${textForAudio.length} > ${maxLength}), summarization disabled.`); textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`; } else try { textForAudio = (await summarizeText({ text: textForAudio, targetLength: maxLength, cfg: params.cfg, config, timeoutMs: config.timeoutMs })).summary; wasSummarized = true; if (textForAudio.length > config.maxTextLength) { logVerbose(`TTS: summary exceeded hard limit (${textForAudio.length} > ${config.maxTextLength}); truncating.`); textForAudio = `${textForAudio.slice(0, config.maxTextLength - 3)}...`; } } catch (err) { logVerbose(`TTS: summarization failed, truncating instead: ${err.message}`); textForAudio = `${textForAudio.slice(0, maxLength - 3)}...`; } textForAudio = stripMarkdown(textForAudio).trim(); if (textForAudio.length < 10) return nextPayload; const ttsStart = Date.now(); const result = await textToSpeech({ text: textForAudio, cfg: params.cfg, prefsPath, channel: params.channel, overrides: directives.overrides }); if (result.success && result.audioPath) { lastTtsAttempt = { timestamp: Date.now(), success: true, textLength: text.length, summarized: wasSummarized, provider: result.provider, latencyMs: result.latencyMs }; const shouldVoice = resolveChannelId(params.channel) === "telegram" && result.voiceCompatible === true; return { ...nextPayload, mediaUrl: result.audioPath, audioAsVoice: shouldVoice || params.payload.audioAsVoice }; } lastTtsAttempt = { timestamp: Date.now(), success: false, textLength: text.length, summarized: wasSummarized, error: result.error }; logVerbose(`TTS: conversion failed after ${Date.now() - ttsStart}ms (${result.error ?? "unknown"}).`); return nextPayload; } //#endregion //#region src/utils/provider-utils.ts /** * Utility functions for provider-specific logic and capabilities. */ /** * Returns true if the provider requires reasoning to be wrapped in tags * (e.g. and ) in the text stream, rather than using native * API fields for reasoning/thinking. */ function isReasoningTagProvider(provider) { if (!provider) return false; const normalized = provider.trim().toLowerCase(); if (normalized === "google-gemini-cli" || normalized === "google-generative-ai") return true; if (normalized.includes("minimax")) return true; return false; } //#endregion //#region src/agents/bootstrap-cache.ts const cache = /* @__PURE__ */ new Map(); async function getOrLoadBootstrapFiles(params) { const existing = cache.get(params.sessionKey); if (existing) return existing; const files = await loadWorkspaceBootstrapFiles(params.workspaceDir); cache.set(params.sessionKey, files); return files; } //#endregion //#region src/agents/bootstrap-hooks.ts async function applyBootstrapHookOverrides(params) { const sessionKey = params.sessionKey ?? params.sessionId ?? "unknown"; const agentId = params.agentId ?? (params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : void 0); const event = createInternalHookEvent("agent", "bootstrap", sessionKey, { workspaceDir: params.workspaceDir, bootstrapFiles: params.files, cfg: params.config, sessionKey: params.sessionKey, sessionId: params.sessionId, agentId }); await triggerInternalHook(event); const updated = event.context.bootstrapFiles; return Array.isArray(updated) ? updated : params.files; } -- const AUDIO_PLACEHOLDER_RE = /^(\s*\([^)]*\))?$/i; const AUDIO_HEADER_RE = /^\[Audio\b/i; const normalizeMediaType = (value) => value.split(";")[0]?.trim().toLowerCase(); const isInboundAudioContext = (ctx) => { if ([typeof ctx.MediaType === "string" ? ctx.MediaType : void 0, ...Array.isArray(ctx.MediaTypes) ? ctx.MediaTypes : []].filter(Boolean).map((type) => normalizeMediaType(type)).some((type) => type === "audio" || type.startsWith("audio/"))) return true; const trimmed = (typeof ctx.BodyForCommands === "string" ? ctx.BodyForCommands : typeof ctx.CommandBody === "string" ? ctx.CommandBody : typeof ctx.RawBody === "string" ? ctx.RawBody : typeof ctx.Body === "string" ? ctx.Body : "").trim(); if (!trimmed) return false; if (AUDIO_PLACEHOLDER_RE.test(trimmed)) return true; return AUDIO_HEADER_RE.test(trimmed); }; const resolveSessionTtsAuto = (ctx, cfg) => { const sessionKey = ((ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : void 0) ?? ctx.SessionKey)?.trim(); if (!sessionKey) return; const agentId = resolveSessionAgentId({ sessionKey, config: cfg }); const storePath = resolveStorePath(cfg.session?.store, { agentId }); try { const store = loadSessionStore(storePath); return normalizeTtsAutoMode((store[sessionKey.toLowerCase()] ?? store[sessionKey])?.ttsAuto); } catch { return; } }; async function dispatchReplyFromConfig(params) { const { ctx, cfg, dispatcher } = params; const diagnosticsEnabled = isDiagnosticsEnabled(cfg); const channel = String(ctx.Surface ?? ctx.Provider ?? "unknown").toLowerCase(); const chatId = ctx.To ?? ctx.From; const messageId = ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast; const sessionKey = ctx.SessionKey; const startTime = diagnosticsEnabled ? Date.now() : 0; const canTrackSession = diagnosticsEnabled && Boolean(sessionKey); const recordProcessed = (outcome, opts) => { if (!diagnosticsEnabled) return; logMessageProcessed({ channel, chatId, messageId, sessionKey, durationMs: Date.now() - startTime, outcome, reason: opts?.reason, error: opts?.error }); }; const markProcessing = () => { if (!canTrackSession || !sessionKey) return; logMessageQueued({ sessionKey, channel, source: "dispatch" }); logSessionStateChange({ sessionKey, state: "processing", reason: "message_start" }); }; const markIdle = (reason) => { if (!canTrackSession || !sessionKey) return; logSessionStateChange({ sessionKey, state: "idle", reason }); }; if (shouldSkipDuplicateInbound(ctx)) { recordProcessed("skipped", { reason: "duplicate" }); return { -- accountId: ctx.AccountId, conversationId, messageId: messageIdForHook, metadata: { to: ctx.To, provider: ctx.Provider, surface: ctx.Surface, threadId: ctx.MessageThreadId, senderId: ctx.SenderId, senderName: ctx.SenderName, senderUsername: ctx.SenderUsername, senderE164: ctx.SenderE164 } })).catch((err) => { logVerbose(`dispatch-from-config: message_received internal hook failed: ${String(err)}`); }); const originatingChannel = ctx.OriginatingChannel; const originatingTo = ctx.OriginatingTo; const currentSurface = (ctx.Surface ?? ctx.Provider)?.toLowerCase(); const shouldRouteToOriginating = isRoutableChannel(originatingChannel) && originatingTo && originatingChannel !== currentSurface; const ttsChannel = shouldRouteToOriginating ? originatingChannel : currentSurface; /** * Helper to send a payload via route-reply (async). * Only used when actually routing to a different provider. * Note: Only called when shouldRouteToOriginating is true, so * originatingChannel and originatingTo are guaranteed to be defined. */ const sendPayloadAsync = async (payload, abortSignal, mirror) => { if (!originatingChannel || !originatingTo) return; if (abortSignal?.aborted) return; const result = await routeReply({ payload, channel: originatingChannel, to: originatingTo, sessionKey: ctx.SessionKey, accountId: ctx.AccountId, threadId: ctx.MessageThreadId, cfg, abortSignal, mirror }); if (!result.ok) logVerbose(`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`); }; markProcessing(); try { const fastAbort = await tryFastAbortFromMessage({ ctx, cfg }); if (fastAbort.handled) { const payload = { text: formatAbortReplyText(fastAbort.stoppedSubagents) }; let queuedFinal = false; let routedFinalCount = 0; if (shouldRouteToOriginating && originatingChannel && originatingTo) { const result = await routeReply({ payload, channel: originatingChannel, to: originatingTo, sessionKey: ctx.SessionKey, accountId: ctx.AccountId, threadId: ctx.MessageThreadId, cfg }); queuedFinal = result.ok; if (result.ok) routedFinalCount += 1; if (!result.ok) logVerbose(`dispatch-from-config: route-reply (abort) failed: ${result.error ?? "unknown error"}`); } else queuedFinal = dispatcher.sendFinalReply(payload); const counts = dispatcher.getQueuedCounts(); counts.final += routedFinalCount; recordProcessed("completed", { reason: "fast_abort" }); markIdle("message_completed"); -- }; } let accumulatedBlockText = ""; let blockCount = 0; const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native"; const resolveToolDeliveryPayload = (payload) => { if (shouldSendToolSummaries) return payload; if (!(Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0)) return null; return { ...payload, text: void 0 }; }; const replyResult = await (params.replyResolver ?? getReplyFromConfig)(ctx, { ...params.replyOptions, onToolResult: (payload) => { const run = async () => { const deliveryPayload = resolveToolDeliveryPayload(await maybeApplyTtsToPayload({ payload, cfg, channel: ttsChannel, kind: "tool", inboundAudio, ttsAuto: sessionTtsAuto })); if (!deliveryPayload) return; if (shouldRouteToOriginating) await sendPayloadAsync(deliveryPayload, void 0, false); else dispatcher.sendToolResult(deliveryPayload); }; return run(); }, onBlockReply: (payload, context) => { const run = async () => { if (payload.isReasoning) return; if (payload.text) { if (accumulatedBlockText.length > 0) accumulatedBlockText += "\n"; accumulatedBlockText += payload.text; blockCount++; } const ttsPayload = await maybeApplyTtsToPayload({ payload, cfg, channel: ttsChannel, kind: "block", inboundAudio, ttsAuto: sessionTtsAuto }); if (shouldRouteToOriginating) await sendPayloadAsync(ttsPayload, context?.abortSignal, false); else dispatcher.sendBlockReply(ttsPayload); }; return run(); } }, cfg); const replies = replyResult ? Array.isArray(replyResult) ? replyResult : [replyResult] : []; let queuedFinal = false; let routedFinalCount = 0; for (const reply of replies) { if (reply.isReasoning) continue; const ttsReply = await maybeApplyTtsToPayload({ payload: reply, cfg, channel: ttsChannel, kind: "final", inboundAudio, ttsAuto: sessionTtsAuto }); if (shouldRouteToOriginating && originatingChannel && originatingTo) { const result = await routeReply({ payload: ttsReply, channel: originatingChannel, to: originatingTo, sessionKey: ctx.SessionKey, accountId: ctx.AccountId, threadId: ctx.MessageThreadId, cfg }); if (!result.ok) logVerbose(`dispatch-from-config: route-reply (final) failed: ${result.error ?? "unknown error"}`); queuedFinal = result.ok || queuedFinal; if (result.ok) routedFinalCount += 1; } else queuedFinal = dispatcher.sendFinalReply(ttsReply) || queuedFinal; } if ((resolveTtsConfig(cfg).mode ?? "final") === "final" && replies.length === 0 && blockCount > 0 && accumulatedBlockText.trim()) try { const ttsSyntheticReply = await maybeApplyTtsToPayload({ payload: { text: accumulatedBlockText }, cfg, channel: ttsChannel, kind: "final", inboundAudio, ttsAuto: sessionTtsAuto }); if (ttsSyntheticReply.mediaUrl) { const ttsOnlyPayload = { mediaUrl: ttsSyntheticReply.mediaUrl, audioAsVoice: ttsSyntheticReply.audioAsVoice }; if (shouldRouteToOriginating && originatingChannel && originatingTo) { const result = await routeReply({ payload: ttsOnlyPayload, channel: originatingChannel, to: originatingTo, sessionKey: ctx.SessionKey, accountId: ctx.AccountId, threadId: ctx.MessageThreadId, cfg }); queuedFinal = result.ok || queuedFinal; if (result.ok) routedFinalCount += 1; if (!result.ok) logVerbose(`dispatch-from-config: route-reply (tts-only) failed: ${result.error ?? "unknown error"}`); } else queuedFinal = dispatcher.sendFinalReply(ttsOnlyPayload) || queuedFinal; } } catch (err) { logVerbose(`dispatch-from-config: accumulated block TTS failed: ${err instanceof Error ? err.message : String(err)}`); } const counts = dispatcher.getQueuedCounts(); counts.final += routedFinalCount; recordProcessed("completed"); markIdle("message_completed"); return { queuedFinal, counts }; } catch (err) { recordProcessed("error", { error: String(err) }); markIdle("message_error"); throw err; } } //#endregion //#region src/auto-reply/reply/dispatcher-registry.ts const activeDispatchers = /* @__PURE__ */ new Set(); let nextId = 0; /** * Register a reply dispatcher for global tracking. * Returns an unregister function to call when the dispatcher is no longer needed. */ function registerDispatcher(dispatcher) { const id = `dispatcher-${++nextId}`; const tracked = { id, pending: dispatcher.pending, waitForIdle: dispatcher.waitForIdle }; activeDispatchers.add(tracked); const unregister = () => { activeDispatchers.delete(tracked); }; return { id, unregister }; } //#endregion //#region src/auto-reply/reply/reply-dispatcher.ts const DEFAULT_HUMAN_DELAY_MIN_MS = 800; const DEFAULT_HUMAN_DELAY_MAX_MS = 2500; /** Generate a random delay within the configured range. */ function getHumanDelay(config) { -- "- Cross-session messaging โ†’ use sessions_send(sessionKey, message)", "- Sub-agent orchestration โ†’ use subagents(action=list|steer|kill)", "- `[System Message] ...` blocks are internal context and are not user-visible by default.", `- If a \`[System Message]\` reports completed cron/subagent work and asks for a user update, rewrite it in your normal assistant voice and send that update (do not forward raw system text or default to ${SILENT_REPLY_TOKEN}).`, "- Never use exec/curl for provider messaging; OpenClaw handles all routing internally.", params.availableTools.has("message") ? [ "", "### message tool", "- Use `message` for proactive sends + channel actions (polls, reactions, etc.).", "- For `action=send`, include `to` and `message`.", `- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`, `- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`, params.inlineButtonsEnabled ? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data,style?}]]`; `style` can be `primary`, `success`, or `danger`." : params.runtimeChannel ? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to set ${params.runtimeChannel}.capabilities.inlineButtons ("dm"|"group"|"all"|"allowlist").` : "", ...params.messageToolHints ?? [] ].filter(Boolean).join("\n") : "", "" ]; } function buildVoiceSection(params) { if (params.isMinimal) return []; const hint = params.ttsHint?.trim(); if (!hint) return []; return [ "## Voice (TTS)", hint, "" ]; } function buildDocsSection(params) { const docsPath = params.docsPath?.trim(); if (!docsPath || params.isMinimal) return []; return [ "## Documentation", `OpenClaw docs: ${docsPath}`, "Mirror: https://docs.openclaw.ai", "Source: https://github.com/openclaw/openclaw", "Community: https://discord.com/invite/clawd", "Find new skills: https://clawhub.com", "For OpenClaw behavior, commands, config, or architecture: consult local docs first.", "When diagnosing issues, run `openclaw status` yourself when possible; only ask the user if you lack access (e.g., sandboxed).", "" ]; } function buildAgentSystemPrompt(params) { const coreToolSummaries = { read: "Read file contents", write: "Create or overwrite files", edit: "Make precise edits to files", apply_patch: "Apply multi-file patches", grep: "Search file contents for patterns", find: "Find files by glob pattern", ls: "List directory contents", exec: "Run shell commands (pty available for TTY-required CLIs)", process: "Manage background exec sessions", web_search: "Search the web (Brave API)", web_fetch: "Fetch and extract readable content from a URL", browser: "Control web browser", canvas: "Present/eval/snapshot the Canvas", nodes: "List/describe/notify/camera/screen on paired nodes", cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)", message: "Send messages and channel actions", gateway: "Restart, apply config, or run updates on the running OpenClaw process", agents_list: "List agent ids allowed for sessions_spawn", sessions_list: "List other sessions (incl. sub-agents) with filters/last", sessions_history: "Fetch history for another session/sub-agent", sessions_send: "Send a message to another session/sub-agent", sessions_spawn: "Spawn a sub-agent session", subagents: "List, steer, or kill sub-agent runs for this requester session", session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (๐Ÿ“Š session_status); optional per-session model override", image: "Analyze an image with the configured image model" }; -- params.sandboxInfo.elevated?.allowed ? "You may also send /elevated on|off|ask|full when needed." : "", params.sandboxInfo.elevated?.allowed ? `Current elevated level: ${params.sandboxInfo.elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).` : "" ].filter(Boolean).join("\n") : "", params.sandboxInfo?.enabled ? "" : "", ...buildUserIdentitySection(ownerLine, isMinimal), ...buildTimeSection({ userTimezone }), "## Workspace Files (injected)", "These user-editable files are loaded by OpenClaw and included below in Project Context.", "", ...buildReplyTagsSection(isMinimal), ...buildMessagingSection({ isMinimal, availableTools, messageChannelOptions, inlineButtonsEnabled, runtimeChannel, messageToolHints: params.messageToolHints }), ...buildVoiceSection({ isMinimal, ttsHint: params.ttsHint }) ]; if (extraSystemPrompt) { const contextHeader = promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context"; lines.push(contextHeader, extraSystemPrompt, ""); } if (params.reactionGuidance) { const { level, channel } = params.reactionGuidance; const guidanceText = level === "minimal" ? [ `Reactions are enabled for ${channel} in MINIMAL mode.`, "React ONLY when truly relevant:", "- Acknowledge important user requests or confirmations", "- Express genuine sentiment (humor, appreciation) sparingly", "- Avoid reacting to routine messages or your own replies", "Guideline: at most 1 reaction per 5-10 exchanges." ].join("\n") : [ `Reactions are enabled for ${channel} in EXTENSIVE mode.`, "Feel free to react liberally:", "- Acknowledge messages with appropriate emojis", "- Express sentiment and personality through reactions", "- React to interesting content, humor, or notable events", "- Use reactions to confirm understanding or agreement", "Guideline: react whenever it feels natural." ].join("\n"); lines.push("## Reactions", guidanceText, ""); } if (reasoningHint) lines.push("## Reasoning Format", reasoningHint, ""); const validContextFiles = (params.contextFiles ?? []).filter((file) => typeof file.path === "string" && file.path.trim().length > 0); if (validContextFiles.length > 0) { const hasSoulFile = validContextFiles.some((file) => { const normalizedPath = file.path.trim().replace(/\\/g, "/"); return (normalizedPath.split("/").pop() ?? normalizedPath).toLowerCase() === "soul.md"; }); lines.push("# Project Context", "", "The following project context files have been loaded:"); if (hasSoulFile) lines.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it."); lines.push(""); for (const file of validContextFiles) lines.push(`## ${file.path}`, "", file.content, ""); } if (!isMinimal) lines.push("## Silent Replies", `When you have nothing to say, respond with ONLY: ${SILENT_REPLY_TOKEN}`, "", "โš ๏ธ Rules:", "- It must be your ENTIRE message โ€” nothing else", `- Never append it to an actual response (never include "${SILENT_REPLY_TOKEN}" in real replies)`, "- Never wrap it in markdown or code blocks", "", `โŒ Wrong: "Here's help... ${SILENT_REPLY_TOKEN}"`, `โŒ Wrong: "${SILENT_REPLY_TOKEN}"`, `โœ… Right: ${SILENT_REPLY_TOKEN}`, ""); if (!isMinimal) lines.push("## Heartbeats", heartbeatPromptLine, "If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:", "HEARTBEAT_OK", "OpenClaw treats a leading/trailing \"HEARTBEAT_OK\" as a heartbeat ack (and may discard it).", "If something needs attention, do NOT include \"HEARTBEAT_OK\"; reply with the alert text instead.", ""); lines.push("## Runtime", buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel), `Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`); return lines.filter(Boolean).join("\n"); } function buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities = [], defaultThinkLevel) { return `Runtime: ${[ runtimeInfo?.agentId ? `agent=${runtimeInfo.agentId}` : "", runtimeInfo?.host ? `host=${runtimeInfo.host}` : "", runtimeInfo?.repoRoot ? `repo=${runtimeInfo.repoRoot}` : "", runtimeInfo?.os ? `os=${runtimeInfo.os}${runtimeInfo?.arch ? ` (${runtimeInfo.arch})` : ""}` : runtimeInfo?.arch ? `arch=${runtimeInfo.arch}` : "", runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", -- const defaultModelRef = resolveDefaultModelForAgent({ cfg: params.config ?? {}, agentId: params.agentId }); const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`; const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ config: params.config, agentId: params.agentId, workspaceDir: params.workspaceDir, cwd: process.cwd(), runtime: { host: "openclaw", os: `${os.type()} ${os.release()}`, arch: os.arch(), node: process.version, model: params.modelDisplay, defaultModel: defaultModelLabel, shell: detectRuntimeShell() } }); const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : void 0; const ownerDisplay = resolveOwnerDisplaySetting(params.config); return buildAgentSystemPrompt({ workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, ownerDisplay: ownerDisplay.ownerDisplay, ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint: false, heartbeatPrompt: params.heartbeatPrompt, docsPath: params.docsPath, runtimeInfo, toolNames: params.tools.map((tool) => tool.name), modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, userTimeFormat, contextFiles: params.contextFiles, ttsHint, memoryCitationsMode: params.config?.memory?.citations }); } function normalizeCliModel(modelId, backend) { const trimmed = modelId.trim(); if (!trimmed) return trimmed; const direct = backend.modelAliases?.[trimmed]; if (direct) return direct; const lower = trimmed.toLowerCase(); const mapped = backend.modelAliases?.[lower]; if (mapped) return mapped; return trimmed; } function toUsage(raw) { const pick = (key) => typeof raw[key] === "number" && raw[key] > 0 ? raw[key] : void 0; const input = pick("input_tokens") ?? pick("inputTokens"); const output = pick("output_tokens") ?? pick("outputTokens"); const cacheRead = pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead"); const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite"); const total = pick("total_tokens") ?? pick("total"); if (!input && !output && !cacheRead && !cacheWrite && !total) return; return { input, output, cacheRead, cacheWrite, total }; } function collectText(value) { if (!value) return ""; if (typeof value === "string") return value; if (Array.isArray(value)) return value.map((entry) => collectText(entry)).join(""); if (!isRecord$1(value)) return ""; if (typeof value.text === "string") return value.text; if (typeof value.content === "string") return value.content; if (Array.isArray(value.content)) return value.content.map((entry) => collectText(entry)).join(""); if (isRecord$1(value.message)) return collectText(value.message); return ""; } function pickSessionId(parsed, backend) { const fields = backend.sessionIdFields ?? [ "session_id", "sessionId", "conversation_id", "conversationId" ]; for (const field of fields) { const value = parsed[field]; if (typeof value === "string" && value.trim()) return value.trim(); -- voiceSettings: { ...base.elevenlabs?.voiceSettings, ...override.elevenlabs?.voiceSettings } }, openai: { ...base.openai, ...override.openai }, edge: { ...base.edge, ...override.edge } }; } function resolveVoiceTtsConfig(params) { if (!params.override) return { cfg: params.cfg, resolved: resolveTtsConfig(params.cfg) }; const merged = mergeTtsConfig(params.cfg.messages?.tts ?? {}, params.override); const messages = params.cfg.messages ?? {}; const cfg = { ...params.cfg, messages: { ...messages, tts: merged } }; return { cfg, resolved: resolveTtsConfig(cfg) }; } function buildWavBuffer(pcm) { const blockAlign = CHANNELS * BIT_DEPTH / 8; const byteRate = SAMPLE_RATE * blockAlign; const header = Buffer.alloc(44); header.write("RIFF", 0); header.writeUInt32LE(36 + pcm.length, 4); header.write("WAVE", 8); header.write("fmt ", 12); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(CHANNELS, 22); header.writeUInt32LE(SAMPLE_RATE, 24); header.writeUInt32LE(byteRate, 28); header.writeUInt16LE(blockAlign, 32); header.writeUInt16LE(BIT_DEPTH, 34); header.write("data", 36); header.writeUInt32LE(pcm.length, 40); return Buffer.concat([header, pcm]); } let warnedOpusFallback = false; function createOpusDecoder() { try { const { OpusEncoder } = require("@discordjs/opus"); return { decoder: new OpusEncoder(SAMPLE_RATE, CHANNELS), name: "@discordjs/opus" }; } catch (nativeErr) { try { const OpusScript = require("opusscript"); const decoder = new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO); if (!warnedOpusFallback) { warnedOpusFallback = true; logger.warn(`discord voice: @discordjs/opus unavailable (${formatErrorMessage(nativeErr)}); using opusscript fallback`); } return { decoder, name: "opusscript" }; } catch (jsErr) { logger.warn(`discord voice: opus decoder init failed: ${formatErrorMessage(nativeErr)}`); logger.warn(`discord voice: opusscript init failed: ${formatErrorMessage(jsErr)}`); } -- filePath: wavPath }); if (!transcript) { logVoiceVerbose(`transcription empty: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`); return; } logVoiceVerbose(`transcription ok (${transcript.length} chars): guild ${entry.guildId} channel ${entry.channelId}`); const speakerLabel = await this.resolveSpeakerLabel(entry.guildId, userId); const replyText = ((await agentCommand({ message: speakerLabel ? `${speakerLabel}: ${transcript}` : transcript, sessionKey: entry.route.sessionKey, agentId: entry.route.agentId, messageChannel: "discord", deliver: false }, this.params.runtime)).payloads ?? []).map((payload) => payload.text).filter((text) => typeof text === "string" && text.trim()).join("\n").trim(); if (!replyText) { logVoiceVerbose(`reply empty: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`); return; } logVoiceVerbose(`reply ok (${replyText.length} chars): guild ${entry.guildId} channel ${entry.channelId}`); const { cfg: ttsCfg, resolved: ttsConfig } = resolveVoiceTtsConfig({ cfg: this.params.cfg, override: this.params.discordConfig.voice?.tts }); const directive = parseTtsDirectives(replyText, ttsConfig.modelOverrides); const speakText = directive.overrides.ttsText ?? directive.cleanedText.trim(); if (!speakText) { logVoiceVerbose(`tts skipped (empty): guild ${entry.guildId} channel ${entry.channelId} user ${userId}`); return; } const ttsResult = await textToSpeech({ text: speakText, cfg: ttsCfg, channel: "discord", overrides: directive.overrides }); if (!ttsResult.success || !ttsResult.audioPath) { logger.warn(`discord voice: TTS failed: ${ttsResult.error ?? "unknown error"}`); return; } const audioPath = ttsResult.audioPath; logVoiceVerbose(`tts ok (${speakText.length} chars): guild ${entry.guildId} channel ${entry.channelId}`); this.enqueuePlayback(entry, async () => { logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} file ${path.basename(audioPath)}`); const resource = createAudioResource(audioPath); entry.player.play(resource); await entersState(entry.player, AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS).catch(() => void 0); await entersState(entry.player, AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS).catch(() => void 0); logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`); }); } async resolveSpeakerLabel(guildId, userId) { try { const member = await this.params.client.fetchMember(guildId, userId); return member.nickname ?? member.user?.globalName ?? member.user?.username ?? userId; } catch { try { const user = await this.params.client.fetchUser(userId); return user.globalName ?? user.username ?? userId; } catch { return userId; } } } }; var DiscordVoiceReadyListener = class extends ReadyListener { constructor(manager) { super(); this.manager = manager; } async handle() { await this.manager.autoJoin(); } }; function isVoiceChannel(type) { return type === ChannelType$1.GuildVoice || type === ChannelType$1.GuildStageVoice; } //#endregion //#region src/discord/monitor/agent-components.ts const AGENT_BUTTON_KEY = "agent"; const AGENT_SELECT_KEY = "agentsel"; function resolveAgentComponentRoute(params) { return resolveAgentRoute({ cfg: params.ctx.cfg, channel: "discord", accountId: params.ctx.accountId, guildId: params.rawGuildId, memberRoleIds: params.memberRoleIds, peer: { kind: params.isDirectMessage ? "direct" : "channel", id: params.isDirectMessage ? params.userId : params.channelId -- const model = chosen?.model?.trim(); const modelLabel = provider ? model ? `${provider}/${model}` : provider : null; return `${decision.capability}${countLabel} ok${modelLabel ? ` (${modelLabel})` : ""}`; } if (decision.outcome === "no-attachment") return `${decision.capability} none`; if (decision.outcome === "disabled") return `${decision.capability} off`; if (decision.outcome === "scope-deny") return `${decision.capability} denied`; if (decision.outcome === "skipped") { const reason = decision.attachments.flatMap((entry) => entry.attempts.map((attempt) => attempt.reason).filter(Boolean)).find(Boolean); const shortReason = reason ? reason.split(":")[0]?.trim() : void 0; return `${decision.capability} skipped${shortReason ? ` (${shortReason})` : ""}`; } return null; }).filter((part) => part != null); if (parts.length === 0) return null; if (parts.every((part) => part.endsWith(" none"))) return null; return `๐Ÿ“Ž Media: ${parts.join(" ยท ")}`; }; const formatVoiceModeLine = (config, sessionEntry) => { if (!config) return null; const ttsConfig = resolveTtsConfig(config); const prefsPath = resolveTtsPrefsPath(ttsConfig); const autoMode = resolveTtsAutoMode({ config: ttsConfig, prefsPath, sessionAuto: sessionEntry?.ttsAuto }); if (autoMode === "off") return null; return `๐Ÿ”Š Voice: ${autoMode} ยท provider=${getTtsProvider(ttsConfig, prefsPath)} ยท limit=${getTtsMaxLength(prefsPath)} ยท summary=${isSummarizationEnabled(prefsPath) ? "on" : "off"}`; }; function buildStatusMessage(args) { const now = args.now ?? Date.now(); const entry = args.sessionEntry; const selectionConfig = { agents: { defaults: args.agent ?? {} } }; const contextConfig = args.config ? { ...args.config, agents: { ...args.config.agents, defaults: { ...args.config.agents?.defaults, ...args.agent } } } : { agents: { defaults: args.agent ?? {} } }; const resolved = resolveConfiguredModelRef({ cfg: selectionConfig, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL }); const selectedProvider = entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER; const selectedModel = entry?.modelOverride ?? resolved.model ?? DEFAULT_MODEL; const modelRefs = resolveSelectedAndActiveModel({ selectedProvider, selectedModel, sessionEntry: entry }); let activeProvider = modelRefs.active.provider; let activeModel = modelRefs.active.model; let contextTokens = resolveContextTokensForModel({ cfg: contextConfig, provider: activeProvider, model: activeModel, contextTokensOverride: entry?.contextTokens ?? args.agent?.contextTokens, fallbackContextTokens: DEFAULT_CONTEXT_TOKENS }) ?? DEFAULT_CONTEXT_TOKENS; let inputTokens = entry?.inputTokens; let outputTokens = entry?.outputTokens; let cacheRead = entry?.cacheRead; let cacheWrite = entry?.cacheWrite; let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0); if (args.includeTranscriptUsage) { const logUsage = readUsageFromSessionLog(entry?.sessionId, entry, args.agentId, args.sessionKey, args.sessionStorePath); if (logUsage) { const candidate = logUsage.promptTokens || logUsage.total; if (!totalTokens || totalTokens === 0 || candidate > totalTokens) totalTokens = candidate; if (!entry?.model && logUsage.model) { const slashIndex = logUsage.model.indexOf("/"); if (slashIndex > 0) { const provider = logUsage.model.slice(0, slashIndex).trim(); -- workspaceDir, cwd: process.cwd(), runtime: { host: "unknown", os: "unknown", arch: "unknown", node: process.version, model: `${params.provider}/${params.model}`, defaultModel: defaultModelLabel } }); const sandboxInfo = sandboxRuntime.sandboxed ? { enabled: true, workspaceDir, workspaceAccess: "rw", elevated: { allowed: params.elevated.allowed, defaultLevel: params.resolvedElevatedLevel ?? "off" } } : { enabled: false }; const ttsHint = params.cfg ? buildTtsSystemPromptHint(params.cfg) : void 0; return { systemPrompt: buildAgentSystemPrompt({ workspaceDir, defaultThinkLevel: params.resolvedThinkLevel, reasoningLevel: params.resolvedReasoningLevel, extraSystemPrompt: void 0, ownerNumbers: void 0, reasoningTagHint: false, toolNames, toolSummaries, modelAliasLines: [], userTimezone, userTime, userTimeFormat, contextFiles: injectedFiles, skillsPrompt, heartbeatPrompt: void 0, ttsHint, runtimeInfo, sandboxInfo, memoryCitationsMode: params.cfg?.memory?.citations }), tools, skillsPrompt, bootstrapFiles, injectedFiles, sandboxRuntime }; } //#endregion //#region src/auto-reply/reply/commands-context-report.ts function estimateTokensFromChars(chars) { return Math.ceil(Math.max(0, chars) / 4); } function formatInt(n) { return new Intl.NumberFormat("en-US").format(n); } function formatCharsAndTokens(chars) { return `${formatInt(chars)} chars (~${formatInt(estimateTokensFromChars(chars))} tok)`; } function parseContextArgs(commandBodyNormalized) { if (commandBodyNormalized === "/context") return ""; if (commandBodyNormalized.startsWith("/context ")) return commandBodyNormalized.slice(8).trim(); return ""; } function formatListTop(entries, cap) { const sorted = [...entries].toSorted((a, b) => b.value - a.value); const top = sorted.slice(0, cap); const omitted = Math.max(0, sorted.length - top.length); return { lines: top.map((e) => `- ${e.name}: ${formatCharsAndTokens(e.value)}`), omitted }; } async function resolveContextReport(params) { const existing = params.sessionEntry?.systemPromptReport; if (existing && existing.source === "run") return existing; const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg); const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.cfg); const { systemPrompt, tools, skillsPrompt, bootstrapFiles, injectedFiles, sandboxRuntime } = await resolveCommandsSystemPromptBundle(params); return buildSystemPromptReport({ source: "estimate", generatedAt: Date.now(), sessionId: params.sessionEntry?.sessionId, sessionKey: params.sessionKey, provider: params.provider, model: params.model, -- return webLoginPromise; } function loadWebLoginQr() { webLoginQrPromise ??= import("./login-qr-BuDpnoR8.js"); return webLoginQrPromise; } function loadWebChannel() { webChannelPromise ??= import("./web-gvMwnGUU.js"); return webChannelPromise; } function loadWhatsAppActions() { whatsappActionsPromise ??= import("./whatsapp-actions-CHbQZBh7.js"); return whatsappActionsPromise; } function createPluginRuntime() { return { version: resolveVersion(), config: createRuntimeConfig(), system: createRuntimeSystem(), media: createRuntimeMedia(), tts: { textToSpeechTelephony }, tools: createRuntimeTools(), channel: createRuntimeChannel(), logging: createRuntimeLogging(), state: { resolveStateDir } }; } function createRuntimeConfig() { return { loadConfig, writeConfigFile }; } function createRuntimeSystem() { return { enqueueSystemEvent, runCommandWithTimeout, formatNativeDependencyHint }; } function createRuntimeMedia() { return { loadWebMedia, detectMime, mediaKindFromMime, isVoiceCompatibleAudio, getImageMetadata, resizeToJpeg }; } function createRuntimeTools() { return { createMemoryGetTool, createMemorySearchTool, registerMemoryCli }; } function createRuntimeChannel() { return { text: { chunkByNewline, chunkMarkdownText, chunkMarkdownTextWithMode, chunkText, chunkTextWithMode, resolveChunkMode, resolveTextChunkLimit, hasControlCommand, resolveMarkdownTableMode, convertMarkdownTables }, -- status: "accepted", action: "steer", target, runId, sessionKey: resolved.entry.childSessionKey, sessionId, mode: "restart", label: resolveSubagentLabel(resolved.entry), text: `steered ${resolveSubagentLabel(resolved.entry)}.` }); } return jsonResult({ status: "error", error: "Unsupported action." }); } }; } //#endregion //#region src/agents/tools/tts-tool.ts const TtsToolSchema = Type.Object({ text: Type.String({ description: "Text to convert to speech." }), channel: Type.Optional(Type.String({ description: "Optional channel id to pick output format (e.g. telegram)." })) }); function createTtsTool(opts) { return { label: "TTS", name: "tts", description: `Convert text to speech. Audio is delivered automatically from the tool result โ€” reply with ${SILENT_REPLY_TOKEN} after a successful call to avoid duplicate messages.`, parameters: TtsToolSchema, execute: async (_toolCallId, args) => { const params = args; const text = readStringParam(params, "text", { required: true }); const channel = readStringParam(params, "channel"); const result = await textToSpeech({ text, cfg: opts?.config ?? loadConfig(), channel: channel ?? opts?.agentChannel }); if (result.success && result.audioPath) { const lines = []; if (result.voiceCompatible) lines.push("[[audio_as_voice]]"); lines.push(`MEDIA:${result.audioPath}`); return { content: [{ type: "text", text: lines.join("\n") }], details: { audioPath: result.audioPath, provider: result.provider } }; } return { content: [{ type: "text", text: result.error ?? "TTS conversion failed" }], details: { error: result.error } }; } }; } //#endregion //#region src/agents/tools/web-fetch-visibility.ts const HIDDEN_STYLE_PATTERNS = [ ["display", /^\s*none\s*$/i], ["visibility", /^\s*hidden\s*$/i], ["opacity", /^\s*0\s*$/], ["font-size", /^\s*0(px|em|rem|pt|%)?\s*$/i], ["text-indent", /^\s*-\d{4,}px\s*$/], ["color", /^\s*transparent\s*$/i], ["color", /^\s*rgba\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)\s*$/i], ["color", /^\s*hsla\s*\(\s*[\d.]+\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*,\s*0(?:\.0+)?\s*\)\s*$/i] ]; const HIDDEN_CLASS_NAMES = new Set([ -- } trackSessionManagerAccess(sessionFile); } catch {} } //#endregion //#region src/agents/pi-embedded-runner/system-prompt.ts function buildEmbeddedSystemPrompt(params) { return buildAgentSystemPrompt({ workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, reasoningLevel: params.reasoningLevel, extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, ownerDisplay: params.ownerDisplay, ownerDisplaySecret: params.ownerDisplaySecret, reasoningTagHint: params.reasoningTagHint, heartbeatPrompt: params.heartbeatPrompt, skillsPrompt: params.skillsPrompt, docsPath: params.docsPath, ttsHint: params.ttsHint, workspaceNotes: params.workspaceNotes, reactionGuidance: params.reactionGuidance, promptMode: params.promptMode, runtimeInfo: params.runtimeInfo, messageToolHints: params.messageToolHints, sandboxInfo: params.sandboxInfo, toolNames: params.tools.map((tool) => tool.name), toolSummaries: buildToolSummaryMap(params.tools), modelAliasLines: params.modelAliasLines, userTimezone: params.userTimezone, userTime: params.userTime, userTimeFormat: params.userTimeFormat, contextFiles: params.contextFiles, memoryCitationsMode: params.memoryCitationsMode }); } function createSystemPromptOverride(systemPrompt) { const override = systemPrompt.trim(); return (_defaultPrompt) => override; } function applySystemPromptOverrideToSession(session, override) { const prompt = typeof override === "function" ? override() : override.trim(); session.agent.setSystemPrompt(prompt); const mutableSession = session; mutableSession._baseSystemPrompt = prompt; mutableSession._rebuildSystemPrompt = () => prompt; } //#endregion //#region src/agents/pi-embedded-runner/tool-name-allowlist.ts function addName(names, value) { if (typeof value !== "string") return; const trimmed = value.trim(); if (trimmed) names.add(trimmed); } function collectAllowedToolNames(params) { const names = /* @__PURE__ */ new Set(); for (const tool of params.tools) addName(names, tool.name); for (const tool of params.clientTools ?? []) addName(names, tool.function?.name); return names; } //#endregion //#region src/agents/pi-tool-definition-adapter.ts function isAbortSignal(value) { return typeof value === "object" && value !== null && "aborted" in value; } function isLegacyToolExecuteArgs(args) { const third = args[2]; const fifth = args[4]; -- capabilities: runtimeCapabilities, channelActions }; const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); const reasoningTagHint = isReasoningTagProvider(provider); const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat); const userTime = formatUserTime(/* @__PURE__ */ new Date(), userTimezone, userTimeFormat); const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, config: params.config }); const isDefaultAgent = sessionAgentId === defaultAgentId; const promptMode = isSubagentSessionKey(params.sessionKey) || isCronSessionKey(params.sessionKey) ? "minimal" : "full"; const docsPath = await resolveOpenClawDocsPath({ workspaceDir: effectiveWorkspace, argv1: process.argv[1], cwd: process.cwd(), moduleUrl: import.meta.url }); const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : void 0; const ownerDisplay = resolveOwnerDisplaySetting(params.config); const systemPromptOverride = createSystemPromptOverride(buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: params.thinkLevel, reasoningLevel: params.reasoningLevel ?? "off", extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, ownerDisplay: ownerDisplay.ownerDisplay, ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint, heartbeatPrompt: isDefaultAgent ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) : void 0, skillsPrompt, docsPath: docsPath ?? void 0, ttsHint, promptMode, runtimeInfo, reactionGuidance, messageToolHints, sandboxInfo, tools, modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, userTimeFormat, contextFiles, memoryCitationsMode: params.config?.memory?.citations })); const sessionLock = await acquireSessionWriteLock({ sessionFile: params.sessionFile, maxHoldMs: resolveSessionLockMaxHoldFromTimeout({ timeoutMs: EMBEDDED_COMPACTION_TIMEOUT_MS }) }); try { await repairSessionFileIfNeeded({ sessionFile: params.sessionFile, warn: (message) => log$6.warn(message) }); await prewarmSessionFile(params.sessionFile); const transcriptPolicy = resolveTranscriptPolicy({ modelApi: model.api, provider, modelId }); const sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), { agentId: sessionAgentId, sessionKey: params.sessionKey, allowSyntheticToolResults: transcriptPolicy.allowSyntheticToolResults, allowedToolNames }); trackSessionManagerAccess(params.sessionFile); const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); applyPiCompactionSettingsFromConfig({ settingsManager, cfg: params.config }); const extensionFactories = buildEmbeddedExtensionFactories({ cfg: params.config, sessionManager, provider, modelId, model }); let resourceLoader; if (extensionFactories.length > 0) { resourceLoader = new DefaultResourceLoader({ -- "apply_patch", "browser", "canvas", "cron", "edit", "exec", "gateway", "image", "memory_get", "memory_search", "message", "nodes", "process", "read", "session_status", "sessions_history", "sessions_list", "sessions_send", "sessions_spawn", "subagents", "tts", "web_fetch", "web_search", "write" ]); const HTTP_URL_RE = /^https?:\/\//i; function isToolResultMediaTrusted(toolName) { if (!toolName) return false; const normalized = normalizeToolName(toolName); return TRUSTED_TOOL_RESULT_MEDIA.has(normalized); } function filterToolResultMediaUrls(toolName, mediaUrls) { if (mediaUrls.length === 0) return mediaUrls; if (isToolResultMediaTrusted(toolName)) return mediaUrls; return mediaUrls.filter((url) => HTTP_URL_RE.test(url.trim())); } /** * Extract media file paths from a tool result. * * Strategy (first match wins): * 1. Parse `MEDIA:` tokens from text content blocks (all OpenClaw tools). * 2. Fall back to `details.path` when image content exists (OpenClaw imageResult). * * Returns an empty array when no media is found (e.g. Pi SDK `read` tool * returns base64 image data but no file path; those need a different delivery * path like saving to a temp file). */ function extractToolResultMediaPaths(result) { if (!result || typeof result !== "object") return []; const record = result; const content = Array.isArray(record.content) ? record.content : null; if (!content) return []; const paths = []; let hasImageContent = false; for (const item of content) { if (!item || typeof item !== "object") continue; const entry = item; if (entry.type === "image") { hasImageContent = true; continue; } if (entry.type === "text" && typeof entry.text === "string") { const parsed = splitMediaFromOutput(entry.text); if (parsed.mediaUrls?.length) paths.push(...parsed.mediaUrls); } } if (paths.length > 0) return paths; if (hasImageContent) { const details = record.details; const p = typeof details?.path === "string" ? details.path.trim() : ""; if (p) return [p]; -- host: machineName, os: `${os.type()} ${os.release()}`, arch: os.arch(), node: process.version, model: `${params.provider}/${params.modelId}`, defaultModel: defaultModelLabel, shell: detectRuntimeShell(), channel: runtimeChannel, capabilities: runtimeCapabilities, channelActions } }); const isDefaultAgent = sessionAgentId === defaultAgentId; const promptMode = resolvePromptModeForSession(params.sessionKey); const docsPath = await resolveOpenClawDocsPath({ workspaceDir: effectiveWorkspace, argv1: process.argv[1], cwd: process.cwd(), moduleUrl: import.meta.url }); const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : void 0; const ownerDisplay = resolveOwnerDisplaySetting(params.config); const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: params.thinkLevel, reasoningLevel: params.reasoningLevel ?? "off", extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, ownerDisplay: ownerDisplay.ownerDisplay, ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, reasoningTagHint, heartbeatPrompt: isDefaultAgent ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) : void 0, skillsPrompt, docsPath: docsPath ?? void 0, ttsHint, workspaceNotes, reactionGuidance, promptMode, runtimeInfo, messageToolHints, sandboxInfo, tools, modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, userTimeFormat, contextFiles, memoryCitationsMode: params.config?.memory?.citations }); const systemPromptReport = buildSystemPromptReport({ source: "run", generatedAt: Date.now(), sessionId: params.sessionId, sessionKey: params.sessionKey, provider: params.provider, model: params.modelId, workspaceDir: effectiveWorkspace, bootstrapMaxChars: resolveBootstrapMaxChars(params.config), bootstrapTotalMaxChars: resolveBootstrapTotalMaxChars(params.config), sandbox: (() => { const runtime = resolveSandboxRuntimeStatus({ cfg: params.config, sessionKey: params.sessionKey ?? params.sessionId }); return { mode: runtime.mode, sandboxed: runtime.sandboxed }; })(), systemPrompt: appendPrompt, bootstrapFiles: hookAdjustedBootstrapFiles, injectedFiles: contextFiles, skillsPrompt, tools }); let systemPromptText = createSystemPromptOverride(appendPrompt)(); const sessionLock = await acquireSessionWriteLock({ sessionFile: params.sessionFile, maxHoldMs: resolveSessionLockMaxHoldFromTimeout({ timeoutMs: params.timeoutMs }) }); let sessionManager; let session; let removeToolResultContextGuard; try { await repairSessionFileIfNeeded({ -- runs: listSubagentRunsForRequester(requesterKey), restTokens }; switch (action) { case "help": return handleSubagentsHelpAction(); case "agents": return handleSubagentsAgentsAction(ctx); case "focus": return await handleSubagentsFocusAction(ctx); case "unfocus": return handleSubagentsUnfocusAction(ctx); case "list": return handleSubagentsListAction(ctx); case "kill": return await handleSubagentsKillAction(ctx); case "info": return handleSubagentsInfoAction(ctx); case "log": return await handleSubagentsLogAction(ctx); case "send": return await handleSubagentsSendAction(ctx, false); case "steer": return await handleSubagentsSendAction(ctx, true); case "spawn": return await handleSubagentsSpawnAction(ctx); default: return handleSubagentsHelpAction(); } }; //#endregion //#region src/auto-reply/reply/commands-tts.ts function parseTtsCommand(normalized) { if (normalized === "/tts") return { action: "status", args: "" }; if (!normalized.startsWith("/tts ")) return null; const rest = normalized.slice(5).trim(); if (!rest) return { action: "status", args: "" }; const [action, ...tail] = rest.split(/\s+/); return { action: action.toLowerCase(), args: tail.join(" ").trim() }; } function ttsUsage() { return { text: "๐Ÿ”Š **TTS (Text-to-Speech) Help**\n\n**Commands:**\nโ€ข /tts on โ€” Enable automatic TTS for replies\nโ€ข /tts off โ€” Disable TTS\nโ€ข /tts status โ€” Show current settings\nโ€ข /tts provider [name] โ€” View/change provider\nโ€ข /tts limit [number] โ€” View/change text limit\nโ€ข /tts summary [on|off] โ€” View/change auto-summary\nโ€ข /tts audio โ€” Generate audio from text\n\n**Providers:**\nโ€ข edge โ€” Free, fast (default)\nโ€ข openai โ€” High quality (requires API key)\nโ€ข elevenlabs โ€” Premium voices (requires API key)\n\n**Text Limit (default: 1500, max: 4096):**\nWhen text exceeds the limit:\nโ€ข Summary ON: AI summarizes, then generates audio\nโ€ข Summary OFF: Truncates text, then generates audio\n\n**Examples:**\n/tts provider edge\n/tts limit 2000\n/tts audio Hello, this is a test!" }; } const handleTtsCommands = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const parsed = parseTtsCommand(params.command.commandBodyNormalized); if (!parsed) return null; if (!params.command.isAuthorizedSender) { logVerbose(`Ignoring TTS command from unauthorized sender: ${params.command.senderId || ""}`); return { shouldContinue: false }; } const config = resolveTtsConfig(params.cfg); const prefsPath = resolveTtsPrefsPath(config); const action = parsed.action; const args = parsed.args; if (action === "help") return { shouldContinue: false, reply: ttsUsage() }; if (action === "on") { setTtsEnabled(prefsPath, true); return { shouldContinue: false, reply: { text: "๐Ÿ”Š TTS enabled." } }; } if (action === "off") { setTtsEnabled(prefsPath, false); return { shouldContinue: false, reply: { text: "๐Ÿ”‡ TTS disabled." } }; } if (action === "audio") { if (!args.trim()) return { shouldContinue: false, reply: { text: "๐ŸŽค Generate audio from text.\n\nUsage: /tts audio \nExample: /tts audio Hello, this is a test!" } }; const start = Date.now(); const result = await textToSpeech({ text: args, cfg: params.cfg, channel: params.command.channel, prefsPath }); if (result.success && result.audioPath) { setLastTtsAttempt({ timestamp: Date.now(), success: true, textLength: args.length, summarized: false, provider: result.provider, latencyMs: result.latencyMs }); return { shouldContinue: false, reply: { mediaUrl: result.audioPath, audioAsVoice: result.voiceCompatible === true } }; } setLastTtsAttempt({ timestamp: Date.now(), success: false, textLength: args.length, summarized: false, error: result.error, latencyMs: Date.now() - start }); return { shouldContinue: false, reply: { text: `โŒ Error generating audio: ${result.error ?? "unknown error"}` } }; } if (action === "provider") { const currentProvider = getTtsProvider(config, prefsPath); if (!args.trim()) { const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai")); const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs")); const hasEdge = isTtsProviderConfigured(config, "edge"); return { shouldContinue: false, reply: { text: `๐ŸŽ™๏ธ TTS provider\nPrimary: ${currentProvider}\nOpenAI key: ${hasOpenAI ? "โœ…" : "โŒ"}\nElevenLabs key: ${hasElevenLabs ? "โœ…" : "โŒ"}\nEdge enabled: ${hasEdge ? "โœ…" : "โŒ"}\nUsage: /tts provider openai | elevenlabs | edge` } }; } const requested = args.trim().toLowerCase(); if (requested !== "openai" && requested !== "elevenlabs" && requested !== "edge") return { shouldContinue: false, reply: ttsUsage() }; setTtsProvider(prefsPath, requested); return { shouldContinue: false, reply: { text: `โœ… TTS provider set to ${requested}.` } }; } if (action === "limit") { if (!args.trim()) return { shouldContinue: false, reply: { text: `๐Ÿ“ TTS limit: ${getTtsMaxLength(prefsPath)} characters.\n\nText longer than this triggers summary (if enabled).\nRange: 100-4096 chars (Telegram max).\n\nTo change: /tts limit \nExample: /tts limit 2000` } }; const next = Number.parseInt(args.trim(), 10); if (!Number.isFinite(next) || next < 100 || next > 4096) return { shouldContinue: false, reply: { text: "โŒ Limit must be between 100 and 4096 characters." } }; setTtsMaxLength(prefsPath, next); return { shouldContinue: false, reply: { text: `โœ… TTS limit set to ${next} characters.` } }; } if (action === "summary") { if (!args.trim()) { const enabled = isSummarizationEnabled(prefsPath); const maxLen = getTtsMaxLength(prefsPath); return { shouldContinue: false, reply: { text: `๐Ÿ“ TTS auto-summary: ${enabled ? "on" : "off"}.\n\nWhen text exceeds ${maxLen} chars:\nโ€ข ON: summarizes text, then generates audio\nโ€ข OFF: truncates text, then generates audio\n\nTo change: /tts summary on | off` } }; } const requested = args.trim().toLowerCase(); if (requested !== "on" && requested !== "off") return { shouldContinue: false, reply: ttsUsage() }; setSummarizationEnabled(prefsPath, requested === "on"); return { shouldContinue: false, reply: { text: requested === "on" ? "โœ… TTS auto-summary enabled." : "โŒ TTS auto-summary disabled." } }; } if (action === "status") { const enabled = isTtsEnabled(config, prefsPath); const provider = getTtsProvider(config, prefsPath); const hasKey = isTtsProviderConfigured(config, provider); const maxLength = getTtsMaxLength(prefsPath); const summarize = isSummarizationEnabled(prefsPath); const last = getLastTtsAttempt(); const lines = [ "๐Ÿ“Š TTS status", `State: ${enabled ? "โœ… enabled" : "โŒ disabled"}`, `Provider: ${provider} (${hasKey ? "โœ… configured" : "โŒ not configured"})`, `Text limit: ${maxLength} chars`, `Auto-summary: ${summarize ? "on" : "off"}` ]; if (last) { const timeAgo = Math.round((Date.now() - last.timestamp) / 1e3); lines.push(""); lines.push(`Last attempt (${timeAgo}s ago): ${last.success ? "โœ…" : "โŒ"}`); lines.push(`Text: ${last.textLength} chars${last.summarized ? " (summarized)" : ""}`); if (last.success) { lines.push(`Provider: ${last.provider ?? "unknown"}`); lines.push(`Latency: ${last.latencyMs ?? 0}ms`); } else if (last.error) lines.push(`Error: ${last.error}`); } return { shouldContinue: false, reply: { text: lines.join("\n") } }; } return { shouldContinue: false, reply: ttsUsage() }; }; //#endregion //#region src/auto-reply/reply/commands-core.ts let HANDLERS = null; async function handleCommands(params) { if (HANDLERS === null) HANDLERS = [ handlePluginCommand, handleBashCommand, handleActivationCommand, handleSendPolicyCommand, handleUsageCommand, handleSessionCommand, handleRestartCommand, handleTtsCommands, handleHelpCommand, handleCommandsListCommand, handleStatusCommand, handleAllowlistCommand, handleApproveCommand, handleContextCommand, handleExportSessionCommand, handleWhoamiCommand, handleSubagentsCommand, handleConfigCommand, handleDebugCommand, handleModelsCommand, handleStopCommand, handleCompactCommand, handleAbortTrigger ]; const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/); const resetRequested = Boolean(resetMatch); if (resetRequested && !params.command.isAuthorizedSender) { logVerbose(`Ignoring /reset from unauthorized sender: ${params.command.senderId || ""}`); return { shouldContinue: false }; } if (resetRequested && params.command.isAuthorizedSender) { const commandAction = resetMatch?.[1] ?? "new"; const hookEvent = createInternalHookEvent("command", commandAction, params.sessionKey ?? "", { sessionEntry: params.sessionEntry, previousSessionEntry: params.previousSessionEntry, commandSource: params.command.surface, senderId: params.command.senderId, cfg: params.cfg }); await triggerInternalHook(hookEvent); if (hookEvent.messages.length > 0) { const channel = params.ctx.OriginatingChannel || params.command.channel; -- isGroup, isThread }), resetOverride: resolveChannelResetConfig({ sessionCfg, channel: groupResolution?.channel ?? ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider }) }); const freshEntry = entry ? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy }).fresh : false; if (!isNewSession && freshEntry) { sessionId = entry.sessionId; systemSent = entry.systemSent ?? false; abortedLastRun = entry.abortedLastRun ?? false; persistedThinking = entry.thinkingLevel; persistedVerbose = entry.verboseLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; persistedModelOverride = entry.modelOverride; persistedProviderOverride = entry.providerOverride; persistedLabel = entry.label; } else { sessionId = crypto.randomUUID(); isNewSession = true; systemSent = false; abortedLastRun = false; if (resetTriggered && entry) { persistedThinking = entry.thinkingLevel; persistedVerbose = entry.verboseLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; persistedModelOverride = entry.modelOverride; persistedProviderOverride = entry.providerOverride; persistedLabel = entry.label; } } const baseEntry = !isNewSession && freshEntry ? entry : void 0; const originatingChannelRaw = ctx.OriginatingChannel; const lastChannelRaw = resolveLastChannelRaw({ originatingChannelRaw, persistedLastChannel: baseEntry?.lastChannel, sessionKey }); const lastToRaw = ctx.OriginatingTo || ctx.To || baseEntry?.lastTo; const lastAccountIdRaw = ctx.AccountId || baseEntry?.lastAccountId; const lastThreadIdRaw = ctx.MessageThreadId || (isThread ? baseEntry?.lastThreadId : void 0); const deliveryFields = normalizeSessionDeliveryFields({ deliveryContext: { channel: lastChannelRaw, to: lastToRaw, accountId: lastAccountIdRaw, threadId: lastThreadIdRaw } }); const lastChannel = deliveryFields.lastChannel ?? lastChannelRaw; const lastTo = deliveryFields.lastTo ?? lastToRaw; const lastAccountId = deliveryFields.lastAccountId ?? lastAccountIdRaw; const lastThreadId = deliveryFields.lastThreadId ?? lastThreadIdRaw; sessionEntry = { ...baseEntry, sessionId, updatedAt: Date.now(), systemSent, abortedLastRun, thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel, verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel, reasoningLevel: persistedReasoning ?? baseEntry?.reasoningLevel, ttsAuto: persistedTtsAuto ?? baseEntry?.ttsAuto, responseUsage: baseEntry?.responseUsage, modelOverride: persistedModelOverride ?? baseEntry?.modelOverride, providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride, label: persistedLabel ?? baseEntry?.label, sendPolicy: baseEntry?.sendPolicy, queueMode: baseEntry?.queueMode, queueDebounceMs: baseEntry?.queueDebounceMs, queueCap: baseEntry?.queueCap, queueDrop: baseEntry?.queueDrop, displayName: baseEntry?.displayName, chatType: baseEntry?.chatType, channel: baseEntry?.channel, groupId: baseEntry?.groupId, subject: baseEntry?.subject, groupChannel: baseEntry?.groupChannel, space: baseEntry?.space, deliveryContext: deliveryFields.deliveryContext, lastChannel, lastTo, lastAccountId, lastThreadId }; const metaPatch = deriveSessionMetaPatch({ ctx: sessionCtxForState, sessionKey, existing: sessionEntry, groupResolution }); if (metaPatch) sessionEntry = { ...sessionEntry, ...metaPatch }; if (!sessionEntry.chatType) sessionEntry.chatType = "direct"; const threadLabel = ctx.ThreadLabel?.trim(); if (threadLabel) sessionEntry.displayName = threadLabel; const parentSessionKey = ctx.ParentSessionKey?.trim(); const alreadyForked = sessionEntry.forkedFromParent === true; if (parentSessionKey && parentSessionKey !== sessionKey && sessionStore[parentSessionKey] && !alreadyForked) { log.warn(`forking from parent session: parentKey=${parentSessionKey} โ†’ sessionKey=${sessionKey} parentTokens=${sessionStore[parentSessionKey].totalTokens ?? "?"}`); const forked = forkSessionFromParent({ parentEntry: sessionStore[parentSessionKey], agentId, sessionsDir: path.dirname(storePath) }); if (forked) { sessionId = forked.sessionId; sessionEntry.sessionId = forked.sessionId; sessionEntry.sessionFile = forked.sessionFile; sessionEntry.forkedFromParent = true; log.warn(`forked session created: file=${forked.sessionFile}`);