openclaw/docs/plugins/sdk-runtime.md
Peter Steinberger cf78e64434
fix(plugins): distinguish settled disposal faults from retained cleanup
Report settled inspection disposal errors without marking their managed
resources as retained. Keep rejected instance/cache prerequisites and drain
timeouts classified as retained cleanup. Preserve CLI resource release,
original error causes, borrowed ownership, and exactly-once disposal.

Fix the product regression introduced by bfdb432570 (#160181). Update
caller assertions to require the disposal failure while preserving successful
process close and subsequent healthy publication; document the contract.

Validation on an isolated AWS lease at main 0c1952c56e:
- Original 129-file plugin CI composition reproduced both failures before
  the fix, then passed 1,257 tests with one existing skip in 59.45 seconds.
- Four affected files passed all 60 tests in three consecutive runs
  (40.87s, 23.71s, 23.78s including runner overhead).
- New settled/rejected/pending inspection regressions took 79ms/19ms/17ms
  in the final focused run; the settled case fails on the original code.
- Core and plugins-platform test typechecks, scoped lint, oxfmt --check,
  git diff --check, and independent P0-P2 review passed.

Pre-commit formatting ran on the lease: oxfmt --check passed for all five
changed files. The local hook is skipped because this sparse worktree has
no node_modules.
2026-09-30 17:36:35 -07:00

26 KiB

summary title sidebarTitle read_when
api.runtime -- the injected runtime helpers available to plugins Plugin runtime helpers Runtime helpers
You need to call core helpers from a plugin (TTS, STT, image gen, web search, Gateway, subagent, nodes)
You want to understand what api.runtime exposes
You are accessing config, agent, or media helpers from plugin code
You are implementing model-picker persistence in a channel plugin

Reference for the live api.runtime object available during "full", "discovery", "tool-discovery", and "setup-runtime" registration. During "cli-metadata" and "setup-only" registration, runtime capabilities are intentionally unavailable: accessing one throws an error naming the plugin and mode. Defer runtime access out of register() or, for root CLI commands, declare cliCommands in the plugin manifest. Use runtime 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.
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.

What each page covers

  • Config and utilities — runtime config reads and writes, plus the shared process, error, and model-picker utilities.
  • Agent and sessions — agent identity, directories, session store, transcripts, and sandbox authority.
  • Model helpers — host-owned completions, model-selection policy, and provider auth resolution.
  • Background work — hook agent turns, subagent runs, and native harness completion delivery.
  • Gateway and nodes — in-process Gateway requests, bounded session facts through gateway.readSessionFacts, paired node invocation, and Gateway service events.
  • Media helpers — speech, media understanding, image/video/music generation, web search, and media utilities.
  • State and system — config snapshot, SQLite-backed plugin state, system utilities, events, and logging.
  • Channel helpers — channel-specific runtime helper groups for chunking, routing, pairing, media, and mentions.

Runtime namespaces

Every api.runtime namespace and the page that documents it.

Namespace Page
api.runtime.agent Agent and sessions
api.runtime.agent.defaults Agent and sessions
api.runtime.llm Model helpers
api.runtime.gateway Gateway and nodes
api.runtime.hooks Background work
api.runtime.subagent Background work
api.runtime.sandbox Agent and sessions
api.runtime.nodes Gateway and nodes
api.runtime.tts Media helpers
api.runtime.mediaUnderstanding Media helpers
api.runtime.imageGeneration Media helpers
api.runtime.videoGeneration Media helpers
api.runtime.musicGeneration Media helpers
api.runtime.webSearch Media helpers
api.runtime.media Media helpers
api.runtime.config State and system
api.runtime.system State and system
api.runtime.events State and system
api.runtime.logging State and system
api.runtime.modelConfig Model helpers
api.runtime.modelAuth Model helpers
api.runtime.state State and system
api.runtime.channel Channel helpers

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<PluginRuntime>({
  pluginId: "my-plugin",
  errorMessage: "my-plugin runtime not initialized",
});
```
```typescript import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
// `myPlugin` is your own `ChannelPlugin` object and `store` is the store
// created in the previous step; neither is an SDK export.
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.

Plugin lifecycle and cleanup

A managed plugin instance owns its registered callables, runtime-store slots, and loaded source generation. Retiring the instance stops new calls through its managed handles. Already admitted calls and streams have a bounded chance to finish before disposal; retaining an old function does not make it a current runtime handle.

Ordinary stream results project their payload on the first value read. Nested managed readers share data inspection within that synchronous read, while each reader keeps its own instance admission. An unread terminal payload does not need data inspection. Plain payloads retain their native identity and remain mutable; they are not frozen or transferred. Nested readers recheck later reads for mutations that need executable views. This does not give a closed consumer permission to read a retained active-stream result or call its methods.

Context engines selected by an admitted turn remain owned through that turn's commit and engine disposal. Replacing an enabled plugin waits for those consumers to close before registering its successor. Disabling or removing a plugin can report their cleanup as deferred; starting engine disposal closes normal engine callbacks while cleanup finishes.

Replacement validates metadata and configuration first, then stops services and channels, drains admitted work, runs gateway_stop, and disposes the old instance before invoking the new registration. Pre-publication failure triggers automatic recovery by registering the captured previous code with its previous config; a stopped instance is not assumed to be restartable. A plugin cannot synchronously replace itself from its own active call: the operation rejects before shutdown and can be retried after that call finishes. Cleanup that cannot finish within its budget can prevent safe replacement or recovery. Unaffected instances remain active, and the Gateway process stays running.

Managed instances expose api.lifecycle.signal and api.lifecycle.onDispose(cleanup). The signal aborts when disposal reaches explicit cleanup. onDispose accepts a synchronous or asynchronous callback and returns a function that unregisters it. Callbacks run once, in reverse registration order, within a shared cleanup budget. A throwing or unfinished callback is recorded as a cleanup failure while the remaining cleanup is attempted. These fields are optional in the SDK type because an API host without a managed instance may omit them; feature-detect them before relying on instance cleanup. The existing api.lifecycle.registerRuntimeLifecycle(...) contract remains available for plugin-owned host state.

Inspection release reports settled disposal failures without marking the managed resources as still retained. Prepared-model shutdown records those failures and can finish after cleanup settles. Unfinished disposal and failed host cleanup prerequisites still prevent shutdown from reporting a completed resource release.

Cleanup is best effort. Plugins must explicitly release their own timers, listeners, sockets, watchers, and child processes in onDispose or their service's stop() method. OpenClaw does not intercept those native resources or prove that they have stopped when managed retirement completes. Native plugins remain trusted, in-process code. Plain data and native byte buffers retain their normal identities; lifecycle fencing applies to the managed callable surfaces, not every object a plugin can retain.

Release the stored handle as well as canceling a timer. On Node, a canceled timer object can still retain the async context in which it was created:

clearInterval(timer);
timer = undefined;

This matters for module-level state in native ESM plugins: Node can retain an evaluated module after replacement. Removing the captured files and closing its managed callbacks does not unload that native module or clear its variables. Drop references to stopped resources and other disposable state in cleanup.

Opaque values returned by a plugin can be passed back directly or in data-only records and arrays. Caller-owned objects with methods or accessors are passed unchanged, including any handles inside them.

createPluginRuntimeStore resolves its slot from the invoking managed instance. Preparing another instance does not overwrite that instance's runtime. Calls outside managed instance scope retain the store's existing standalone behavior. Gateway-hosted agent turns use the admitting Gateway's own instance for each unchanged plugin: same source, install, manifest, activation, entry policy, and configuration, in the Gateway's workspace and environment. The lender comes from the admitting Gateway owner, never another Gateway that happens to be process-active. Without an unambiguous live owner, preparation loads separate instances. Borrowing turns run the Gateway's registrationMode: "full" registrations and share its services and runtime store; only plugins the Gateway lacks or configures differently load a separate discovery instance. After openclaw plugins reload, later turns use the reloaded Gateway instance, and the reload waits for turns that still hold the previous one. Borrowed channel methods and read-authority grants expire with the borrowing runtime or invocation scope; retiring the borrower does not retire the Gateway's instance.

Turns that load a plugin separately borrow its tool registrations from the admitting Gateway's current registry, so factories and execution share the instance whose services initialized the runtime. Adoption requires the same plugin source, configuration, non-empty set of declared tool names, and optionality. It preserves discovery's tool membership and order. Without an unambiguous admitting Gateway owner, turns keep their discovery registrations.

SDK helpers that return bare results retain their resources until the owning host closes. Callers do not need to dispose those results; see Prepared simple completions.

Memory runtime replacement

Memory runtimes may implement prepareReload({ retireRuntime, retiringEmbeddingProviders }) and return drain() and resume(). Preparation synchronously fences affected manager acquisition, including lazy and fallback work. Match the exact acquired adapter objects rather than provider IDs. Drain removes affected managers from reuse before attempting to close them. It may return { errors } to report cleanup failures. Resume reopens admission after cancellation, or after publication when the runtime is retained, even if old cleanup remains unfinished. Retiring managers must not publish late results into a replacement manager's caches.

Preparing an unused runtime must leave its manager engine unloaded. For runtimes without this hook, OpenClaw calls the existing closeAllMemorySearchManagers method, when provided, if the runtime or an embedding adapter retires. This closes all of that runtime's managers as best-effort cleanup; it cannot identify dependent managers or prevent concurrent manager acquisition.

Browser meeting transport builders

MeetingPlatformAdapter.createBrowserAdapterOptions builds the browser and parsing options for MeetingPlatformAdapter.create from platform page scripts, permission origins, display names, manual-action prefixes, and retry policy. MeetingPlatformAdapter.createPageScripts assembles status, transcript, audio capture, and leave scripts while the plugin supplies identity and control sources.

createStatusPreludeSource accepts either source strings or callbacks for lifecycleSource and manualActionSource. Callbacks receive shared fragments for guest names, preserved identity, virtual audio input, microphone control, and manual actions. Existing string-based callers keep their generated source.

Browser meeting status ownership

MeetingPlatformAdapter.createStatusCallSource accepts an optional liveOwnershipSource: a JavaScript boolean expression evaluated in the generated status script's page scope. Use it when call ownership can change while device enumeration, speaker routing, or playback is awaiting completion. A false result stops that routing pass, restores matching sources through the session's audio cleanup helpers, retires owned bridges, and reports output as unrouted and retryable. Omitting the option leaves the generated status source unchanged.

Browser meeting participation

The existing openclaw/plugin-sdk/meeting-runtime entry point exposes optional participation methods on MeetingSessionRuntime. Supply its participation options with an SQLite plugin keyed store, current capabilities, action validation, and a provider executor. Providers observe canonical source identity, epoch, revision, and finality through observeParticipationSource; never accept these fields from model arguments. inspectParticipationSource returns a snapshot and a live guard for work that crosses asynchronous boundaries.

The participation-specific named exports are runMeetingParticipationWithBrowser, MeetingBrowserParticipationAdapter, MeetingParticipationRequest, MeetingParticipationSource, and MeetingParticipationAttempt. Other payload and option shapes remain part of the typed runtime and adapter signatures rather than separate top-level SDK aliases.

Each session retains at most 1,024 live sources for two minutes from their first observation. Capacity admission and eviction use original observation order, not snapshot replay or correction time. Repeated snapshots preserve unchanged retained references and guards; older replayed sources cannot displace newer ones from a full live-source window.

Retained transcript rows carry a separate provenance envelope: observer, optional observation/session/document identifiers and observation time, observed speaker label, and native self, other, or unknown attribution. Speaker labels are not participant identities. Missing or malformed attribution remains unknown; a provenance record never grants participation authority. Interim, historical, own-echo, and otherwise non-actionable rows retain provenance independently of source.

This is a retained-snapshot contract, not a revision journal. Unchanged polls keep unchanged observation identifiers; intermediate states between polls need not be retained. Existing transcript storage carries the envelope in metadata.meetingObservationProvenance on the utterances it already stores, under the existing retention policy. There is no separate observation archive. Removing one DOM copy must not finalize a source that still has a live copy.

Browser adapters may implement MeetingBrowserParticipationAdapter and dispatch through runMeetingParticipationWithBrowser. The helper uses the existing tab lock, a pinned route, and the session guard. An optional preparation script may open controls and await readiness, but must not perform the requested action. After preparation the host revalidates authority. The final script checks the page session and URL and performs its effect synchronously before its first await; later waits may observe the result but must not produce another effect. Only a rejected result that proves no requested effect occurred may set correctable: true. Other meeting platforms need no adapter change and continue to report unsupported participation.

Cancellation after browser dispatch is best effort: the effect may occur before the host detects source expiry, correction, or session revocation. The runtime reports that outcome as uncertain; it must not be treated as proof of cancellation or permission to retry with a new request ID. Pre-dispatch authority checks and the adapter's final page-session and URL checks remain required.

Worker provider allocation authority

The Gateway supplies assertCurrent() in the options passed to worker providers' provision and prepareProvision methods. This required runtime callback binds the operation to the live environment owner and any requesting run. Invoke it after awaited preparation and immediately before an allocation, checkpoint fork, or adoption. A non-aborted signal does not prove that the caller still has authority. Providers with project preparation must compose this callback with project.assertCurrent() so both owners remain current.

The callback belongs to the provision attempt. Carry it into a returned prepared allocation closure, but never serialize it or retain it in a durable or reusable preparation record. After the attempt closes, the callback rejects retained work. Teardown keeps its existing cleanup authority and must still settle an owned lease when the requesting run has ended.

The legacy optional parameter shape remains source-compatible until the next declared breaking Plugin SDK revision. It is not a capability-free runtime path: current hosts supply this assertion, and bundled providers reject missing allocation authority before performing work. An older host must be updated to use these providers.

Other top-level api fields

Beyond api.runtime, the API object also provides:

Plugin id. Plugin display name. Read-only config snapshot supplied when this instance registers. With the default hybrid reload mode, changes to this plugin's `plugins.entries.` replace its instance by default and rerun registration. A retained instance keeps its snapshot across unrelated config changes. In long-lived callbacks, prefer the supplied `cfg`, or use `api.runtime.config.current()` when no config is passed. Plugin-specific config from `plugins.entries..config`, captured at registration. Ordinary edits to this config automatically replace the instance in hybrid mode, unless a narrower plugin reload policy applies. Source or manifest edits still need [plugin Reload](/cli/plugins#reload). 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.

Where each section moved

Every section heading and namespace anchor from the previous single-page version keeps its anchor here, so an existing link such as /plugins/sdk-runtime#api-runtime-subagent still resolves. Each entry points at the page that now holds the content.

Decision model runtime

api.runtime.decisions is a closure-bound optional capability for small typed Choice, ordered Score, and Boolean-probability batches. Retained handles reject after consumer retirement. See decision models for provider selection, lifecycle, failure handling, limits, and diagnostics.

The former Tasks runtime is no longer available. See removed Tasks and TaskFlow APIs for native-owner alternatives.