--- summary: "api.runtime -- the injected runtime helpers available to plugins" title: "Plugin runtime helpers" sidebarTitle: "Runtime helpers" read_when: - You need to call core helpers from a plugin (TTS, STT, image gen, web search, subagent, nodes) - You want to understand what api.runtime exposes - You are accessing config, agent, or media helpers from plugin code --- Reference for the `api.runtime` object injected into every plugin during registration. Use these helpers instead of importing host internals directly. Step-by-step guide that uses these helpers in context for channel plugins. Step-by-step guide that uses these helpers in context for provider plugins. ```typescript register(api) { const runtime = api.runtime; } ``` `api.runtime.version` is the current OpenClaw product version, sourced from the shared version resolver so plugins see the same value the CLI reports. ## Config loading and writes Prefer config that was already passed into the active call path, for example `api.config` during registration or a `cfg` argument on channel/provider callbacks. This keeps one process snapshot flowing through the work instead of reparsing config on hot paths. Use `api.runtime.config.current()` only when a long-lived handler needs the current process snapshot and no config was passed to that function. The returned value is readonly; clone or use a mutation helper before editing. Tool factories receive `ctx.runtimeConfig` plus `ctx.getRuntimeConfig()`. Use the getter inside a long-lived tool's `execute` callback when config can change after the tool definition was created. Persist changes with `api.runtime.config.mutateConfigFile(...)` or `api.runtime.config.replaceConfigFile(...)`. Each write must choose an explicit `afterWrite` policy: - `afterWrite: { mode: "auto" }` lets the gateway reload planner decide. - `afterWrite: { mode: "restart", reason: "..." }` forces a clean restart when the writer knows hot reload is unsafe. - `afterWrite: { mode: "none", reason: "..." }` suppresses automatic reload/restart only when the caller owns the follow-up. The mutation helpers return `afterWrite` plus a typed `followUp` summary so callers can log or test whether they requested a restart. The gateway still owns when that restart actually happens. `api.runtime.config.loadConfig()` and `api.runtime.config.writeConfigFile(...)` are deprecated. They warn once per plugin at runtime and remain available only for old external plugins during the migration window. Bundled plugins must not use them: an internal config boundary guard fails the build if plugin code calls them or imports those helpers from plugin SDK subpaths. Use `current()`, a passed-in `cfg`, `mutateConfigFile(...)`, or `replaceConfigFile(...)` instead. For direct SDK imports, prefer the focused config subpaths over the broad `openclaw/plugin-sdk/config-runtime` compatibility barrel: `config-contracts` for types, `plugin-config-runtime` for already-loaded config assertions and plugin entry lookup, `runtime-config-snapshot` for current process snapshots, and `config-mutation` for writes. Bundled plugin tests should mock these focused subpaths directly instead of mocking the broad compatibility barrel. Internal OpenClaw runtime code follows the same direction: load config once at the CLI, gateway, or process boundary, then pass that value through. Successful mutation writes refresh the process runtime snapshot and advance its internal revision; long-lived caches should key off the runtime-owned cache key instead of serializing config locally. Long-lived runtime modules have a zero-tolerance scanner for ambient `loadConfig()` calls; use a passed `cfg`, a request `context.getRuntimeConfig()`, or `getRuntimeConfig()` at an explicit process boundary. Provider and channel execution paths must use the active runtime config snapshot, not a file snapshot returned for config readback or editing. File snapshots preserve source values such as SecretRef markers for UI and writes; provider callbacks need the resolved runtime view. When a helper may be called with either the active source snapshot or the active runtime snapshot, route through `selectApplicableRuntimeConfig()` before reading credentials. ## Reusable runtime utilities Use inbound `botLoopProtection` facts for bot-authored inbound messages. Core applies the shared in-memory sliding-window guard before session record and dispatch, without tying the policy to one channel. The guard tracks `(scopeId, conversationId, participant pair)` keys, counts both directions of a pair together, applies a cooldown once the window budget is exceeded, and prunes inactive entries opportunistically. Channel plugins that expose this behavior to operators should prefer the shared `channels.defaults.botLoopProtection` shape for baseline budgets, then layer channel/provider-specific overrides on top. The shared config uses seconds because it is user-facing: ```typescript type ChannelBotLoopProtectionConfig = { enabled?: boolean; maxEventsPerWindow?: number; windowSeconds?: number; cooldownSeconds?: number; }; ``` Pass normalized bot-pair facts with the resolved turn. Core resolves defaults, unit conversion, and `enabled` semantics: ```typescript return { channel: "example", routeSessionKey, storePath, ctxPayload, recordInboundSession, runDispatch, botLoopProtection: { scopeId: "account-1", conversationId: "channel-1", senderId: "bot-a", receiverId: "bot-b", config: channelConfig.botLoopProtection, defaultsConfig: runtimeConfig.channels?.defaults?.botLoopProtection, defaultEnabled: allowBotsMode !== "off", }, }; ``` Use `openclaw/plugin-sdk/pair-loop-guard-runtime` directly only for custom two-party event loops that do not go through the shared inbound reply runner. ## Runtime namespaces Agent identity, directories, and session management. ```typescript // Resolve the agent's working directory (agentId is required) const agentDir = api.runtime.agent.resolveAgentDir(cfg, agentId); // Resolve agent workspace const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(cfg, agentId); // Get agent identity const identity = api.runtime.agent.resolveAgentIdentity(cfg); // Get default thinking level const thinking = api.runtime.agent.resolveThinkingDefault({ cfg, provider, model, }); // Validate a user-provided thinking level against the active provider profile const policy = api.runtime.agent.resolveThinkingPolicy({ provider, model }); const level = api.runtime.agent.normalizeThinkingLevel("extra high"); if (level && policy.levels.some((entry) => entry.id === level)) { // pass level to an embedded run } // Get agent timeout const timeoutMs = api.runtime.agent.resolveAgentTimeoutMs(cfg); // Ensure workspace exists await api.runtime.agent.ensureAgentWorkspace(cfg); // Run an embedded agent turn const result = await api.runtime.agent.runEmbeddedAgent({ sessionId: "my-plugin:task-1", runId: crypto.randomUUID(), workspaceDir: api.runtime.agent.resolveAgentWorkspaceDir(cfg, agentId), prompt: "Summarize the latest changes", timeoutMs: api.runtime.agent.resolveAgentTimeoutMs(cfg), }); ``` `runEmbeddedAgent(...)` is the neutral helper for starting a normal OpenClaw agent turn from plugin code. It uses the same provider/model resolution and agent-harness selection as channel-triggered replies. `runEmbeddedPiAgent(...)` remains as a deprecated compatibility alias for existing plugins. New code should use `runEmbeddedAgent(...)`. `resolveThinkingPolicy(...)` returns the provider/model's supported thinking levels and optional default. Provider plugins own the model-specific profile through their thinking hooks, so tool plugins should call this runtime helper instead of importing or duplicating provider lists. `normalizeThinkingLevel(...)` converts user text such as `on`, `x-high`, or `extra high` to the canonical stored level before checking it against the resolved policy. **Session store helpers** are under `api.runtime.agent.session`: ```typescript const entry = api.runtime.agent.session.getSessionEntry({ agentId, sessionKey }); for (const { sessionKey, entry } of api.runtime.agent.session.listSessionEntries({ agentId })) { // Iterate session rows without depending on the legacy sessions.json shape. } await api.runtime.agent.session.patchSessionEntry({ agentId, sessionKey, update: (entry) => ({ thinkingLevel: "high" }), }); const storePath = api.runtime.agent.session.resolveStorePath(cfg.session?.store, { agentId }); await api.runtime.agent.session.runWithWorkAdmission( { storePath, sessionKey }, async (signal) => { // Create or update the session, then pass signal to the admitted agent run. }, ); ``` Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted. Use `runWithWorkAdmission(...)` when a plugin starts work on a persisted session. The callback rejects archived or concurrently replaced sessions, keeps archive/reset/delete mutations coordinated through completion, and receives an `AbortSignal` that must be forwarded to the agent run. For transcript reads and writes, import `openclaw/plugin-sdk/session-transcript-runtime` and use `resolveSessionTranscriptIdentity(...)`, `resolveSessionTranscriptTarget(...)`, `readSessionTranscriptEvents(...)`, `appendSessionTranscriptMessageByIdentity(...)`, `publishSessionTranscriptUpdateByIdentity(...)`, or `withSessionTranscriptWriteLock(...)` with `{ agentId, sessionKey, sessionId }`. These APIs let plugins identify a transcript, read its events, append messages, publish updates, and run related operations under the same transcript write lock. Passing `sessionFile`, using `resolveSessionTranscriptLegacyFileTarget(...)`, or importing low-level `appendSessionTranscriptMessage(...)` / `emitSessionTranscriptUpdate(...)` from `openclaw/plugin-sdk/agent-harness-runtime` is deprecated; those paths exist only for legacy code that already receives an active transcript artifact. `resolveStorePath(...)` and `updateSessionStoreEntry(...)` round out the session helpers: `resolveStorePath` resolves the session store path for a given scope, and `updateSessionStoreEntry({ storePath, sessionKey, update })` patches one entry directly by store path when the caller already knows it. `loadSessionStore(...)`, `saveSessionStore(...)`, `updateSessionStore(...)`, and `resolveSessionFilePath(...)` are deprecated compatibility helpers for plugins that still intentionally depend on the legacy whole-store or transcript-file shape. New plugin code must not use those helpers, and existing callers should migrate to entry helpers and transcript identity helpers. Default model and provider constants: ```typescript const model = api.runtime.agent.defaults.model; // e.g. "gpt-5.5" const provider = api.runtime.agent.defaults.provider; // e.g. "openai" ``` Run a host-owned text completion without importing provider internals or duplicating OpenClaw model/auth/base URL preparation. ```typescript const result = await api.runtime.llm.complete({ messages: [{ role: "user", content: "Summarize this transcript." }], purpose: "my-plugin.summary", maxTokens: 512, temperature: 0.2, }); ``` The helper uses the same simple-completion preparation path as OpenClaw's built-in runtime and the host-owned runtime config snapshot. Context engines receive a session-bound `llm.complete` capability, so model calls use the active session's agent and do not silently fall back to the default agent. The result includes provider/model/agent attribution plus normalized token, cache, and estimated cost usage when available. Model overrides require operator opt-in via `plugins.entries..llm.allowModelOverride: true` in config. Use `plugins.entries..llm.allowedModels` to restrict trusted plugins to specific canonical `provider/model` targets. Cross-agent completions require `plugins.entries..llm.allowAgentIdOverride: true`. Launch and manage background subagent runs. ```typescript // Start a subagent run const { runId } = await api.runtime.subagent.run({ sessionKey: "agent:main:subagent:search-helper", message: "Expand this query into focused follow-up searches.", provider: "openai", // optional override model: "gpt-5.5", // optional override deliver: false, }); // Wait for completion const result = await api.runtime.subagent.waitForRun({ runId, timeoutMs: 30000 }); // Read session messages const { messages } = await api.runtime.subagent.getSessionMessages({ sessionKey: "agent:main:subagent:search-helper", limit: 10, }); // Delete a session await api.runtime.subagent.deleteSession({ sessionKey: "agent:main:subagent:search-helper", }); ``` Model overrides (`provider`/`model`) require operator opt-in via `plugins.entries..subagent.allowModelOverride: true` in config. Untrusted plugins can still run subagents, but override requests are rejected. `deleteSession(...)` can delete sessions created by the same plugin through `api.runtime.subagent.run(...)`. Deleting arbitrary user or operator sessions still requires an admin-scoped Gateway request. List connected nodes and invoke a node-host command from Gateway-loaded plugin code or from plugin CLI commands. Use this when a plugin owns local work on a paired device, for example a browser or audio bridge on another Mac. ```typescript const { nodes } = await api.runtime.nodes.list({ connected: true }); const result = await api.runtime.nodes.invoke({ nodeId: "mac-studio", command: "my-plugin.command", params: { action: "start" }, timeoutMs: 30000, }); ``` Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, plugin node-invoke policies, and node-local command handling. Plugins that expose dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`. The policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls and higher-level plugin tools share the same enforcement path. The optional `scopes` field requests Gateway operator scopes for the invocation. OpenClaw honors it only for bundled plugins and trusted official plugin installations; requests from other plugins do not elevate the call. Use it only when a trusted plugin must invoke a node command with a stricter Gateway scope, such as `operator.admin`. Bind Task Flow and Task Run state to an existing OpenClaw session key or trusted tool context. - `api.runtime.tasks.managedFlows` is mutation-capable: create, advance, and cancel Task Flows. - `api.runtime.tasks.flows` and `api.runtime.tasks.runs` are read-only DTO views for listing and status lookups; both expose `bindSession(...)` / `fromToolContext(...)` plus `get`, `list`, `findLatest`, and `resolve`. - `api.runtime.tasks.flow` is a deprecated alias for `managedFlows`. Task Flow tracks durable multi-step workflow state. It is not a scheduler: use Cron or `api.session.workflow.scheduleSessionTurn(...)` for future wakeups, then use `managedFlows` from the scheduled turn when that work needs flow state, child tasks, waits, or cancellation. ```typescript const taskFlow = api.runtime.tasks.managedFlows.fromToolContext(ctx); const created = taskFlow.createManaged({ controllerId: "my-plugin/review-batch", goal: "Review new pull requests", }); const child = taskFlow.runTask({ flowId: created.flowId, runtime: "acp", childSessionKey: "agent:main:subagent:reviewer", task: "Review PR #123", status: "running", startedAt: Date.now(), }); const waiting = taskFlow.setWaiting({ flowId: created.flowId, expectedRevision: created.revision, currentStep: "await-human-reply", waitJson: { kind: "reply", channel: "telegram" }, }); ``` Use `bindSession({ sessionKey, requesterOrigin })` when you already have a trusted OpenClaw session key from your own binding layer. Do not bind from raw user input. Text-to-speech synthesis. ```typescript // Standard TTS const clip = await api.runtime.tts.textToSpeech({ text: "Hello from OpenClaw", cfg: api.config, }); // Telephony-optimized TTS const telephonyClip = await api.runtime.tts.textToSpeechTelephony({ text: "Hello from OpenClaw", cfg: api.config, }); // List available voices const voices = await api.runtime.tts.listVoices({ provider: "elevenlabs", cfg: api.config, }); ``` Uses core `messages.tts` configuration and provider selection. Returns PCM audio buffer + sample rate. `textToSpeechStream` is also available for streaming synthesis. Image, audio, and video analysis. ```typescript // Describe an image const image = await api.runtime.mediaUnderstanding.describeImageFile({ filePath: "/tmp/inbound-photo.jpg", cfg: api.config, agentDir: "/tmp/agent", }); // Transcribe audio const { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({ filePath: "/tmp/inbound-audio.ogg", cfg: api.config, mime: "audio/ogg", // optional, for when MIME cannot be inferred }); // Describe a video const video = await api.runtime.mediaUnderstanding.describeVideoFile({ filePath: "/tmp/inbound-video.mp4", cfg: api.config, }); // Generic file analysis const result = await api.runtime.mediaUnderstanding.runFile({ filePath: "/tmp/inbound-file.pdf", cfg: api.config, }); // Structured image extraction through a specific provider/model. // Include at least one image; text inputs are supplemental context. const evidence = await api.runtime.mediaUnderstanding.extractStructuredWithModel({ provider: "codex", model: "gpt-5.5", input: [ { type: "image", buffer: receiptImageBuffer, fileName: "receipt.png", mime: "image/png", }, { type: "text", text: "Prefer the printed total over handwritten notes." }, ], instructions: "Extract vendor, total, and searchable tags.", schemaName: "receipt.evidence", jsonSchema: { type: "object", properties: { vendor: { type: "string" }, total: { type: "number" }, tags: { type: "array", items: { type: "string" } }, }, required: ["vendor", "total"], }, cfg: api.config, }); ``` Returns `{ text: undefined }` when no output is produced (e.g. skipped input). `describeImageFileWithModel(...)` describes an already-known image through a specific provider/model, bypassing the default active-model resolution that `describeImageFile(...)` uses. `api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias for `api.runtime.mediaUnderstanding.transcribeAudioFile(...)`. Image generation. ```typescript const result = await api.runtime.imageGeneration.generate({ prompt: "A robot painting a sunset", cfg: api.config, }); const providers = api.runtime.imageGeneration.listProviders({ cfg: api.config }); ``` Video generation, mirroring the image generation shape. ```typescript const result = await api.runtime.videoGeneration.generate({ prompt: "A drone shot flying over a coastline at sunrise", cfg: api.config, }); const providers = api.runtime.videoGeneration.listProviders({ cfg: api.config }); ``` Music generation, mirroring the image generation shape. ```typescript const result = await api.runtime.musicGeneration.generate({ prompt: "An upbeat lo-fi track for a coding session", cfg: api.config, }); const providers = api.runtime.musicGeneration.listProviders({ cfg: api.config }); ``` Web search. ```typescript const providers = api.runtime.webSearch.listProviders({ config: api.config }); const result = await api.runtime.webSearch.search({ config: api.config, args: { query: "OpenClaw plugin SDK", count: 5 }, }); ``` Low-level media utilities. ```typescript const webMedia = await api.runtime.media.loadWebMedia(url); const mime = await api.runtime.media.detectMime(buffer); const kind = api.runtime.media.mediaKindFromMime("image/jpeg"); // "image" const isVoice = api.runtime.media.isVoiceCompatibleAudio(filePath); const metadata = await api.runtime.media.getImageMetadata(filePath); const resized = await api.runtime.media.resizeToJpeg(buffer, { maxWidth: 800 }); const terminalQr = await api.runtime.media.renderQrTerminal("https://openclaw.ai"); const pngQr = await api.runtime.media.renderQrPngBase64("https://openclaw.ai", { scale: 6, // 1-12 marginModules: 4, // 0-16 }); const pngQrDataUrl = await api.runtime.media.renderQrPngDataUrl("https://openclaw.ai"); const tmpRoot = resolvePreferredOpenClawTmpDir(); const pngQrFile = await api.runtime.media.writeQrPngTempFile("https://openclaw.ai", { tmpRoot, dirPrefix: "my-plugin-qr-", fileName: "qr.png", }); ``` Current runtime config snapshot and transactional config writes. Prefer config that was already passed into the active call path; use `current()` only when the handler needs the process snapshot directly. ```typescript const cfg = api.runtime.config.current(); await api.runtime.config.mutateConfigFile({ afterWrite: { mode: "auto" }, mutate(draft) { draft.plugins ??= {}; }, }); ``` `mutateConfigFile(...)` and `replaceConfigFile(...)` return a `followUp` value, for example `{ mode: "restart", requiresRestart: true, reason }`, which records the writer intent without taking restart control away from the gateway. System-level utilities. ```typescript await api.runtime.system.enqueueSystemEvent(event); api.runtime.system.requestHeartbeat({ source: "other", intent: "event", reason: "plugin-event", }); api.runtime.system.requestHeartbeatNow({ reason: "plugin-event" }); // Deprecated compatibility alias. const heartbeatResult = await api.runtime.system.runHeartbeatOnce({ reason: "plugin-triggered-check", }); const output = await api.runtime.system.runCommandWithTimeout(cmd, args, opts); const hint = api.runtime.system.formatNativeDependencyHint(pkg); ``` `runHeartbeatOnce(...)` runs a single heartbeat cycle immediately, bypassing the normal coalesce timer. Pass `{ heartbeat: { target: "last" } }` to force delivery to the last active channel instead of the default `target: "none"` suppression. `runCommandWithTimeout(...)` returns captured `stdout` and `stderr`, optional truncation counts, `code`, `signal`, `killed`, `termination`, and `noOutputTimedOut`. Timeout and no-output-timeout results report `code: 124` when the child process does not provide a non-zero exit code. Non-timeout signal exits can still return `code: null`, so use `termination` and `noOutputTimedOut` to distinguish timeout reasons. Event subscriptions. ```typescript api.runtime.events.onAgentEvent((event) => { /* ... */ }); api.runtime.events.onSessionTranscriptUpdate((update) => { /* ... */ }); ``` Logging. ```typescript const verbose = api.runtime.logging.shouldLogVerbose(); const childLogger = api.runtime.logging.getChildLogger({ plugin: "my-plugin" }, { level: "debug" }); ``` Model and provider auth resolution. ```typescript const auth = await api.runtime.modelAuth.getApiKeyForModel({ model, cfg }); // Request-ready auth, including provider runtime exchanges (e.g. OAuth refresh) const runtimeAuth = await api.runtime.modelAuth.getRuntimeAuthForModel({ model, cfg }); const providerAuth = await api.runtime.modelAuth.resolveApiKeyForProvider({ provider: "openai", cfg, }); ``` State directory resolution and SQLite-backed keyed storage. ```typescript const stateDir = api.runtime.state.resolveStateDir(process.env); const store = api.runtime.state.openKeyedStore({ namespace: "my-feature", maxEntries: 200, defaultTtlMs: 15 * 60_000, }); await store.register("key-1", { value: "hello" }); const claimed = await store.registerIfAbsent("dedupe-key", { value: "first" }); const value = await store.lookup("key-1"); await store.consume("key-1"); await store.clear(); ``` Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Use `registerIfAbsent(...)` for atomic dedupe claims: it returns `true` when the key was missing or expired and registered, or `false` when a live value already exists without overwriting its value, creation time, or TTL. Limits: `maxEntries` per namespace, 50,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry. When a write would exceed the plugin row cap, the runtime sheds the oldest live rows from the namespace being written; sibling namespaces are not evicted for that write, and the write still fails if the namespace cannot free enough rows. `openSyncKeyedStore(...)` returns the same store shape with synchronous methods (`register`, `registerIfAbsent`, `lookup`, `consume`, `clear` all return values directly instead of promises) for callers that cannot await. `openChannelIngressQueue(...)` opens a persisted ingress queue scoped to the calling plugin, for buffering inbound events that need at-least-once processing across restarts. `openKeyedStore`, `openSyncKeyedStore`, and `openChannelIngressQueue` are available only to bundled plugins and trusted official plugin installations in this release. Channel-specific runtime helpers (available when a channel plugin is loaded). Grouped by concern: | Group | Purpose | | --- | --- | | `text` | Chunking (`chunkText`, `chunkMarkdownText`, `resolveChunkMode`), control-command detection, Markdown table conversion. | | `reply` | Buffered-block reply dispatch, envelope formatting, effective messages/human-delay config resolution. | | `routing` | `buildAgentSessionKey`, `resolveAgentRoute`. | | `pairing` | `buildPairingReply`, allowlist reads, pairing-request upserts. | | `media` | Remote media download/save (see below). | | `activity` | Record/read last channel activity. | | `session` | Session metadata from inbound events, last-route updates. | | `mentions` | Mention-policy helpers (see below). | | `reactions` | Ack-reaction handles for in-flight processing indicators. | | `groups` | Group policy and require-mention resolution. | | `debounce` | Inbound message debouncing. | | `commands` | Command authorization and text-command gating. | | `outbound` | Load a channel's outbound adapter. | | `inbound` | Build inbound event context and run the shared inbound-event/reply kernel. | | `threadBindings` | Adjust idle-timeout/max-age for bound session threads. | | `runtimeContexts` | Register, read, and watch process-local per-channel/account/capability context. | `api.runtime.channel.media` is the preferred surface for channel media downloads and storage: ```typescript const saved = await api.runtime.channel.media.saveRemoteMedia({ url, subdir: "inbound", maxBytes, filePathHint: fileName, }); ``` Use `saveRemoteMedia(...)` when a remote URL should become OpenClaw media. Use `saveResponseMedia(...)` when the plugin already fetched a `Response` with plugin-owned auth, redirect, or allowlist handling. Use `readRemoteMediaBuffer(...)` only when the plugin needs raw bytes for inspection, transforms, decryption, or reupload. `fetchRemoteMedia(...)` remains a deprecated compatibility alias for `readRemoteMediaBuffer(...)`. `api.runtime.channel.mentions` is the shared inbound mention-policy surface for bundled channel plugins that use runtime injection: ```typescript const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, { mentionRegexes, mentionPatterns, }); const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({ facts: { canDetectMention: true, wasMentioned: mentionMatch.matched, implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen( "reply_to_bot", isReplyToBot, ), }, policy: { isGroup, requireMention, allowTextCommands, hasControlCommand, commandAuthorized, }, }); ``` Available mention helpers: - `buildMentionRegexes` - `matchesMentionPatterns` - `matchesMentionWithExplicit` - `implicitMentionKindWhen` - `resolveInboundMentionDecision` `api.runtime.channel.mentions` intentionally does not expose the older `resolveMentionGating*` compatibility helpers. Prefer the normalized `{ facts, policy }` path. Several fields under `reply`, `session`, and `inbound` carry per-field `@deprecated` notes pointing at the current channel-turn kernel or channel-outbound adapters; check the inline JSDoc on the specific helper before building new code on it. ## Storing runtime references Use `createPluginRuntimeStore` to store the runtime reference for use outside the `register` callback: ```typescript import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store"; const store = createPluginRuntimeStore({ pluginId: "my-plugin", errorMessage: "my-plugin runtime not initialized", }); ``` ```typescript export default defineChannelPluginEntry({ id: "my-plugin", name: "My Plugin", description: "Example", plugin: myPlugin, setRuntime: store.setRuntime, }); ``` ```typescript export function getRuntime() { return store.getRuntime(); // throws if not initialized } export function tryGetRuntime() { return store.tryGetRuntime(); // returns null if not initialized } ``` Prefer `pluginId` for the runtime-store identity. The lower-level `key` form is for uncommon cases where one plugin intentionally needs more than one runtime slot. ## Other top-level `api` fields Beyond `api.runtime`, the API object also provides: Plugin id. Plugin display name. Current config snapshot (active in-memory runtime snapshot when available). Plugin-specific config from `plugins.entries..config`. Scoped logger (`debug`, `info`, `warn`, `error`). Current load mode: `"full"` (live activation), `"discovery"` / `"tool-discovery"` (read-only capability discovery), `"setup-only"` (lightweight setup entry), `"setup-runtime"` (setup flow that also needs the runtime channel entry), or `"cli-metadata"` (CLI command metadata collection). Resolve a path relative to the plugin root. ## Related - [Plugin internals](/plugins/architecture) — capability model and registry - [SDK entry points](/plugins/sdk-entrypoints) — `definePluginEntry` options - [SDK overview](/plugins/sdk-overview) — subpath reference