On this page

On this page

Plugin internals

Plugin internals

This is the deep architecture reference for the OpenClaw plugin system. For practical guides, start with one of the focused pages below.

Public capability model

Capabilities are the public native plugin model inside OpenClaw. Native plugins can register one or more capability types:

Capability Registration method Example plugins
Text inference api.registerProvider(...) anthropic, openai
CLI inference backend api.registerCliBackend(...) anthropic, openai
Embeddings api.registerEmbeddingProvider(...) Provider-owned vector plugins
Speech api.registerSpeechProvider(...) elevenlabs, microsoft
Realtime transcription api.registerRealtimeTranscriptionProvider(...) openai
Realtime voice api.registerRealtimeVoiceProvider(...) google, openai
Media understanding api.registerMediaUnderstandingProvider(...) google, openai
Transcripts source api.registerTranscriptSourceProvider(...) discord, google-meet, teams-meetings, zoom-meetings
Image generation api.registerImageGenerationProvider(...) fal, google, openai
Music generation api.registerMusicGenerationProvider(...) fal, google, minimax
Video generation api.registerVideoGenerationProvider(...) fal, google, qwen
Web fetch api.registerWebFetchProvider(...) firecrawl
Web search api.registerWebSearchProvider(...) brave, firecrawl, google
Channel / messaging api.registerChannel(...) matrix, msteams
Gateway discovery api.registerGatewayDiscoveryService(...) bonjour
Migration api.registerMigrationProvider(...) migrate-claude, migrate-hermes

External compatibility stance

The capability model is landed in core and used by bundled/native plugins today, but external plugin compatibility still needs a tighter bar than "it is exported, therefore it is frozen."

Plugin situation Guidance
Existing external plugins Keep hook-based integrations working; this is the compatibility baseline.
New bundled/native plugins Prefer explicit capability registration over vendor-specific reach-ins or new hook-only designs.
External plugins adopting capability registration Allowed, but treat capability-specific helper surfaces as evolving unless docs mark them stable.

Capability registration is the intended direction. Legacy hooks remain the safest no-breakage path for external plugins during the transition. Exported helper subpaths are not all equal — prefer narrow documented contracts over incidental helper exports.

Plugin shapes

OpenClaw classifies every loaded plugin into a shape based on its actual registration behavior (not just static metadata):

plain-capability

Registers exactly one capability type (for example a provider-only plugin like arcee or chutes).

hybrid-capability

Registers multiple capability types (for example openai owns text inference, speech, media understanding, and image generation).

hook-only

Registers only hooks (typed or custom), no capabilities, tools, commands, or services.

non-capability

Registers tools, commands, services, or routes but no capabilities.

Use openclaw plugins inspect <id> to see a plugin's shape and capability breakdown. See CLI reference for details.

Compatibility signals

openclaw doctor, openclaw plugins inspect <id>, openclaw status --all, and openclaw plugins doctor surface these compatibility notices:

Signal Meaning
config valid Config parses fine and plugins resolve
hook-only (info) Plugin registers only hooks; a supported path, but not migrated to capability registration yet
deprecated memory-embedding API (warn) Non-bundled plugin uses the old memory-specific embedding provider API instead of registerEmbeddingProvider
hard error Config is invalid or plugin failed to load

None of the advisory/warn signals break your plugin today. These signals also appear in openclaw status --all and openclaw plugins doctor.

Architecture overview

OpenClaw's plugin system has four layers:

  • Manifest + discovery

    OpenClaw finds candidate plugins from configured paths, workspace roots, global plugin roots, and bundled plugins. Discovery reads native openclaw.plugin.json manifests plus supported bundle manifests first.

  • Enablement + validation

    Core decides whether a discovered plugin is enabled, disabled, blocked, or selected for an exclusive slot such as memory.

  • Runtime loading

    Native OpenClaw plugins are loaded in-process and register capabilities into a central registry. Managed instances load JavaScript through Node and compile TypeScript source when needed. Compatible bundles are normalized into registry records without importing runtime code.

  • Surface consumption

    The rest of OpenClaw reads the registry to expose tools, channels, provider setup, hooks, HTTP routes, CLI commands, and services.

  • For plugin CLI specifically, root command discovery is split in two phases:

    • parse-time metadata comes from registerCli(..., { descriptors: [...] })
    • the real plugin CLI module can stay lazy and register on first invocation

    That keeps plugin-owned CLI code inside the plugin while still letting OpenClaw reserve root command names before parsing.

    The important design boundary:

    • manifest/config validation should work from manifest/schema metadata without executing plugin code
    • native capability discovery may load trusted plugin entry code to build a non-activating registry snapshot
    • native runtime behavior comes from the plugin module's register(api) path with api.registrationMode === "full"

    That split lets OpenClaw validate config, explain missing/disabled plugins, and build UI/schema hints before the full runtime is active.

    Failed registrations remain visible in plugin diagnostics after their contributions are rolled back. Those records do not enter execution scopes or block healthy plugins and core context-engine admission; the loader still owns their cleanup.

    Runtime preparation checks pending data migrations through the shared-state read worker before registering plugins. Once checked, the process reuses that ledger result across plugin generations until a committed deferred-migration write invalidates it. Unrelated state writes do not repeat the check. Explicit artifact-preserving inspection keeps its isolated snapshot, and plugins with pending data migrations remain blocked until Doctor completes them.

    Gateway startup confirms completed plugin migrations through the admitted live database reader, so checking a small migration ledger does not copy the entire shared database. Offline Doctor inspection retains its artifact-preserving snapshots.

    Gateway startup and reload yield to the event loop between plugin registrations so health probes and existing requests can run. Registration order is unchanged, and the Gateway publishes the registry only after the complete generation is ready. Each plugin's module evaluation and register() remain synchronous; plugins should defer expensive work to their asynchronous service lifecycle or load execution-only modules on first asynchronous use. Register callable hooks before readiness, even when their implementations load lazily.

    The llama.cpp plugin follows this split for setup, server management, embeddings, image descriptions, and streaming. Registration exposes its capabilities and embedding index identities immediately; the corresponding asynchronous operation loads its implementation on first use.

    Web-provider discovery honors exact prepared generations, including empty selections. It reuses an ordinary request-owned registry when it covers the selected providers; otherwise an empty result requires a complete inspected manifest inventory. Partial capability callbacks retain discovery of undeclared providers. Prepared cron runs and Doctor tool construction do not register the same plugins again merely to check whether web search is configured. Doctor keeps provider-specific schema normalization outside its selected tool generation.

    Plugin metadata snapshot and lookup table

    One PluginCache starts on the first plugin metadata access, including CLI preflight before Gateway startup, and fills progressively as metadata and artifacts are needed. Gateway startup retains that owner and builds its immutable PluginMetadataSnapshot. The snapshot includes plugin metadata from all configured agent workspaces, including disabled plugins, with source precedence and workspace provenance preserved. It stores the installed plugin index, manifest registry, manifest diagnostics, owner maps, and a plugin id normalizer. Package contents and lazily loaded module exports belong to other typed views of the same cache, not the snapshot itself.

    Plugin-aware config validation, startup auto-enable, and Gateway plugin bootstrap consume that snapshot instead of rebuilding manifest/index metadata independently. PluginLookUpTable is derived from the same snapshot and adds the startup plugin plan for the current runtime config.

    Startup auto-enable also retains the snapshot's scoped managed-install facts when reusing its manifest registry and discovery. Managed native binaries keep their existing reference admission; unmanaged captures and metadata isolation keep their existing policies.

    On POSIX, successful checked entry opens also publish their canonical paths to the metadata cache. For ordinary metadata paths, discovery resolves each parent directory once and reuses it for non-symlink children. These path facts last until the owner invalidates metadata; symlinks retain filesystem resolution, and actual file opens retain their descriptor-bound security checks. Windows keeps separate path resolution because admission can return a trusted root alias rather than the native spelling.

    Async config validation requests metadata when a rule or default needs it. Core-only settings do not load the plugin inventory; successful config get uses core redaction hints outside plugin, channel, and agent configuration, building the JSON schema only for missing-path diagnostics. Agent configuration keeps provider model-parameter schemas from plugins. Native-catalog privacy preferences still receive their plugin schema checks.

    When metadata is needed, preparation reads the machine-owned bundled-discovery policy and installed index together through the shared-state read worker. Metadata and ownership-lease reads load only their handlers; the general command registry stays deferred. Later reads reuse the plugin cache until its owner refreshes it. Private inspection snapshots keep their own policy facts without replacing the live process cache.

    Cold promise-only metadata preparation retains one read-only child for snapshot allocation, copying, and retirement. A shared-state worker inside that child admits the source and reads the private bytes. It exits before the child retires its snapshot, keeping SQL off the CLI thread without loading a separate reader into the CLI process. Source files remain unchanged. Existing prepared snapshots, live native readers, and schema-maintenance scopes retain their existing read and ownership paths. Readers that need synchronous servicing retain their transport and cleanup contract.

    Channel setup catalogs retain the requested workspace and load-path scope, including raw plugin shadows, so trust filtering can select the appropriate installed alternative.

    After startup, runtime readers reuse that inventory without filesystem discovery, manifest rereads, or freshness checks. Narrow plugin selections are in-memory views of the same inventory. Changing an account or an agent's run workspace does not invalidate it. Explicit plugin lifecycle operations prepare a new inventory for installs, updates, removals, source or manifest edits, and discovery-root changes before publishing it to the running Gateway.

    Plugin enablement readers can pass rootConfig to resolveEffectiveEnableState without constructing a normalized config copy. The policy owner reuses the published runtime configuration's prepared policy and refreshes it on publication; an explicitly supplied normalized config still takes precedence. Public normalization helpers continue to return caller-owned mutable copies.

    Plugin inventory responses share prepared enablement and package-ownership facts between install-health diagnostics and catalog presentation. Those facts belong to one synchronous response preparation, after hosted catalog loading and before optional category loading; maintenance operations still prepare their own current ownership checks.

    Bundled official catalog identities are admitted and indexed once per process. Inventory reads reuse those static entries instead of rescanning the bundled catalog for each installed plugin. Hosted catalog refresh and installed-package provenance checks keep their own lifecycles.

    Runs outside the agent's resolved workspace reuse the agent's prepared plugin generation instead of loading the plugins again. Rooted background runs, including skill workshop reviews and isolated cron jobs, qualify when their bootstrap workspace resolves to that agent's resolved workspace. In a Gateway, every other run workspace also qualifies because it resolves to the same boot inventory. Their execution directory and filesystem confinement remain at the task root, including during compaction. Hosts without that shared inventory, such as direct CLI runs in another workspace, retain their own workspace-scoped generation.

    Tool resolution treats plugins that the prepared generation recorded as disabled or failed (for example, a memory plugin outside the selected memory slot) as settled outcomes and does not load them again for each turn.

    Completed plugin generations reuse their registration grouping and manifest-owner lookup across turns. These facts belong to the registry publication and metadata snapshot; registration in progress stays uncached. Tool factories still receive fresh turn contexts, and returned tools retain that turn's authority, delivery, memory, and sandbox bindings rather than borrowing a later turn's permissions.

    Plugin reload reconciles config watcher events after asynchronous metadata preparation. An unchanged source event does not cancel the operation; newer writes or changed config, install records, or source ownership still supersede it.

    Ordinary plugin source captures use independent files, including copies reached through aliases. On filesystems with copy-on-write support, these captures can share storage without sharing file identity, reducing data writes for large plugins. File containment and receipt validation run during capture; later source edits take effect on the next reload rather than invalidating the existing snapshot.

    Source capture preserves referenced URL assets, including directory URLs that the plugin uses. Unused standalone URL calculations do not cause the surrounding directory to be copied into the plugin generation.

    Asynchronous host loading prepares selected non-bundled plugin sources and bundled state-operation import closures in a shared worker before evaluating or registering plugin code. Bundled operation captures preserve the same immutable bytes and recovery ownership without retaining the source transpiler in the Gateway main thread. Selection, trust checks, and entry-path validation happen first; the host checks inventory retirement and the expected source digest before installing the prepared capture. Each artifact keeps its own source, admission facts, callbacks, and recovery custody inside that worker. Synchronous source resolution services sibling callbacks and completed tasks, so a sibling's pending host publication does not hold the worker slot. Existing registry and plugin-instance caching provide reuse; the worker retires after its last artifact and recovery snapshot are disposed. A failed acquisition records that plugin's load error without discarding successfully prepared siblings. Unexpected worker exit invalidates its captures and queued preparation; subsequent preparation starts a new worker. Synchronous CLI loading and loading inside an existing compute worker retain their inline acquisition path. Plugin evaluation and registration still run in their original owner; moving source preparation does not make arbitrary plugin initialization nonblocking.

    Model-runtime build diagnostics report source preparation work next to runtimePluginMs, under pluginPreparation.*. The counters distinguish captures and retained-source reuse, selected plugin and dependency files, logical original and copied byte volumes, and receipt validation reads and passes. hashReadBytes counts bytes read for content hashing, including capture and receipt verification. Original bytes exclude reused ordinary source files; copied bytes include completed retained-source tree copies and may use filesystem cloning, so neither field measures physical disk traffic. workerMs measures worker operations and hostMs measures host request-and-settlement time. They overlap and must not be added together. Metrics cover the measured preparation phase; later lazy imports do not change an already reported startup result.

    Legacy session-key migration selects plugins that declare that capability before checking channel presence. Owners already eligible under migration policy do not need a channel-presence check. Scoped selections check persisted credentials only for their channel owners, so unrelated authentication modules stay unloaded during Doctor repairs. This credential scope does not limit environment-based presence signals: configured channels with missing plugins still produce installation and recovery hints.

    Provider-reference ownership is indexed once per immutable manifest registry, including runtime auth/catalog aliases and CLI backend fallbacks. Lookups share frozen owner lists; a refreshed or narrowed registry gets its own index while retained generations keep their original ownership facts.

    Model-id normalization policies are prepared with each snapshot or narrowed view. Model selection, catalogs, and runtime normalization carry that view forward instead of rebuilding policies from its plugin list. Config mutations acquire those policies only for authored provider catalog model IDs; generic agent model references do not need the plugin inventory. An empty view remains authoritative and cannot inherit policies from a broader process snapshot.

    Fleet model-runtime preparation captures one immutable config and authored-source view per build. Plugin argument handling, activation fingerprints, and agent lookups reuse those captured facts across agents. Dynamic model hooks still receive each agent's directory, workspace, and model registry. Preparation yields to the event loop between agents so Gateway requests can run during a large fleet build; config refresh creates a new capture. Existing installations need no configuration changes or migration.

    The snapshot and lookup table keep repeated startup decisions on the fast path:

    • channel ownership
    • startup plugin planning
    • startup plugin ids
    • provider and CLI backend ownership
    • setup provider, command alias, model catalog provider, and manifest contract ownership
    • plugin config schema and channel config schema validation
    • startup auto-enable decisions

    Startup and hot replacement share one prepared registry publisher and the same inventory across all configured agent workspaces. Reload preserves workspace provenance so an unchanged linked plugin is not replaced when another plugin changes. Replacement retains unchanged plugin instances and validates candidate metadata before draining affected services and channels. A retained instance keeps its original registration and api object; it is not registered again. Plugin-wide runtime capabilities such as api.runtime.llm.complete select the committed plugin inventory when each call starts, so a retained context engine can still start new completions after another plugin is replaced, including from work that restores an earlier turn's context. Reordering object keys in equivalent metadata or settings does not replace a registration; changed values, ordered lists, and explicit reload requests still do. It stops and disposes the previous registration before registering its replacement, then publishes runtime methods and metadata together. Connected clients refresh their plugin capabilities after publication. If replacement fails before publication and cleanup succeeds, recovery registers captured previous code and configuration with fresh resource ownership. A failure after publication reports the committed generation. Plugin runtime imports remain lazy; retaining metadata does not activate every discovered plugin.

    Config-only service reloads keep a pending restart when cleanup exceeds the five-second observation deadline. The Gateway logs the deferred restart, retains the original cleanup work, and automatically starts the replacement after cleanup succeeds. Newer config reloads and registry handoffs supply the replacement configuration; shutdown cancels the pending restart. Rejected cleanup remains a visible service failure and never permits a replacement to acquire its resources.

    Durable final channel replies can use the admitting Gateway's current registry after an unrelated reload only when their exact channel registration is retained. The handoff also requires unchanged channel settings, shared channel defaults, and owning-plugin settings. Channels may add sender preparation for credentials that can change outside config: Telegram checks its resolved bot credential and pins it for the final send, so changed token-file contents, environment tokens, and SecretRef values cannot select another bot. Channels without sender preparation deliver with the unchanged successor config. New or replaced channel registrations, changed settings, and closing Gateways remain blocked. This never falls back to another Gateway or retries a send that may already have reached the provider.

    The prepared-model replacement gate holds new runs while already admitted runs finish using their original callbacks. Ordinary quiescence closes plugin admission before resource replacement; there is no separate reservation fence on retained work. Detailed readiness and logs report the retained-work count and drain deadline; the final RPC receipt reports application and any drain notices. Reload from the instance's own active callback or a live turn borrowing that same instance fails during preparation to avoid waiting on itself. Idle prepared publications and closed turn scopes do not block replacement. Nested static model acquisition that matches a still-open turn lease borrows that turn’s original generation before checking the replacement gate, so the turn can finish while an external reload drains it. Borrowing cannot change the agent, workspace, configuration, or selected plugin owners. Explicit live catalog discovery still follows ordinary admission and refresh. Callbacks and consumers without a matching turn lease still cannot wait on their own reload, including an external --wait request. Arbitrary nested cross-plugin dependency chains are best effort; cancel --wait or restart if such a chain cannot settle.

    Reply model preparation uses the configured agent’s inventory workspace across session execution directories. A nested run with matching agent and configuration can reuse its still-open parent lease’s inventory workspace, including a launch override, while tools keep the requested execution directory. Different selections and closed leases still pass through the prepared-runtime admission owner.

    A delivery-enabled requester continuation refused at its initial runtime acquisition finishes as failed and uses ordinary final delivery for a failure notice. Check the conversation and logs before retrying after the reload. Cancelled or revoked work cannot send this notice; a refusal after earlier execution keeps its existing error path.

    While admitted work drains, ordinary model catalog, auth-status, and chat metadata reads continue using the active publication. Catalog refresh work, downloaded catalog adoption, and new execution still wait. Published facts retire when resource replacement begins; an admitted-work timeout keeps those facts available without rebuilding them. An unfinished startup publication still follows its existing cancellation and recovery path.

    Session lists wait for replacement provider policy when the previous runtime has retired. Disconnecting a reader cancels only that reader’s wait; other readers and the reload continue. Lists resume with the published replacement catalog and never invoke retired provider callbacks.

    Retained work and in-flight calls share a 60-second pre-stop budget. Retained runs finish before ordinary call admission closes. Existing calls then settle while published metadata remains readable. Sidecars release their capability consumers before the remaining finite consumers drain; memory teardown retains cleanup authority for its exact retiring provider instances until the raw cleanup settles. Replacement stops services and channels only after that handoff. Idle service and channel custody stops later with its owners. If work does not finish, the reload fails once, resumes admission, and clears the reload status; the previous plugin generation keeps serving. The deadline does not cancel agent runs or permit disposal of unfinished writes. Retry openclaw plugins reload <id> after that work finishes, or explicitly select --wait for a shared ten-minute pre-stop drain budget, matching the plugin lifecycle wait budget. Both modes hold new agent runs behind the replacement gate until completion or rollback; status exposes the pending reload and its drain deadline in JSON. The extended wait also expires with the previous generation serving. The explicit wait belongs to its requesting connection: Ctrl+C or disconnect cancels the pre-publication wait and runs the same rollback. Cancellation never disposes admitted work, bypasses cleanup, or reverses a committed publication. Service shutdown and recovery retain their bounded deadlines. Successful publication logs the applied replacement and emits plugins.changed.

    Automatic config reloads treat an admitted-work timeout with successful rollback as deferred. The previous generation resumes serving, and the config watcher retries the latest saved configuration after that generation's work and cleanup settle. A waiting config-write receipt still reports that the bounded attempt did not apply: keeping it open could prevent the writing turn itself from releasing the old generation. Check config.get after the work settles to confirm the active revision. The retry observation does not keep plugin admission closed or extend the drain budget. Explicit plugin reload requests still report their bounded drain failure; cleanup failures and failures after publication remain errors.

    A provider or harness plugin load failure remains recorded in its runtime generation. It makes that plugin unavailable without superseding the generation or blocking models that use healthy plugins. Inspect the failing owner with openclaw plugins inspect <id> --runtime --json. Use openclaw doctor --fix for supported installation repairs, or fix the reported problem in plugin code, then request plugins.reload through the admin Gateway API to load the repaired plugin.

    Read-only model validation, effective tool inventory, and isolated model checks acquire their own registrations when they need executable provider or harness hooks. Concurrent callers share the prepared generation, and its lifecycle disposers run after the final borrower and any unfinished preparation or catalog work settle. Cancellation does not close a registration while its callback is still running. Process shutdown revokes these registry views before joining their remaining work and disposal. Catalog reads that need only metadata do not acquire these executable registrations. Effective tool inventory prepares only configured and session-selected model facts, including captured catalogs from enabled providers; it does not refresh the full model catalog. Session selections do not change the configured model picker.

    Cold inspections within a Gateway inventory reuse native ESM code from unchanged npm installations across their separate registrations. Reuse is bounded by that inventory's installed execution entries, configured workspaces, and SDK selection. Inventory retirement releases retained source captures and preparation workers after the final inspection borrower finishes. Editable installations and inspections outside the prepared inventory keep independent captures. Node retains already evaluated ESM modules until process exit; this reuse prevents another graph from being evaluated on every cold inspection, rather than unloading old graphs after a plugin replacement.

    Each plugin service startup attempt owns one cleanup operation, including failed starts. Hot replacement observes candidate startup and service cleanup with five-second deadlines. Candidate startup failure rejects the replacement. Replacing a loaded plugin requires successful cleanup before another registration can acquire its resources; pending cleanup or a cleanup error can therefore reject replacement and prevent automatic recovery. A pending startup retains its resources until it finishes and its one stop operation settles. Plugin removal can report deferred cleanup; Gateway shutdown joins that work before releasing the plugin's resources. Disposal stops new registered calls while physical cleanup finishes. Service cleanup is not invoked a second time merely because an observer timed out. If a command catalog refresh also stopped unchanged channels, failed replacement resumes those healthy registrations while the failed plugin remains fenced.

    A replacement that fails before publication automatically tries to restore the previous code and configuration. If its channels have already stopped, recovery observes pending service cleanup and admitted work once, for up to 60 seconds. Its observation has an independent Gateway-owned cancellation signal, so a disconnected reload requester cannot interrupt recovery. Only service-stop observer timeouts qualify for this wait; channel-stop failures, rejected service cleanup, and failed candidate cleanup still prevent recovery. The wait joins the original service stop promises, including services whose shared stop deadline was already exhausted. After cleanup and work settle successfully, recovery disposes the old registration, registers its captured code with fresh resource ownership, and restarts its channels. Manually stopped accounts stay stopped. A timeout never grants another registration ownership of an unfinished write's resources, and retries do not repeat service stop or resource disposal. Uncommitted failures also attempt to republish the prepared model runtime against the previous config, even when plugin recovery fails or cannot safely run, unless the Gateway is shutting down.

    For plugin-only replacements that fail after publication, the Gateway attempts to republish model catalogs and reply dispatch against the committed configuration and plugin generation. The original plugin failure remains visible, including failed channel readiness. If model publication also fails, the operation reports both failures and asks the operator to retry the plugin reload or restart the Gateway. Shutdown or Gateway replacement cancels this recovery; mixed config changes with unfinished runtime work retain their existing restart recovery.

    When recovery cannot safely finish, the operation settles as failed and releases its channel reload pauses. Channel health reads retain captured account facts without invoking unavailable plugin code; healthy registrations can restart normally. Detailed readiness reports failing: ["plugin-reload"] with the affected plugin IDs and an actionable recovery reason, instead of leaving an indefinite "reloading" state. Retry openclaw plugins reload <id> after admitted work and pending cleanup settle, or restart the Gateway. Permanent cleanup failures still prevent another registration from acquiring those resources.

    A later reload can retry after pending cleanup completes successfully, without capturing code from the disposed instance again. If draining fails after services or channels stop but before disposal starts, the quiesced registration retains its original loader for the next reload's recovery capture; ordinary plugin calls remain closed when recovery fails. Rejected replacements and failed recovery registrations retain the same cleanup barrier. Recovery preserves existing error records as diagnostics without executing their code. It never substitutes current package files for an already retired registration whose captured source has been released.

    Missing captured source files produce a replacement warning instead of preventing a fresh plugin load. Recovery snapshots must contain every previously captured input, including companion files. If replacement then fails, recovery restores only plugins with available captured code; it does not substitute changed installed files for a missing snapshot. Existing call-drain and resource-cleanup requirements still apply.

    At the end of a reload, the Gateway awaits disposal of unused recovery snapshots without blocking the event loop on their preparation workers. Cleanup attempts every snapshot before reporting failures, so one failed disposal does not strand unrelated captured source.

    Gateway shutdown also joins actual harness, MCP, LSP, embedding, and media cleanup after their initial grace periods. When clearing the active registry, plugin host cleanup can advance to later hooks after a timeout, but registry resets and shared database closure wait for its actual completion. These waits preserve resources for cleanup; they do not restore a retired plugin's runtime authority.

    Shutdown closes admission before waiting for config reloads to settle. Those reloads can still defer cleanup held by existing consumers. Final Gateway close releases those consumers, joins retained cleanup, and reports its failures before another Gateway can start.

    Gateway config reload and startup metadata persistence use process-bound plugin lifecycle leases. After a forced stop, the next process can reclaim a lease whose recorded same-host process identity is proven dead. Package installation leases retain their expiry protection because installer subprocesses can outlive their parent. A contended lease logs its holder and observed expiry while waiting; older leases without process identity must still expire before startup can safely refresh the plugin index. This uses the existing state schema and requires no update migration.

    Executable CLI cleanup reports each disposer that exceeds five seconds and proceeds with later cleanup without canceling the pending work. On macOS with Node's system CA support enabled, automatic exit after command completion waits for this pending cleanup to finish. Explicit command exit requests and the update exit watchdog retain their bounded behavior.

    Standalone plugin and Codex supervision MCP stdio services retain their discovered registrations through accepted tool work, harness cleanup, and nested SDK provider lookups. Terminal shutdown cancels and joins handlers before releasing these registrations and awaiting their resource disposers. Transport-close and registration-disposal failures reach the serving caller. Programmatic servers created from supplied tools leave those resources with the caller; closing and reconnecting the same server does not dispose them.

    Hot registry publication does not wait for retired host cleanup; terminal shutdown also joins cleanup already started by earlier registry replacements before resetting shared state. Activation and rollback apply only to their captured registry version. An activation superseded by a lifecycle callback reports an error.

    The cache rule is documented in Plugin architecture internals: Gateway retains one cache generation, while explicit management operations use isolated generations of the same cache. There are no wall-clock TTLs for Gateway metadata.

    Gateway plugins.list serves installed metadata without waiting for hosted catalog or category enrichment. While the generation's shared enrichment is pending, the response uses bundled presentation metadata and includes a pending diagnostic; refresh the list to read the completed facts. Unavailable category enrichment remains recorded until an explicit catalog refresh or metadata generation change. Config validation, enablement, and runtime service health still use current local facts on each read.

    Install, update, registry refresh, and doctor flows may read fresh package metadata to validate their changes. A management snapshot or installed-index write alone does not replace the running Gateway's inventory: the Gateway lifecycle owner must prepare and publish it. Runtime flows use their selected snapshot or lookup table instead of falling back to cold management paths.

    Runtime instance and source lifetime

    Inference verification fingerprints the artifact selected by the runtime's source/build preference, including explicit bundled source overrides. Loading an unchanged plugin preserves that proof; changing its selected runtime files invalidates it. For bundled modules embedded in a standalone executable, the fingerprint combines the image's build identity, compilation mode, and build flavor with the retained plugin sidecar files. Embedded JavaScript does not need a second copy on disk.

    A managed runtime instance owns its module results, registered callables, and runtime-store slots. Non-bundled instances also own a captured source artifact. Package plugins capture their package inputs when the instance is created. Standalone files capture their entry and statically known inputs without copying the surrounding workspace. Bundled runtime and setup modules, including TypeScript source entries, share the host's code identity; each inventory owns its registered callbacks and cleanup. Explicit bundled reload refreshes those registrations and reports restartRequired: true. Restart the Gateway to load updated bundled, host, or SDK code, and rebuild first when the installation loads compiled output. Embedded executable code changes require a newly built executable. External on-disk plugins continue to capture updated source and dependencies on explicit reload.

    WebAssembly libraries remain ordinary captured assets even when they use a .so filename. Directory asset references reuse an active capture walk; filesystem directory cycles still fail capture.

    Source preparation workers wake synchronous host readers after posting results, callback requests, and consumption acknowledgements. The shared sequence is a progress hint only: the host still services the transport and checks the actual task receipt and deadline. Readers retain bounded polling for worker failures.

    Doctor retains an unexecuted source snapshot across its maintenance phases. Each phase admits fresh callback instances with private module files and settles their cleanup before releasing its lifecycle lease. The snapshot is reused for the whole operation; edit plugin sources between Doctor runs, not between phases. The next invocation captures current files and dependencies. Retained source custody ends when Doctor finishes, including before a diagnostic process exit. This does not change the running Gateway's inventory or require an installation migration.

    Conditional package aliases retain their package metadata, and native Node conditions, including module-sync, select the target from that captured metadata. Source inspection uses the same synchronous-module condition without evaluating plugin code. Captured source retains the difference between authored imports and require calls, so Bun's compiler resolution previews do not acquire a deferred dependency before its first call. Missing selected targets remain absent for that captured generation. Legacy packages without an exports map also admit their existing main or index entry without executing unselected code. Native entries reuse the recorded admission below. The selected package's remaining body is captured before execution. Dependency links retain existing nested installation locations. Dependencies installed beside a package remain siblings in the capture, including optional platform packages whose native assets are read through relative filesystem paths. Other ancestor dependencies link at the captured package root. Capture does not add node_modules beside individual source files, so native-addon loaders can still locate their package root and its build assets.

    Native artifacts are admitted with their owning package directory, preserving the binary's package-relative path and declared companion library dependencies. Artifacts without an admitted package root retain their containing directory. Installer-owned directories use same-filesystem hardlinks or an existing retained-directory reference; capture onto another filesystem copies the native package and its companions. Files inspected by plugin safety checks keep independent copies. Mutable source trees retain one private directory snapshot per admitted identity, preserving old binary and companion bytes through in-place edits. Files in this namespace are prepared at admission; module execution remains on demand. Registrations share admission facts without sharing their runtime authority. Managed npm plugins support capture storage on another filesystem, including a tmpfs mount, and npm roots reached through symlinks. Retained native directories validate the admitting plugin's OpenClaw peer against the selected host's resolved package root. A hoisted native dependency does not need its own host link, but any host it resolves must match. A mismatch names the peer path, resolved target, and selected host; run openclaw doctor --fix with that host, then reload the affected plugin. The loader records the failure for that plugin and continues loading unrelated plugins. Private Doctor inspections keep their native admission facts separate from the operator's state. Temporary captures copy native packages and their companions, so inspecting an update rehearsal never adds hardlinks to its private data. Their temporary captures never become deferred writes to the installed index after inspection ends; ordinary deferred writes retain their original state directory. When native packages share a dependency, admission reconciles identities only for its own hardlinks, even when the filesystem's ctime has not advanced. Recorded digests are checked against installed bytes before promotion, including companions previously captured as independent copies. Unchanged companions remain valid during Doctor and reload; source content checks still reject edits. When file symlinks are unavailable, a generation can use hardlinks only if its directory preserves every captured companion and the selected host SDK. Otherwise that plugin reports a load error asking for file symlink support; the update continues with the existing plugin-failure warning behavior. Within a capture, admission checks each immutable namespace and companion-directory mapping once, resolving each member once even when parent and child native directories overlap at the same placement. Preparing more modules reuses those facts and checks newly admitted placements. Replacement captures, host selection, and recovery copies validate again, so Doctor and Gateway preparation avoid repeated walks without reusing another capture's verdict. The existing installed-index SQLite payload records directory membership, device, inode, mode, size, mtime, and ctime identities, SHA-256 digests, and the initial generation receipt. Unchanged warm startup reuses those facts. Added, removed, or changed companions require admission again. If a recorded capture directory is missing, fresh admission reads the installed package without promoting the missing capture's identities. This also applies to post-update Doctor with older updaters. Ctime-only uncertainty is resolved with a bounded rehash, including ordinary companion files whose inodes another capture retains or releases. First-read metadata stamps from Windows security software do not invalidate otherwise unchanged native companions. Mutable source copies are compared with their source bytes before accepting ctime-only drift, and completed companion digests retain the observed file identity. Legacy reload receipts keep their framed raw-byte value, so a changed receipt still requires streaming its native payloads. Read-only inspection never publishes its native admissions during later settlement. Deferred publication retains the original database authority and state directory. Gateway startup uses the admitted in-memory receipts without waiting for their advisory persistence. Model workers receive those same admitted facts, and cache retirement joins pending receipt writes before releasing native captures. Doctor's private checks keep captured code in profile-qualified temporary storage, so removing their database snapshots cannot remove an image still loaded in the process. An entirely missing retired capture can be rebuilt from its installed source; missing files in a retained or partially present namespace remain load errors. Identity reuse cannot detect an edit that preserves every recorded identity field. Source code outside an admitted native namespace is captured and verified separately. Published native captures survive ordinary scratch cleanup. Doctor maintenance removes unreferenced captures while preserving installed-index references, warm generations, and live owners. System-temp fallback captures are scoped to their state directory; captures with unknown ownership are preserved.

    Gateway idle cleanup runs after startup and repeats hourly after each completed pass, deferring while the Gateway is busy and stopping during shutdown. It checks capture directories and npm retention markers before acquiring the plugin lifecycle lease. An empty scan makes no shared-state writes and leaves the Gateway's metadata caches intact. When candidates exist, cleanup uses a private operation-scoped cache and one fresh installed-index payload for install records and native receipt protection. Invalid receipts still preserve captures, and every deletion retains its live lease and custody checks. The private cache is disposed before the lease is released. These best-effort scans do not freeze the filesystem: artifacts created or retired after inspection remain for a later cleanup attempt.

    Each captured generation links the selected host openclaw package so Workers and child processes started from its modules can resolve the host SDK. This link does not depend on the main thread's module hooks and is recreated during recovery. Imports of resolved SDK file URLs and absolute paths keep the same host identity; they do not create a selective copy of the host package or its runtime chunks. Deferred SDK imports and import.meta.resolve() retain the generation's selected source or built host even after the active plugin cache changes. Snapshot cleanup and update source inspection do not descend through these links into the host package.

    After the existing runtime, setup, or executable-discovery checks admit an entry, its instance captures imported shared files and dependency modules on demand. Relative, absolute, and file-URL imports use captured files; TypeScript dependency entries compile in their own package scope. Metadata and install inspection remain confined and do not acquire executable shared inputs.

    Executable loading can follow a shared-module link by capturing its selected module separately. Cold source snapshots still reject links outside the plugin root, so deferred install batches and install-digest settlement require those inputs to be packaged as dependencies. Linked non-module resources outside the plugin root are not captured by this module-loading path.

    A first-demand import() or require() can observe later source edits; captured metadata and entry bytes remain unchanged. The initial source digest covers the creation-time capture; later inputs extend explicit source-current checks without changing that digest. Invalid optional package metadata fails only when selected. Module acquisition uses the instance's current admission, and disposal closes further capture. Runtime and setup retirement use the existing five-second instance shutdown budget. If calls, retained consumers, or cleanup exceed that budget, logical retirement returns a forced-retirement diagnostic with the still-running call and consumer counts. Ordinary invocation authority closes and late successful results are refused. Physical cleanup continues asynchronously: captured files and module resolvers remain until calls, consumers, and cleanup tails actually settle. An explicitly retained consumer keeps its admitted turn and cleanup authority until its host closes or releases it. Its callbacks and late results are refused after release. Shared writable state stays owned until its cleanup finishes; late cleanup failures remain failures of the resource handoff. Web provider descriptors keep their registration identity; runtime projections bind their factories and returned tools to the selected plugin instance. Synchronous source inspection and failed capture still clean up before returning.

    Default source captures live under <stateDir>/tmp/plugin-captures/<instanceId>/captures/, with a random instance ID and one owner.sqlite token holding its native lifetime lease. Gateway metadata and its source captures retain the same process-local instance; a concurrent CLI process owns a separate instance. Releasing one capture cannot retire another capture or a still-running metadata owner. During updates with an explicit TMPDIR, source captures use that temporary volume instead. Their native custody and invocation cleanup remain unchanged. Gateway metadata supplies its scheduler for hourly cleanup; executable CLI commands own maintenance through their invocation scope. Capturing source alone does not create a timer. Cleanup does not retain the first command's invocation context. The managed tmp/plugin-captures subtree is excluded from source snapshots when the state directory is inside a plugin's source directory. Recovery can still load a preserved source package from within that subtree.

    Executable CLI commands retire their plugin inventory through the existing invocation resource scope on success and failure. Inventory adopted by Gateway publication remains with Gateway metadata retirement. At process exit, the capture owner synchronously retires any remaining instance that holds this process's custody, including explicit exits and CLI-handled signals. Forced termination still relies on startup reclamation. Plugin cleanup owns this capture subtree; reclamation removes captured payload before retiring the token so a partial deletion remains retryable. The token closes before its directory is removed, including on Windows.

    A native library mapped in the current process retains its capture and token through process exit, even after its JavaScript module cache entry is removed. Cleanup records retained-by-loaded-module once for that capture lifetime instead of trying to unlink a loaded Windows image. Unchanged native package identities continue to share the retained payload across reloads. Native-load attempts also retain their capture when initialization throws: the native image can remain mapped after the initialization error.

    Before runtime plugin loading, startup attempts receipt-aware cleanup under exclusive maintenance ownership. It can reclaim a retired, unlocked instance immediately, including unpublished native payloads retained until the previous process exited. Published native payloads still referenced by the installed index remain available. If another process holds state ownership, this opportunistic cleanup silently skips without asking the operator to stop a healthy Gateway. Other maintenance or cleanup failures produce a warning and startup continues; observational reads leave captures untouched.

    Hourly cleanup also inspects this owned subtree. An instance becomes eligible after one hour. Age alone never authorizes removal: for token-bearing instances, cleanup must also acquire the existing token's exclusive native lease to prove released custody. This works after process termination or reboot without PID or boot-namespace records and preserves the same cleanup contract for shipped owner.sqlite markers. Cleanup rechecks directory and token identity before removal. Live leases, unreadable entries, symlinks, and invalid tokens preserve files. An aged instance left without a token by interrupted allocation or older partial cleanup is reclaimed directly on all platforms if it has no native payload and is not retained by the current process. The plugin source capture directory owner rechecks its directory identity and, where available, the current UID before removal. Recent tokenless roots and roots containing native payloads are preserved. Allocation creates the token before creating payload, and disposal removes payload before its token. The token belongs to its capture instance; captures do not create a global coordination database. Removal remains asynchronous and advisory. This subtree is excluded from state backups because its captured package bytes are reconstructible.

    Metadata retention does not create directories until a source capture is needed. If the state directory cannot accept captures, loading falls back to an isolated system-temporary instance and reports a warning. Normal disposal still removes that instance; automatic cleanup does not scan unrelated system-temporary roots. There is no total disk quota, and an active instance may legitimately exceed the one-hour cleanup grace period. If its payload directory is removed while the instance still holds custody, the next capture recreates that directory under the same lease. This does not restore previously deleted captured files or recreate a missing ownership directory.

    Hourly cleanup also reclaims tokenless openclaw-plugin-build-* and openclaw-model-catalog-* roots in the selected state's temporary directory and the current system temporary directory. Roots must be older than one hour and have no custody token or native payload. The same directory identity, current-process retention, and UID checks apply on all platforms; no process or open-file census is required. Legacy producers that outlive the grace period receive best-effort cleanup. Inspection or removal failures are reported once per sweep and retried later without interrupting loading or updates. Managed instances with custody tokens continue to use their native lease on all platforms.

    Older openclaw-plugin-build-* directories in the system temporary directory have no owner record proving whether their producer is still alive. Doctor reports tokenless openclaw-plugin-build-* and openclaw-model-catalog-* roots under the state temporary directory, ~/.openclaw/tmp even when another state directory is selected, the current system temporary directory, /tmp on POSIX hosts, and recorded managed-service TMPDIR locations. It deduplicates directory aliases and reports each capture's path and regular-file size without following links inside captures. Catalog roots include all nested openclaw-plugin-build-* trees in their reported size and removal receipt.

    openclaw doctor --fix reclaims these legacy roots only while Doctor holds Gateway maintenance and a complete host process census finds no other OpenClaw producer. The rule rechecks both conditions before each removal and prints a receipt listing the paths removed and their sizes. A live sibling, unavailable census, or missing maintenance authority leaves the captures in place with an explanatory message. Captures created or changed during the current process and token-bearing captures remain untouched. Linux and macOS preserve native argument boundaries when inspecting processes. macOS can also identify native system services under another user by their kernel executable path and valid Apple platform signature; unavailable arguments for other live processes keep cleanup blocked with a reason. On hosts without a complete process census (including Windows and recognized container environments), Doctor reports legacy captures but skips their removal. For a container sharing the host's temporary directory, run maintenance on the host after stopping its OpenClaw containers. Doctor's maintenance repair remains separate from runtime reclamation: its broader inventory includes old service temporary locations the current runtime does not use. Modern captures retain their custody-token cleanup.

    Configured Gateway agents share one model-catalog worker per plugin-inventory lifetime. Agent and authentication facts belong to each task; plugin registrations and captured source remain with the shared inventory. Standalone hosts that supply their own environment retain an isolated catalog worker for that environment. Provider-discovery entries use the exact selected runtime instance's captured source when it is already loaded, so discovery does not create a second copy of the same plugin package. Standalone discovery keeps its own setup lifetime. Public API sidecars follow the loader-selected setup or runtime artifact and its plugin root. A built Gateway uses the selected JavaScript sidecars even when discovery metadata still points at the source checkout; source-based instances keep their TypeScript sidecars and captured-source membership. The loader records its scoped registration inputs with their metadata inventory, so catalog workers reuse those facts instead of rerunning setup activation. Each worker retains the current plugin registration context for each loader workspace, shared by agents with matching configuration, environment, and plugin inventory. Alternating unchanged workspaces reuse their captured source; replacing one workspace does not evict another. Within that worker, an unchanged installed native ESM entry keeps one module evaluation and capture URL per workspace across configuration revisions. Its capture and compiled TypeScript helpers remain available for lazy imports after the originating generation is released. Each generation still owns its registrations and resolution hooks; released generations cannot use a replacement generation's API to register providers. Different workspaces keep separate modules. Node retains each native module until worker retirement, so new installed paths or workspaces still add resident modules. Installed source replacements are picked up when the worker restarts; explicit recovery keeps its separate source custody. CommonJS capture and disposal are unchanged. Agent credentials and configured model facts travel with each request; catalog jobs do not rebuild the agent workspace. Discovery reuses the registrations already acquired by that context. The first catalog request prepares registrations for the agent's known configured and credential providers together; only the requested providers run catalog hooks. Newly observed owners extend that context without discarding earlier owners. Replacement releases them after admitted work settles. Native admission runs outside the 180-second catalog refresh deadline, so a slow filesystem does not repeatedly discard and recapture the same package. Each verified native namespace member advances a counter, forwarded through the existing worker task channel at most once per second unless the active plugin changes. The parent allows admission to continue while that counter advances. After 180 seconds without progress, it records a failure naming the plugin and native reference verification stage and closes the worker without automatically recapturing that inventory. Reload the plugin or restart the Gateway to retry. Parent probes and queued requests remain bounded; provider discovery starts its own 180-second deadline after admission. Inventory retirement and shutdown still close the worker. After successful physical cleanup, retired plugin instances release their registry references while preserving revocation. Native module exports no longer retain the disposed registry through instance ownership, and stale calls remain rejected. Pending or failed cleanup retains its custody. Successfully disposed registrations leave their plugin caches. Failed model-runtime refresh diagnostics retain the error message, name, code, and materialized stack, not the failed build or its cause graph. Active publication settlement still uses the original failure for lifecycle recovery.

    Catalog observation is passive. Inventory requests can ask the catalog owner to renew expired providers while returning its accepted rows. Chat metadata and session projections only observe publication, so a refresh cannot schedule itself through its own notifications. Selected native-model discovery has an independent acquisition owner and does not wait for provider inventory renewal. Both owners merge their results with the latest accepted counterpart before publication. Catalog workers use a 512 MiB V8 old-generation limit rather than inheriting the Gateway's heap budget. Startup clears V8's process-wide heap overrides after the main isolate is initialized, so explicit heap flags still size the Gateway while worker limits remain effective. Explicit V8 flag-freezing or contradictory-flag checks prevent this reset and emit a warning. Native and external allocations are outside the worker limit. When a Gateway catalog worker fails, the Gateway logs a warning with the reason and counts the failure in status as workerPools.modelCatalog.workerFailures. Other than stalled native admission, failures republish the affected agent catalogs on a new worker.

    Catalog and authentication refresh tasks carry the host's prepared Template consent provenance. Worker config reconstruction and provider imports consume these facts without opening or copying the shared state database. Host config publication and Doctor retain their existing provenance refresh and artifact-preserving inspection paths; stored data, schemas, and update/rollback behavior are unchanged.

    Credential persistence publishes fresh shared-store ownership before credential discovery. Login and explicit auth refresh join the credential owner's publication instead of creating another catalog generation for the same change.

    Model-catalog workers keep their captured plugin files in a worker-owned directory under the same managed capture instance, with custody retained by their producer. The parent removes any remaining captures after that worker exits, including cancellation and crashes. Files remain available while the worker is running, and retiring one worker does not remove another generation's captures. If the whole Gateway is killed, the existing hourly cleanup reclaims the abandoned instance only when it can acquire the existing token's exclusive native lease. The same rule covers shipped SQLite markers and works after a reboot. Age alone never releases captures. Cancellation releases compute capacity after the worker exits; terminal shutdown also waits for file cleanup. Failed file removal is reported as a cleanup warning.

    Loading metadata alone does not execute every plugin, and registration remains synchronous. Synchronously loaded TypeScript entries and their synchronous TypeScript imports retain Jiti's CommonJS compilation behavior, including .mts and .mtsx entries. Node evaluates the captured output. Keep top-level await out of synchronous entrypoints; start asynchronous work through lifecycle callbacks or a later dynamic import.

    Dynamic TypeScript imports preserve asynchronous CommonJS execution, including top-level await and module.exports, while source loaded from native JavaScript follows Node's module format. The first evaluation fixes that mode for the instance; resolving a module alone does not evaluate it. Compiler outputs are allocated on first resolution, not for every captured TypeScript file. A resolve-only call returns a stable existing path without compiling the module. Jiti-backed modules use Jiti's transformed runtime bindings for loader helpers, without a separate TypeScript analysis pass. Source import.meta.resolve retains Jiti's optional parent URL and resolution options, including custom conditions and try. The one-argument resolver uses the source's directory and package scope. Preparing a package module defers its capture resolver until an import or source inspection needs it. Explicit JITI_TSCONFIG_PATHS configuration still initializes the resolver at module preparation so later tsconfig edits cannot change the selected mapping. Preparation workers retain separate artifact isolation. Captures reuse resolution state for local TypeScript imports such as ./helper.js when only helper.ts exists, avoiding repeated resolver setup and exception-based file probing. Existing JavaScript and Jiti's alternative filename precedence remain unchanged. Resolver state is released with its capture; new captures select current source inputs independently. Captured package manifests and sorted dependency declarations are reused for that capture's lifetime; new imports still resolve and validate dependency links. Native directory freshness checks continue to inspect current source metadata. Entries loaded from captured source retain evaluation failures for their instance instead of retrying through another loader. Core-shipped JavaScript and libraries loaded outside a captured plugin instance keep their existing native/Jiti loading behavior.

    The host Plugin SDK always stays on the host's native module graph, including when Jiti compiles a plugin entry. SDK aliases use resolved filesystem paths so symlinked checkouts cannot create another host owner. The running host selects source or built SDK modules; a plugin's file extension does not select a second SDK graph. Source hosts need a native TypeScript loader such as the repository's tooling preload. An SDK that cannot load natively fails instead of being evaluated again by the plugin transformer. Plugin reloads may create new instances of the plugin's private code, while existing and replacement plugins share host SDK identity and authority.

    Captured packages also retain a link to the selected host installation so child workers and processes can import its public SDK. These separate isolates use the installation's normal package exports; they do not inherit the parent's source aliases or authority. Capture disposal removes the link, never the host package.

    Managed TypeScript filename metadata (import.meta.url, import.meta.filename, import.meta.dirname, __filename, and __dirname) identifies the captured source so relative asset reads stay within that generation. Node executes compiled JavaScript from a separate directory; its module URLs and CommonJS cache keys can differ from the source filenames.

    Node owns plugin resolution through Module.registerHooks; Bun keeps its native/Jiti loader and Bun.plugin resolver even when Module.registerHooks exists.

    Bun uses its native/Jiti loader with a separate captured source artifact for each managed instance. Reload prepares fresh TypeScript entries and helpers while existing consumers retain their old instance. Disposal removes that instance's captured cache records and files without evicting its replacement or the host SDK. Native imports and Jiti imports retain their respective package conditions.

    Bun needs local package import/export targets to exist before native resolution. Selective captures therefore acquire existing files matched by those declarations, including conditional branches and wildcard targets, before evaluation. Unselected source remains raw bytes; its code and TypeScript configuration are not evaluated. This can read more files at startup than Node's demand-driven capture. Other deferred imports still acquire source on first use through the instance's current admission; already prepared modules need no new acquisition. Package imports that select a dependency promote its retained files together, including physical aliases of a prefetched entry. Compiler previews of deferred require calls do not acquire nested dependency bodies before the call executes. Bun keeps ownership of built-in package-import targets and their native validation.

    When using Jiti's TypeScript path settings, keep the original tsconfig files and configuration dependencies available while the plugin is active. Loaded modules retain their selected path mappings; previously unvisited modules may read those configuration files on first use. Newly loaded instances select the current path settings. On Windows, explicit and relative tsconfig paths still resolve managed imports from the instance's captured source, including lazy imports after source files are edited.

    Registry retirement revokes managed execution separately from physical resource release. An acquired inspection can release its execution authority while a borrower still holds the underlying registration resources; the last physical claim owns their disposal, including a cleanup work scope that remains usable after the releasing request has ended. Bare SDK provider results retain their own instance consumer, so their callbacks remain usable until the owning SDK host closes. That host joins admitted callback work before releasing consumers and resources; releasing the inspection still prevents new borrows. Gateway shutdown keeps shared dependencies until the owners that still need them have joined. These ownership rules do not make native plugins a sandbox or automatically close plugin-created resources.

    After admitted calls and plugin cleanup finish, captured-source membership retires before module resources begin asynchronous release. Unrelated public-surface lookups cannot re-enter a retiring capture worker; the registration and cleanup scope remain retained until resource release finishes.

    See Plugin lifecycle and cleanup for the plugin author's cleanup contract.

    An admitted agent turn keeps its original context engine through accepted commit and engine disposal. Reload can report deferred cleanup while that turn finishes. Starting engine disposal closes its normal callbacks immediately; the engine's cleanup remains owned until it settles.

    Activation planning

    Activation planning is part of the control plane. Callers can ask which plugins are relevant to a concrete command, provider, channel, route, agent harness, or capability before loading broader runtime registries.

    The planner keeps current manifest behavior compatible:

    • activation.* fields are explicit planner hints
    • providers, channels, commandAliases, setup.providers, contracts.tools, and hooks remain manifest ownership fallback
    • the ids-only planner API stays available for existing callers
    • the plan API reports reason labels so diagnostics can distinguish explicit hints from ownership fallback

    Channel plugins and the shared message tool

    Channel plugins do not need to register a separate send/edit/react tool for normal chat actions. OpenClaw keeps one shared message tool in core, and channel plugins own the channel-specific discovery and execution behind it.

    The current boundary is:

    • core owns the shared message tool host, prompt wiring, session/thread bookkeeping, and execution dispatch
    • channel plugins own scoped action discovery, capability discovery, and any channel-specific schema fragments
    • channel plugins own provider-specific session conversation grammar, such as how conversation ids encode thread ids or inherit from parent conversations
    • channel plugins execute the final action through their action adapter

    For channel plugins, the SDK surface is ChannelMessageActionAdapter.describeMessageTool(...). That unified discovery call lets a plugin return its visible actions, capabilities, and schema contributions together so those pieces do not drift apart.

    Message action names use a deliberately closed, core-owned vocabulary so every transport can render every action. Plugins add action names through a core PR; runtime registration is intentionally unsupported.

    When a channel-specific message-tool param carries a media source such as a local path or remote media URL, the plugin should also return mediaSourceParams from describeMessageTool(...). Core uses that explicit list to apply sandbox path normalization and outbound media-access hints without hardcoding plugin-owned param names. Prefer action-scoped maps there, not one channel-wide flat list, so a profile-only media param does not get normalized on unrelated actions like send.

    Core passes runtime scope into that discovery step. Important fields include:

    • accountId
    • currentChannelId
    • chatType (direct, group, or channel when the inbound route establishes it)
    • currentThreadTs
    • currentMessageId
    • sessionKey
    • sessionId
    • agentId
    • trusted inbound requesterSenderId

    That matters for context-sensitive plugins. A channel can hide or expose message actions based on the active account, current room/thread/message, authoritative conversation type, or trusted requester identity without hardcoding channel-specific branches in the core message tool. Treat chatType as discovery scope supplied by the current inbound route, not something to infer again from an opaque channel id; it is absent when that route did not establish the conversation type.

    This is why embedded-runner routing changes are still plugin work: the runner is responsible for forwarding the current chat/session identity into the plugin discovery boundary so the shared message tool exposes the right channel-owned surface for the current turn.

    For channel-owned execution helpers, channel plugins should keep the execution runtime inside their own plugin modules. Core no longer owns the Discord, Slack, Telegram, or WhatsApp message-action runtimes under src/agents/tools. We do not publish separate plugin-sdk/*-action-runtime subpaths, and those plugins should import their own local runtime code directly from their plugin-owned modules.

    The same boundary applies to provider-named SDK interfaces in general: core should not import channel-specific convenience barrels for Discord, Signal, Slack, WhatsApp, or similar plugins. If core needs a behavior, either consume the bundled plugin's own api.ts / runtime-api.ts barrel or promote the need into a narrow generic capability in the shared SDK.

    Bundled plugins follow the same rule. A bundled plugin's runtime-api.ts should not re-export its own branded openclaw/plugin-sdk/<plugin-id> facade. Those branded facades remain compatibility shims for external plugins and older consumers, but bundled plugins should use local exports plus narrow generic SDK subpaths such as openclaw/plugin-sdk/channel-policy, openclaw/plugin-sdk/runtime-store, or openclaw/plugin-sdk/webhook-ingress. New code should not add plugin-id-specific SDK facades unless the compatibility boundary for existing external integrations requires it.

    For polls specifically, there are two execution paths:

    • outbound.sendPoll is the shared baseline for channels that fit the common poll model
    • actions.handleAction("poll") is the preferred path for channel-specific poll semantics or extra poll parameters

    Core now defers shared poll parsing until after plugin poll dispatch declines the action, so plugin-owned poll handlers can accept channel-specific poll fields without being blocked by the generic poll parser first.

    See Plugin architecture internals for the full startup sequence.

    Capability ownership model

    OpenClaw treats a native plugin as the ownership boundary for a company or a feature, not as a grab bag of unrelated integrations.

    That means:

    • a company plugin should usually own all of that company's OpenClaw-facing surfaces
    • a feature plugin should usually own the full feature surface it introduces
    • channels should consume shared core capabilities instead of re-implementing provider behavior ad hoc
    Vendor multi-capability

    google owns text inference, CLI backend, embeddings, speech, realtime voice, media understanding, image/music/video generation, and web search. openai owns text inference, embeddings, speech, realtime transcription, realtime voice, media understanding, image generation. minimax owns text inference plus media understanding, speech, image/music/video generation, and web search.

    Vendor single-capability

    arcee and chutes own text inference only; microsoft owns speech only. A vendor plugin can stay this narrow until it needs to cover more of that vendor's surface.

    Feature plugin

    voice-call owns call transport, tools, CLI, routes, and Twilio media-stream bridging, but consumes shared speech, realtime transcription, and realtime voice capabilities instead of importing vendor plugins directly.

    The intended end state is:

    • a vendor's OpenClaw-facing surface lives in one plugin even if it spans text models, speech, images, and video
    • other vendors can do the same for their own features
    • channels do not care which vendor plugin owns the provider; they consume the shared capability contract exposed by core

    This is the key distinction:

    • plugin = ownership boundary
    • capability = core contract that multiple plugins can implement or consume

    So if OpenClaw adds a new domain such as video, the first question is not "which provider should hardcode video handling?" The first question is "what is the core video capability contract?" Once that contract exists, vendor plugins can register against it and channel/feature plugins can consume it.

    If the capability does not exist yet, the right move is usually:

  • Define the capability

    Define the missing capability in core.

  • Expose through the SDK

    Expose it through the plugin API/runtime in a typed way.

  • Wire consumers

    Wire channels/features against that capability.

  • Vendor implementations

    Let vendor plugins register implementations.

  • This keeps ownership explicit while avoiding core behavior that depends on a single vendor or a one-off plugin-specific code path.

    Capability layering

    Use this mental model when deciding where code belongs:

    Core capability layer

    Shared orchestration, policy, fallback, config merge rules, delivery semantics, and typed contracts.

    Vendor plugin layer

    Vendor-specific APIs, auth, model catalogs, speech synthesis, image generation, video backends, usage endpoints.

    Channel/feature plugin layer

    Discord/Slack/voice-call/etc. integration that consumes core capabilities and presents them on a surface.

    For example, TTS follows this shape:

    • core owns reply-time TTS policy, fallback order, prefs, and channel delivery
    • elevenlabs, google, microsoft, and openai own synthesis implementations
    • voice-call consumes the telephony TTS runtime helper

    That same pattern should be preferred for future capabilities.

    Multi-capability company plugin example

    A company plugin should feel cohesive from the outside. If OpenClaw has shared contracts for models, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch, and web search, a vendor can own all of its surfaces in one place:

    ts
    import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";import { exampleAiMedia } from "./exampleai-media.js"; export default definePluginEntry({  id: "exampleai",  name: "ExampleAI",  description: "ExampleAI models and media capabilities.",  register(api) {    api.registerProvider({      id: "exampleai",      // auth/model catalog/runtime hooks    });     api.registerSpeechProvider({      id: "exampleai",      // vendor speech config — implement the SpeechProviderPlugin interface directly    });     api.registerMediaUnderstandingProvider({      id: "exampleai",      capabilities: ["image", "audio", "video"],      describeImage: (req) => exampleAiMedia.describeImage(req),      transcribeAudio: (req) => exampleAiMedia.transcribeAudio(req),      describeVideo: (req) => exampleAiMedia.describeVideo(req),    });     api.registerWebSearchProvider({      id: "exampleai-search",      createTool() {        // Return the vendor-owned web search tool.      },    });  },});

    What matters is not the exact helper names. The shape matters:

    • one plugin owns the vendor surface
    • core still owns the capability contracts
    • provider request translation and HTTP helpers stay in the vendor plugin
    • channels and feature plugins consume api.runtime.* helpers, not vendor code
    • contract tests can assert that the plugin registered the capabilities it claims to own

    Capability example: video understanding

    OpenClaw already treats image/audio/video understanding as one shared capability. The same ownership model applies there:

  • Core defines the contract

    Core defines the media-understanding contract.

  • Vendor plugins register

    Vendor plugins register describeImage, transcribeAudio, and describeVideo as applicable.

  • Consumers use the shared behavior

    Channels and feature plugins consume the shared core behavior instead of wiring directly to vendor code.

  • That avoids baking one provider's video assumptions into core. The plugin owns the vendor surface; core owns the capability contract and fallback behavior.

    Video generation already uses that same sequence: core owns the typed capability contract and runtime helper, and vendor plugins register api.registerVideoGenerationProvider(...) implementations against it.

    Need a concrete rollout checklist? See Adding capabilities.

    Contracts and enforcement

    The plugin API surface is intentionally typed and centralized in OpenClawPluginApi. That contract defines the supported registration points and the runtime helpers a plugin may rely on.

    Why this matters:

    • plugin authors get one stable internal standard
    • core can reject duplicate ownership such as two plugins registering the same provider id
    • startup can surface actionable diagnostics for malformed registration
    • contract tests can enforce bundled-plugin ownership and prevent silent drift

    There are two layers of enforcement:

    Runtime registration enforcement

    The plugin registry validates registrations as plugins load. Examples: duplicate provider ids, duplicate speech provider ids, and malformed registrations produce plugin diagnostics instead of undefined behavior.

    Contract tests

    Bundled plugins are captured in contract registries during test runs so OpenClaw can assert ownership explicitly. Today this is used for model providers, speech providers, web search providers, and bundled registration ownership.

    The practical effect is that OpenClaw knows, up front, which plugin owns which surface. That lets core and channels work together because ownership is declared, typed, and testable rather than implicit.

    What belongs in a contract

    Good contracts

    • typed
    • small
    • capability-specific
    • owned by core
    • reusable by multiple plugins
    • consumable by channels/features without vendor knowledge

    Bad contracts

    • vendor-specific policy hidden in core
    • one-off plugin escape hatches that bypass the registry
    • channel code reaching straight into a vendor implementation
    • ad hoc runtime objects that are not part of OpenClawPluginApi or api.runtime

    When in doubt, raise the abstraction level: define the capability first, then let plugins plug into it.

    Skill previews

    The Control UI previews a declared plugin skill without installing or executing it. Opening a preview reads its file inventory and only the entry SKILL.md body. Selecting another file reads that body on demand; navigation never prefetches sibling contents. The file tree remains available while a selected file loads, and failed reads can be retried in place.

    Catalog reads stay pinned to the selected package version and validate the inventory’s paths, sizes, and SHA-256 hashes. Installed reads stay inside the resolved plugin root, reject unsafe links, and reject a changed plugin version. Both paths retain the file-count, tree-depth, per-file, and aggregate bundle limits. The aggregate limit applies to the inventory, not selection order.

    Loaded bodies and pending reads belong to one open preview and Gateway connection. Closing, reopening, navigating away, or reconnecting retires that cache. A late file response can populate its own cache entry but cannot change the selected file. New selected-file requests revalidate the inventory; only already loaded bodies are reused. Installed files edited in place become visible on reopening.

    Execution model

    Native OpenClaw plugins run in-process with the Gateway. They are not sandboxed. A loaded native plugin has the same process-level trust boundary as core code.

    Plugin authors must handle asynchronous errors from their own clients and sockets; see Handle native client errors.

    Compatible bundles are safer by default because OpenClaw currently treats them as metadata/content packs. In current releases, that mostly means bundled skills.

    Use allowlists and explicit install/load paths for non-bundled plugins. Treat workspace plugins as development-time code, not production defaults.

    For bundled workspace package names, keep the plugin id anchored in the npm name: @openclaw/<id> by default, or an approved typed suffix such as -provider, -plugin, -speech, -sandbox, or -media-understanding when the package intentionally exposes a narrower plugin role.

    Export boundary

    OpenClaw exports capabilities, not implementation convenience.

    Keep capability registration public. Trim non-contract helper exports:

    • bundled-plugin-specific helper subpaths
    • runtime plumbing subpaths not intended as public API
    • vendor-specific convenience helpers
    • setup/onboarding helpers that are implementation details

    Reserved bundled-plugin helper subpaths have been retired from the generated SDK export map. Keep owner-specific helpers inside the owning plugin package; promote only reusable host behavior to generic SDK contracts such as plugin-sdk/gateway-runtime, plugin-sdk/security-runtime, and injected plugin API capabilities.

    Internals and reference

    For the load pipeline, registry model, provider runtime hooks, Gateway HTTP routes, message tool schemas, channel target resolution, provider catalogs, context engine plugins, and the guide to adding a new capability, see Plugin architecture internals.

    Was this useful?