On this page

On this page

Plugin internals

Plugin SDK migration

OpenClaw replaced a broad backwards-compatibility layer with a modern plugin architecture built from small, focused imports. If your plugin predates that change, this guide gets it onto the current contracts.

For synchronous SQLite APIs, use the complete async migration guide. The final compatibility day is 2026-12-31; removal ships in the first January 2027 stable release, without a Plugin SDK major version. The tables cover every affected member and its awaited or prepared replacement.

Memory flush model selection

Remove model from plans returned by flushPlanResolver or providerFlushPlanResolver. MemoryFlushPlan, MemoryFlushFilePlanDraft, and MemoryFlushToolsPlan no longer expose a model override. Existing JavaScript plans that still return model have that field ignored. All memory flushes use the active session model and its normal fallback chain.

This intentional removal gives the session ownership of model selection. To keep memory extraction local, select a local session model and local fallbacks. Doctor removes the corresponding retired agents.defaults.compaction.memoryFlush.model setting with a warning; see Config migrations.

Prompt hook history is read-only

before_prompt_build and agent_turn_prepare now share frozen transcript payload leaves instead of making mutable deep copies of the entire history. Plugins that mutated nested message content in these hooks now get a TypeError in strict mode; return the hook result instead. For example, replace a history edit intended to add instructions with an explicit contribution:

ts
api.on("before_prompt_build", () => ({  prependContext: "Use the project's documented conventions for this turn.",}));

Both hooks accept prependContext and appendContext. Ordinary before_prompt_build also accepts systemPrompt, prependSystemContext, appendSystemContext, and toolsAllow; authorized enrichment only consumes the context contributions. None of these fields rewrites stored history.

Message arrays and envelopes remain fresh per dispatch, so array edits and whole-field assignments remain isolated from core history and later dispatches. Handlers in the same dispatch still share those edits in priority order. Do not rely on nested values being mutable, even when a particular harness supplies non-owned values that are cloned. If a plugin needs a mutable working copy for its own computation, explicitly clone that data inside the plugin. Return only the supported contribution fields. See Prompt and session hooks.

MCP result compatibility window

OpenClaw's session and node MCP clients use the split MCP SDK v2 packages while continuing to negotiate legacy MCP. The published plugin boundary retains its MCP SDK v1 result shapes for one stable-release window: the first stable release containing this migration. Replacing those shapes with v2 types is scheduled for the following stable release; no calendar date is assigned yet.

The affected contracts are SessionMcpRuntime.callTool, listTools, listResourceTemplates, and getPrompt from openclaw/plugin-sdk/agent-harness-runtime, its prepareHarnessNativeMcpAppPreview.toolResult input, and the runtime accepted by openclaw/plugin-sdk/codex-mcp-projection. Keep the existing v1 types when implementing these adapters during the compatibility window. Ordinary plugins that register tools or MCP connection resolvers do not need a new dependency.

Host results cross this boundary losslessly. An incompatible result throws an error with code: "MCP_SDK_RESULT_UNREPRESENTABLE" and the MCP method name rather than dropping fields. This includes non-object structuredContent (arrays, scalars, or null), output schemas without type: "object", non-object properties or non-string-array required in an output schema, scalar or null values in a tool's input/output schema properties, and nested fields that the v1 result schemas would discard. Metadata, resource content, prompt messages, and pagination cursors are preserved when representable.

When the next stable release adopts v2 result types, import result and client types from @modelcontextprotocol/client, schema validators from @modelcontextprotocol/core, and the AJV adapter from @modelcontextprotocol/client/validators/ajv. Treat structuredContent as unknown and narrow it before reading properties; preserve JSON Schema values without assuming every property schema is an object. For example:

ts
import type { CallToolResult } from "@modelcontextprotocol/client"; function readSummary(result: CallToolResult): string | undefined {  const value = result.structuredContent;  if (value && typeof value === "object" && "summary" in value) {    return typeof value.summary === "string" ? value.summary : undefined;  }  return undefined;}

The compatibility window does not enable modern protocol serving or change MCP server configuration.

What changed

Several wide-open import surfaces used to let plugins reach almost anything from a single entry point:

  • openclaw/plugin-sdk and openclaw/plugin-sdk/compat - re-exported dozens of helpers while the focused SDK was being built. Both roots are now removed. Import a documented subpath instead.
  • openclaw/plugin-sdk/infra-runtime - a broad barrel mixing system events, heartbeat state, delivery queues, fetch/proxy helpers, file helpers, approval types, and unrelated utilities.
  • openclaw/plugin-sdk/config-runtime - a removed broad config barrel, including its deprecated direct loadConfig and writeConfigFile exports.
  • openclaw/plugin-sdk/channel-lifecycle, channel-message, and channel-reply-pipeline - removed channel compatibility facades. Use the focused outbound and inbound contracts, checking each named export.
  • openclaw/extension-api - a removed bridge that gave plugins direct access to host-side helpers like the embedded agent runner.
  • api.registerEmbeddedExtensionFactory(...) - a removed embedded-runner-only hook that observed embedded-runner events such as tool_result. Use agent tool-result middleware instead (see Migrate embedded tool-result extensions to middleware).

The root SDK, compat barrel, extension bridge, embedded extension factory, and five channel/config/infrastructure compatibility facades have been removed. The latter retirement received explicit SDK-owner approval on September 30, 2026; see the removal timeline.

OpenClaw's default compatibility policy preserves documented plugin behavior through a compatibility adapter, diagnostics, docs, and a deprecation window. That applies to SDK imports, manifest fields, setup APIs, hooks, and runtime registration behavior. The three session-controller changes below are explicit exceptions with immediate enforcement and no compatibility aliases or window.

ChatCommandDefinition.category retains the "docks" value accepted by the 2026.8.1 SDK. Command lists display these legacy definitions under Tools. The category does not enable channel docking or restore retired docking commands. New definitions should use "tools".

Bundled conversation-binding adapters can use resolveThreadBindingTargetKind from openclaw/plugin-sdk/thread-bindings-session-runtime to select their stored "subagent", "session", or "acp" kind. The helper preserves explicit subagents and recognizes ACP session keys without loading storage. Other session targets, including main/global aliases, use the canonical asynchronous ACP metadata reader; unreadable stores reject classification rather than defaulting to ordinary sessions. Pass the current config, optional agent owner, channel display name, and a required assertCurrent callback covering both manager and caller authority. Adapters must still recheck authority at their native-create and binding-commit boundaries.

Bundled ACP integrations should await readAcpSessionEntryAsync from the local openclaw/plugin-sdk/acp-runtime facade for session metadata reads. This facade remains a JavaScript compatibility export; its declarations are excluded from the typed public SDK. File-backed reads run on the SQLite workers and preserve the session lifecycle throughout the metadata join. File-backed results are detached snapshots, including when clone: false is supplied. The returned storeReadFailed flag still distinguishes an unreadable session store from missing metadata; startup cleanup must keep bindings when that flag is set. Unbound legacy ACP metadata can still accompany that flag; it does not certify the session row. Source retirement and worker failures reject the read and must not be treated as an absent session.

The synchronous readAcpSessionEntry and getAcpSessionManager().resolveSession() contracts shipped in v2026.9.4 remain available for durable sessions and are deprecated for runtime use. Incognito targets reject synchronous access with IncognitoSessionSyncAccessError; use readAcpSessionEntryAsync and getAcpSessionManager().resolveSessionAsync() instead. The asynchronous path acquires the existing memory session actor and joins global ACP metadata through the shared-state worker. It does not create a missing incognito session.

Discord and Telegram startup cleanup use prepareAcpSessionEntryRead for incognito targets. Its prepared reader retains the selected memory owner and carries current caller authority through conditional binding deletion. Closing the session ends that authority; cleanup does not reopen the retired native incognito database.

IncognitoSessionEndedError and IncognitoSessionSyncAccessError, exposed through the local openclaw/plugin-sdk/acp-runtime facade, must propagate through cleanup and status probes. Use rethrowIncognitoSessionError(error) before treating other failures as an absent session or an uncertain probe; it also recognizes nested refusals. The deprecated synchronous contracts for durable sessions remain available through 2026-12-31 and are removed in the first January 2027 stable release.

Session-keyed harness registration

The session-controller cutover changes standalone session-keyed setActiveEmbeddedRun(...) registration immediately, with no compatibility window. Register native runs from the host-provided, admitted harness attempt. Calling the function without that admission throws AgentHarnessPreflightError, with code SESSION_CONTROLLER_ADMISSION_REQUIRED and migration guidance.

A hosted call inherits the host's captured operation; passing a session key, agent ID, or a plugin-created operation does not obtain admission. Host-owned detached work can carry a session key for policy, but cannot own that session's turn, queue, or Stop. See Harness attempt runtime for the supported registration and cleanup path. Registration accepts only the five public identity arguments and returns a cleanup function bound to that exact run. The optional replyOperation refers to the same admitted controller and retains its documented tool-authority methods; it does not authorize standalone registration. Host admission state and native attachment records stay private to the host.

Gateway session control

GatewayRequestHandlerOptions.context no longer exposes the mutable chatAbortControllers or chatQueuedTurns maps. The session controller owns admission, queued input, and Stop; plugin handlers cannot register or cancel work by changing shared maps. Reading either removed property fails with code PLUGIN_CAPABILITY_REMOVED and the exact capability name.

Keep api.registerGatewayMethod(...) for plugin methods. To compose existing session operations from an authenticated handler, declare contracts: { gatewayMethodDispatch: ["authenticated-request"] } in the plugin manifest and import dispatchGatewayMethod from openclaw/plugin-sdk/gateway-method-runtime. Use chat.send to submit input, chat.abort with { sessionKey, runId? } to cancel a chat run, or sessions.abort with { key, runId?, clearQueued? } for session cancellation. These calls retain the current caller's authority and the target method's authorization; the declaration does not grant additional scopes. Use agent.wait to await a run, and sessions.list for activity observations. Bundled or trusted official plugins can also use api.runtime.gateway.request, session facts, and subscriptions; those runtime capabilities retain their existing trust restriction. See Gateway runtime for the supported read and control paths.

Agent turn start observation

AgentTurnStartOwner.observe() now returns startDeadlineAtMs instead of expiresAtMs. The old property has no alias; reading it fails with code PLUGIN_CAPABILITY_REMOVED. Update consumers of the onStartOwner callback exposed through context.createAgentTurnFacade to inspect the new field.

A finite startDeadlineAtMs is the controller's deadline for execution to start. null means a live controller-owned wait has no finite start deadline; an absent field means no controller deadline is available yet. An undefined observation means the captured owner is no longer current. Stop watching for start once executionStarted is true. This field does not extend execution authority or describe the run's execution timeout. Other API fields named expiresAtMs retain their existing contracts.

Session and command preparation

Use getSessionEntryAsync, readSessionUpdatedAtAsync, getConversationSessionAsync, readAmbientTranscriptWatermarkAsync, loadTranscriptEvents, and readTranscriptStatsAsync from openclaw/plugin-sdk/session-store-runtime for ordinary session reads. The runtime equivalent is api.runtime.agent.session.getSessionEntryAsync. These operations read through the existing session worker and observe committed in-process writes. Missing entries remain undefined; read failures propagate. loadTranscriptEventsSync and readTranscriptStatsSync are deprecated; their awaited replacements preserve ordered events and transcript statistics while keeping durable reads off the Gateway thread.

Await UserTurnTranscriptRecorder.completeProcessingAsync(outcome) to settle an accepted input through its session owner. The deprecated completeProcessing method preserves synchronous completion and returns after the terminal outcome commits, with a migration warning, through 2026-12-31. Recorders without an accepted input keep their existing no-op behavior.

Use readSessionTranscriptCatalogTitleAsync from openclaw/plugin-sdk/session-transcript-runtime for catalog titles. The synchronous readSessionTranscriptCatalogTitle remains deprecated through 2026-12-31.

Use resolveStoredModelOverrideAsync and resolveCommandArgMenuAsync from openclaw/plugin-sdk/command-auth-native when preparing native commands. resolveCommandArgMenuAsync is also exported by openclaw/plugin-sdk/native-command-registry. An inherited-model loader may return a promise; direct overrides still need no parent read. Menu preparation reads the current session's verbose setting before returning its choices.

The corresponding synchronous APIs are deprecated for ordinary runtime use. Released signatures remain supported through 2026-12-31. New legacy calls emit a bounded deprecation warning. Bundled callers use the awaited operations; final tool, disclosure, and mutation authority checks retain their current owner and timing.

For a complete cross-session store view, await loadCombinedSessionStoreForGatewayAsync from openclaw/plugin-sdk/session-transcript-hit. It preserves full session entries and excludes incognito sessions while reading durable stores through the existing database worker. The synchronous loadCombinedSessionStoreForGateway retains its return value and emits a bounded deprecation warning; it remains supported through 2026-12-31.

Native session lineage

Use resolvePreparedNativeSessionBinding or retainNativeSessionGenerationAuthority from openclaw/plugin-sdk/agent-harness-session-runtime to prepare lineage in the session worker. Both return an owned capture: call release() in finally after the last effect and any terminal cleanup. Repeated authority checks use the session owner's published facts and immediately reject revoked lineage.

Use authority.withLiveness(assertCurrent) when cleanup needs its own lifecycle check. This borrows the same source; it does not extend the capture's lifetime. Use createNativeSessionBindingScope when there is no stored session lineage. The released createNativeSessionBindingAuthority, combineNativeSessionBindingAuthority, prepareNativeSessionGenerationAuthority, and resolveNativeSessionBindingWithAuthority contracts remain available but deprecated. Their synchronous readers are compatibility paths; bundled harnesses use the prepared API.

Exec approval reads

Await loadExecApprovalsReadOnlyAsync or readExecApprovalsSnapshotAsync from openclaw/plugin-sdk/exec-approvals-runtime. Both use the existing shared-state read worker. The synchronous loadExecApprovals and readExecApprovalsSnapshot exports retain their signatures through 2026-12-31, emit a bounded deprecation warning, and are scheduled for removal in the first January 2027 stable release. Bundled runtime callers use the awaited reads. The execution owner still checks current approval policy immediately before command execution using committed in-process policy publications; that final check does not query SQLite.

The SDK's readExecApprovalsSnapshotAsync returns a private mutable snapshot for edit-then-save callers. The deprecated synchronous readers likewise retain private mutable results through their compatibility window. loadExecApprovalsReadOnlyAsync shares frozen policy on cached reads: treat its file and nested grants as read-only, and use structuredClone(file) before preparing an edit. Internal execution reads still reuse frozen owner snapshots. Retaining any snapshot does not retain execution authority: pending updates and revocations still fail closed at the final execution check.

Durable-send destination history

Remove mirror from calls to sendDurableMessageBatch and withDurableMessageSendContext in openclaw/plugin-sdk/channel-outbound. Confirmed sends now record their delivered content automatically in the resolved destination conversation. The caller's own conversation is not recorded twice.

session.key now names the conversation whose turn produced the send. A send to that same conversation is skipped because the producing turn already recorded it. For proactive or scheduled sends that have no producing turn, pass the destination as session.policyKey (for example with buildOutboundSessionContext({ policySessionKey })) instead of session.key; otherwise the send is treated as the producer's own reply and adds no history row.

The mirror parameter and DeliveryMirror type remain source-compatible with the 2026.10.1 SDK until the next Plugin SDK major, when removal requires explicit breaking-release approval. Legacy calls emit one deprecation warning per plugin and process. The old field does not select a different history destination or override delivered text or media; a mismatched mirror.sessionKey creates no extra conversation row.

Session reset freshness

Channel runtime consumers should await runtime.channel.session.resolveEntryResetFreshnessAsync(...) when deciding whether a session needs reset. File-backed entries and transcript lifecycle timestamps are read from the same worker snapshot, with the original database owner retained through validation and cleanup. The synchronous resolveEntryResetFreshness(...) method shipped in v2026.9.8 remains available for existing JavaScript consumers and is deprecated for runtime use; removal ships in the first January 2027 stable release after the 2026-12-31 compatibility cutoff. Both methods retain the existing reset policy and process-held incognito behavior.

Why

  • Slow startup - importing one helper loaded dozens of unrelated modules.
  • Circular dependencies - broad re-exports made import cycles easy to create.
  • Unclear API surface - no way to tell stable exports from internal ones.

The typed public SDK is organized into focused subpaths with documented contracts. Not every SDK build entrypoint is a public plugin API.

Legacy provider convenience interfaces for bundled channels are gone too - channel-branded helper shortcuts were private mono-repo conveniences, not stable plugin contracts. Use narrow generic SDK subpaths instead. Inside the bundled plugin workspace, keep provider-owned helpers in that plugin's own api.ts or runtime-api.ts:

  • Anthropic keeps Claude-specific stream helpers in its own api.ts / contract-api.ts interface.
  • OpenAI keeps provider builders, default-model helpers, and realtime provider builders in its own api.ts.
  • OpenRouter keeps provider builder and onboarding/config helpers in its own api.ts.

Where each topic lives

Every section of the single-page version lives on one of the six pages below. The anchors from the single-page version still resolve here.

Migration steps

How to migrate a plugin — the ordered migration steps.

Import paths

Import path reference — which typed-public subpath replaces each legacy import.

Removed surfaces and replacements

Removed surfaces and replacements — what was removed, and the replacement for each legacy API.

Talk and voice

Talk and realtime voice migration — the unified Talk session API and its method map.

Compatibility records

Compatibility policy and records — what is retained, why, and on what condition it can be removed.

Timeline

Removal timeline — when deprecated surfaces become eligible for removal.

For the removed Tasks and TaskFlow surfaces, see Tasks and TaskFlow API removal.

The agent-runtime snapshot publisher now provides replaceRuntimeAuthProfileStoreSnapshotsAsync. Await it when publishing a default shared-owner snapshot; it resolves cold ownership through the database worker. The synchronous replaceRuntimeAuthProfileStoreSnapshots remains deprecated until a later stable release and retains native cold-owner admission for compatibility. Entries with explicit database paths or agent directories remain in-memory publications.

Was this useful?