From d6f70a96cb1cd0515290bf51a0dc4615c737c27f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 19:30:47 -0700 Subject: [PATCH] fix(plugins): native commands execute the selected plugin (#121544) * fix(plugins): preserve selected command identity * test(telegram): use scoped command registries * test(telegram): isolate command runtime fixtures * test(telegram): warm native command runtime * refactor(plugins): keep command metadata private * fix(plugins): accept synchronous command handlers * fix(plugins): scope command drain bypass to live execution * test(telegram): use scoped command registry fixtures * test(telegram): isolate native menu runtime fixtures * test(telegram): isolate login session store * test(telegram): surface login flow failures * test(telegram): preload native login module * test(telegram): scope native command registries * fix(plugins): complete command dispatch contracts * fix(plugins): break command dispatch import cycles * fix(plugins): stabilize command dispatch contracts * fix(channels): keep plugin dispatch options internal * fix(plugins): keep command dispatch carrier opaque * test(channels): align delivery adapter fixtures * test(delivery): align custody ownership coverage * test(delivery): align latest queue reconciliation * test(channels): drop obsolete delivery wrappers * fix(plugins): rebind channel reload starts * fix(plugins): scope command catalog reloads * fix(ci): align current runtime contracts * chore(plugin-sdk): refresh API baseline --- docs/.generated/plugin-sdk-api-baseline.jsonl | 201 ++++---- docs/plugins/sdk-channel-plugins.md | 27 ++ docs/plugins/sdk-subpaths.md | 1 + .../message-handler.process-progress.ts | 4 +- .../src/monitor/native-command-agent-reply.ts | 6 + .../src/monitor/native-command-arg-ui.ts | 1 + .../src/monitor/native-command-dispatch.ts | 2 + .../native-command-model-picker-apply.ts | 1 + .../monitor/native-command.options.test.ts | 217 +++++---- .../native-command.plugin-dispatch.test.ts | 242 +++++----- .../src/monitor/native-command.runtime.ts | 3 - .../native-command.status-direct.test.ts | 32 +- .../discord/src/monitor/native-command.ts | 40 +- .../discord/src/monitor/provider-runtime.ts | 2 - .../src/monitor/provider.commands.test.ts | 49 +- .../discord/src/monitor/provider.commands.ts | 32 +- .../src/monitor/provider.interactions.ts | 4 +- .../src/monitor/provider.test-support.ts | 3 - .../discord/src/monitor/provider.test.ts | 13 +- extensions/discord/src/monitor/provider.ts | 1 - .../src/test-support/provider.test-support.ts | 10 - .../monitor/slash-plugin-commands.runtime.ts | 2 - extensions/slack/src/monitor/slash.test.ts | 172 ++++++- extensions/slack/src/monitor/slash.ts | 56 ++- ...t-message-dispatch.delivery-basics.test.ts | 2 +- .../src/bot-native-command-deps.runtime.ts | 5 - .../telegram/src/bot-native-command-menu.ts | 26 +- .../src/bot-native-commands.login.test.ts | 85 ++-- .../src/bot-native-commands.registry.test.ts | 220 ++++++--- .../src/bot-native-commands.runtime.ts | 5 - .../bot-native-commands.session-meta.test.ts | 204 ++++---- ...t-native-commands.skills-allowlist.test.ts | 42 +- .../src/bot-native-commands.test-helpers.ts | 28 +- .../telegram/src/bot-native-commands.test.ts | 105 ++-- .../telegram/src/bot-native-commands.ts | 46 +- .../src/test-support/plugin-command.ts | 23 - package.json | 4 + scripts/lib/plugin-sdk-doc-metadata.ts | 3 + scripts/lib/plugin-sdk-entrypoints.json | 1 + scripts/plugin-sdk-surface-report.mts | 10 +- src/auto-reply/reply/commands-plugin.test.ts | 201 ++++---- src/auto-reply/reply/commands-plugin.ts | 32 +- .../reply/dispatch-from-config.gather.ts | 8 +- .../dispatch-from-config.plugin-binding.ts | 28 +- .../dispatch-from-config.prepare-operation.ts | 10 +- ...ispatch-from-config.reply-dispatch.test.ts | 352 -------------- .../dispatch-from-config.test-harness.ts | 55 ++- src/auto-reply/reply/get-reply.types.ts | 2 + .../reply/provider-dispatcher.types.ts | 3 +- src/channels/turn/types.ts | 6 +- src/gateway/channel-health-monitor.test.ts | 1 + src/gateway/server-channels.test.ts | 69 +++ src/gateway/server-channels.ts | 79 +++- src/gateway/server-core-runtime.ts | 10 +- .../session-catalog-entry-snapshot.test.ts | 1 + .../server-methods/session-catalog.test.ts | 1 + src/gateway/server-reload-channel-restart.ts | 32 +- src/gateway/server-reload-contracts.ts | 5 +- src/gateway/server-reload-handlers.test.ts | 156 +++++- src/gateway/server-reload-hot.ts | 109 ++++- src/gateway/server/readiness.test.ts | 1 + .../deliver.queue-integration.test.ts | 2 +- src/plugin-sdk/channel-test-helpers.ts | 1 + src/plugin-sdk/plugin-command-runtime.ts | 13 + .../test-helpers/outbound-delivery.ts | 1 + src/plugins/command-execution-lock.ts | 90 ++++ src/plugins/command-registration.ts | 26 +- src/plugins/command-registry-state.ts | 26 +- src/plugins/command-specs.ts | 37 +- src/plugins/commands.test.ts | 44 ++ src/plugins/commands.ts | 415 ++-------------- .../plugin-command-account-start-scope.ts | 34 ++ .../plugin-command-dispatch-contract.ts | 8 + src/plugins/plugin-command-execution.ts | 299 ++++++++++++ src/plugins/plugin-command-matcher.ts | 75 +++ src/plugins/plugin-command-metadata.ts | 61 +++ src/plugins/plugin-command-registry.ts | 24 + src/plugins/plugin-command-runtime.test.ts | 447 ++++++++++++++++++ src/plugins/plugin-command-runtime.ts | 264 +++++++++++ src/plugins/registry-empty.ts | 1 - src/plugins/registry-types.ts | 1 - src/plugins/runtime-state.ts | 2 + src/plugins/runtime.ts | 73 ++- 83 files changed, 3214 insertions(+), 1821 deletions(-) delete mode 100644 extensions/slack/src/monitor/slash-plugin-commands.runtime.ts delete mode 100644 extensions/telegram/src/test-support/plugin-command.ts create mode 100644 src/plugin-sdk/plugin-command-runtime.ts create mode 100644 src/plugins/command-execution-lock.ts create mode 100644 src/plugins/plugin-command-account-start-scope.ts create mode 100644 src/plugins/plugin-command-dispatch-contract.ts create mode 100644 src/plugins/plugin-command-execution.ts create mode 100644 src/plugins/plugin-command-matcher.ts create mode 100644 src/plugins/plugin-command-metadata.ts create mode 100644 src/plugins/plugin-command-registry.ts create mode 100644 src/plugins/plugin-command-runtime.test.ts create mode 100644 src/plugins/plugin-command-runtime.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index e430ebec0229..365a76d163db 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -53,15 +53,15 @@ {"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} -{"closureHash":"5669e9b316c800e0e301aa5094517ba45d364aec117468556e669784d50681f9","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} +{"closureHash":"8d32ce35b76f9cd17041cdfbd140f3a0f7eb8c07cf85ec966c76795f2827da36","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} {"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"} -{"closureHash":"3a9021d4c54eb31b36b05dbcf2cb32cbeb75f0d8c7a93ee1c4642a8f2d279cf5","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} +{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} {"closureHash":"4ad0f830bf11a2db48faefb1439b7fbb08d9f22d22083f5e301327e1696f2310","declaration":"export type AgentToolResultMiddleware = AgentToolResultMiddleware;","entrypoint":"agent-harness","exportName":"AgentToolResultMiddleware","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} {"closureHash":"008b6c78d5e44a9d1df1d7e32e6015382ee9ba1cb9b10bd4c2b467619d1c89d4","declaration":"export type AgentToolResultMiddlewareEvent = AgentToolResultMiddlewareEvent;","entrypoint":"agent-harness","exportName":"AgentToolResultMiddlewareEvent","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} {"closureHash":"7414a2a79c2d78c1d52538250c827305bba1a2bb25010448837c8524a9f95b3c","declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"agent-harness","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} -{"closureHash":"2aa8488e9a6766ee64ad3e287540924e462a45a5fd2739aaea9fdf38d4b0f652","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} +{"closureHash":"1b8fadbce01f681a395e1c1a8d3ab9416a4bd4c212a60241f4346b1cf4e0b7e1","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} {"closureHash":"25c6985c3bef2c6efc6a96e70af6d66fe073be0b0d311681cfc0dfc186528d9a","declaration":"export type OpenClawAgentToolResult = OpenClawAgentToolResult;","entrypoint":"agent-harness","exportName":"OpenClawAgentToolResult","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"type","recordType":"export"} {"category":"runtime","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","recordType":"module"} {"closureHash":"fab99dfcb01d475a1bfdbe7a8fac9be10a93dac5f12ac78c2dea673c8ab86abf","declaration":"export function abortAgentHarnessRun(sessionId: string): boolean;\nexport function abortAgentHarnessRun(sessionId: undefined, opts: { mode: \"all\" | \"compacting\"; reason?: \"restart\"; }): boolean;","entrypoint":"agent-harness-runtime","exportName":"abortAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} @@ -80,8 +80,8 @@ {"closureHash":"f8ad3d762c9753fdc36b2b9af0980167fdc54090c7bcca2f3dbe7c8951e86821","declaration":"export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): AgentRuntimePlan;","entrypoint":"agent-harness-runtime","exportName":"buildAgentRuntimePlan","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"528d11abf061b0b303722737de5a6406e7e55bffa38c45144c77d896ae1b3b0a","declaration":"export function buildBootstrapContextForFiles(bootstrapFiles: WorkspaceBootstrapFile[], params: { config?: OpenClawConfig; agentId?: string | null; warn?: (message: string) => void; }): EmbeddedContextFile[];","entrypoint":"agent-harness-runtime","exportName":"buildBootstrapContextForFiles","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"44d5a4667d2f3dc3a28e5c9d7b46765b1fb3c62820288e40251f5174911c8a6a","declaration":"export function buildEmbeddedAttemptToolRunContext(params: { trigger?: EmbeddedRunTrigger; jobId?: string; memoryFlushWritePath?: string; toolsAllow?: string[]; conversationToolPolicy?: GroupToolPolicyConfig; trace?: DiagnosticTraceContext; }): { trigger?: EmbeddedRunTrigger; jobId?: string; memoryFlushWritePath?: string; runtimeToolAllowlist?: string[]; conversationToolPolicy?: GroupToolPolicyConfig; trace?: DiagnosticTraceContext; };","entrypoint":"agent-harness-runtime","exportName":"buildEmbeddedAttemptToolRunContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} -{"closureHash":"013f89f642233dc35de99545dd5dd1b2e4ee50eafafb5e32389800a0aff786d2","declaration":"export function buildHarnessContextEngineRuntimeContext(params: Parameters[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} -{"closureHash":"4cd980d71b9c0fd924ab4b1e1786f2275a9758055e1fb6b9ddcbf7d5ceb0afe6","declaration":"export function buildHarnessContextEngineRuntimeContextFromUsage(params: Parameters[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContextFromUsage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} +{"closureHash":"8b961c250987af36d795b20b160b532d69a77e95db8b4e8e0c4f5584f9dd4e86","declaration":"export function buildHarnessContextEngineRuntimeContext(params: Parameters[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} +{"closureHash":"bad017021139532a5fa0fa8d9b4b15c1475bfb3ed23436a55aa40871bcf12579","declaration":"export function buildHarnessContextEngineRuntimeContextFromUsage(params: Parameters[0]): ContextEngineRuntimeContext;","entrypoint":"agent-harness-runtime","exportName":"buildHarnessContextEngineRuntimeContextFromUsage","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"33a7cd8d63d4ef045ee3a42f806026a8b621e7dd32d8d6fbf4e7b44452d5e0ce","declaration":"export function buildNativeHookRelayCommand(params: { provider: NativeHookRelayProvider; relayId: string; generation?: string; event: NativeHookRelayEvent; preToolUseUnavailable?: \"noop\"; timeoutMs?: number; executable?: string; nice?: number | false; nodeExecutable?: string; }): string;","entrypoint":"agent-harness-runtime","exportName":"buildNativeHookRelayCommand","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"3846647c3c2fb0242f70591cf95c2e02f2a5a3c44447efb4cf7a31b2dc2122fd","declaration":"export function buildSkillWorkshopPromptSection(): string[];","entrypoint":"agent-harness-runtime","exportName":"buildSkillWorkshopPromptSection","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"32e977ece65144a83ae1b0a3c8d1b9b00a2a2506ad9ef1db3b2b9770ce762dbe","declaration":"export function buildWatchedSessionsHarnessContext(params: { config?: OpenClawConfig; sessionKey?: string; sandboxed?: boolean; toolNames: Iterable; capabilityToolNames?: Iterable; }): string | undefined;","entrypoint":"agent-harness-runtime","exportName":"buildWatchedSessionsHarnessContext","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} @@ -98,7 +98,7 @@ {"closureHash":"51f76e9e37b65b358f0313e10152d950b003bc3e50f4bb26fe29a988237772e2","declaration":"export function consumePreExecutionBlockedToolCall(toolCallId: string, runId?: string): boolean;","entrypoint":"agent-harness-runtime","exportName":"consumePreExecutionBlockedToolCall","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise; };","entrypoint":"agent-harness-runtime","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise>; };","entrypoint":"agent-harness-runtime","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} -{"closureHash":"2201c18c1db4d9b4be3be2d3fc24141bb4381fc34e54a31166c270e8f4443436","declaration":"export function deliverAgentHarnessUserInputPrompt(params: PromptDeliveryParams, questions: readonly AgentHarnessUserInputQuestion[], options?: AgentHarnessUserInputPromptOptions): Promise;","entrypoint":"agent-harness-runtime","exportName":"deliverAgentHarnessUserInputPrompt","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} +{"closureHash":"833c4cb48c22eda95bb09d894910cdfbc93782287e548a42e170892bd4a2fa54","declaration":"export function deliverAgentHarnessUserInputPrompt(params: PromptDeliveryParams, questions: readonly AgentHarnessUserInputQuestion[], options?: AgentHarnessUserInputPromptOptions): Promise;","entrypoint":"agent-harness-runtime","exportName":"deliverAgentHarnessUserInputPrompt","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"6f436485257f69fa07a18203b91d4341690522f12aff4f25805c7a1e21326ee3","declaration":"export function detectAndLoadAgentHarnessPromptImages(params: { prompt: string; workspaceDir: string; model: { input?: string[]; }; existingImages?: ImageContent[]; imageOrder?: PromptImageOrderEntry[]; media?: MediaFact[]; config?: OpenClawConfig; workspaceOnly?: boolean; localRoots?: readonly string[]; sandbox?: { root: string; bridge: SandboxFsBridge; }; }): Promise<{ images: ImageContent[]; detectedRefs: Array<{ raw: string; resolved: string; type: \"path\" | \"media-uri\"; }>; loadedCount: number; skippedCount: number; }>;","entrypoint":"agent-harness-runtime","exportName":"detectAndLoadAgentHarnessPromptImages","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise;","entrypoint":"agent-harness-runtime","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"c38cd390190bb07ddcd247420d6e0b489a0229bd402ddca0440417217a57251a","declaration":"export function emitAgentEvent(event: Omit): void;","entrypoint":"agent-harness-runtime","exportName":"emitAgentEvent","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} @@ -195,7 +195,7 @@ {"closureHash":"e4784aeda4c24b60cd49b7f5983e70a380a4759d5176c74d30f2900952ba848f","declaration":"export function runAgentHarnessBeforeAgentFinalizeHook(params: { event: PluginHookBeforeAgentFinalizeEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): Promise;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeAgentFinalizeHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"8fec73ee8c7c9be3335bd6dc1bac9278f0be5beef22a5975eb06e56f504f4bd6","declaration":"export function runAgentHarnessBeforeCompactionHook(params: { sessionFile: string; messages?: AgentMessage[]; ctx: AgentHarnessHookContext; }): Promise;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeCompactionHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"021b81820bef2e8d54c50a5fc159d6665b23b50a1f5c40dae76a4833839d89c9","declaration":"export function runAgentHarnessBeforeMessageWriteHook(params: { message: AgentMessage; agentId?: string; sessionKey?: string; }): AgentMessage | null;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessBeforeMessageWriteHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} -{"closureHash":"e60cc6d57d00042ebc24e306abbb3862f7946eceb5925c41f93b421d7555de13","declaration":"export function runAgentHarnessGatewayQuestion(params: RunAgentHarnessGatewayQuestionParams): Promise;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessGatewayQuestion","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} +{"closureHash":"77c7c0c1adee10a2ead7993b809294430fbcfae4c63366cdf54b267e925aeec7","declaration":"export function runAgentHarnessGatewayQuestion(params: RunAgentHarnessGatewayQuestionParams): Promise;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessGatewayQuestion","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"50a4d7cb20dd8f884abbcb62d77b5b2df183fbfcd058cc7aa76edacd1f7b90d8","declaration":"export function runAgentHarnessLlmInputHook(params: { event: PluginHookLlmInputEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): void;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessLlmInputHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"1bd55c141c249b1c04470cafe0cd4d41dce334ea2883ce83235af56d24a15cca","declaration":"export function runAgentHarnessLlmOutputHook(params: { event: PluginHookLlmOutputEvent; ctx: AgentHarnessHookContext; hookRunner?: AgentHarnessHookRunner; }): void;","entrypoint":"agent-harness-runtime","exportName":"runAgentHarnessLlmOutputHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} {"closureHash":"c4c5131ab7065decd8d8e935ac3779e3baa2c52fbbbd95a6ccb624377db09ca1","declaration":"export function runBeforeToolCallHook(args: { toolName: string; params: unknown; toolKind?: PluginHookToolKind; toolInputKind?: PluginHookToolInputKind; toolCallId?: string; ctx?: HookContext; signal?: AbortSignal; approvalMode?: \"request\" | \"report\" | \"deny\" | \"defer\"; }): Promise;","entrypoint":"agent-harness-runtime","exportName":"runBeforeToolCallHook","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"function","recordType":"export"} @@ -221,8 +221,8 @@ {"closureHash":"f237bbc11e1d389679cd4e2ad7aa9055951b3029348e3374b81f769786a51d55","declaration":"export type AbortAndDrainAgentHarnessRunResult = AbortAndDrainEmbeddedAgentRunResult;","entrypoint":"agent-harness-runtime","exportName":"AbortAndDrainAgentHarnessRunResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"6f802f46717d28d260bbcb2cf4790dfed4e9ab28c4fe822d259228189cca4665","declaration":"export type AgentApprovalEventData = AgentApprovalEventData;","entrypoint":"agent-harness-runtime","exportName":"AgentApprovalEventData","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"12aac3a57ce200b0f082002be237625729f84ed7c337f1bac90bcd0688a53319","declaration":"export type AgentEventPayload = AgentEventPayload;","entrypoint":"agent-harness-runtime","exportName":"AgentEventPayload","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} -{"closureHash":"3a9021d4c54eb31b36b05dbcf2cb32cbeb75f0d8c7a93ee1c4642a8f2d279cf5","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} -{"closureHash":"3a9021d4c54eb31b36b05dbcf2cb32cbeb75f0d8c7a93ee1c4642a8f2d279cf5","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} +{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"agent-harness-runtime","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} +{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarnessAttemptParams = AgentHarnessAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"2dc2a62deea63b316142cc28d40afb88fb0d1e5ce3e048f141420726cda38248","declaration":"export type AgentHarnessAttemptResult = AgentHarnessAttemptResult;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAttemptResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"c7e642234081160830fd9e18a301900f8ee1ea8c88382e5a286f0102de65b714","declaration":"export type AgentHarnessAuthBindingFingerprintParams = AgentHarnessAuthBindingFingerprintParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessAuthBindingFingerprintParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"8f496ce75f294f2aa440a49c3c5b065ddff3a4714df775116af5e53f7fd14fe5","declaration":"export type AgentHarnessCompactParams = CompactEmbeddedAgentSessionParams;","entrypoint":"agent-harness-runtime","exportName":"AgentHarnessCompactParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} @@ -271,7 +271,7 @@ {"closureHash":"f30b66fd82f4cd61c60880c42a3b8f152ca64a2216036c99ab4129648ed329e7","declaration":"export type EmbeddedAgentCompactResult = EmbeddedAgentCompactResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedAgentCompactResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"1bbefad0193436f7340f9d0dec50be5d482973325c3c65170b1053dd05566f0a","declaration":"export type EmbeddedContextFile = EmbeddedContextFile;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedContextFile","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"f30b66fd82f4cd61c60880c42a3b8f152ca64a2216036c99ab4129648ed329e7","declaration":"export type EmbeddedPiCompactResult = EmbeddedAgentCompactResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedPiCompactResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} -{"closureHash":"2aa8488e9a6766ee64ad3e287540924e462a45a5fd2739aaea9fdf38d4b0f652","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} +{"closureHash":"1b8fadbce01f681a395e1c1a8d3ab9416a4bd4c212a60241f4346b1cf4e0b7e1","declaration":"export type EmbeddedRunAttemptParams = EmbeddedRunAttemptParams;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptParams","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"a6cd2eebe0d06e6578ec8438a207c5acda10ddf01041102d84e4c576a0b6a42f","declaration":"export type EmbeddedRunAttemptResult = EmbeddedRunAttemptResult;","entrypoint":"agent-harness-runtime","exportName":"EmbeddedRunAttemptResult","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"f6cb2d6411db22cb04220777720d97bcca977351de1cb344ce2ff6ab126a3a08","declaration":"export type ExecApprovalDecision = ExecApprovalDecision;","entrypoint":"agent-harness-runtime","exportName":"ExecApprovalDecision","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} {"closureHash":"7acb0dffc7bcda6c727e4afc49c955fa8e14f4f8b7b45d197ed794dfa0d73b8e","declaration":"export type ExecAutoReviewDecision = ExecAutoReviewDecision;","entrypoint":"agent-harness-runtime","exportName":"ExecAutoReviewDecision","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime","kind":"type","recordType":"export"} @@ -756,7 +756,7 @@ {"closureHash":"9fc7e45343d7bfbc6ed56d05c9f824e2f9cf6dafc3ca4b25437d1f65c2200727","declaration":"export function clearAccountEntryFields(params: { accounts?: Record; accountId: string; fields: string[]; isValueSet?: (value: unknown) => boolean; markClearedOnFieldPresence?: boolean; }): { nextAccounts?: Record; changed: boolean; cleared: boolean; };","entrypoint":"channel-core","exportName":"clearAccountEntryFields","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} {"closureHash":"f4e084b64b5376268885a73fe507aa5d72bee17a88ec2d7ae1d19b36ea5f1ccb","declaration":"export function createChannelConfigUiHints(params: { channelLabel: string; dmPolicy?: { channelKey: string; includeLegacyNestedPolicy?: boolean; legacyNestedPolicyOrder?: \"before\" | \"after\"; }; configWrites?: boolean; mentionPatterns?: { targetDescription: string; policyTargetDescription?: string; policyNote?: string; denyNote?: string; }; nativeCommands?: boolean; implicitMentions?: boolean; progress?: { includeCommentary?: boolean; commentaryOrder?: \"before-command\" | \"after-command\"; labels?: \"openclaw\"; titleWording?: boolean; }; streaming?: Partial>; retry?: boolean; }): HintMap;","entrypoint":"channel-core","exportName":"createChannelConfigUiHints","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} {"closureHash":"b8e27bea2add3eaafac7074cbd44fcf2acea30a1ac125c3551925e8cd0b3378d","declaration":"export function createChatChannelPlugin(params: { base: ChatChannelPluginBase; security?: ChannelSecurityAdapter | ChatChannelSecurityOptions; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin;","entrypoint":"channel-core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} -{"closureHash":"0f2201e81ac7e4578b83d7973dd5e5e6d5c82e8d115f3786a8e10b5eab604b76","declaration":"export function defineChannelPluginEntry({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions): DefinedChannelPluginEntry;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} +{"closureHash":"d0215218f4367a9b95487036431f4fc5453ebb39960ec6deccde5acb1cab032c","declaration":"export function defineChannelPluginEntry({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions): DefinedChannelPluginEntry;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} {"closureHash":"0e0a9f6b22c433bdb1165e7707b1f9a1ddac1a544a8add00b503048cf157921f","declaration":"export function defineSetupPluginEntry(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"channel-core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} {"closureHash":"8895f7af11b994dcd383ebc45d8cfb3c3b1e6d88a64a001b04f604a83f77e517","declaration":"export function parseOptionalDelimitedEntries(value?: string): string[] | undefined;","entrypoint":"channel-core","exportName":"parseOptionalDelimitedEntries","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} {"closureHash":"3f97534b10d08eda6a556044eaba623b3744b200a2033877e48d2f2ca924d298","declaration":"export function recoverCurrentThreadSessionId(params: { route: ChannelOutboundSessionRoute; currentSessionKey?: string | null; canRecover?: (context: ThreadAwareOutboundSessionRouteRecoveryContext) => boolean; }): string | undefined;","entrypoint":"channel-core","exportName":"recoverCurrentThreadSessionId","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"} @@ -768,24 +768,24 @@ {"closureHash":"8126b651a679d5435eaa5d346916792c3600e5f0f75d8492b077dd361854f7dd","declaration":"export type ChannelOutboundSessionRouteParams = { cfg: OpenClawConfig; agentId: string; accountId?: string | null; target: string; currentSessionKey?: string; resolvedTarget?: { to: string; kind: ChannelDirectoryEntryKind | \"channel\"; display?: string; source: \"normalized\" | \"directory\"; }; replyToId?: string | null; threadId?: string | number | null;};","entrypoint":"channel-core","exportName":"ChannelOutboundSessionRouteParams","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} {"closureHash":"18fcab0d5899575a31205732d2be92597e533ad944c7fe11cac2af6fc718ea34","declaration":"export type ChannelPlugin = ChannelPlugin;","entrypoint":"channel-core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} {"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"channel-core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} {"closureHash":"4bf9c20e2a621ecc84998961937887666d84884ef6f8f1993a34c87c11000b60","declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"channel-core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"type","recordType":"export"} {"category":"channel","entrypoint":"channel-dm-policy","importSpecifier":"openclaw/plugin-sdk/channel-dm-policy","recordType":"module"} {"closureHash":"821fe6ccd0d9323322bf70c4d1ca5697071a0e32032f44c1e14157162c2c729d","declaration":"export function createChannelDmPolicy(params: CreateChannelDmPolicyParams): ChannelSetupDmPolicy & { promptAllowFrom: NonNullable; };","entrypoint":"channel-dm-policy","exportName":"createChannelDmPolicy","importSpecifier":"openclaw/plugin-sdk/channel-dm-policy","kind":"function","recordType":"export"} {"category":null,"entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","recordType":"module"} -{"closureHash":"e98973621a6d98525f970af1d02a41a6078d361497a2efb6a6a05c64fa677676","declaration":"export function defineBundledChannelEntry({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions): BundledChannelEntryContract;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"} -{"closureHash":"e33981a9f791ee1e487060346c997b305dd6bc26eb5c1524de6baa0b198595b4","declaration":"export function defineBundledChannelSetupEntry({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"} +{"closureHash":"d53f1d525e8942681d9d38c92211fabd88c571b284998e667e1920cfbc4991f5","declaration":"export function defineBundledChannelEntry({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions): BundledChannelEntryContract;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"} +{"closureHash":"2224da993e2c72c18cf17c5f2fa43ca5e95a7ab823c453dd51d07bcd07ff92c4","declaration":"export function defineBundledChannelSetupEntry({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"} {"closureHash":"cd150287d7e286e7a0d7d6caeb05d0a1efa63b5f351f75f69ba932510a108afd","declaration":"export function loadBundledEntryExportSync(importMetaUrl: string, reference: BundledEntryModuleRef, options?: BundledEntryModuleLoadOptions): T;","entrypoint":"channel-entry-contract","exportName":"loadBundledEntryExportSync","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"} {"closureHash":"7781a3d570d08929af5b3b6cffbb9ef4b9c8e8f1bed55234e0015a681868c812","declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"channel-entry-contract","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} -{"closureHash":"f0d016173b59953381cc58d9b46465394a5d105112cede439ac0926de3f13dd2","declaration":"export type BundledChannelEntryContract = BundledChannelEntryContract;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} +{"closureHash":"e52edea0d5c41728b7e7703483f93656ae6d55bd09cff9ca481ffad8d9058666","declaration":"export type BundledChannelEntryContract = BundledChannelEntryContract;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"f4b82dfd995bd6f08b232984743389d1c854866cb845a8dbb3605acb28818688","declaration":"export type BundledChannelEntryFeatures = BundledChannelEntryFeatures;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryFeatures","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"960693591b9bdc974bf3f0097076d90490078db3dceb35349a4f851736cde4fc","declaration":"export type BundledChannelLegacySessionSurface = BundledChannelLegacySessionSurface;","entrypoint":"channel-entry-contract","exportName":"BundledChannelLegacySessionSurface","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"edf79e6a736e5c4977e3b86ef21e7fdf0c0b2bc7deacd8edbee2ca76cb8cef43","declaration":"export type BundledChannelLegacyStateMigrationDetector = BundledChannelLegacyStateMigrationDetector;","entrypoint":"channel-entry-contract","exportName":"BundledChannelLegacyStateMigrationDetector","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} -{"closureHash":"f05db4cfc25aa231c95fd4de2ab54174cba702744deba4c8a2216a6459406a3f","declaration":"export type BundledChannelSetupEntryContract = BundledChannelSetupEntryContract;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} +{"closureHash":"4ae358d564c9980c4bee18ce79fb5807dfd0de0b75ec18586b43b469227c7377","declaration":"export type BundledChannelSetupEntryContract = BundledChannelSetupEntryContract;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"24679ecdf2ff34157b6345a37b58ec1363ec8cb1969b522599d0d92bb7a113b7","declaration":"export type BundledChannelSetupEntryFeatures = BundledChannelSetupEntryFeatures;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryFeatures","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"367219c821915e5be4d1ba3a86754475a5f8a4194512f0401de3be11cc649c60","declaration":"export type BundledEntryModuleLoadOptions = BundledEntryModuleLoadOptions;","entrypoint":"channel-entry-contract","exportName":"BundledEntryModuleLoadOptions","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} -{"closureHash":"e59a1f2a15dfc847ccb175924ff3ca01af4322b60990835aca300b7340b402e9","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} +{"closureHash":"68af6ae4d6f23d25812c404922603aeed7504041683a14ce966ed6adad5046d4","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"f22bde542d2dc632b20569c7679ee30a4e88851e0e9dec89350e120f6f456b15","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"channel-entry-contract","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"closureHash":"a0103d278095a8687e571bcc0917c5c87fc1a1c79f151a07c7225c4d8f54a39d","declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"channel-entry-contract","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"} {"category":null,"entrypoint":"channel-feedback","importSpecifier":"openclaw/plugin-sdk/channel-feedback","recordType":"module"} @@ -828,10 +828,10 @@ {"closureHash":"dd949b517f4b679f06ad1b7bf97b3ab71c3ef22ffdaaee6616c786dcc925785c","declaration":"export function createDirectDmPreCryptoGuardPolicy(overrides?: DirectDmPreCryptoGuardPolicyOverrides): DirectDmPreCryptoGuardPolicy;","entrypoint":"channel-inbound","exportName":"createDirectDmPreCryptoGuardPolicy","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"d2a3f9dd1ffc16941bb39360012138d3a3cbcf5e8beb08106182831b1c269054","declaration":"export function createInboundDebouncer(params: InboundDebounceCreateParams): { enqueue: (item: T) => Promise; flushKey: (key: string) => Promise; cancelKey: (key: string) => boolean; drain: () => Promise; };","entrypoint":"channel-inbound","exportName":"createInboundDebouncer","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"0589d27aa139324b1ed177b814368d633ef11a77d07697d63ff9b7a6c11f4500","declaration":"export function createPreCryptoDirectDmAuthorizer(params: { resolveAccess: (senderId: string) => Promise | ResolvedInboundDirectDmAccess>; issuePairingChallenge?: (params: { senderId: string; reply: (text: string) => Promise; }) => Promise; onBlocked?: (params: { senderId: string; reason: string; reasonCode: DmGroupAccessReasonCode; }) => void; }): (input: { senderId: string; reply: (text: string) => Promise; }) => Promise<\"allow\" | \"block\" | \"pairing\">;","entrypoint":"channel-inbound","exportName":"createPreCryptoDirectDmAuthorizer","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} -{"closureHash":"15981a7038c0ca7f916c13c9724248d103e662aa74bf869df907da70f227265b","declaration":"export function dispatchChannelInboundReply(params: AssembledInboundReply): Promise;","entrypoint":"channel-inbound","exportName":"dispatchChannelInboundReply","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} -{"closureHash":"34a78dd9b4240e1b9b468b8971985b4a894e8bb2f043733e0346fef6c9ff1a44","declaration":"export function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan<\"provider_message_sending\">): Promise;\nexport function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan): Promise;","entrypoint":"channel-inbound","exportName":"dispatchChannelInboundTurn","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} +{"closureHash":"e088ddef111b171ef5d034d9de6062cc551d6775412ddc8cfa7b72ca8f31c62d","declaration":"export function dispatchChannelInboundReply(params: AssembledInboundReply): Promise;","entrypoint":"channel-inbound","exportName":"dispatchChannelInboundReply","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} +{"closureHash":"62087d926004c2b89264e732b1d2b74e92a071ba85dcff4afd6f3a6703d4396a","declaration":"export function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan<\"provider_message_sending\">): Promise;\nexport function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan): Promise;","entrypoint":"channel-inbound","exportName":"dispatchChannelInboundTurn","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"2f997d08980c2c3f531382428c58204ca04e423a64ca201798f7667da15e774e","declaration":"export function dispatchInboundDirectDm(params: DispatchInboundDirectDmParams): Promise<{ route: DirectDmRoute; ctxPayload: FinalizedMsgContext; }>;","entrypoint":"channel-inbound","exportName":"dispatchInboundDirectDm","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} -{"closureHash":"fb62c9a14a57aeb6833f1d0b5f8d994df37d2de20891c9cf0f79c516ba6b60c3","declaration":"export function dispatchInboundDirectDmWithRuntime(params: DispatchInboundDirectDmParams & { runtime: PluginRuntime; }): Promise<{ route: DirectDmRoute; storePath: string; ctxPayload: FinalizedMsgContext; }>;","entrypoint":"channel-inbound","exportName":"dispatchInboundDirectDmWithRuntime","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} +{"closureHash":"338c242e378b029bd403fa61ad55153fb72055d141c9e1aedcb7bd589c582eda","declaration":"export function dispatchInboundDirectDmWithRuntime(params: DispatchInboundDirectDmParams & { runtime: PluginRuntime; }): Promise<{ route: DirectDmRoute; storePath: string; ctxPayload: FinalizedMsgContext; }>;","entrypoint":"channel-inbound","exportName":"dispatchInboundDirectDmWithRuntime","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"33cb9d760233f3d322599f7745a42e0b9fe2c653ffb9310d8b579f2d70f7638c","declaration":"export function filterChannelInboundQuoteContext(contextVisibility: ContextVisibilityMode | undefined, quote: SupplementalContextFacts[\"quote\"] | undefined): SupplementalContextFacts[\"quote\"] | undefined;","entrypoint":"channel-inbound","exportName":"filterChannelInboundQuoteContext","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"793b9b84aacb8f5b12daf857de82efc579fa018ba6b2357b0acd044be3dcec5f","declaration":"export function filterChannelInboundSupplementalContext(params: { supplemental?: SupplementalContextFacts; contextVisibility?: ContextVisibilityMode; }): SupplementalContextFacts | undefined;","entrypoint":"channel-inbound","exportName":"filterChannelInboundSupplementalContext","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"ff1797f8e6adf2331d98af9947dcefef02d743c2c7619fc8b5a2462980cb7126","declaration":"export function finalizeChannelInboundContext>(params: FinalizeChannelInboundContextAsyncParams): Promise>;\nexport function finalizeChannelInboundContext>(params: FinalizeChannelInboundContextParams): FinalizeChannelInboundContextResult;","entrypoint":"channel-inbound","exportName":"finalizeChannelInboundContext","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} @@ -870,7 +870,7 @@ {"closureHash":"5af56d49219f28e2d3a8104b771d2e758f14b4cd784e8ffc892b5a94634a3e73","declaration":"export function resolveMentionPatternPolicy(params: ResolveMentionPatternPolicyParams): ResolvedMentionPatternPolicy;","entrypoint":"channel-inbound","exportName":"resolveMentionPatternPolicy","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"1fb79e56b0b0e11f768f2075c9cdc060c3af281c79bbcd5334e5d887b99e5946","declaration":"export function resolveUnmentionedGroupInboundPolicy(params: { cfg: OpenClawConfig; agentId?: string; }): InboundEventKind;","entrypoint":"channel-inbound","exportName":"resolveUnmentionedGroupInboundPolicy","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"b06b5999074830bb40a3d6348a582fc5e1795997442acef561fca6a541d2b015","declaration":"export function runChannelFeedbackReflection(params: { cfg: OpenClawConfig; channel: string; channelLabel: string; accountId?: string; agentId: string; sessionKey: string; conversationId: string; conversationKind: \"direct\" | \"group\" | \"channel\"; thumbedDownResponse?: string; userComment?: string; cooldownMs?: number; onRecordError?: (error: unknown) => void; onDispatchError?: (error: unknown) => void; }): Promise;","entrypoint":"channel-inbound","exportName":"runChannelFeedbackReflection","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} -{"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"channel-inbound","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} +{"closureHash":"4cd63266d379acc2cdf9aba02f50b8ceb55500cf820787c370fc98d173fb1950","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"channel-inbound","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply(params: PreparedChannelTurn): Promise>;","entrypoint":"channel-inbound","exportName":"runPreparedInboundReply","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"7cd0e032be61b330962c9cee9ba6a4e67c31a90e22fd79850e28c97ea3200bd1","declaration":"export function shouldDebounceTextInbound(params: { text: string | null | undefined; cfg: OpenClawConfig; hasMedia?: boolean; commandOptions?: CommandNormalizeOptions; allowDebounce?: boolean; }): boolean;","entrypoint":"channel-inbound","exportName":"shouldDebounceTextInbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} {"closureHash":"8289a274f0ea2c1c7f0daaae77c4e9e0c5588f6a17148240e83720a59ec23604","declaration":"export function toHistoryMediaEntries(media: readonly ChannelInboundMediaInput[] | null | undefined, defaults?: { kind?: InboundMediaFacts[\"kind\"]; messageId?: string; }): HistoryMediaEntry[];","entrypoint":"channel-inbound","exportName":"toHistoryMediaEntries","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"} @@ -880,7 +880,7 @@ {"closureHash":"c700a03f4222eb20560db6abb3c775940b35cfbdfb3d122b2644cdc521809fb4","declaration":"export const DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS: 300000;","entrypoint":"channel-inbound","exportName":"DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"const","recordType":"export"} {"closureHash":"03bfa4a5365916c2d9f59e4e3c5c6ae3ee7952b2aec209c232bf73a5ccd2ae94","declaration":"export const filterChannelTurnSupplementalContext: (params: { supplemental?: SupplementalContextFacts; contextVisibility?: ContextVisibilityMode;}) => SupplementalContextFacts | undefined;","entrypoint":"channel-inbound","exportName":"filterChannelTurnSupplementalContext","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"const","recordType":"export"} {"closureHash":"b028978582295be29c22e06673b8f1b48819511b6b8c26cfff9e322ddc2834e3","declaration":"export type AccessGroupMembershipResolver = AccessGroupMembershipResolver;","entrypoint":"channel-inbound","exportName":"AccessGroupMembershipResolver","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} -{"closureHash":"278dc8a410230012ddbc3552ec621a224e6f7c5dd4cd3904c943ac98dd4259e4","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"channel-inbound","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} +{"closureHash":"ab30d0470348e3af7c18a273afc0d169d91f204fba557c75c4694e8a0ea6c936","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"channel-inbound","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"16e04f044a9af3553fa26fec210bf6d2e6e101c4fe4db18b8914fce21dce573f","declaration":"export type BuildChannelInboundEventContextAsyncParams = BuildChannelInboundEventContextAsyncParams;","entrypoint":"channel-inbound","exportName":"BuildChannelInboundEventContextAsyncParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"bfda516454e1d03a16f685f9470c8ce4329328d5658b5ca26bc703c70c5d17c8","declaration":"export type BuildChannelInboundEventContextParams = BuildChannelInboundEventContextParams;","entrypoint":"channel-inbound","exportName":"BuildChannelInboundEventContextParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"b6ce3464585a1f042b94574c385dd57ca9be83740ef0fc929db769710dc78061","declaration":"export type BuildChannelTurnContextParams = BuildChannelTurnContextParams;","entrypoint":"channel-inbound","exportName":"BuildChannelTurnContextParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} @@ -891,11 +891,11 @@ {"closureHash":"bf7e13e0308a3f54e1686373d312f9883b1de82bf771ea1763379a5bf8f6e716","declaration":"export type ChannelFeedbackReflectionResult = ChannelFeedbackReflectionResult;","entrypoint":"channel-inbound","exportName":"ChannelFeedbackReflectionResult","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelInboundDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"channel-inbound","exportName":"ChannelInboundDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"359ec07a928d613ac5a9f96ded5561f4e3087fd1bf741e1f8e5e8a3b0fb30d29","declaration":"export type ChannelInboundEnvelopeInput = ChannelInboundEnvelopeInput;","entrypoint":"channel-inbound","exportName":"ChannelInboundEnvelopeInput","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} -{"closureHash":"6a461bb329045db0f4fe7cf7170b8469d875c7862344aebd3a9b11ff3b5f63f4","declaration":"export type ChannelInboundEventRunnerParams = ChannelInboundEventRunnerParams;","entrypoint":"channel-inbound","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} +{"closureHash":"809fd66291f793b3d61ddd275ed153cb2050b1d7a31767ef577489a20ee582d0","declaration":"export type ChannelInboundEventRunnerParams = ChannelInboundEventRunnerParams;","entrypoint":"channel-inbound","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"fce252e3d1ade4690deb2b6368c79ebee42243d49dffca65670e2ee7a9883e89","declaration":"export type ChannelInboundMediaInput = ChannelInboundMediaInput;","entrypoint":"channel-inbound","exportName":"ChannelInboundMediaInput","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"1a0f5a78203bfe24fd1e02389bd2535fbb1937daad45a11058f6fe27641bc792","declaration":"export type ChannelInboundMediaPayload = ChannelInboundMediaPayload;","entrypoint":"channel-inbound","exportName":"ChannelInboundMediaPayload","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"5725f1ae1cf4ac225068feb9ba49057cc24fb99862d768a9cfffe5485d6f410c","declaration":"export type ChannelInboundSupplementalResolutionOptions = ChannelInboundSupplementalResolutionOptions;","entrypoint":"channel-inbound","exportName":"ChannelInboundSupplementalResolutionOptions","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} -{"closureHash":"f2dd9397d43b50a0a864d7db6d489979cbb2d4ca22e6b660cc36495210dfdc9d","declaration":"export type ChannelInboundTurnPlan = ChannelInboundTurnPlan;","entrypoint":"channel-inbound","exportName":"ChannelInboundTurnPlan","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} +{"closureHash":"70e8655f976ef1cea98653416b9c454aa64b4151e9824d238dbdc8799b477839","declaration":"export type ChannelInboundTurnPlan = ChannelInboundTurnPlan;","entrypoint":"channel-inbound","exportName":"ChannelInboundTurnPlan","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"a5d806d7e29220e98d1fa3ce08ce3ffb12f07d70d6b24b22a3ee4907ed3b1677","declaration":"export type ChannelPartialDeliveryError = ChannelPartialDeliveryError;","entrypoint":"channel-inbound","exportName":"ChannelPartialDeliveryError","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelTurnDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"channel-inbound","exportName":"ChannelTurnDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} {"closureHash":"fce252e3d1ade4690deb2b6368c79ebee42243d49dffca65670e2ee7a9883e89","declaration":"export type ChannelTurnMediaInput = ChannelInboundMediaInput;","entrypoint":"channel-inbound","exportName":"ChannelTurnMediaInput","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"} @@ -1097,7 +1097,7 @@ {"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-message","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-message","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} -{"closureHash":"cd2d66f5b953f01a4bf6f98bb741ea33052d9d218af83ca06d529ad61c4fe2af","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} +{"closureHash":"fa7bd40b875ccb81b2ee3d27680069c09ab683be31ddea7b4070c159b227ad9e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-message","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"const","recordType":"export"} {"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-message","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} {"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-message","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} {"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-message","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-message","kind":"type","recordType":"export"} @@ -1226,7 +1226,7 @@ {"closureHash":"9555a73ba2717bba0bf94888017be4cecef2c51cc3887ed34f8d8ef3c79d6abf","declaration":"export const DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS: number;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"3a2df3ab656149630c004d2d9f71b8ceda99aa81eb6b72b0cc7b3982ed0ce044","declaration":"export const DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS: 8;","entrypoint":"channel-outbound","exportName":"DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"38c735a14c65067aa7825390d1c5c8876c651d0603e84980dfaf947f5235747a","declaration":"export const INGRESS_CLAIM_PROCESS_ID: string;","entrypoint":"channel-outbound","exportName":"INGRESS_CLAIM_PROCESS_ID","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} -{"closureHash":"cd2d66f5b953f01a4bf6f98bb741ea33052d9d218af83ca06d529ad61c4fe2af","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} +{"closureHash":"fa7bd40b875ccb81b2ee3d27680069c09ab683be31ddea7b4070c159b227ad9e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"channel-outbound","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"const","recordType":"export"} {"closureHash":"2c52eff200a5b9bd2d93ee72209c2626bbe165f59b538591db661b94773023b0","declaration":"export type AgentPlanStep = AgentPlanStep;","entrypoint":"channel-outbound","exportName":"AgentPlanStep","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} {"closureHash":"1e8bcb2c8d8f5327f5b81b8c217f722f090dccba21ba3943324010f4f80a5a50","declaration":"export type AgentPlanStepStatus = AgentPlanStepStatus;","entrypoint":"channel-outbound","exportName":"AgentPlanStepStatus","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} {"closureHash":"b036fc26e5330dfd7da1af8839bb3c23e85283b9e16e252a659dd4bb0c35cd5e","declaration":"export type ChannelDeliveryStreamingConfig = ChannelDeliveryStreamingConfig;","entrypoint":"channel-outbound","exportName":"ChannelDeliveryStreamingConfig","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} @@ -1270,13 +1270,13 @@ {"closureHash":"424d0b4b21b078d07a49a7bc52c3b63250be032a3e17f47c0cc824178b828ded","declaration":"export type TextChunkMode = TextChunkMode;","entrypoint":"channel-outbound","exportName":"TextChunkMode","importSpecifier":"openclaw/plugin-sdk/channel-outbound","kind":"type","recordType":"export"} {"category":"channel","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing","recordType":"module"} {"closureHash":"3d2325df85dcb397e7df5c9f380c5d6c517976527acbe7d94d59df8777810d1f","declaration":"export function createChannelPairingChallengeIssuer(params: { channel: ChannelId; accountId?: string; upsertPairingRequest: Parameters[0][\"upsertPairingRequest\"]; }): (challenge: Omit[0], \"channel\" | \"accountId\" | \"upsertPairingRequest\">) => Promise<{ created: boolean; code?: string; }>;","entrypoint":"channel-pairing","exportName":"createChannelPairingChallengeIssuer","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} -{"closureHash":"5cf88e1467fc76050a94ffaa29e702991457b236a70ea0f51743f5f6147d5092","declaration":"export function createChannelPairingController(params: { core: PluginRuntime; channel: ChannelId; accountId: string; }): ChannelPairingController;","entrypoint":"channel-pairing","exportName":"createChannelPairingController","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} +{"closureHash":"3bb1d26fbd755096dc8055b1cfc03aadee3cde19ec6b7d54e5b413334032c1eb","declaration":"export function createChannelPairingController(params: { core: PluginRuntime; channel: ChannelId; accountId: string; }): ChannelPairingController;","entrypoint":"channel-pairing","exportName":"createChannelPairingController","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} {"closureHash":"39648d8f2d5ec14a204569c6266100fef1e455dd53dd6b05ae3034dc98986875","declaration":"export function createLoggedPairingApprovalNotifier(format: string | ((params: PairingNotifyParams) => string), log?: (message: string) => void): NonNullable;","entrypoint":"channel-pairing","exportName":"createLoggedPairingApprovalNotifier","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} {"closureHash":"d5198bb50f3f1c60b384feb93ae07621ceeca6f7d7f7c729ec96dabe984eca10","declaration":"export function createPairingPrefixStripper(prefixRe: RegExp, map?: (entry: string) => string): NonNullable;","entrypoint":"channel-pairing","exportName":"createPairingPrefixStripper","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} {"closureHash":"b914a433d104666e368d9e2c85d328e0015168b8ad3ade34bc993d3cc7141513","declaration":"export function createTextPairingAdapter(params: { idLabel: string; message: string; normalizeAllowEntry?: ChannelPairingAdapter[\"normalizeAllowEntry\"]; notify: (params: PairingNotifyParams & { message: string; }) => Promise | void; }): ChannelPairingAdapter;","entrypoint":"channel-pairing","exportName":"createTextPairingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} {"closureHash":"ec2b230fc869584bf4114bc30b34546f3e3e9369d7b6f49d9ff3bc8b19dd1989","declaration":"export function readChannelAllowFromStore(channel: PairingChannel, env?: NodeJS.ProcessEnv, accountId?: string): Promise;","entrypoint":"channel-pairing","exportName":"readChannelAllowFromStore","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} {"closureHash":"efe4915252d6eb26af5b1c52dadf22e05b4a13f1668a10cc508c0aa4bae11e39","declaration":"export function readChannelAllowFromStoreSync(channel: PairingChannel, env?: NodeJS.ProcessEnv, accountId?: string): string[];","entrypoint":"channel-pairing","exportName":"readChannelAllowFromStoreSync","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"function","recordType":"export"} -{"closureHash":"0fca29fd309bc475460f073a3265a9176afca3b6093158ebdc36a3077c9c7152","declaration":"export type ChannelPairingController = ChannelPairingController;","entrypoint":"channel-pairing","exportName":"ChannelPairingController","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"type","recordType":"export"} +{"closureHash":"499d5f0ccb5716b57e37afd0fbacd5008e51a5c4a9775299ed47e2b9d329a31d","declaration":"export type ChannelPairingController = ChannelPairingController;","entrypoint":"channel-pairing","exportName":"ChannelPairingController","importSpecifier":"openclaw/plugin-sdk/channel-pairing","kind":"type","recordType":"export"} {"category":null,"entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","recordType":"module"} {"closureHash":"19a3d4c1fdad2b565e7428da26e0dafa76f08498bcf06ac3df098486ff4afc2c","declaration":"export function applyAccountNameToChannelSection(params: { cfg: OpenClawConfig; channelKey: string; accountId: string; name?: string; alwaysUseAccounts?: boolean; }): OpenClawConfig;","entrypoint":"channel-plugin-common","exportName":"applyAccountNameToChannelSection","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"function","recordType":"export"} {"closureHash":"e97ff96104ab3b7f8462f20d39d2596908a59cb03a634c6c2fb0d7ff80e5bb54","declaration":"export function buildChannelConfigSchema(schema: ZodTypeAny, options?: BuildChannelConfigSchemaOptions): ChannelConfigSchema;","entrypoint":"channel-plugin-common","exportName":"buildChannelConfigSchema","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"function","recordType":"export"} @@ -1292,8 +1292,8 @@ {"closureHash":"cd6b0d3f962c8cb40d405e92bb06bb8e9285b386200897f550d21036022262c1","declaration":"export const PAIRING_APPROVED_MESSAGE: \"✅ OpenClaw access approved. Send a message to start chatting.\";","entrypoint":"channel-plugin-common","exportName":"PAIRING_APPROVED_MESSAGE","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"const","recordType":"export"} {"closureHash":"712435252d2fe5135542ca6b5a4375a6edea24e83829338011dc6ae7e9802fbb","declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"channel-plugin-common","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} {"closureHash":"18fcab0d5899575a31205732d2be92597e533ad944c7fe11cac2af6fc718ea34","declaration":"export type ChannelPlugin = ChannelPlugin;","entrypoint":"channel-plugin-common","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-plugin-common","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"channel-plugin-common","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"channel-plugin-common","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common","kind":"type","recordType":"export"} {"category":null,"entrypoint":"channel-policy","importSpecifier":"openclaw/plugin-sdk/channel-policy","recordType":"module"} {"closureHash":"017ab29d16d0f3d02d60f3513e65a373868d26496bc93d020d8fc8c17fd875f4","declaration":"export function buildAccountScopedDmSecurityPolicy(params: { cfg: OpenClawConfig; channelKey: string; accountId?: string | null; fallbackAccountId?: string | null; policy?: string | null; allowFrom?: Array | null; defaultPolicy?: string; allowFromPathSuffix?: string; policyPathSuffix?: string; approveChannelId?: string; approveHint?: string; normalizeEntry?: (raw: string) => string; inheritSharedDefaultsFromDefaultAccount?: boolean; }): ChannelSecurityDmPolicy;","entrypoint":"channel-policy","exportName":"buildAccountScopedDmSecurityPolicy","importSpecifier":"openclaw/plugin-sdk/channel-policy","kind":"function","recordType":"export"} {"closureHash":"628863a501a389d1fee7ab465a4e9da1751189011e9086a54ab22bfc1f970c8a","declaration":"export function buildChannelGroupsScopeTree(cfg: OpenClawConfig, channel: ChannelId, accountId?: string | null): ScopeTree;","entrypoint":"channel-policy","exportName":"buildChannelGroupsScopeTree","importSpecifier":"openclaw/plugin-sdk/channel-policy","kind":"function","recordType":"export"} @@ -1891,8 +1891,8 @@ {"closureHash":"b8e27bea2add3eaafac7074cbd44fcf2acea30a1ac125c3551925e8cd0b3378d","declaration":"export function createChatChannelPlugin(params: { base: ChatChannelPluginBase; security?: ChannelSecurityAdapter | ChatChannelSecurityOptions; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin;","entrypoint":"core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"785b0f750c5bd20f9f3ca081886a5af79ee0e4d2c38f78f769bf994b234f5076","declaration":"export function createDedupeCache(options: DedupeCacheOptions): DedupeCache;","entrypoint":"core","exportName":"createDedupeCache","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"core","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} -{"closureHash":"0f2201e81ac7e4578b83d7973dd5e5e6d5c82e8d115f3786a8e10b5eab604b76","declaration":"export function defineChannelPluginEntry({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions): DefinedChannelPluginEntry;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} -{"closureHash":"3128153742c64b1132068d7b51a35bf22103c5ae2c82de42c8eca242c43d180a","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} +{"closureHash":"d0215218f4367a9b95487036431f4fc5453ebb39960ec6deccde5acb1cab032c","declaration":"export function defineChannelPluginEntry({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions): DefinedChannelPluginEntry;","entrypoint":"core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} +{"closureHash":"de4da3c814c237aa1c66cb2f911beae164b8de9ff041572849c7a0a78f8df3a0","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"core","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"0e0a9f6b22c433bdb1165e7707b1f9a1ddac1a544a8add00b503048cf157921f","declaration":"export function defineSetupPluginEntry(plugin: TPlugin): { plugin: TPlugin; };","entrypoint":"core","exportName":"defineSetupPluginEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"3c5ce0c3733cbb038aeb914af1df5d701f362f0521921b8abd0c9214db9cc4b8","declaration":"export function delegateCompactionToRuntime(params: Parameters[0]): Promise;","entrypoint":"core","exportName":"delegateCompactionToRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"2c282e0e308f9442bca7cdeb8e89a097531b1d54a692c9d86e5f39a8db1c2537","declaration":"export function deleteAccountFromConfigSection(params: { cfg: OpenClawConfig; sectionKey: string; accountId: string; clearBaseFields?: string[]; }): OpenClawConfig;","entrypoint":"core","exportName":"deleteAccountFromConfigSection","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} @@ -1939,7 +1939,7 @@ {"closureHash":"2e6ce1f4c1692270a00dc0cf08e2752a26739c3143f8091cb270063c6034605d","declaration":"export function tryReadSecretFileSync(filePath: string | undefined, label: string, options: CredentialFileReadOptions): string | undefined;\nexport function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: FsSafeSecretFileReadOptions): string | undefined;\nexport function tryReadSecretFileSync(filePath: string, label: string, options: FsSafeSecretFileReadOptions | undefined, diagnostic: { configPath: string; }): ConfiguredCredentialResult;\nexport function tryReadSecretFileSync(filePath: string | undefined, label: string, options: FsSafeSecretFileReadOptions | undefined, diagnostic: { configPath: string; }): CredentialResult;","entrypoint":"core","exportName":"tryReadSecretFileSync","importSpecifier":"openclaw/plugin-sdk/core","kind":"function","recordType":"export"} {"closureHash":"b40933a11984425d14a7a479bda0d2cd14cbfa9371164774f39bebbf12861df7","declaration":"export const DEFAULT_ACCOUNT_ID: \"default\";","entrypoint":"core","exportName":"DEFAULT_ACCOUNT_ID","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export"} {"closureHash":null,"declaration":"export const DEFAULT_SECRET_FILE_MAX_BYTES: number;","entrypoint":"core","exportName":"DEFAULT_SECRET_FILE_MAX_BYTES","importSpecifier":"openclaw/plugin-sdk/core","kind":"const","recordType":"export"} -{"closureHash":"3a9021d4c54eb31b36b05dbcf2cb32cbeb75f0d8c7a93ee1c4642a8f2d279cf5","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"core","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} +{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"core","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"d2ff4de9ee3fd111c2cc64a3ee7e557d9f161af1b6b437ee7886bcb16e426317","declaration":"export type AgentPromptGuidance = AgentPromptGuidance;","entrypoint":"core","exportName":"AgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"2841127a90e04467adfea8126cbf03655f44ddb089ad9e0d48833c2beea39cbd","declaration":"export type AgentPromptGuidanceEntry = AgentPromptGuidanceEntry;","entrypoint":"core","exportName":"AgentPromptGuidanceEntry","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"aa0412acefe62caff12e5271ead0545504874e824380dc21cfc0566388a05f33","declaration":"export type AgentPromptSurfaceKind = \"openclaw_main\" | \"pi_main\" | \"codex_app_server\" | \"cli_backend\" | \"acp_backend\" | \"subagent\";","entrypoint":"core","exportName":"AgentPromptSurfaceKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} @@ -1971,10 +1971,10 @@ {"closureHash":"54ea426dde51859b572b4c1079d2b70b412096f73da2b8c265f2749d9f01cbb5","declaration":"export type NormalizedLocation = NormalizedLocation;","entrypoint":"core","exportName":"NormalizedLocation","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"5189698d102b8b064eefe1442b008312973c3994fd6b203d2205725d7ee9d118","declaration":"export type OpenClawPluginActiveModelContext = OpenClawPluginActiveModelContext;","entrypoint":"core","exportName":"OpenClawPluginActiveModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"69fefc8092c1cea366c48505c8423ff0f8f995caef3d259d177346b1687541aa","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} -{"closureHash":"0ab866d1a087763f742ffcce26b644d2f1ac6deec6841ef2def1f4701484b354","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} +{"closureHash":"b17322e564d6fa98de2dcce9f6d5b58794438251f356fe59c0889046f675a07b","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"a7ed83f0924f818b23cecb5a1f3591ff28b9499ccac9667a2b8505c2bc608ea9","declaration":"export type OpenClawPluginGatewayEventScope = OpenClawPluginGatewayEventScope;","entrypoint":"core","exportName":"OpenClawPluginGatewayEventScope","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"f81f224d9b837e6353b7a546c5a76b124bcf568fd1f1fd0c6881fda30c53120a","declaration":"export type OpenClawPluginGatewayEvents = OpenClawPluginGatewayEvents;","entrypoint":"core","exportName":"OpenClawPluginGatewayEvents","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"ddc2738dd0ece94ddde9a7c655f92c15ec6416645ecf932d4aa404c7b7761159","declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} @@ -2008,7 +2008,7 @@ {"closureHash":"f361587d79916f3d1cb73b4d296e1f37f86d799a641ce4a590ee784413d0b7ef","declaration":"export type PluginNextTurnInjectionRecord = PluginNextTurnInjectionRecord;","entrypoint":"core","exportName":"PluginNextTurnInjectionRecord","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"e4f6093e70826c8c7c02433082b9ac1b91755640096339111331fc45a494ca88","declaration":"export type PluginRunContextGetParams = PluginRunContextGetParams;","entrypoint":"core","exportName":"PluginRunContextGetParams","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"f9d14d70abca2a8771c0763736871361e4c223a8e4cbe3ae5c7a17751630b68e","declaration":"export type PluginRunContextPatch = PluginRunContextPatch;","entrypoint":"core","exportName":"PluginRunContextPatch","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"4a005fd56ceaa1de7ff2c6b45f08a48f48d7ff30e21d0e88d783d86a49e274f4","declaration":"export type PluginRuntimeLifecycleRegistration = PluginRuntimeLifecycleRegistration;","entrypoint":"core","exportName":"PluginRuntimeLifecycleRegistration","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"c599fdbfd9439bdcbbbf621dca995d2d6f87c7aba9606dc1970e1205dfea6357","declaration":"export type PluginSessionActionContext = PluginSessionActionContext;","entrypoint":"core","exportName":"PluginSessionActionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} {"closureHash":"85a706fb9b56d5c5f41756729736cd4581b98d5cb0ab9a693b8261e4d936e499","declaration":"export type PluginSessionActionRegistration = PluginSessionActionRegistration;","entrypoint":"core","exportName":"PluginSessionActionRegistration","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export"} @@ -2209,8 +2209,8 @@ {"closureHash":"0db968dad99d84ecef5202167daadd092e3b4f456270283d3fd61585ac2bbd5a","declaration":"export type DiscordComponentSendResult = DiscordComponentSendResult;","entrypoint":"discord","exportName":"DiscordComponentSendResult","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} {"closureHash":"084b46d7dd510eb182a4705559251e824220b57dbfa6dc54392507b72ba1815e","declaration":"export type DiscordOutboundTargetResolution = DiscordOutboundTargetResolution;","entrypoint":"discord","exportName":"DiscordOutboundTargetResolution","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} {"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"discord","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"discord","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"discord","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"discord","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} {"closureHash":"6b9967e919c1cd420c8356b4f9d40dbc81932b4b15a733eecdcfb2ad84f8e5d8","declaration":"export type ResolvedDiscordAccount = ResolvedDiscordAccount;","entrypoint":"discord","exportName":"ResolvedDiscordAccount","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} {"closureHash":"6bed5ca896b7034117e4757e2c6b4111dd5293dbad9416e3828dd6afa4e858ce","declaration":"export type ThreadBindingRecord = ThreadBindingRecord;","entrypoint":"discord","exportName":"ThreadBindingRecord","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} {"closureHash":"eaabd6155d6f25cbba09ef3c8f2b306f2202affdca4b47e5d67311ff4073fb0d","declaration":"export type ThreadBindingTargetKind = ThreadBindingTargetKind;","entrypoint":"discord","exportName":"ThreadBindingTargetKind","importSpecifier":"openclaw/plugin-sdk/discord","kind":"type","recordType":"export"} @@ -2344,21 +2344,21 @@ {"category":null,"entrypoint":"inbound-event-delivery","importSpecifier":"openclaw/plugin-sdk/inbound-event-delivery","recordType":"module"} {"closureHash":"bd8acef57a602ffc7a70848afb14bad6e37c892ded35b20ba0d3452336088830","declaration":"export function createInboundEventDeliveryCorrelation(params: { targetsMatch: (expected: string, actual: string) => boolean; }): { begin(sessionKey: string | undefined, event: ActiveInboundEvent, options?: { inboundEventKind?: string; }): () => void; notify(notification: InboundEventDeliveryNotification): void; };","entrypoint":"inbound-event-delivery","exportName":"createInboundEventDeliveryCorrelation","importSpecifier":"openclaw/plugin-sdk/inbound-event-delivery","kind":"function","recordType":"export"} {"category":null,"entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","recordType":"module"} -{"closureHash":"15981a7038c0ca7f916c13c9724248d103e662aa74bf869df907da70f227265b","declaration":"export function dispatchChannelInboundReply(params: AssembledInboundReply): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"dispatchChannelInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} -{"closureHash":"9d9c2d3d3840097d861c42c4f649890592c7ed971e82b76294ee1c22cd596f64","declaration":"export function dispatchInboundReplyWithBase(params: BuildInboundReplyDispatchBaseParams & Pick): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"dispatchInboundReplyWithBase","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} +{"closureHash":"e088ddef111b171ef5d034d9de6062cc551d6775412ddc8cfa7b72ca8f31c62d","declaration":"export function dispatchChannelInboundReply(params: AssembledInboundReply): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"dispatchChannelInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} +{"closureHash":"42faf8201585d82c339d991a2d1969f1723956fb1c8561f586ba831e376bf8e7","declaration":"export function dispatchInboundReplyWithBase(params: BuildInboundReplyDispatchBaseParams & Pick): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"dispatchInboundReplyWithBase","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"8a142089acc4d9428ed552977f90f17b2283c109cd1c66b91b51b3653d455eea","declaration":"export function hasFinalInboundReplyDispatch(result: ChannelTurnDispatchResultLike, signals?: Pick): boolean;","entrypoint":"inbound-reply-dispatch","exportName":"hasFinalInboundReplyDispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"bb4c861ed6a9b1d70200aea7f6356a65a71491b098f8f3e8866241e5481f9aec","declaration":"export function hasVisibleInboundReplyDispatch(result: ChannelTurnDispatchResultLike, signals?: ChannelTurnVisibleDeliverySignals): boolean;","entrypoint":"inbound-reply-dispatch","exportName":"hasVisibleInboundReplyDispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"65f5f2725c1124096e437bcf5894983eee1023f7033be886f1a506044b77c58e","declaration":"export function recordChannelBotPairLoopAndCheckSuppression(params: ChannelBotLoopProtectionFacts): PairLoopGuardResult;","entrypoint":"inbound-reply-dispatch","exportName":"recordChannelBotPairLoopAndCheckSuppression","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"33b4280a69de6a4a87acf915c6d694b9ee41fc4f0262a1bb9836c46d53fe337c","declaration":"export function recordDroppedChannelInboundHistory(params: { input: NormalizedTurnInput; preflight: PreflightFacts; admission?: ChannelTurnAdmission; }): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"recordDroppedChannelInboundHistory","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"33b4280a69de6a4a87acf915c6d694b9ee41fc4f0262a1bb9836c46d53fe337c","declaration":"export function recordDroppedChannelTurnHistory(params: { input: NormalizedTurnInput; preflight: PreflightFacts; admission?: ChannelTurnAdmission; }): Promise;","entrypoint":"inbound-reply-dispatch","exportName":"recordDroppedChannelTurnHistory","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"98e0cc16352b7dfcceb4957f8cd2acfe3c2b3acb223c42a5cfb885e088fcb04c","declaration":"export function resolveInboundReplyDispatchCounts(result: ChannelTurnDispatchResultLike): Record;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} -{"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} +{"closureHash":"4cd63266d379acc2cdf9aba02f50b8ceb55500cf820787c370fc98d173fb1950","declaration":"export function runChannelInboundEvent(params: RunChannelTurnParams): Promise>;\nexport function runChannelInboundEvent(params: ChannelInboundEventRunnerParams): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} {"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply(params: PreparedChannelTurn): Promise>;","entrypoint":"inbound-reply-dispatch","exportName":"runPreparedInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"} -{"closureHash":"cd2d66f5b953f01a4bf6f98bb741ea33052d9d218af83ca06d529ad61c4fe2af","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"} -{"closureHash":"278dc8a410230012ddbc3552ec621a224e6f7c5dd4cd3904c943ac98dd4259e4","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"inbound-reply-dispatch","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} +{"closureHash":"fa7bd40b875ccb81b2ee3d27680069c09ab683be31ddea7b4070c159b227ad9e","declaration":"export const deliverInboundReplyWithMessageSendContext: (params: DurableInboundReplyDeliveryParams) => Promise<{ status: \"not_applicable\"; reason: \"non_final\";} | { status: \"unsupported\"; reason: \"missing_channel\" | \"missing_target\" | \"missing_outbound_handler\" | \"capability_mismatch\"; capability?: DurableFinalDeliveryRequirement;} | { status: \"handled_visible\"; delivery: ChannelDeliveryResult;} | { status: \"handled_no_send\"; reason: \"no_visible_result\"; delivery: ChannelDeliveryResult;} | { status: \"failed\"; error: unknown; sentBeforeError?: true;}>;","entrypoint":"inbound-reply-dispatch","exportName":"deliverInboundReplyWithMessageSendContext","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"const","recordType":"export"} +{"closureHash":"ab30d0470348e3af7c18a273afc0d169d91f204fba557c75c4694e8a0ea6c936","declaration":"export type AssembledInboundReply = AssembledChannelTurn;","entrypoint":"inbound-reply-dispatch","exportName":"AssembledInboundReply","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"3de0d6d1bb8f9a83bd1c6e57aa8618e3fea71054236cd530c555e348bd0b7702","declaration":"export type ChannelBotLoopProtectionFacts = ChannelBotLoopProtectionFacts;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelBotLoopProtectionFacts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelInboundDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} -{"closureHash":"6a461bb329045db0f4fe7cf7170b8469d875c7862344aebd3a9b11ff3b5f63f4","declaration":"export type ChannelInboundEventRunnerParams = ChannelInboundEventRunnerParams;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} +{"closureHash":"809fd66291f793b3d61ddd275ed153cb2050b1d7a31767ef577489a20ee582d0","declaration":"export type ChannelInboundEventRunnerParams = ChannelInboundEventRunnerParams;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"f0da845bbdf49b30f6718a4b7ff012a0536deb98f65418583555dc90825797ca","declaration":"export type ChannelTurnDroppedHistoryOptions = ChannelTurnDroppedHistoryOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelTurnDroppedHistoryOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"b145a01f8a24a98b66e723274b24f5a87c3b7034ac7d5c551a92b0cfdb041d51","declaration":"export type ChannelTurnRecordOptions = ChannelTurnRecordOptions;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelTurnRecordOptions","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} {"closureHash":"4c1290dcec6d7fac7aec703ccec8b6b790ebbd4fa12c3c3c1d07f55b86075b5e","declaration":"export type DurableInboundReplyDeliveryParams = DurableInboundReplyDeliveryParams;","entrypoint":"inbound-reply-dispatch","exportName":"DurableInboundReplyDeliveryParams","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"} @@ -3186,34 +3186,34 @@ {"closureHash":"0a8eee30ac3d7beb17a67ed364602e375580d8fe64711e4a88bb77eabb1354f0","declaration":"export function addMeetingSetupCheck(status: MeetingSetupStatus, check: MeetingSetupCheck): MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"addMeetingSetupCheck","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"a4294716c3bb64b8732f33b331506a14e4f2fba59f382699961c9db8677e6774","declaration":"export function asMeetingBrowserTabs(result: unknown): MeetingBrowserCandidateTab[];","entrypoint":"meeting-runtime","exportName":"asMeetingBrowserTabs","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"b39bf4bdb91f0aeabac8cbd0ee8dc21afceae20faf463c6a2657258f2aa07976","declaration":"export function buildMeetingSoxAudioCommands(params: MeetingSoxAudioCommandParams): { inputCommand: string[]; outputCommand: string[]; };","entrypoint":"meeting-runtime","exportName":"buildMeetingSoxAudioCommands","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"50394942e0a454c742e80a4f3c8739c80258b387ade498eb5aed66df887108be","declaration":"export function callMeetingBrowserProxyOnNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; } & MeetingBrowserRequestParams): Promise;","entrypoint":"meeting-runtime","exportName":"callMeetingBrowserProxyOnNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"815786388b89eefc4f411e1796ee4330c17751ce5799afd7ee3f99bc0d414cd0","declaration":"export function callMeetingBrowserProxyOnNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; } & MeetingBrowserRequestParams): Promise;","entrypoint":"meeting-runtime","exportName":"callMeetingBrowserProxyOnNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c5fcd1d157edcb4773235adfaf8051e3ec7ca428cb7916b6d55f2511affab56c","declaration":"export function convertMeetingBridgeAudioForStt(audio: Buffer, audioFormat: MeetingRealtimeAudioFormat): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingBridgeAudioForStt","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"aa9063c27c5ff714d3653571c4a2a71e9d2eea5609b161f03e4738e15411358b","declaration":"export function convertMeetingTtsAudioForBridge(audio: Buffer, sampleRate: number, audioFormat: MeetingRealtimeAudioFormat, outputFormat?: string, platformName?: string): Buffer;","entrypoint":"meeting-runtime","exportName":"convertMeetingTtsAudioForBridge","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"de5cb98246ebe453ea05133787d592d020cc685b77f6924a0cbc6201d2f4e403","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"9cb63eefd2e5a70aea960bf17fcf570ef02a2d7b5afd5ad9451802964c653fcf","declaration":"export function createMeetingBrowserNodeCaller(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; }): MeetingBrowserRequestCaller;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeCaller","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"f814c972c694aa3d3217eda67387707a23112637bff7f591c362d4e30fc65ca2","declaration":"export function createLocalMeetingRealtimeAudioTransport(params: { inputCommand: string[]; outputCommand: string[]; bargeInInputCommand?: string[]; bargeInRmsThreshold: number; bargeInPeakThreshold: number; bargeInCooldownMs: number; logger: RuntimeLogger; logScope: string; audioFormat?: MeetingRealtimeAudioFormat; spawn?: MeetingRealtimeAudioSpawn; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createLocalMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"ca1fd149af9fb3117fc513993fe54e476822c82f414403040041b53f7624e767","declaration":"export function createMeetingBrowserNodeCaller(params: { runtime: PluginRuntime; adapter: NodeAdapter; nodeId: string; }): MeetingBrowserRequestCaller;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeCaller","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"f244ca8c47f556c9f388953cf9a7581c7d4d413af93e0dfa1462779e6a687a7c","declaration":"export function createMeetingBrowserNodeInvokePolicy(options: MeetingBrowserNodePolicyOptions): OpenClawPluginNodeInvokePolicy;","entrypoint":"meeting-runtime","exportName":"createMeetingBrowserNodeInvokePolicy","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"1aa4e3ff39a2cbe162421c62b1fa4a362973381d4d9ff15c671249b1ebd566bc","declaration":"export function createMeetingNodeHost(options: MeetingNodeHostOptions): { handleCommand(paramsJSON?: string | null): Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingNodeHost","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"852b92d914dbb3681185244be3a5b6ccb5ab8383681a3caba2e4f64a0bae0e03","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"933a6d1275bb9b60523553911d302f981768089b7f5fdc7e83411364ebff5516","declaration":"export function createMeetingRealtimeEngineBindings(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { agentId?: string; toolPolicy: RealtimeVoiceAgentConsultToolPolicy; }; }; fullConfig: OpenClawConfig; runtime: PluginRuntime; logger: RuntimeLogger; }): { platform: MeetingRuntimePlatform; consultAgent: (consult: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (call: MeetingRealtimeToolCallParams) => Promise; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin; createdAt: string; }): MeetingSessionRecord;","entrypoint":"meeting-runtime","exportName":"createMeetingSession","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"fb276e07e395ab19177b3d7967fd0a8fcda79130883f15092a4a306b826481b9","declaration":"export function createMeetingSetupStatus(checks: MeetingSetupCheck[]): MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"createMeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"93c4bb70429a2739151e6f900e67bed81f3cd8f597811220e790341edb0d6008","declaration":"export function createMeetingVoiceCallGateway(params: { config: MeetingVoiceCallConfig; runtime: PluginRuntime; surface: MeetingVoiceCallSurface; connectClient: (params: { config: MeetingVoiceCallConfig; surface: MeetingVoiceCallSurface; }) => Promise; }): MeetingVoiceCallGateway;","entrypoint":"meeting-runtime","exportName":"createMeetingVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"9952da224defa6a00c175b3e243ad923e9682b0ff5d102c40dc9fd962ba4edc5","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"cfa51d8c3420f0abc4043525ec709c4cfd629bb8accd3fde275ffacc84ae549c","declaration":"export function createMeetingVoiceCallGateway(params: { config: MeetingVoiceCallConfig; runtime: PluginRuntime; surface: MeetingVoiceCallSurface; connectClient: (params: { config: MeetingVoiceCallConfig; surface: MeetingVoiceCallSurface; }) => Promise; }): MeetingVoiceCallGateway;","entrypoint":"meeting-runtime","exportName":"createMeetingVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"38ded229da204e58668c0c205d424bfdd64e420e757e002944e23c2fc4457c43","declaration":"export function createNodeMeetingRealtimeAudioTransport(params: { runtime: PluginRuntime; nodeId: string; bridgeId: string; logger: RuntimeLogger; commandName: string; logScope: string; logPrefix: string; audioFormat?: MeetingRealtimeAudioFormat; }): MeetingRealtimeAudioTransport;","entrypoint":"meeting-runtime","exportName":"createNodeMeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c0987b15a391a92d53d3e4e12b61eb122f9547fcf4f45478cdd3c5a8973a3e52","declaration":"export function endMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"endMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"b3ebc10fe7ab372e1317eaa8f78621cf277bba1e33fda57acf2254f6759d1e9d","declaration":"export function getMeetingVoiceCallGatewayCall(params: { gateway: MeetingVoiceCallGateway; callId: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"getMeetingVoiceCallGatewayCall","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"aac088df8e80d9ce89c45feccdb04ace3ae4417b3fcdd0f058410ddf25c96b46","declaration":"export function isMeetingVoiceCallMissingError(error: unknown): boolean;","entrypoint":"meeting-runtime","exportName":"isMeetingVoiceCallMissingError","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"9966beaf93d09474ccc549a9996d3e6f8660aef6d613b0f960f547fed434b387","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"f9e8b2ce4f61824ad9df637287186c40d3db05ca4823a288a36bd8a917267d64","declaration":"export function joinMeetingViaVoiceCallGateway(params: { config: MeetingVoiceCallConfig; gateway: MeetingVoiceCallGateway; surface: MeetingVoiceCallSurface; dialInNumber: string; dtmfSequence?: string; logger?: RuntimeLogger; message?: string; requesterSessionKey?: string; agentId?: string; sessionKey?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser(params: { adapter: BrowserAdapter; callBrowser: MeetingBrowserRequestCaller; launch: boolean; meetingSessionId?: string; meetingUrl: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise<{ left: boolean; note: string; }>;","entrypoint":"meeting-runtime","exportName":"leaveMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"c7b1aeac134b2070d053adf33026d876998b852ccd65cd2c9667119b1a1abee2","declaration":"export function openMeetingWithBrowser, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter; callBrowser: MeetingBrowserRequestCaller; config: MeetingBrowserControllerConfig; session: Session; }): Promise<{ launched: boolean; browser?: Health; tab?: MeetingBrowserTab; }>;","entrypoint":"meeting-runtime","exportName":"openMeetingWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"34858979bd248799c2215d9c7bdd5d4a6b27660c9173bfca2e50dd4373ac27db","declaration":"export function readMeetingBrowserTab(result: unknown): MeetingBrowserCandidateTab | undefined;","entrypoint":"meeting-runtime","exportName":"readMeetingBrowserTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"566d131fb9ec888fb5441ac6e7ae1623e8f5d12a2870d3fc94db607d57f70205","declaration":"export function readMeetingTranscriptWithBrowser(params: { adapter: BrowserAdapter; callBrowser: MeetingBrowserRequestCaller; finalize: boolean; meetingUrl: string; meetingSessionId: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise;","entrypoint":"meeting-runtime","exportName":"readMeetingTranscriptWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"0b42493cc0197096c4847c2d6e149c5581580abdda448d07eb1c6d77a775bc4a","declaration":"export function recoverMeetingBrowserTab(params: { adapter: BrowserAdapter; allowSessionAdoption?: boolean; autoJoin?: boolean; callBrowser: MeetingBrowserRequestCaller; captureCaptions?: boolean; config: MeetingBrowserControllerConfig; locationLabel: string; meetingSessionId?: string; mode: Mode; requestedMeetingUrl: string | undefined; readOnly?: boolean; timeoutMs?: number; trackedMeetingUrl: string | undefined; trackedTargetId: string | undefined; }): Promise<{ found: boolean; targetId?: string; tab?: MeetingBrowserCandidateTab; browser?: Health; message: string; }>;","entrypoint":"meeting-runtime","exportName":"recoverMeetingBrowserTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"8564816ab6a67bb7d293e1d401c4f1570ca1f3363db04646929e2e6b078219a5","declaration":"export function resolveLocalMeetingBrowserRequest(runtime: PluginRuntime): Promise;","entrypoint":"meeting-runtime","exportName":"resolveLocalMeetingBrowserRequest","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"80d2e3a407a2edb309f227a1926132d7157cca09d5bd13d43effdf61c87bda01","declaration":"export function resolveMeetingBrowserNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"56874c5b2ced5af647b1dff296c931f0d8b85b1318e98c741aa88b58a9c5c606","declaration":"export function resolveMeetingBrowserNodeInfo(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"da54392a9712ff0a2ee42a3fcd532e68ff7edac473f17a00d6965f9cde07b160","declaration":"export function resolveLocalMeetingBrowserRequest(runtime: PluginRuntime): Promise;","entrypoint":"meeting-runtime","exportName":"resolveLocalMeetingBrowserRequest","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"ed3f6883b6d8f1414f0e766a1cdb0294a9af099f450158b4b9dbc2e3dda622de","declaration":"export function resolveMeetingBrowserNode(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNode","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"95664d3db077437976f13b8729c5434e20bdafac6de8327207010087c9458c51","declaration":"export function resolveMeetingBrowserNodeInfo(params: { runtime: PluginRuntime; adapter: NodeAdapter; requestedNode?: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"f21196c9ea8ab90248ffdcdef3e942f1247ee2b9064ddb9757fffe352362c779","declaration":"export function resolveMeetingRealtimeAudioFormat(audioFormat: MeetingRealtimeAudioFormat): RealtimeVoiceAudioFormat;","entrypoint":"meeting-runtime","exportName":"resolveMeetingRealtimeAudioFormat","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"521dc5c262524a709cdd27cdc88e95004073da8223537bcbc73d5d423172dc2b","declaration":"export function speakMeetingViaVoiceCallGateway(params: { gateway: MeetingVoiceCallGateway; callId: string; message: string; }): Promise;","entrypoint":"meeting-runtime","exportName":"speakMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"bb7759749903f227ffde8ed14c505d8e29ecb8def9d32ad4f06cb9b0dc127d4f","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} -{"closureHash":"9762635b79f2497c6fe1d3ba79111a69e49ac1a5b46748b5e33731a0df7b3cba","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"4500474f3d7b3e9c83d148c60e3534eb083ae66b3eac38f0b7e10392e7200997","declaration":"export function startMeetingAgentRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeTranscriptionProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingAgentRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} +{"closureHash":"6f71ca02f3c80d864bd7c6a4dc29b6a1b9afe7424eae2e57601c9f88e02dfae3","declaration":"export function startMeetingRealtimeEngine(params: { config: MeetingRealtimeEngineConfig; fullConfig: OpenClawConfig; runtime: PluginRuntime; platform: MeetingRuntimePlatform; meetingSessionId: string; requesterSessionKey?: string; logPrefix?: \"node\"; talkSessionId?: string; talkContext?: { nodeId: string; bridgeId: string; }; transport: MeetingRealtimeAudioTransport; logger: RuntimeLogger; providers?: RealtimeVoiceProviderPlugin[]; consultAgent: (params: MeetingAgentConsultParams) => Promise<{ text: string; }>; tools: RealtimeVoiceTool[]; handleToolCall: (params: MeetingRealtimeToolCallParams) => Promise; }): Promise;","entrypoint":"meeting-runtime","exportName":"startMeetingRealtimeEngine","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"} {"closureHash":"663b9c77231b21af697efd5042e22d1c33e9ee972adeb312ad86d81a2b2e36f7","declaration":"export type MeetingAgentConsultParams = MeetingAgentConsultParams;","entrypoint":"meeting-runtime","exportName":"MeetingAgentConsultParams","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"adf0745c611bf737fc28978d766fc2a25b7104e71b6b2fed4e9b95e519c7b115","declaration":"export type MeetingBrowserCandidateTab = MeetingBrowserCandidateTab;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserCandidateTab","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"59b1526172b7491b85f14f13c1ee26d8d6726f33093770748580c767b1689a17","declaration":"export type MeetingBrowserControllerConfig = MeetingBrowserControllerConfig;","entrypoint":"meeting-runtime","exportName":"MeetingBrowserControllerConfig","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} @@ -3245,7 +3245,7 @@ {"closureHash":"8244b408803f57cf2004157e96f2ce11bb036cdf864123b15ba2fac7f03b5d43","declaration":"export type MeetingSessionRuntimeHandles = MeetingSessionRuntimeHandles;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages = MeetingSessionRuntimeMessages;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} -{"closureHash":"9d1dd094ee987d5333be26e2e99f53337907101c4da2f9b2ce36d5d16ce9ed77","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} +{"closureHash":"0bc52ea7f7d9ac4512e80911a3504280d3a41bae58631fe09a591dd91f118985","declaration":"export type MeetingSessionRuntimeOptions, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"d3c730cba02d30512c4cdae6e2367c662982e2c25671833ecd8701283924c642","declaration":"export type MeetingSessionState = MeetingSessionState;","entrypoint":"meeting-runtime","exportName":"MeetingSessionState","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"2496931157bf64fdb16013172ab9c023e60c5f85b98f46a3ee9532ce58d636d6","declaration":"export type MeetingSetupCheck = MeetingSetupCheck;","entrypoint":"meeting-runtime","exportName":"MeetingSetupCheck","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"f0aed6f7955a43fc387d8bbc072cb6276b0f2d18e7f829441a2ad45974bb3831","declaration":"export type MeetingSetupStatus = MeetingSetupStatus;","entrypoint":"meeting-runtime","exportName":"MeetingSetupStatus","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} @@ -3259,9 +3259,9 @@ {"closureHash":"20c70a94483aeddda4d1500cfcc5e71fab844b474ba4061c55d6c8693d40821b","declaration":"export type MeetingVoiceCallJoinResult = MeetingVoiceCallJoinResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallJoinResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"4fb2c5928aa6f809dd4c604d71c5c99cdfe3e98b6e9c1662601c9b4ad9738162","declaration":"export type MeetingVoiceCallStatusResult = MeetingVoiceCallStatusResult;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallStatusResult","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} {"closureHash":"5ef812397b6409a4ca228943dd1080661365959477b42e705a9aaa219757d14f","declaration":"export type MeetingVoiceCallSurface = MeetingVoiceCallSurface;","entrypoint":"meeting-runtime","exportName":"MeetingVoiceCallSurface","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"} -{"closureHash":"c9d32128207258f3c7043ae37f6fbe916a5be709f651ddf2165c728565b34ec2","declaration":"export interface MeetingPlatformAdapter extends MeetingPlatformAdapterContract {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} +{"closureHash":"15b888a20bea3b0304902ccc345c44bfb3b1b8c5b6c19c8a37596c6cb1f63c2d","declaration":"export interface MeetingPlatformAdapter extends MeetingPlatformAdapterContract {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} {"closureHash":"79884bd1df1ea0d07d5ceb4c62d93c1ee744b75d31c94c988845869c0f3cbd2d","declaration":"export interface MeetingRealtimeAudioTransport {\n onFatal(handler: () => void): void;\n startInput(onAudio: (audio: Buffer) => void): void;\n beginOutput?(): void;\n stop(): Promise;\n writeOutput(audio: Buffer): Promise;\n clearOutput(): Promise;\n dispose(): Promise;\n getHealth?(): MeetingRealtimeAudioTransportHealth;\n startBargeInMonitor?(onBargeIn: (audio: Buffer) => boolean): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingRealtimeAudioTransport","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"} -{"closureHash":"018156d8367dd84fd7101850a7fc11be5ada82a3f363d1fcddcfbf077bbf771c","declaration":"export class MeetingSessionRuntime, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"} +{"closureHash":"885d121a6dd595b0628a956f52f6e853542098b8f9c1c3f86fe22eff2e35a263","declaration":"export class MeetingSessionRuntime, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map;\n readonly #sessionLeaves: Map>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map Promise>;\n readonly #sessionSpeakers: Map void>;\n readonly #sessionHealth: Map Partial>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts;\n readonly #transcriptStore: MeetingSessionTranscriptStore;\n constructor(private readonly options: MeetingSessionRuntimeOptions);\n list(): TSession[];\n getSession(sessionId: string): TSession | undefined;\n async status(sessionId?: string): Promise<{\n found: boolean;\n session?: TSession;\n sessions?: TSession[];\n }>;\n async transcript(sessionId: string, options: {\n sinceIndex?: number;\n }): Promise<{ found: boolean; sessionId?: string; startIndex?: number; nextIndex?: number; droppedLines?: number; evicted?: boolean; lines?: MeetingTranscriptLine[]; }>;\n async startTranscriptSource(request: TranscriptStartRequest): Promise;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin): boolean;\n async join(request: TRequest): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async leave(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async speak(sessionId: string, instructions?: string): Promise<{\n found: boolean;\n spoken: boolean;\n session?: TSession;\n }>;\n async speakWhenReady(session: TSession, instructions: string): Promise;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise;\n async refreshCaptionHealth(session: TSession): Promise;\n refreshSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n markSessionEnded(session: TSession, reason: string): void;\n async #joinUnlocked(request: TRequest, resolved: MeetingResolvedJoin): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise>;\n #meetingKey(transport: TTransport, url: string): string;\n #inheritBrowserTabOwnership(params: {\n session: TSession;\n transport: TTransport;\n nodeId?: string;\n meetingUrl: string;\n tab?: TTab;\n }): TTab | undefined;\n async #settleRetainedBrowserTabs(retained: Array<{\n session: TSession;\n tab: TTab;\n }>, adopted?: {\n transport: TTransport;\n nodeId?: string;\n tab: TTab;\n }): Promise;\n async #rollbackFailedJoinSession(session: TSession): Promise;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles): void;\n #dropRuntimeHandles(sessionId: string): void;\n #isManagedBrowserSession(session: TSession): boolean;\n #evaluateSpeechReadiness(session: TSession): {\n ready: boolean;\n reason?: TSpeechBlockedReason;\n message?: string;\n };\n #noteSession(session: TSession, note: string): void;\n}","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"class","recordType":"export"} {"category":null,"entrypoint":"memory-core-host-engine-foundation","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","recordType":"module"} {"closureHash":"e2df8d235401aebf58eb71652fbeaedb42ca00aaa5042a76909ff2c48ba6fd54","declaration":"export function createSubsystemLogger(subsystem: string): SubsystemLogger;","entrypoint":"memory-core-host-engine-foundation","exportName":"createSubsystemLogger","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"} {"closureHash":null,"declaration":"export function isPathInside(root: string, target: string): boolean;","entrypoint":"memory-core-host-engine-foundation","exportName":"isPathInside","importSpecifier":"openclaw/plugin-sdk/memory-core-host-engine-foundation","kind":"function","recordType":"export"} @@ -3378,6 +3378,15 @@ {"closureHash":"fc15c20717489affd4ea19797f5e6167790114dd11e86d60f7f2c57b0a90fbe3","declaration":"export type PersistentDedupeLegacyPathOptions = PersistentDedupeLegacyPathOptions;","entrypoint":"persistent-dedupe","exportName":"PersistentDedupeLegacyPathOptions","importSpecifier":"openclaw/plugin-sdk/persistent-dedupe","kind":"type","recordType":"export"} {"closureHash":"e982df4834c8e5e2b45ccc4cde433658e190dcf2a7f3b0d2c700d4ef6eb1632d","declaration":"export type PersistentDedupeOptions = PersistentDedupeOptions;","entrypoint":"persistent-dedupe","exportName":"PersistentDedupeOptions","importSpecifier":"openclaw/plugin-sdk/persistent-dedupe","kind":"type","recordType":"export"} {"closureHash":"dfc56d38508426a6a78afd761e1a1288222c4467ffda62eb27d7eac30671bc21","declaration":"export type PersistentDedupePluginStateOptions = PersistentDedupePluginStateOptions;","entrypoint":"persistent-dedupe","exportName":"PersistentDedupePluginStateOptions","importSpecifier":"openclaw/plugin-sdk/persistent-dedupe","kind":"type","recordType":"export"} +{"category":"runtime","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","recordType":"module"} +{"closureHash":"e16134c3941adc06c6a9db07e64328620689e97d0d26ac7f0cbc34b1591b653f","declaration":"export function createPluginCommandRuntime(): PluginCommandRuntime;","entrypoint":"plugin-command-runtime","exportName":"createPluginCommandRuntime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"function","recordType":"export"} +{"closureHash":"00d993432f711b0223902a7d79b6da2e8ce785bac94b048d069ad8fcbe75db6f","declaration":"export const PLUGIN_COMMAND_DISPATCH: typeof PLUGIN_COMMAND_DISPATCH;","entrypoint":"plugin-command-runtime","exportName":"PLUGIN_COMMAND_DISPATCH","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"const","recordType":"export"} +{"closureHash":"40004d1dc7c5662023c1a0758067aae99f262717c23e5532271b60a7a0bb527c","declaration":"export type PluginCommandCatalogDecision = PluginCommandCatalogDecision;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandCatalogDecision","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} +{"closureHash":"0bad7c29c037dec5842f285ae9033bffcb8420494554ae44f38ea6bcf7a1205a","declaration":"export type PluginCommandDispatch = Readonly<{ kind: \"plugin\"; execute: (context: PluginCommandDispatchContext) => Promise; [pluginCommandDispatchBrand]: true;}>;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandDispatch","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} +{"closureHash":"0be75bcadf35030116e2f502f128cace3c1102a8ec429056c8bff25730f6e30e","declaration":"export type PluginCommandDispatchContext = Readonly<{ senderId?: string; channel: string; channelId?: PluginCommandContext[\"channelId\"]; isAuthorizedSender: boolean; senderIsOwner?: boolean; gatewayClientScopes?: PluginCommandContext[\"gatewayClientScopes\"]; agentId?: string; sessionKey?: PluginCommandContext[\"sessionKey\"]; sessionId?: PluginCommandContext[\"sessionId\"]; sessionTarget?: PluginCommandContext[\"sessionTarget\"]; sessionFile?: PluginCommandContext[\"sessionFile\"]; authProfileId?: string; commandBody: string; config: OpenClawConfig; from?: PluginCommandContext[\"from\"]; to?: PluginCommandContext[\"to\"]; originatingTo?: string; accountId?: PluginCommandContext[\"accountId\"]; messageThreadId?: PluginCommandContext[\"messageThreadId\"]; threadParentId?: PluginCommandContext[\"threadParentId\"]; diagnosticsSessions?: PluginCommandContext[\"diagnosticsSessions\"]; diagnosticsUploadApproved?: PluginCommandContext[\"diagnosticsUploadApproved\"]; diagnosticsPreviewOnly?: PluginCommandContext[\"diagnosticsPreviewOnly\"]; diagnosticsPrivateRouted?: PluginCommandContext[\"diagnosticsPrivateRouted\"];}>;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandDispatchContext","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} +{"closureHash":"a2f973143e14353450bfcadbc9a1f38c304f724988d09febc97a9b4782fcfcdc","declaration":"export type PluginCommandNativeCandidate = Readonly<{ name: string; description: string; descriptionLocalizations?: Readonly>; acceptsArgs: boolean; requireAuth: boolean; progressMessage?: string; prepareDispatch: (rawArgs?: string) => PluginCommandCatalogDecision;}>;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandNativeCandidate","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} +{"closureHash":"02fc8b5c9bcf5902ce275b0e26f7c219025d431dbecc4d9a605062b03904df3b","declaration":"export type PluginCommandReplyOptions = Readonly<{ [PLUGIN_COMMAND_DISPATCH]?: Readonly<{ kind: \"plugin\" | \"non-plugin\"; }>;}>;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandReplyOptions","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} +{"closureHash":"cf6a7ca36540f1bceef8e100926e781ddb7aa6662b9d2a792dd316161e09b3b2","declaration":"export type PluginCommandRuntime = Readonly<{ listNativeCandidates: (provider: string) => readonly PluginCommandNativeCandidate[]; retainNativeCatalog: (provider: string) => void;}>;","entrypoint":"plugin-command-runtime","exportName":"PluginCommandRuntime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime","kind":"type","recordType":"export"} {"category":null,"entrypoint":"plugin-config-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-config-runtime","recordType":"module"} {"closureHash":"5f7bee4e3a0121f16c56af8da4317eab4a9b874f72e1936866231412b1299d3e","declaration":"export function mergeDeep(base: unknown, override: unknown, options?: DeepMergeOptions): unknown;","entrypoint":"plugin-config-runtime","exportName":"mergeDeep","importSpecifier":"openclaw/plugin-sdk/plugin-config-runtime","kind":"function","recordType":"export"} {"closureHash":"605ebf4cd3d4d4028a5355b3b3520b99543cf4572b64afcf346b228c1f5cfab3","declaration":"export function requireRuntimeConfig(config: OpenClawConfig, context: string): OpenClawConfig;","entrypoint":"plugin-config-runtime","exportName":"requireRuntimeConfig","importSpecifier":"openclaw/plugin-sdk/plugin-config-runtime","kind":"function","recordType":"export"} @@ -3388,9 +3397,9 @@ {"category":"core","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","recordType":"module"} {"closureHash":"3d27406644141af2733cfca6e8c1240fc62327910109f585ce9073cafd7fd89c","declaration":"export function buildJsonPluginConfigSchema(schema: JsonSchemaObject, options?: BuildJsonPluginConfigSchemaOptions): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"buildJsonPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"} {"closureHash":"aff66c792d532642afb9c6e5a03c8d220f219250c3a8b49733f32058f22f98ca","declaration":"export function buildPluginConfigSchema(schema: ZodTypeAny, options?: BuildPluginConfigSchemaOptions): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"buildPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"} -{"closureHash":"3128153742c64b1132068d7b51a35bf22103c5ae2c82de42c8eca242c43d180a","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"} +{"closureHash":"de4da3c814c237aa1c66cb2f911beae164b8de9ff041572849c7a0a78f8df3a0","declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, reload, nodeHostCommands, securityAuditCollectors, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"} {"closureHash":"0ef1dfa3b5deeb456480e292c0e835c3cd6d13e7bb7059fc735546507da51594","declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export"} -{"closureHash":"3a9021d4c54eb31b36b05dbcf2cb32cbeb75f0d8c7a93ee1c4642a8f2d279cf5","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"plugin-entry","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} +{"closureHash":"8f10e2b53d9bdaf5142c404eb968670f225d2a9e368879ca6c399dbef95b6746","declaration":"export type AgentHarness = AgentHarness;","entrypoint":"plugin-entry","exportName":"AgentHarness","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"d2ff4de9ee3fd111c2cc64a3ee7e557d9f161af1b6b437ee7886bcb16e426317","declaration":"export type AgentPromptGuidance = AgentPromptGuidance;","entrypoint":"plugin-entry","exportName":"AgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"2841127a90e04467adfea8126cbf03655f44ddb089ad9e0d48833c2beea39cbd","declaration":"export type AgentPromptGuidanceEntry = AgentPromptGuidanceEntry;","entrypoint":"plugin-entry","exportName":"AgentPromptGuidanceEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"aa0412acefe62caff12e5271ead0545504874e824380dc21cfc0566388a05f33","declaration":"export type AgentPromptSurfaceKind = \"openclaw_main\" | \"pi_main\" | \"codex_app_server\" | \"cli_backend\" | \"acp_backend\" | \"subagent\";","entrypoint":"plugin-entry","exportName":"AgentPromptSurfaceKind","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} @@ -3400,16 +3409,16 @@ {"closureHash":"732f3c1b0e4942089773fad9da886d7b7bed41e7f1bb45d79c26deac15f6770b","declaration":"export type MigrationDetection = MigrationDetection;","entrypoint":"plugin-entry","exportName":"MigrationDetection","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"f40046e61c3037ec599a627e1088b8c78d1fe057d858e13cfaff1eab800e9f1f","declaration":"export type MigrationItem = MigrationItem;","entrypoint":"plugin-entry","exportName":"MigrationItem","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"934363b212df1e356365d44e0684401985228a2aeaf859aa13c840bae6e2190b","declaration":"export type MigrationPlan = MigrationPlan;","entrypoint":"plugin-entry","exportName":"MigrationPlan","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} -{"closureHash":"87647213976cb71438ffa3c38d8931a74c661df6f7e6278ee87a378f15e09748","declaration":"export type MigrationProviderContext = MigrationProviderContext;","entrypoint":"plugin-entry","exportName":"MigrationProviderContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} -{"closureHash":"0ad30e2b3042940eb8911ffe3a4d0e9801518b33d798a46e9788fd94c32fca8a","declaration":"export type MigrationProviderPlugin = MigrationProviderPlugin;","entrypoint":"plugin-entry","exportName":"MigrationProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} +{"closureHash":"19c530c24f4781929b8c11bce0454c6554e5775e89214dca24f4a8ec3d86243b","declaration":"export type MigrationProviderContext = MigrationProviderContext;","entrypoint":"plugin-entry","exportName":"MigrationProviderContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} +{"closureHash":"99c38f36f20fb23008f1086294e7526a1c35e398cf3faf717cd40299fdd6d218","declaration":"export type MigrationProviderPlugin = MigrationProviderPlugin;","entrypoint":"plugin-entry","exportName":"MigrationProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"cea9632d824ebf247f131958f8db4916ae60a37d4d5493e06f789d883dab6574","declaration":"export type MigrationSummary = MigrationSummary;","entrypoint":"plugin-entry","exportName":"MigrationSummary","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"8054ffbe71b91afced900ef8a06f5e368ac10c4f99ffc1c6f538a802856f61c6","declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"2f6e368fff73e942044cdf95c7c751b46c741e22ac0d2d4d97f45a3158f758ae","declaration":"export type OpenClawGatewayDiscoveryAdvertiseContext = OpenClawGatewayDiscoveryAdvertiseContext;","entrypoint":"plugin-entry","exportName":"OpenClawGatewayDiscoveryAdvertiseContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"b198058e0065e5c60b729a5bdf568f7e2436a006d8c4a7c63de764c7283f96ba","declaration":"export type OpenClawGatewayDiscoveryService = OpenClawGatewayDiscoveryService;","entrypoint":"plugin-entry","exportName":"OpenClawGatewayDiscoveryService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"69fefc8092c1cea366c48505c8423ff0f8f995caef3d259d177346b1687541aa","declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} -{"closureHash":"0ab866d1a087763f742ffcce26b644d2f1ac6deec6841ef2def1f4701484b354","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} +{"closureHash":"b17322e564d6fa98de2dcce9f6d5b58794438251f356fe59c0889046f675a07b","declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"a7ed83f0924f818b23cecb5a1f3591ff28b9499ccac9667a2b8505c2bc608ea9","declaration":"export type OpenClawPluginGatewayEventScope = OpenClawPluginGatewayEventScope;","entrypoint":"plugin-entry","exportName":"OpenClawPluginGatewayEventScope","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"f81f224d9b837e6353b7a546c5a76b124bcf568fd1f1fd0c6881fda30c53120a","declaration":"export type OpenClawPluginGatewayEvents = OpenClawPluginGatewayEvents;","entrypoint":"plugin-entry","exportName":"OpenClawPluginGatewayEvents","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} {"closureHash":"7470b211ac4d3449cd0fc353ee2eb48e9fbc2dbc9259d21c680d0203577aa94f","declaration":"export type OpenClawPluginHttpRouteHandler = OpenClawPluginHttpRouteHandler;","entrypoint":"plugin-entry","exportName":"OpenClawPluginHttpRouteHandler","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export"} @@ -3558,20 +3567,20 @@ {"closureHash":"5e57188ea41861c5108f8a07d6a2a1902ced52b5f1708a70d702dc4ba3ce5e0b","declaration":"export function executePluginCommand(params: { command: RegisteredPluginCommand; args?: string; senderId?: string; channel: string; channelId?: PluginCommandContext[\"channelId\"]; isAuthorizedSender: boolean; senderIsOwner?: boolean; gatewayClientScopes?: PluginCommandContext[\"gatewayClientScopes\"]; agentId?: string; sessionKey?: PluginCommandContext[\"sessionKey\"]; sessionId?: PluginCommandContext[\"sessionId\"]; sessionTarget?: PluginCommandContext[\"sessionTarget\"]; sessionFile?: PluginCommandContext[\"sessionFile\"]; authProfileId?: string; commandBody: string; config: OpenClawConfig; from?: PluginCommandContext[\"from\"]; to?: PluginCommandContext[\"to\"]; originatingTo?: string; accountId?: PluginCommandContext[\"accountId\"]; messageThreadId?: PluginCommandContext[\"messageThreadId\"]; threadParentId?: PluginCommandContext[\"threadParentId\"]; diagnosticsSessions?: PluginCommandContext[\"diagnosticsSessions\"]; diagnosticsUploadApproved?: PluginCommandContext[\"diagnosticsUploadApproved\"]; diagnosticsPreviewOnly?: PluginCommandContext[\"diagnosticsPreviewOnly\"]; diagnosticsPrivateRouted?: PluginCommandContext[\"diagnosticsPrivateRouted\"]; }): Promise;","entrypoint":"plugin-runtime","exportName":"executePluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"7e75fc04d621dc8d12f459d5b5b0c039a39b77c30c2b51af481e123c352f57b8","declaration":"export function getGlobalHookRunner(): HookRunner | null;","entrypoint":"plugin-runtime","exportName":"getGlobalHookRunner","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"db6583607237e4081814e2c5d4f8fb6f216ad3bbfe623bf16fc1a9c09c15f64b","declaration":"export function getPluginCommandSpecs(provider?: string, options?: PluginCommandSpecOptions): Array<{ name: string; description: string; descriptionLocalizations?: Record; acceptsArgs: boolean; }>;","entrypoint":"plugin-runtime","exportName":"getPluginCommandSpecs","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} -{"closureHash":"116a44ac149d34212a440274db168905fb85a7252b5e94fa8d3ef7543ab3c242","declaration":"export function getPluginRuntimeGatewayRequestScope(): PluginRuntimeGatewayRequestScope | undefined;","entrypoint":"plugin-runtime","exportName":"getPluginRuntimeGatewayRequestScope","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} +{"closureHash":"e03d09bdacb32f4dbeef1de2c8d6d832fbdbc75417d8cc645922af43ccec6111","declaration":"export function getPluginRuntimeGatewayRequestScope(): PluginRuntimeGatewayRequestScope | undefined;","entrypoint":"plugin-runtime","exportName":"getPluginRuntimeGatewayRequestScope","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"cfe277ce6de0c6ed50f1f54b66c889f6c95e91b058e8bcdc27aa37317a93a447","declaration":"export function listRegisteredPluginAgentPromptGuidance(params?: { surface?: AgentPromptSurfaceKind; includeLegacyGlobalGuidance?: boolean; }): string[];","entrypoint":"plugin-runtime","exportName":"listRegisteredPluginAgentPromptGuidance","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"ef4872995ac1acbe1b2046397e3ddc5d9a4315cb0bd0dfddfac6222418e7c048","declaration":"export function matchPluginCommand(commandBody: string, options?: { channel?: string; }): { command: RegisteredPluginCommand; args?: string; } | null;","entrypoint":"plugin-runtime","exportName":"matchPluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"e02d3651398ab23c38e91a52fc1502e7a97490f17047b4f558b87c94731b0344","declaration":"export function registerPluginCommand(pluginId: string, command: OpenClawPluginCommandDefinition, opts?: { pluginName?: string; pluginRoot?: string; allowReservedCommandNames?: boolean; allowOwnerStatusExposure?: boolean; }): CommandRegistrationResult;","entrypoint":"plugin-runtime","exportName":"registerPluginCommand","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"16986e5d431f584c2d5053289eca3d2225bbe47a92cdfbbc2af91b264a165846","declaration":"export function registerPluginInteractiveHandler(pluginId: string, registration: PluginInteractiveHandlerRegistration, opts?: { pluginName?: string; pluginRoot?: string; }): InteractiveRegistrationResult;","entrypoint":"plugin-runtime","exportName":"registerPluginInteractiveHandler","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"f20c07a48e2c949fbc889e7a32760f4e52339d9f6d3eab49e00ff249266e2396","declaration":"export function startLazyPluginServiceModule(params: { skipEnvVar?: string; overrideEnvVar?: string; validateOverrideSpecifier?: (specifier: string) => string; loadDefaultModule: () => Promise; loadOverrideModule?: (specifier: string) => Promise; startExportNames: string[]; stopExportNames?: string[]; }): Promise;","entrypoint":"plugin-runtime","exportName":"startLazyPluginServiceModule","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"function","recordType":"export"} {"closureHash":"bad43ffe69d1d9d6c92c79df4c388b3dda685013a92392daef456f1622ade659","declaration":"export type LazyPluginServiceHandle = LazyPluginServiceHandle;","entrypoint":"plugin-runtime","exportName":"LazyPluginServiceHandle","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} -{"closureHash":"d1692cf0fa81307282fc36f6b027869c880111f6a80caa519b8903193ffc600b","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} +{"closureHash":"45b7846051dab3e383af64f2bb37ec89ca04e9d2884b1df233a7bfd4f427ccaa","declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"365beb5cee974639c2f35fc7a39088721ca8e9c618442830298b98033e9c768c","declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-runtime","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"f06379ada10ce0eca4e66bdf978db2c0035e054a135dc044c6d831ac7797c08f","declaration":"export type PluginConversationBinding = PluginConversationBinding;","entrypoint":"plugin-runtime","exportName":"PluginConversationBinding","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"a1223d674b2b66918a576ae05b04318d7b8cbe9b1d3bc6dd7a08d1a4fa79b291","declaration":"export type PluginConversationBindingRequestParams = PluginConversationBindingRequestParams;","entrypoint":"plugin-runtime","exportName":"PluginConversationBindingRequestParams","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"93c034724a937bb1239a3b4a386a20ac75fbcf66229842cc4c702c3360409e4c","declaration":"export type PluginConversationBindingRequestResult = PluginConversationBindingRequestResult;","entrypoint":"plugin-runtime","exportName":"PluginConversationBindingRequestResult","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"590c148a7b82d14242a172bee5027840ddd5366025200e48704ef4ae8f4bf0ee","declaration":"export type PluginInteractiveRegistration = PluginInteractiveRegistration;","entrypoint":"plugin-runtime","exportName":"PluginInteractiveRegistration","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"plugin-runtime","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"plugin-runtime","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"closureHash":"f16f4b33d2dd78e5d5da7b1aaeee0441ab35cd232b032e8a33219def7cf65f0f","declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"plugin-runtime","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk/plugin-runtime","kind":"type","recordType":"export"} {"category":null,"entrypoint":"provider-auth","importSpecifier":"openclaw/plugin-sdk/provider-auth","recordType":"module"} {"closureHash":"d5f31c750f1d545b3a51177ba4a008d6cf646165a8fc0c0c1328fa9999b8d6e2","declaration":"export function applyAuthProfileConfig(cfg: OpenClawConfig, params: { profileId: string; provider: string; mode: \"api_key\" | \"aws-sdk\" | \"oauth\" | \"token\"; email?: string; displayName?: string; preferProfileFirst?: boolean; }): OpenClawConfig;","entrypoint":"provider-auth","exportName":"applyAuthProfileConfig","importSpecifier":"openclaw/plugin-sdk/provider-auth","kind":"function","recordType":"export"} @@ -3655,10 +3664,10 @@ {"closureHash":"e66adcb55dec79be73a52bc76594b7e9210fd93c69a85b56098fbb58cecbb354","declaration":"export type WriteOAuthCredentialsOptions = WriteOAuthCredentialsOptions;","entrypoint":"provider-auth","exportName":"WriteOAuthCredentialsOptions","importSpecifier":"openclaw/plugin-sdk/provider-auth","kind":"type","recordType":"export"} {"category":null,"entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","recordType":"module"} {"closureHash":"b5665596a3f011454a5614681513516acd50b6c749536c43842d811980cf7ad3","declaration":"export function augmentModelCatalogWithProviderPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; metadataSnapshot?: PluginMetadataSnapshot; context: ProviderAugmentModelCatalogContext; }): Promise;","entrypoint":"provider-catalog-runtime","exportName":"augmentModelCatalogWithProviderPlugins","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"addf95f7299a3a52692c3880ce309712a712d73e790cd003f02b887afab20d7c","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"b7501b7de01153c0536e5ec1799e56a5ef42897d47221009d90404824a40c445","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"680aed9f4a22b75e18e149062a923fba358890d092a29bec8c889ca5e379f0c2","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} -{"closureHash":"4411f7db770c8a9d9bb45968c8e5621ac840192725ad8470d80cd6e40871ee60","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"bd46fa62ca95a7a993353fd0a896e8c5d1a82cb1a98f7f7db5e962e02561ad79","declaration":"export function isPluginProvidersLoadInFlight(params: Parameters[0]): boolean;","entrypoint":"provider-catalog-runtime","exportName":"isPluginProvidersLoadInFlight","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"8235f75dcb3f68b509423228b65b877cdeef44230ee60416178d31134277b37b","declaration":"export function resolveCatalogHookProviderPluginIds(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; metadataSnapshot?: ProviderManifestLoadParams[\"metadataSnapshot\"]; }): string[];","entrypoint":"provider-catalog-runtime","exportName":"resolveCatalogHookProviderPluginIds","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"666150e78e4bcc4b04d2b68d0596a685ec176d7c5c6e8f6327d5f1039979db81","declaration":"export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: Pick; }): string[] | undefined;","entrypoint":"provider-catalog-runtime","exportName":"resolveOwningPluginIdsForProvider","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} +{"closureHash":"a3016a8e22aef2df4d1d22d96a875d337ea05e40e6f55dbcfc5115a6cc25d726","declaration":"export function resolvePluginProviders(params: { config?: PluginLoadOptions[\"config\"]; workspaceDir?: string; env?: PluginLoadOptions[\"env\"]; bundledProviderVitestCompat?: boolean; onlyPluginIds?: string[]; providerRefs?: readonly string[]; modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; applyAutoEnable?: boolean; pluginSdkResolution?: PluginLoadOptions[\"pluginSdkResolution\"]; mode?: \"runtime\" | \"setup\"; includeUntrustedWorkspacePlugins?: boolean; pluginMetadataSnapshot?: PluginMetadataRegistryView; skipIfLoadInFlight?: boolean; }): ProviderPlugin[];","entrypoint":"provider-catalog-runtime","exportName":"resolvePluginProviders","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime","kind":"function","recordType":"export"} {"category":null,"entrypoint":"proxy-capture","importSpecifier":"openclaw/plugin-sdk/proxy-capture","recordType":"module"} {"closureHash":"d0e4e026d736bc00b1758d1089c44c3c5f3a09c7254291584de8cb297365804c","declaration":"export function acquireDebugProxyCaptureStore(dbPath: string, blobDir: string): { store: LegacyDebugProxyCaptureStore; release: () => void; };\nexport function acquireDebugProxyCaptureStore(options?: DebugProxyCaptureStoreOptions): { store: SharedDebugProxyCaptureStore; release: () => void; };","entrypoint":"proxy-capture","exportName":"acquireDebugProxyCaptureStore","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"} {"closureHash":"d41cbb9de51b7aee1c6cf8c713b0bc44f27ac926fbb6422c05726f196d11964e","declaration":"export function captureHttpExchange(params: { url: string; method: string; requestHeaders?: Headers | Record | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record; }, resolved?: DebugProxySettings, deps?: DebugProxyCaptureRuntimeDeps): void;","entrypoint":"proxy-capture","exportName":"captureHttpExchange","importSpecifier":"openclaw/plugin-sdk/proxy-capture","kind":"function","recordType":"export"} @@ -3694,11 +3703,11 @@ {"closureHash":"d75cb355b23e89fd45f6948ffa5df92b066be54cfd71e78f7db7a3419bfa7114","declaration":"export function finalizeInboundContext>(ctx: T, opts?: FinalizeInboundContextOptions): T & FinalizedMsgContext & CanonicalInboundText;","entrypoint":"reply-dispatch-runtime","exportName":"finalizeInboundContext","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"function","recordType":"export"} {"closureHash":"b3f4adaa19f9703051c85d1f567f8f01e7ba8ca4444c826d3cf9c60bf2f8dbec","declaration":"export function generateConversationLabel(params: ConversationLabelParams): Promise;","entrypoint":"reply-dispatch-runtime","exportName":"generateConversationLabel","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"function","recordType":"export"} {"closureHash":"00b39d28eb3338c9356418e4914c4d8a1bee35e3b160f12ad9bd434912055da1","declaration":"export function resolveChunkMode(cfg: OpenClawConfig | undefined, provider?: TextChunkProvider, accountId?: string | null): ChunkMode;","entrypoint":"reply-dispatch-runtime","exportName":"resolveChunkMode","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"function","recordType":"export"} -{"closureHash":"ee80cc7c76638e2cf0865b8994a972d702a3a04429da3e819f3b88c286cb53a7","declaration":"export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"dispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"const","recordType":"export"} -{"closureHash":"3ca90129919db190bc08d8739a4a034691d51dc3483f0f25490b8d0eac260ab2","declaration":"export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"dispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"const","recordType":"export"} +{"closureHash":"bdbc9917db0e39f393db3b92c8f86bf5f541a4418d219c7d7f336f30633ca479","declaration":"export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"dispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"const","recordType":"export"} +{"closureHash":"4254267e28d9275fa94ed751b5b732e400558718c8a0ad4c746cbf04ebfb47f2","declaration":"export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"dispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"const","recordType":"export"} {"closureHash":"6c907ee25b775b850dfb0961e1505f88e85159971379eb0e00d5eea51b284635","declaration":"export type CommandTurnContext = CommandTurnContext;","entrypoint":"reply-dispatch-runtime","exportName":"CommandTurnContext","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} -{"closureHash":"d74619569354e0f74674a89ed7260dc188029bbe0d65dba460761c6477339d6e","declaration":"export type DispatchReplyWithBufferedBlockDispatcher = DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"DispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} -{"closureHash":"04baa1f0ff8a9d45cd5260d45f39b8096b765ddbe0cea862c74182d18f8333a0","declaration":"export type DispatchReplyWithDispatcher = DispatchReplyWithDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"DispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} +{"closureHash":"8bc39626b8ceb9b425258f71d21c4b444c43d300466b4ce07bd11767b58e032e","declaration":"export type DispatchReplyWithBufferedBlockDispatcher = DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"DispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} +{"closureHash":"aac7b6b4b06544484a628bfa599b41958bc1e5fe08b0b7497efba6a6a3b75ef0","declaration":"export type DispatchReplyWithDispatcher = DispatchReplyWithDispatcher;","entrypoint":"reply-dispatch-runtime","exportName":"DispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} {"closureHash":"0d3bf245385d3b70f6d85138ad89f9bbe9c43df378b27c18c6f8168ad7caec93","declaration":"export type ReplyPayload = ReplyPayload;","entrypoint":"reply-dispatch-runtime","exportName":"ReplyPayload","importSpecifier":"openclaw/plugin-sdk/reply-dispatch-runtime","kind":"type","recordType":"export"} {"category":null,"entrypoint":"reply-history","importSpecifier":"openclaw/plugin-sdk/reply-history","recordType":"module"} {"closureHash":"e903bc1c38ce4fc1b15cec63e7355300c4ce71783fc766da7cf2dd18a9de5fc7","declaration":"export function buildHistoryContext(params: { historyText: string; currentMessage: string; lineBreak?: string; }): string;","entrypoint":"reply-history","exportName":"buildHistoryContext","importSpecifier":"openclaw/plugin-sdk/reply-history","kind":"function","recordType":"export"} @@ -3765,9 +3774,9 @@ {"closureHash":"a29840f2fa36fb88ccf967b5b3a1e6a7d3a113310ad63daa878dd907bb1fe316","declaration":"export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDispatcher;","entrypoint":"reply-runtime","exportName":"createReplyDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} {"closureHash":"2e17e0e3d7d0cb9f2c28eac997b6ad209055aa9865de07590403f75bab059ffc","declaration":"export function createReplyDispatcherWithTyping(options: ReplyDispatcherWithTypingOptions): ReplyDispatcherWithTypingResult;","entrypoint":"reply-runtime","exportName":"createReplyDispatcherWithTyping","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} {"closureHash":"0278a79e0d38060a68e7a421fe438d438964fd3516146c2361bdb771e22bad8c","declaration":"export function createReplyReferencePlanner(options: { replyToMode: ReplyToMode; existingId?: string; startId?: string; allowReference?: boolean; hasReplied?: boolean; }): ReplyReferencePlanner;","entrypoint":"reply-runtime","exportName":"createReplyReferencePlanner","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} -{"closureHash":"3306f2d288e1014be61620fe6d0abcfbdf5ec288432ca65f3786ffbc63a69bca","declaration":"export function dispatchInboundMessage(params: { ctx: MsgContext | FinalizedMsgContext; cfg: OpenClawConfig; dispatcher: ReplyDispatcher; toolsAllow?: string[]; replyOptions?: InternalDispatchReplyOptions; replyResolver?: InternalGetReplyFromConfig; onSessionMetadataChanges?: (changes: CommandSessionMetadataChange[]) => void; replyPayloadRunState?: ReplyPayloadRunState; outboundHooks?: \"enabled\" | \"disabled\"; onSettled?: () => void | Promise; }): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessage","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} -{"closureHash":"491045cedc08b713771d0546174b0ae54f63992fa505cb830e1190dc3a6d9943","declaration":"export function dispatchInboundMessageWithBufferedDispatcher(params: BufferedInboundDispatcherParams): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessageWithBufferedDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} -{"closureHash":"e966edabd815b8d708b74b4866a05910821a1aab647b9c7d16bdbbb72366cc73","declaration":"export function dispatchInboundMessageWithDispatcher(params: { ctx: MsgContext | FinalizedMsgContext; cfg: OpenClawConfig; dispatcherOptions: ReplyDispatcherOptions; toolsAllow?: string[]; replyOptions?: InternalDispatchReplyOptions; replyResolver?: InternalGetReplyFromConfig; }): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessageWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} +{"closureHash":"246e10fe145e9c45e1bd6da2b0b6322ad5cb3841a6b5542fe552eb767ecf45a6","declaration":"export function dispatchInboundMessage(params: { ctx: MsgContext | FinalizedMsgContext; cfg: OpenClawConfig; dispatcher: ReplyDispatcher; toolsAllow?: string[]; replyOptions?: InternalDispatchReplyOptions; replyResolver?: InternalGetReplyFromConfig; onSessionMetadataChanges?: (changes: CommandSessionMetadataChange[]) => void; replyPayloadRunState?: ReplyPayloadRunState; outboundHooks?: \"enabled\" | \"disabled\"; onSettled?: () => void | Promise; }): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessage","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} +{"closureHash":"7e381c648547325fa1411a95399241208425027e0146ec663b8457f24dc2a33d","declaration":"export function dispatchInboundMessageWithBufferedDispatcher(params: BufferedInboundDispatcherParams): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessageWithBufferedDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} +{"closureHash":"ceaca0046c6cb3397b5aeacfa2d3108cbd206c67c693927c32208b81311b3cfb","declaration":"export function dispatchInboundMessageWithDispatcher(params: { ctx: MsgContext | FinalizedMsgContext; cfg: OpenClawConfig; dispatcherOptions: ReplyDispatcherOptions; toolsAllow?: string[]; replyOptions?: InternalDispatchReplyOptions; replyResolver?: InternalGetReplyFromConfig; }): Promise;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessageWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} {"closureHash":"d75cb355b23e89fd45f6948ffa5df92b066be54cfd71e78f7db7a3419bfa7114","declaration":"export function finalizeInboundContext>(ctx: T, opts?: FinalizeInboundContextOptions): T & FinalizedMsgContext & CanonicalInboundText;","entrypoint":"reply-runtime","exportName":"finalizeInboundContext","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} {"closureHash":"b3f4adaa19f9703051c85d1f567f8f01e7ba8ca4444c826d3cf9c60bf2f8dbec","declaration":"export function generateConversationLabel(params: ConversationLabelParams): Promise;","entrypoint":"reply-runtime","exportName":"generateConversationLabel","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} {"closureHash":"e2aa6fe2024c51101e29a70d1fbdc9b35ef7f70990b18955f4de64f4ab2d4a43","declaration":"export function getReplyFromConfig(ctx: MsgContext, opts?: GetReplyOptions, configOverride?: OpenClawConfig): Promise;","entrypoint":"reply-runtime","exportName":"getReplyFromConfig","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"} @@ -3788,8 +3797,8 @@ {"closureHash":"be781caf196c49db72b18eaf346d967c09cc0fe0c410496f2bfc1bf2f6f8eef7","declaration":"export const HEARTBEAT_PROMPT: \"Follow the heartbeat monitor scratch context when provided. Recurring tasks are automations; create or change their schedules with the automations tool, not heartbeat scratch. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.\";","entrypoint":"reply-runtime","exportName":"HEARTBEAT_PROMPT","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} {"closureHash":"d6230a363d069736f57476bc7371b93fe8f68f57a4edc3bdff025c7669629743","declaration":"export const HEARTBEAT_TOKEN: \"HEARTBEAT_OK\";","entrypoint":"reply-runtime","exportName":"HEARTBEAT_TOKEN","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} {"closureHash":"c8d9faa79ecc7382a0a292d949fea76b3ee2a6308b0fd7d87da84d7847ed805b","declaration":"export const SILENT_REPLY_TOKEN: \"NO_REPLY\";","entrypoint":"reply-runtime","exportName":"SILENT_REPLY_TOKEN","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} -{"closureHash":"ee80cc7c76638e2cf0865b8994a972d702a3a04429da3e819f3b88c286cb53a7","declaration":"export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-runtime","exportName":"dispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} -{"closureHash":"3ca90129919db190bc08d8739a4a034691d51dc3483f0f25490b8d0eac260ab2","declaration":"export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher;","entrypoint":"reply-runtime","exportName":"dispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} +{"closureHash":"bdbc9917db0e39f393db3b92c8f86bf5f541a4418d219c7d7f336f30633ca479","declaration":"export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher;","entrypoint":"reply-runtime","exportName":"dispatchReplyWithBufferedBlockDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} +{"closureHash":"4254267e28d9275fa94ed751b5b732e400558718c8a0ad4c746cbf04ebfb47f2","declaration":"export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher;","entrypoint":"reply-runtime","exportName":"dispatchReplyWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"const","recordType":"export"} {"closureHash":"637e2c34a20cf4a423372af22cd046bde2dd8aebc042835882f9cc15cdde8572","declaration":"export type BlockReplyContext = BlockReplyContext;","entrypoint":"reply-runtime","exportName":"BlockReplyContext","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"type","recordType":"export"} {"closureHash":"abd46ad532e63ff006c75f1e73913aaefe11578e4ba7237ca7683526cae150bd","declaration":"export type ChannelStructuredContextEntry = ChannelStructuredContextEntry;","entrypoint":"reply-runtime","exportName":"ChannelStructuredContextEntry","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"type","recordType":"export"} {"closureHash":"4c544030d69a87c7592778a4063380509d30ed980ea58d45eb08bb7d34803166","declaration":"export type ChunkMode = ChunkMode;","entrypoint":"reply-runtime","exportName":"ChunkMode","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"type","recordType":"export"} @@ -3907,7 +3916,7 @@ {"closureHash":"47c7a11095835696862fe7b72fccf28736af6308347543c26b5b2f538d76860b","declaration":"export const GROUP_POLICY_BLOCKED_LABEL: { readonly group: \"group messages\"; readonly guild: \"guild messages\"; readonly room: \"room messages\"; readonly channel: \"channel messages\"; readonly space: \"space messages\";};","entrypoint":"runtime-group-policy","exportName":"GROUP_POLICY_BLOCKED_LABEL","importSpecifier":"openclaw/plugin-sdk/runtime-group-policy","kind":"const","recordType":"export"} {"category":"runtime","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store","recordType":"module"} {"closureHash":"e11d2c2694b927c79b6e07789be6bb56269d5ef1ec95b395a6e9c46f43883cbc","declaration":"export function createPluginRuntimeStore(errorMessage: string): { setRuntime: (next: T) => void; clearRuntime: () => void; tryGetRuntime: () => T | null; getRuntime: () => T; };\nexport function createPluginRuntimeStore(options: PluginRuntimeStoreOptions): { setRuntime: (next: T) => void; clearRuntime: () => void; tryGetRuntime: () => T | null; getRuntime: () => T; };","entrypoint":"runtime-store","exportName":"createPluginRuntimeStore","importSpecifier":"openclaw/plugin-sdk/runtime-store","kind":"function","recordType":"export"} -{"closureHash":"3203c47bc265736977eb102837cf12d2fe8f8dc459389c21f319d9426d83dc8e","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"runtime-store","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/runtime-store","kind":"type","recordType":"export"} +{"closureHash":"9d00622b1392033196b86c5856c8bb3e7da3c29acf03e01f179f26f138cf70ff","declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"runtime-store","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/runtime-store","kind":"type","recordType":"export"} {"category":null,"entrypoint":"secret-file","importSpecifier":"openclaw/plugin-sdk/secret-file","recordType":"module"} {"closureHash":null,"declaration":"export function createSecretFileAtomic(params: SecretFileWriteParams): Promise;","entrypoint":"secret-file","exportName":"createSecretFileAtomic","importSpecifier":"openclaw/plugin-sdk/secret-file","kind":"function","recordType":"export"} {"closureHash":null,"declaration":"export function readSecretFile(filePath: string, label: string, options?: SecretFileReadOptions): Promise;","entrypoint":"secret-file","exportName":"readSecretFile","importSpecifier":"openclaw/plugin-sdk/secret-file","kind":"function","recordType":"export"} @@ -4018,8 +4027,8 @@ {"closureHash":"a7a32cdc228b5a4b027448fe2e79e332d7b5c5ff50f2afdcb9684bf8eb3e1fce","declaration":"export function deleteSessionUpstreamLink(sessionKey: string, agentId: string, options?: OpenClawStateDatabaseOptions): void;","entrypoint":"session-catalog","exportName":"deleteSessionUpstreamLink","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} {"closureHash":"194cb8ed04679bd319517a0e228373ac6da06736200610a11a4d274339c92f87","declaration":"export function importSessionCatalogHistory(params: { catalogId: string; threadId: string; read: (params: { cursor?: string; limit: number; }) => Promise; sessionId: string; sessionKey: string; agentId: string; cwd?: string; config: OpenClawConfig; }): Promise;","entrypoint":"session-catalog","exportName":"importSessionCatalogHistory","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} {"closureHash":"fb9a9c6a327c81eb3ccfb3550b0d6a3c4b62d7db43f5c248d41ff2abecf0bbc2","declaration":"export function isExternalUserText(probe: SessionUpstreamProbe, text: string | undefined): boolean;","entrypoint":"session-catalog","exportName":"isExternalUserText","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} -{"closureHash":"7fe41161d2b3214e7959f025a026196a2523491b297dd22c918661b48a65699b","declaration":"export function listAdoptedSessionCatalogSessions(params: { config: OpenClawConfig; pluginId: string; runtime: PluginRuntime; sessionEntries?: SessionCatalogEntrySnapshot; sourceFromEntry: (entry: SessionCatalogEntry) => SessionCatalogAdoptedSource | undefined; }): Map;","entrypoint":"session-catalog","exportName":"listAdoptedSessionCatalogSessions","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} -{"closureHash":"30c298ed1ede10f28e844f3c0778a6e47e884fd85317f0baed87dbb86cd20624","declaration":"export function listSessionCatalogEntries(params: { config: OpenClawConfig; runtime: PluginRuntime; sessionEntries?: SessionCatalogEntrySnapshot; }): SessionCatalogAgentEntry[];","entrypoint":"session-catalog","exportName":"listSessionCatalogEntries","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} +{"closureHash":"18f92bbd038288b570f37fd0b7b827adc53fd2233c9c56c16a962e611e575f03","declaration":"export function listAdoptedSessionCatalogSessions(params: { config: OpenClawConfig; pluginId: string; runtime: PluginRuntime; sessionEntries?: SessionCatalogEntrySnapshot; sourceFromEntry: (entry: SessionCatalogEntry) => SessionCatalogAdoptedSource | undefined; }): Map;","entrypoint":"session-catalog","exportName":"listAdoptedSessionCatalogSessions","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} +{"closureHash":"6e6831908e0d4a0c4b94d0316ea44d047fdc6d09b24e4efaa674932ad5f06f70","declaration":"export function listSessionCatalogEntries(params: { config: OpenClawConfig; runtime: PluginRuntime; sessionEntries?: SessionCatalogEntrySnapshot; }): SessionCatalogAgentEntry[];","entrypoint":"session-catalog","exportName":"listSessionCatalogEntries","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} {"closureHash":"bcb164340d2d8ff229bd4bb7c68126cfc44cfb171a76f363ef7c78c1cdf16f37","declaration":"export function normalizeUserText(text: string): string;","entrypoint":"session-catalog","exportName":"normalizeUserText","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} {"closureHash":"7fdcd1b85ee1e56ea8faf0afba025d3a9be0a34a8b7fda9f5f9ead922729ecdc","declaration":"export function sessionCatalogAdoptedSessionKey(prefix: string, source: string): string;","entrypoint":"session-catalog","exportName":"sessionCatalogAdoptedSessionKey","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} {"closureHash":"1e09ce9731a5b5932ec01307e90f9ab93ec0a100616a2a6f3b6416b768e5170b","declaration":"export function sessionCatalogAdoptedSourceKey(hostId: string, threadId: string): string;","entrypoint":"session-catalog","exportName":"sessionCatalogAdoptedSourceKey","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"function","recordType":"export"} @@ -4033,9 +4042,9 @@ {"closureHash":"1b1c2b933616e206af0fa0f9d5f50435c041013b9699e19891c1117af32f2d42","declaration":"export type SessionCatalogDescriptor = { id: string; capabilities: { createSession?: { startTerminal?: boolean | undefined; model: string; } | undefined; openTerminal?: boolean | undefined; archive: boolean; continueSession: boolean; }; label: string;};","entrypoint":"session-catalog","exportName":"SessionCatalogDescriptor","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"1c83786a73f37881bd0202129ee916e9d0204393584da802152ba83f5fc6d1ec","declaration":"export type SessionCatalogEntrySnapshot = SessionCatalogEntrySnapshot;","entrypoint":"session-catalog","exportName":"SessionCatalogEntrySnapshot","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"7761b7c4cccd9df93de1309fe9544bc17ed08fe87f1d833d621f335d52a78512","declaration":"export type SessionCatalogHost = { error?: { message: string; code: string; } | undefined; nodeId?: string | undefined; nextCursor?: string | undefined; sessions: { source?: string | undefined; name?: string | undefined; cwd?: string | undefined; sessionKey?: string | undefined; updatedAt?: number | undefined; createdActor?: { id?: string | undefined; label?: string | undefined; avatarUrl?: string | undefined; type: \"system\" | \"agent\" | \"human\"; } | undefined; createdAt?: number | undefined; modelProvider?: string | undefined; recencyAt?: number | undefined; cliVersion?: string | undefined; gitBranch?: string | undefined; customGroup?: string | undefined; pullRequest?: { state: \"open\" | \"draft\" | \"closed\" | \"merged\"; numbers: number[]; } | undefined; canOpenTerminal?: boolean | undefined; status: string; archived: boolean; threadId: string; canContinue: boolean; canArchive: boolean; }[]; kind: \"gateway\" | \"node\"; label: string; hostId: string; connected: boolean;};","entrypoint":"session-catalog","exportName":"SessionCatalogHost","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} -{"closureHash":"d331e916f9677a1bafa206a9788a87dcfb674bec04e96ddb8f096da6f907a4b5","declaration":"export type SessionCatalogListProviderParams = SessionCatalogListProviderParams;","entrypoint":"session-catalog","exportName":"SessionCatalogListProviderParams","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} +{"closureHash":"e233a83c7d7f8e59db3136018a0d7ff361afde0606ef00a4e977fc3d4342f22f","declaration":"export type SessionCatalogListProviderParams = SessionCatalogListProviderParams;","entrypoint":"session-catalog","exportName":"SessionCatalogListProviderParams","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"1b8060a038d38a9dfcdb65119d42049deeb0fa9410a78298cd29bfd9639d2721","declaration":"export type SessionCatalogLocator = { threadId: string; catalogId: string; hostId: string;};","entrypoint":"session-catalog","exportName":"SessionCatalogLocator","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} -{"closureHash":"91f48023abc9b97826c037b7f19026ad94f26ebc163e937ef589dd3346a4e5b6","declaration":"export type SessionCatalogProvider = SessionCatalogProvider;","entrypoint":"session-catalog","exportName":"SessionCatalogProvider","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} +{"closureHash":"8ca878d19652c5c51b93b3a653e539b855f2ee54f395d1faf32afc7d9876f5a9","declaration":"export type SessionCatalogProvider = SessionCatalogProvider;","entrypoint":"session-catalog","exportName":"SessionCatalogProvider","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"031fa3a50e41f0c7d5672cfbfefe6b5f3a74d7b09824d3a0cf386ab08bdc2ce6","declaration":"export type SessionCatalogPullRequestSummary = { state: \"open\" | \"draft\" | \"closed\" | \"merged\"; numbers: number[];};","entrypoint":"session-catalog","exportName":"SessionCatalogPullRequestSummary","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"2fddff39ee355ab2f5efa463b1a2ff1fdce03da36515d6b4d21988ea28bcd45f","declaration":"export type SessionCatalogReadProviderParams = SessionCatalogReadProviderParams;","entrypoint":"session-catalog","exportName":"SessionCatalogReadProviderParams","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} {"closureHash":"c5e0c59d9f38e43b780d5d9cbf38ebd4299694e0f52221eee8c72e3a29d27e42","declaration":"export type SessionCatalogSession = { source?: string | undefined; name?: string | undefined; cwd?: string | undefined; sessionKey?: string | undefined; updatedAt?: number | undefined; createdActor?: { id?: string | undefined; label?: string | undefined; avatarUrl?: string | undefined; type: \"system\" | \"agent\" | \"human\"; } | undefined; createdAt?: number | undefined; modelProvider?: string | undefined; recencyAt?: number | undefined; cliVersion?: string | undefined; gitBranch?: string | undefined; customGroup?: string | undefined; pullRequest?: { state: \"open\" | \"draft\" | \"closed\" | \"merged\"; numbers: number[]; } | undefined; canOpenTerminal?: boolean | undefined; status: string; archived: boolean; threadId: string; canContinue: boolean; canArchive: boolean;};","entrypoint":"session-catalog","exportName":"SessionCatalogSession","importSpecifier":"openclaw/plugin-sdk/session-catalog","kind":"type","recordType":"export"} @@ -4620,16 +4629,16 @@ {"closureHash":"e365d7ad911453b5139ba700931b061e560cff80b50d4cbe5e9615b5d7e13a52","declaration":"export type ScopedExpiringIdCache = ScopedExpiringIdCache;","entrypoint":"text-runtime","exportName":"ScopedExpiringIdCache","importSpecifier":"openclaw/plugin-sdk/text-runtime","kind":"type","recordType":"export"} {"closureHash":"af5502c92594c2e9049a4b4994e17c00d3adee3f6d1e68edd818a539c74c8787","declaration":"export interface CodeRegion {\n start: number;\n end: number;\n}","entrypoint":"text-runtime","exportName":"CodeRegion","importSpecifier":"openclaw/plugin-sdk/text-runtime","kind":"interface","recordType":"export"} {"category":null,"entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","recordType":"module"} -{"closureHash":"bc8db89ca8e704499face27583b4fc397a1ed6e05452347a76e8cc67c8002067","declaration":"export function defineToolPlugin(definition: DefineToolPluginOptions): DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"defineToolPlugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"} +{"closureHash":"fb5203d8d0fb7f790e856e8bd91f095ef09bfc4e07c847051815b511eec89b3e","declaration":"export function defineToolPlugin(definition: DefineToolPluginOptions): DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"defineToolPlugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"} {"closureHash":"36ef907ce20f885b5f3068cbf0475dc442a68da9652579bb5bf8fb83ad34c38b","declaration":"export function getToolPluginMetadata(entry: unknown): ToolPluginMetadata | undefined;","entrypoint":"tool-plugin","exportName":"getToolPluginMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"} {"closureHash":"bdc58154e36b2625fb3daccca3eab248d981077d04d747f0ad507cb2ff565198","declaration":"export const toolPluginMetadataSymbol: typeof toolPluginMetadataSymbol;","entrypoint":"tool-plugin","exportName":"toolPluginMetadataSymbol","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"const","recordType":"export"} -{"closureHash":"d475ec199bbabf1daef2fae73f3212e797471df018c74fdd6231b78293c58897","declaration":"export type DefineToolPluginOptions = DefineToolPluginOptions;","entrypoint":"tool-plugin","exportName":"DefineToolPluginOptions","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} -{"closureHash":"cfb60cf545bf5d2768050d461f9eec73b9d48903ec3e9d2651dab1147d7195dd","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} -{"closureHash":"948e30fb4ed9b369e8ccf5b467b3fc5b49830c67de0c964a3d197cfe403211ec","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} -{"closureHash":"32e67ffc04a5f91d505482970b608292e6cbb2aa6f74727de0f0550675d17bb7","declaration":"export type ToolPluginFactoryContext = ToolPluginFactoryContext;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} +{"closureHash":"396ce432df15713c3d8293d12c91941da5c10ca2ba2b09d9e31166d541769cf1","declaration":"export type DefineToolPluginOptions = DefineToolPluginOptions;","entrypoint":"tool-plugin","exportName":"DefineToolPluginOptions","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} +{"closureHash":"658ca7829c3539c33a8a26c7195b86a0e9b92aaffd6ab34743abb16e85eb8b62","declaration":"export type DefinedToolPluginEntry = DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"DefinedToolPluginEntry","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} +{"closureHash":"43b9db3bef8eac16bcb60378f1f8ec6193af0d2ce8fc2f6f30b15038d42d6012","declaration":"export type ToolPluginExecutionContext = ToolPluginExecutionContext;","entrypoint":"tool-plugin","exportName":"ToolPluginExecutionContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} +{"closureHash":"a75a6f9e2cfd805d6a87622b2cf7038de57983e32de0bb7fc66de7fea0833a61","declaration":"export type ToolPluginFactoryContext = ToolPluginFactoryContext;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} {"closureHash":"f774bd092f51a31fbb34847213f9583234f585f66918d0af55e81bbbae40463b","declaration":"export type ToolPluginMetadata = ToolPluginMetadata;","entrypoint":"tool-plugin","exportName":"ToolPluginMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} {"closureHash":"414a89d0c80eaaf682dc25d306e7223c981c6fd25366adb973a106f4268a6a54","declaration":"export type ToolPluginStaticToolMetadata = ToolPluginStaticToolMetadata;","entrypoint":"tool-plugin","exportName":"ToolPluginStaticToolMetadata","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} -{"closureHash":"99f4d7756fabcd13e1882973cabe0f9a1c8bbeb13470876e060baf482fbd0b68","declaration":"export type ToolPluginToolDefinition = ToolPluginToolDefinition;","entrypoint":"tool-plugin","exportName":"ToolPluginToolDefinition","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} +{"closureHash":"b55fee969fe9db614f96a26d4e3bcb563448ce20a5b96341ce9bca98b5d60bb1","declaration":"export type ToolPluginToolDefinition = ToolPluginToolDefinition;","entrypoint":"tool-plugin","exportName":"ToolPluginToolDefinition","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"} {"category":null,"entrypoint":"tool-results","importSpecifier":"openclaw/plugin-sdk/tool-results","recordType":"module"} {"closureHash":"c5b0951c3629834dff78f92317cf1421a6bbc0faf9307be1cabb915055665280","declaration":"export function jsonResult(payload: TDetails): AgentToolResult;","entrypoint":"tool-results","exportName":"jsonResult","importSpecifier":"openclaw/plugin-sdk/tool-results","kind":"function","recordType":"export"} {"closureHash":"b9aefc637a694c74e028e92006004bef18185ed569af48ac43d36b646b0af2f3","declaration":"export function textResult(text: string, details: TDetails): AgentToolResult;","entrypoint":"tool-results","exportName":"textResult","importSpecifier":"openclaw/plugin-sdk/tool-results","kind":"function","recordType":"export"} @@ -4663,9 +4672,9 @@ {"closureHash":"9f9174f1023a8a451232f5d97d1e922b7faa88b76b58cffbfbe246a1488e0ddd","declaration":"export function readJsonWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; maxBytes?: number; timeoutMs?: number; profile?: WebhookBodyReadProfile; emptyObjectOnEmpty?: boolean; invalidJsonMessage?: string; }): Promise<{ ok: true; value: unknown; } | { ok: false; }>;","entrypoint":"webhook-ingress","exportName":"readJsonWebhookBodyOrReject","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"1bbddd5d1c8297e315275db4d5e1c8748ff09c0be7c37b0cf456db4fc69f8909","declaration":"export function readRequestBodyWithLimit(req: IncomingMessage, options: ReadRequestBodyOptions): Promise;","entrypoint":"webhook-ingress","exportName":"readRequestBodyWithLimit","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"e8a1174cac83ce346a231fd25e82bb5f7618ffb57cdd9af6eaa208411f70646d","declaration":"export function readWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; maxBytes?: number; timeoutMs?: number; profile?: WebhookBodyReadProfile; invalidBodyMessage?: string; }): Promise<{ ok: true; value: string; } | { ok: false; }>;","entrypoint":"webhook-ingress","exportName":"readWebhookBodyOrReject","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} -{"closureHash":"ca3a646582c2ade5f5170b5018ec6653c334a4aff890eec9b4d772d97bc9fdda","declaration":"export function registerPluginHttpRoute(params: { path?: string | null; fallbackPath?: string | null; handler: PluginHttpRouteHandler; auth: PluginHttpRouteRegistration[\"auth\"]; match?: PluginHttpRouteRegistration[\"match\"]; gatewayRuntimeScopeSurface?: PluginHttpRouteRegistration[\"gatewayRuntimeScopeSurface\"]; replaceExisting?: boolean; reuseExistingSameOwner?: boolean; throwOnFailure?: boolean; pluginId?: string; source?: string; accountId?: string; log?: (message: string) => void; registry?: PluginRegistry; }): () => void;","entrypoint":"webhook-ingress","exportName":"registerPluginHttpRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} +{"closureHash":"57a1079fdcd9e90c3f6454d824dbc259d7fc8d38d015ebca59305cffcff9b007","declaration":"export function registerPluginHttpRoute(params: { path?: string | null; fallbackPath?: string | null; handler: PluginHttpRouteHandler; auth: PluginHttpRouteRegistration[\"auth\"]; match?: PluginHttpRouteRegistration[\"match\"]; gatewayRuntimeScopeSurface?: PluginHttpRouteRegistration[\"gatewayRuntimeScopeSurface\"]; replaceExisting?: boolean; reuseExistingSameOwner?: boolean; throwOnFailure?: boolean; pluginId?: string; source?: string; accountId?: string; log?: (message: string) => void; registry?: PluginRegistry; }): () => void;","entrypoint":"webhook-ingress","exportName":"registerPluginHttpRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"05835f2f66f906f736b82b332a84a9714bc3a6b211890a62d8fc4dbf9258a7d4","declaration":"export function registerWebhookTarget(targetsByPath: Map, target: T, opts?: RegisterWebhookTargetOptions): RegisteredWebhookTarget;","entrypoint":"webhook-ingress","exportName":"registerWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} -{"closureHash":"20864ae868c6f3a5e205e36cf187ba0dc317b863f08eab544292311f797a14c6","declaration":"export function registerWebhookTargetWithPluginRoute(params: { targetsByPath: Map; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} +{"closureHash":"742e865c4abaf46d2b6b6c7ff40f54547c34cf1a61ec51c94eeb6d9358396f70","declaration":"export function registerWebhookTargetWithPluginRoute(params: { targetsByPath: Map; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"ec2f47e9bd7a075a3dd4302ac9e4c0bfe20630445db6735cf803f2d354275841","declaration":"export function requestBodyErrorToText(code: RequestBodyLimitErrorCode): string;","entrypoint":"webhook-ingress","exportName":"requestBodyErrorToText","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"5e1f3b468d56eb0f82104184077fdf4b6bd0c1444aa10b71a65fcaa2ec0298a2","declaration":"export function resolveRequestClientIp(req?: IncomingMessage, trustedProxies?: string[], allowRealIpFallback?: boolean): string | undefined;","entrypoint":"webhook-ingress","exportName":"resolveRequestClientIp","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} {"closureHash":"56c3e9868dfa6ee2a7a128eb02039a16b52696759adadbd3fe0a3bd7a6a07eeb","declaration":"export function resolveSingleWebhookTarget(targets: readonly T[], isMatch: (target: T) => boolean): WebhookTargetMatchResult;","entrypoint":"webhook-ingress","exportName":"resolveSingleWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"} @@ -4683,7 +4692,7 @@ {"closureHash":"efb74e84339c15d6f607b38d1d477b0504d47d04ae78e77083a1893c756a0b54","declaration":"export const WEBHOOK_RATE_LIMIT_DEFAULTS: Readonly<{ windowMs: 60000; maxRequests: 120; maxTrackedKeys: 4096;}>;","entrypoint":"webhook-ingress","exportName":"WEBHOOK_RATE_LIMIT_DEFAULTS","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"const","recordType":"export"} {"closureHash":"13b28ec9acae43c3bdcd683b9831154f88118dee8142e42b7eebfd59f08c602c","declaration":"export type BoundedCounter = BoundedCounter;","entrypoint":"webhook-ingress","exportName":"BoundedCounter","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} {"closureHash":"9e737ae8c17038ef7becbd2bf1b756cfeaed571e2ded368c0fe3a446ebd1f666","declaration":"export type FixedWindowRateLimiter = FixedWindowRateLimiter;","entrypoint":"webhook-ingress","exportName":"FixedWindowRateLimiter","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} -{"closureHash":"b12e578e77de540a5bf794ed1d12000043b4fb0a62fdcd00444846c4bcbe6b1f","declaration":"export type RegisterWebhookPluginRouteOptions = RegisterWebhookPluginRouteOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookPluginRouteOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} +{"closureHash":"79176802848490718e5c3afea19009924d1649a4030833d74926832edae58c2d","declaration":"export type RegisterWebhookPluginRouteOptions = RegisterWebhookPluginRouteOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookPluginRouteOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} {"closureHash":"b3e98619854462acfac6538ac9a2d0cefb88704a0eac712563f0996b07364152","declaration":"export type RegisterWebhookTargetOptions = RegisterWebhookTargetOptions;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookTargetOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} {"closureHash":"964c2bfd8f268c411ee7b982c9de02c207f4bec402dd62c9f998a59752bb9d1f","declaration":"export type RegisteredWebhookTarget = RegisteredWebhookTarget;","entrypoint":"webhook-ingress","exportName":"RegisteredWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} {"closureHash":"aa552e5bdaefc9eb94d3b5a5cdf8c413fad555d9f452c6a2a6af5a3ce695c9d1","declaration":"export type WebhookAnomalyTracker = WebhookAnomalyTracker;","entrypoint":"webhook-ingress","exportName":"WebhookAnomalyTracker","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"} diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index b31b79e4c643..0108eeb28c02 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -921,6 +921,33 @@ unrelated inbound runtime helpers. an adapter test that proves the mode skips a wildcard `toolsBySender` entry without dropping the matching base `tools` restriction. + ### Native plugin command ownership + + Channel plugins that publish provider-native command catalogs should use + `openclaw/plugin-sdk/plugin-command-runtime`. Create one runtime while + planning the catalog, merge its candidates with built-in and skill entries, + and retain the winning candidate object in the registered handler closure. + Once the provider catalog is finalized, call + `retainNativeCatalog(provider)` when at least one plugin candidate remains; + if listener registration can fail synchronously, call it after those + listeners are installed. This records the current channel-account lifecycle + so a registry reload restarts only accounts whose handlers retain that + registry generation. + Call `prepareDispatch(rawArgs)` only on that winner and execute the returned + dispatch with `dispatch.execute(context)`. Carry an explicit + `{ kind: "non-plugin" }` decision for retained built-in and skill winners. + This keeps the advertised command and + its executable plugin registration on the same registry generation. + + Candidates expose only immutable display/auth/progress metadata plus an + opaque process-local dispatch. They do not expose handlers, plugin roots, + or registry rows. Dispatches cannot cross runtime factories or channels, + and a registry replacement makes new executions return an unavailable + result instead of rematching command text against the replacement registry. + A command already admitted before retirement may finish on its captured + generation. Do not serialize candidates or dispatches; project only their + display fields into provider API payloads. + diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 2f02855c0634..cd8dc20b9eb9 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -228,6 +228,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It | `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers | | `plugin-sdk/matrix` | Deprecated Matrix compatibility facade for older third-party channel packages; new plugins should import `plugin-sdk/run-command` directly | | `plugin-sdk/runtime-store` | `createPluginRuntimeStore` | + | `plugin-sdk/plugin-command-runtime` | Registry-generation-bound native plugin command candidates, terminal catalog decisions, and exact selected dispatch execution | | `plugin-sdk/plugin-runtime` | Deprecated broad barrel for plugin command/hook/http/interactive helpers; prefer focused plugin runtime subpaths | | `plugin-sdk/hook-runtime` | Deprecated broad barrel for webhook/internal hook pipeline helpers; prefer focused hook/plugin runtime subpaths | | `plugin-sdk/lazy-runtime` | Lazy runtime import/binding helpers such as `createLazyRuntimeModule`, `createLazyRuntimeMethod`, and `createLazyRuntimeSurface` | diff --git a/extensions/discord/src/monitor/message-handler.process-progress.ts b/extensions/discord/src/monitor/message-handler.process-progress.ts index 6fd16a997ebe..41d3135b0314 100644 --- a/extensions/discord/src/monitor/message-handler.process-progress.ts +++ b/extensions/discord/src/monitor/message-handler.process-progress.ts @@ -1,12 +1,12 @@ import type { StatusReactionController } from "openclaw/plugin-sdk/channel-feedback"; -import type { ChannelInboundTurnPlan } from "openclaw/plugin-sdk/channel-inbound"; // Discord plugin module owns progress-window state and agent-event rendering. import { createChannelProgressReceiptTracker } from "openclaw/plugin-sdk/channel-outbound"; +import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime"; import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; import type { createDiscordDraftPreviewController } from "./message-handler.draft-preview.js"; import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js"; -type ReplyOptions = NonNullable; +type ReplyOptions = Omit; type CallbackPayload = NonNullable extends (...args: infer Args) => unknown ? Args[0] : never; type DraftPreview = ReturnType; diff --git a/extensions/discord/src/monitor/native-command-agent-reply.ts b/extensions/discord/src/monitor/native-command-agent-reply.ts index 032430038732..ebcbe8f9a5ad 100644 --- a/extensions/discord/src/monitor/native-command-agent-reply.ts +++ b/extensions/discord/src/monitor/native-command-agent-reply.ts @@ -7,6 +7,10 @@ import { import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime"; +import { + PLUGIN_COMMAND_DISPATCH, + type PluginCommandCatalogDecision, +} from "openclaw/plugin-sdk/plugin-command-runtime"; import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import type { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; @@ -52,6 +56,7 @@ export async function dispatchDiscordNativeAgentReply(params: { responseEphemeral?: boolean; suppressReplies?: boolean; log: ReturnType; + pluginCommandDispatch: PluginCommandCatalogDecision; }): Promise { const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(params.discordConfig); @@ -139,6 +144,7 @@ export async function dispatchDiscordNativeAgentReply(params: { }, replyOptions: { skillFilter: params.channelConfig?.skills, + [PLUGIN_COMMAND_DISPATCH]: params.pluginCommandDispatch, disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : undefined, }, diff --git a/extensions/discord/src/monitor/native-command-arg-ui.ts b/extensions/discord/src/monitor/native-command-arg-ui.ts index abae60aa9151..bd2363eaf121 100644 --- a/extensions/discord/src/monitor/native-command-arg-ui.ts +++ b/extensions/discord/src/monitor/native-command-arg-ui.ts @@ -124,6 +124,7 @@ async function handleDiscordCommandArgInteraction(params: { preferFollowUp: true, threadBindings: ctx.threadBindings, responseEphemeral: resolveDiscordSlashCommandConfig(ctx.discordConfig?.slashCommand).ephemeral, + pluginCommandDispatch: { kind: "non-plugin" }, }); } diff --git a/extensions/discord/src/monitor/native-command-dispatch.ts b/extensions/discord/src/monitor/native-command-dispatch.ts index 5978afca86d8..f91411b6952d 100644 --- a/extensions/discord/src/monitor/native-command-dispatch.ts +++ b/extensions/discord/src/monitor/native-command-dispatch.ts @@ -1,6 +1,7 @@ // Discord plugin module implements native command dispatch behavior. import type { ChatCommandDefinition, CommandArgs } from "openclaw/plugin-sdk/command-auth-native"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import type { PluginCommandCatalogDecision } from "openclaw/plugin-sdk/plugin-command-runtime"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; import type { @@ -25,6 +26,7 @@ type DispatchDiscordCommandInteractionParams = { threadBindings: ThreadBindingManager; responseEphemeral?: boolean; suppressReplies?: boolean; + pluginCommandDispatch: PluginCommandCatalogDecision; }; export type DispatchDiscordCommandInteractionResult = { diff --git a/extensions/discord/src/monitor/native-command-model-picker-apply.ts b/extensions/discord/src/monitor/native-command-model-picker-apply.ts index fc76f76a693a..5a08f49f6c26 100644 --- a/extensions/discord/src/monitor/native-command-model-picker-apply.ts +++ b/extensions/discord/src/monitor/native-command-model-picker-apply.ts @@ -65,6 +65,7 @@ export async function applyDiscordModelPickerSelection(params: { preferFollowUp: true, threadBindings: params.threadBindings, suppressReplies: true, + pluginCommandDispatch: { kind: "non-plugin" }, }), 12000, ); diff --git a/extensions/discord/src/monitor/native-command.options.test.ts b/extensions/discord/src/monitor/native-command.options.test.ts index 56963389d7fb..39ddec59d7fc 100644 --- a/extensions/discord/src/monitor/native-command.options.test.ts +++ b/extensions/discord/src/monitor/native-command.options.test.ts @@ -7,7 +7,6 @@ import { setRuntimeConfigSnapshot, } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { nativeCommandRuntime } from "./native-command.runtime.js"; const { loadModelCatalogMock, logVerboseMock } = vi.hoisted(() => ({ loadModelCatalogMock: vi.fn(), @@ -435,78 +434,73 @@ describe("createDiscordNativeCommand option wiring", () => { }); it("keeps plugin command autocomplete aligned with dispatch owner checks", async () => { - const restoreMatchPluginCommand = nativeCommandRuntime.matchPluginCommand; - nativeCommandRuntime.matchPluginCommand = (prompt) => - prompt === "/pair" ? ({ command: { name: "pair" }, args: "" } as never) : null; - try { - const command = createDiscordNativeCommand({ - command: { - name: "pair", - description: "Pair", - acceptsArgs: true, - args: [ - { - name: "mode", - description: "Pairing mode", - type: "string", - preferAutocomplete: true, - choices: () => [ - { label: "fast", value: "fast" }, - { label: "secure", value: "secure" }, - ], - }, - ], - }, - cfg: createAllowedGuildAutocompleteConfig({ - ownerAllowFrom: ["user:owner-user"], + const command = createDiscordNativeCommand({ + command: { + name: "pair", + description: "Pair", + acceptsArgs: true, + args: [ + { + name: "mode", + description: "Pairing mode", + type: "string", + preferAutocomplete: true, + choices: () => [ + { label: "fast", value: "fast" }, + { label: "secure", value: "secure" }, + ], + }, + ], + requireAuth: true, + prepareDispatch: () => ({ + kind: "plugin" as const, + invocation: { + runtime: { execute: vi.fn() }, + selection: Object.freeze({}), + }, }), - discordConfig: { - groupPolicy: "allowlist", - guilds: { - "guild-1": { - channels: { - "channel-1": { - enabled: true, - requireMention: false, - }, + } as never, + cfg: createAllowedGuildAutocompleteConfig({ + ownerAllowFrom: ["user:owner-user"], + }), + discordConfig: { + groupPolicy: "allowlist", + guilds: { + "guild-1": { + channels: { + "channel-1": { + enabled: true, + requireMention: false, }, }, }, }, - accountId: "default", - sessionPrefix: "discord:slash", - ephemeralDefault: true, - threadBindings: createNoopThreadBindingManager("default"), - }); - const mode = requireOption(command, "mode"); - const autocomplete = requireAutocomplete( - mode, - "plugin mode option did not wire autocomplete", - ); - const respond = await runAutocomplete(autocomplete, { - userId: "blocked-user", - username: "blocked", - globalName: "Blocked", - channelType: ChannelType.GuildText, - channelId: "channel-1", - channelName: "general", - guildId: "guild-1", - focusedValue: "", - }); + }, + accountId: "default", + sessionPrefix: "discord:slash", + ephemeralDefault: true, + threadBindings: createNoopThreadBindingManager("default"), + }); + const mode = requireOption(command, "mode"); + const autocomplete = requireAutocomplete(mode, "plugin mode option did not wire autocomplete"); + const respond = await runAutocomplete(autocomplete, { + userId: "blocked-user", + username: "blocked", + globalName: "Blocked", + channelType: ChannelType.GuildText, + channelId: "channel-1", + channelName: "general", + guildId: "guild-1", + focusedValue: "", + }); - expect(respond).toHaveBeenCalledWith([ - { name: "fast", value: "fast" }, - { name: "secure", value: "secure" }, - ]); - } finally { - nativeCommandRuntime.matchPluginCommand = restoreMatchPluginCommand; - } + expect(respond).toHaveBeenCalledWith([ + { name: "fast", value: "fast" }, + { name: "secure", value: "secure" }, + ]); }); it("refreshes autocomplete authorization and dynamic choices between invocations", async () => { - const restoreMatchPluginCommand = nativeCommandRuntime.matchPluginCommand; - nativeCommandRuntime.matchPluginCommand = (prompt) => - prompt === "/scope" ? ({ command: { name: "scope" }, args: "" } as never) : null; const sourceCfg = { session: { dmScope: "main" }, channels: { @@ -526,56 +520,57 @@ describe("createDiscordNativeCommand option wiring", () => { }, }, } as OpenClawConfig; - try { - const command = createDiscordNativeCommand({ - command: { - name: "scope", - description: "Scope", - acceptsArgs: true, - args: [ - { - name: "value", - description: "Scope value", - type: "string", - preferAutocomplete: true, - choices: ({ cfg }) => { - const dmScope = cfg?.session?.dmScope ?? "missing"; - return [{ label: dmScope, value: dmScope }]; - }, + const command = createDiscordNativeCommand({ + command: { + name: "scope", + description: "Scope", + acceptsArgs: true, + args: [ + { + name: "value", + description: "Scope value", + type: "string", + preferAutocomplete: true, + choices: ({ cfg }: { cfg?: OpenClawConfig }) => { + const dmScope = cfg?.session?.dmScope ?? "missing"; + return [{ label: dmScope, value: dmScope }]; }, - ], - }, - cfg: sourceCfg, - discordConfig: sourceCfg.channels?.discord ?? {}, - accountId: "default", - sessionPrefix: "discord:slash", - ephemeralDefault: true, - threadBindings: createNoopThreadBindingManager("default"), - }); - const value = requireOption(command, "value"); - const autocomplete = requireAutocomplete( - value, - "scope value option did not wire autocomplete", - ); - const autocompleteParams = { - userId: "owner", - channelType: ChannelType.DM, - channelId: "dm-1", - channelName: "dm-1", - focusedValue: "", - } as const; + }, + ], + requireAuth: true, + prepareDispatch: () => ({ + kind: "plugin" as const, + invocation: { + runtime: { execute: vi.fn() }, + selection: Object.freeze({}), + }, + }), + } as never, + cfg: sourceCfg, + discordConfig: sourceCfg.channels?.discord ?? {}, + accountId: "default", + sessionPrefix: "discord:slash", + ephemeralDefault: true, + threadBindings: createNoopThreadBindingManager("default"), + }); + const value = requireOption(command, "value"); + const autocomplete = requireAutocomplete(value, "scope value option did not wire autocomplete"); + const autocompleteParams = { + userId: "owner", + channelType: ChannelType.DM, + channelId: "dm-1", + channelName: "dm-1", + focusedValue: "", + } as const; - const blockedRespond = await runAutocomplete(autocomplete, autocompleteParams); - expect(blockedRespond).toHaveBeenCalledWith([]); + const blockedRespond = await runAutocomplete(autocomplete, autocompleteParams); + expect(blockedRespond).toHaveBeenCalledWith([]); - setRuntimeConfigSnapshot(runtimeCfg, runtimeCfg); - const refreshedRespond = await runAutocomplete(autocomplete, autocompleteParams); - expect(refreshedRespond).toHaveBeenCalledWith([ - { name: "per-channel-peer", value: "per-channel-peer" }, - ]); - } finally { - nativeCommandRuntime.matchPluginCommand = restoreMatchPluginCommand; - } + setRuntimeConfigSnapshot(runtimeCfg, runtimeCfg); + const refreshedRespond = await runAutocomplete(autocomplete, autocompleteParams); + expect(refreshedRespond).toHaveBeenCalledWith([ + { name: "per-channel-peer", value: "per-channel-peer" }, + ]); }); it("returns no autocomplete choices outside the Discord allowlist when commands.useAccessGroups is false and commands.allowFrom is not configured", async () => { diff --git a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts index 478e5bd99e0e..42cf73e89b7c 100644 --- a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts +++ b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts @@ -6,13 +6,13 @@ import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { PlatformMessageNotDispatchedError } from "openclaw/plugin-sdk/error-runtime"; import { - clearPluginCommands, - executePluginCommand, - matchPluginCommand, - registerPluginCommand, -} from "openclaw/plugin-sdk/plugin-runtime"; + createPluginCommandRuntime, + PLUGIN_COMMAND_DISPATCH, +} from "openclaw/plugin-sdk/plugin-command-runtime"; +import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; import { createTestRegistry, + getActivePluginRegistry, setActivePluginRegistry, } from "openclaw/plugin-sdk/plugin-test-runtime"; import { setReplyPayloadMetadata } from "openclaw/plugin-sdk/reply-payload-testing"; @@ -34,8 +34,7 @@ import { createNoopThreadBindingManager } from "./thread-bindings.manager.js"; let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand; const runtimeModuleMocks = vi.hoisted(() => ({ - matchPluginCommand: vi.fn(), - executePluginCommand: vi.fn(), + pluginCommandHandler: vi.fn(), dispatchReplyWithDispatcher: vi.fn(), resolveDirectStatusReplyForSession: vi.fn(), getSessionEntry: vi.fn(), @@ -297,13 +296,34 @@ function expectNoFollowUpContent(interaction: MockCommandInteraction, content: s expect(matched).toBe(false); } -async function createPluginCommand(params: { cfg: OpenClawConfig; name: string }) { +async function createPluginCommand(params: { + cfg: OpenClawConfig; + name: string; + registeredName?: string; +}) { + const registration = getActivePluginRegistry()?.commands.find( + (entry) => + entry.command.name === (params.registeredName ?? params.name) || + entry.command.nativeNames?.discord === params.name, + ); + if (!registration) { + throw new Error(`expected plugin command registration ${params.name}`); + } + const originalHandler = registration.command.handler; + registration.command.handler = async (ctx) => + await runtimeModuleMocks.pluginCommandHandler({ + ...ctx, + command: { name: registration.command.name }, + run: () => originalHandler(ctx), + }); + const candidate = createPluginCommandRuntime() + .listNativeCandidates("discord") + .find((entry) => entry.name === params.name); + if (!candidate) { + throw new Error(`expected plugin command candidate ${params.name}`); + } return createDiscordNativeCommand({ - command: { - name: params.name, - description: "Pair", - acceptsArgs: true, - } satisfies NativeCommandSpec, + command: candidate, cfg: params.cfg, discordConfig: params.cfg.channels?.discord ?? {}, accountId: "default", @@ -313,6 +333,19 @@ async function createPluginCommand(params: { cfg: OpenClawConfig; name: string } }); } +async function createMockPluginNativeCommand(cfg: OpenClawConfig, spec: NativeCommandSpec) { + expect( + registerPluginCommand(`test-${spec.name}`, { + name: spec.name, + description: spec.description, + acceptsArgs: spec.acceptsArgs, + requireAuth: true, + handler: async () => ({ text: "ok" }), + }), + ).toEqual({ ok: true }); + return await createPluginCommand({ cfg, name: spec.name }); +} + function registerPairPlugin(params?: { discordNativeName?: string }) { expect( registerPluginCommand("demo-plugin", { @@ -358,9 +391,10 @@ async function expectPairCommandReply(params: { const command = await createPluginCommand({ cfg: params.cfg, name: params.commandName, + registeredName: params.expectedRegisteredName ?? "pair", }); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher; - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "paired:now", }); await (command as { run: (interaction: unknown) => Promise }).run( @@ -406,7 +440,6 @@ async function expectBoundStatusCommandDirectReply(params: { interaction: MockCommandInteraction; expectedPattern: RegExp; }) { - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher; const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession; const command = await createStatusCommand(params.cfg); @@ -431,8 +464,6 @@ describe("Discord native plugin command dispatch", () => { afterAll(() => { clearPluginCommands(); setActivePluginRegistry(createTestRegistry()); - nativeCommandRuntime.matchPluginCommand = matchPluginCommand; - nativeCommandRuntime.executePluginCommand = executePluginCommand; nativeCommandRuntime.dispatchChannelInboundTurn = dispatchChannelInboundTurn; nativeCommandRuntime.resolveDirectStatusReplyForSession = resolveDirectStatusReplyForSession; nativeCommandRuntime.resolveDiscordNativeInteractionRouteState = @@ -445,10 +476,10 @@ describe("Discord native plugin command dispatch", () => { vi.clearAllMocks(); clearPluginCommands(); setActivePluginRegistry(createTestRegistry()); - runtimeModuleMocks.matchPluginCommand.mockReset(); - runtimeModuleMocks.matchPluginCommand.mockImplementation(matchPluginCommand); - runtimeModuleMocks.executePluginCommand.mockReset(); - runtimeModuleMocks.executePluginCommand.mockImplementation(executePluginCommand); + runtimeModuleMocks.pluginCommandHandler.mockReset(); + runtimeModuleMocks.pluginCommandHandler.mockImplementation( + async (params: { run?: () => Promise }) => await params.run?.(), + ); runtimeModuleMocks.dispatchReplyWithDispatcher.mockReset(); runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({ counts: { @@ -463,10 +494,6 @@ describe("Discord native plugin command dispatch", () => { }); runtimeModuleMocks.getSessionEntry.mockReset(); runtimeModuleMocks.getSessionEntry.mockReturnValue(undefined); - nativeCommandRuntime.matchPluginCommand = - runtimeModuleMocks.matchPluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand; - nativeCommandRuntime.executePluginCommand = - runtimeModuleMocks.executePluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").executePluginCommand; nativeCommandRuntime.dispatchChannelInboundTurn = dispatchChannelInboundTurnForTest; nativeCommandRuntime.resolveDirectStatusReplyForSession = runtimeModuleMocks.resolveDirectStatusReplyForSession as typeof resolveDirectStatusReplyForSession; @@ -548,7 +575,42 @@ describe("Discord native plugin command dispatch", () => { }); }); - it("passes the active auth profile to Discord plugin commands", async () => { + it("carries the built-in catalog winner through the interaction", async () => { + const cfg = createConfig(); + const pluginHandler = vi.fn(async () => ({ text: "wrong plugin" })); + getActivePluginRegistry()!.commands.push({ + pluginId: "shadow-plugin", + source: "test", + command: { + name: "help", + description: "Shadow help", + channels: ["discord"], + requireAuth: false, + handler: pluginHandler, + }, + }); + const help: NativeCommandSpec = { + name: "help", + description: "Show help", + acceptsArgs: false, + }; + const command = await createNativeCommand(cfg, help); + + await (command as { run: (interaction: unknown) => Promise }).run( + createInteraction() as unknown, + ); + + expect(pluginHandler).not.toHaveBeenCalled(); + const dispatchParams = requireRecord( + firstMockArg(runtimeModuleMocks.dispatchReplyWithDispatcher, "core dispatch"), + "core dispatch", + ); + expect( + (dispatchParams.replyOptions as Record)[PLUGIN_COMMAND_DISPATCH], + ).toEqual({ kind: "non-plugin" }); + }); + + it("resolves the active auth profile before Discord plugin execution", async () => { const cfg = createConfig(); const interaction = createInteraction(); runtimeModuleMocks.getSessionEntry.mockReturnValue({ @@ -562,7 +624,7 @@ describe("Discord native plugin command dispatch", () => { cfg, name: "pair", }); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "paired:now", }); @@ -579,18 +641,17 @@ describe("Discord native plugin command dispatch", () => { expectPluginCommandExecution({ mock: executeSpy, commandName: "pair", - expected: { - authProfileId: "openai:owner@example.com", - }, + expected: {}, }); + expect(runtimeModuleMocks.getSessionEntry).toHaveBeenCalled(); }); it.each([ { ownerAllowFrom: ["discord:*"], senderIsOwner: false }, { ownerAllowFrom: ["discord:123456789012345678"], senderIsOwner: true }, ])( - "passes host owner status $senderIsOwner for command owners $ownerAllowFrom", - async ({ ownerAllowFrom, senderIsOwner }) => { + "does not expose host owner status to ordinary plugins for $ownerAllowFrom", + async ({ ownerAllowFrom }) => { const cfg = { ...createConfig(), commands: { ownerAllowFrom }, @@ -600,7 +661,7 @@ describe("Discord native plugin command dispatch", () => { interaction.options.getString.mockReturnValue("now"); registerPairPlugin(); const command = await createPluginCommand({ cfg, name: "pair" }); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "paired:now", }); @@ -608,11 +669,12 @@ describe("Discord native plugin command dispatch", () => { interaction as unknown, ); - expectPluginCommandExecution({ + const payload = expectPluginCommandExecution({ mock: executeSpy, commandName: "pair", - expected: { senderIsOwner }, + expected: {}, }); + expect(payload.senderIsOwner).toBeUndefined(); }, ); @@ -645,7 +707,7 @@ describe("Discord native plugin command dispatch", () => { cfg, name: "pair", }); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "paired:now", }); @@ -665,7 +727,6 @@ describe("Discord native plugin command dispatch", () => { expected: { agentId: "codex", sessionKey: pluginSessionKey, - authProfileId: "openai:owner@example.com", }, }); expect(runtimeModuleMocks.getSessionEntry).toHaveBeenCalledWith({ @@ -785,9 +846,9 @@ describe("Discord native plugin command dispatch", () => { handler: async ({ args }) => ({ text: `open:${args ?? ""}` }), }), ).toEqual({ ok: true }); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createPluginCommand({ cfg, name: commandSpec.name }); - const executeSpy = runtimeModuleMocks.executePluginCommand; + const executeSpy = runtimeModuleMocks.pluginCommandHandler; const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue( {} as never, ); @@ -847,10 +908,10 @@ describe("Discord native plugin command dispatch", () => { handler: async ({ args }) => ({ text: `open:${args ?? ""}` }), }), ).toEqual({ ok: true }); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "open:now", }); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createPluginCommand({ cfg, name: commandSpec.name }); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -907,10 +968,10 @@ describe("Discord native plugin command dispatch", () => { handler: async ({ args }) => ({ text: `open:${args ?? ""}` }), }), ).toEqual({ ok: true }); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "open:now", }); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createPluginCommand({ cfg, name: commandSpec.name }); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -965,25 +1026,13 @@ describe("Discord native plugin command dispatch", () => { acceptsArgs: false, }; const interaction = createInteraction(); - const pluginMatch = { - command: { - name: "cron_jobs", - description: "List cron jobs", - pluginId: "cron-jobs", - acceptsArgs: false, - handler: vi.fn().mockResolvedValue({ text: "jobs" }), - }, - args: undefined, - }; - - runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "direct plugin output", }); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue( {} as never, ); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createMockPluginNativeCommand(cfg, commandSpec); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -997,7 +1046,6 @@ describe("Discord native plugin command dispatch", () => { it("returns an explicit warning instead of success when dispatch produces zero visible replies", async () => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({ counts: { final: 0, block: 0, tool: 0 }, queuedFinal: false, @@ -1040,6 +1088,7 @@ describe("Discord native plugin command dispatch", () => { channelConfig: null, mediaLocalRoots: [], preferFollowUp: true, + pluginCommandDispatch: { kind: "non-plugin" }, log: { error: vi.fn() } as never, }); @@ -1055,7 +1104,6 @@ describe("Discord native plugin command dispatch", () => { it("settles deliberate command silence without an empty warning", async () => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({ counts: { final: 0, block: 0, tool: 0 }, queuedFinal: false, @@ -1077,7 +1125,6 @@ describe("Discord native plugin command dispatch", () => { it("warns when a final delivery observer does not report its outcome", async () => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); nativeCommandRuntime.dispatchChannelInboundTurn = async (plan) => { await plan.delivery.onDelivered?.({ text: "unreported" }, { kind: "final" }, undefined); return { @@ -1110,7 +1157,6 @@ describe("Discord native plugin command dispatch", () => { it.each([1, 2])("settles %i suppressed finals without an empty warning", async (count) => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); nativeCommandRuntime.dispatchChannelInboundTurn = async (plan) => { for (let index = 0; index < count; index += 1) { await plan.delivery.onDelivered?.( @@ -1187,6 +1233,7 @@ describe("Discord native plugin command dispatch", () => { channelConfig: null, mediaLocalRoots: [], preferFollowUp: true, + pluginCommandDispatch: { kind: "non-plugin" }, suppressReplies: true, log: { error: vi.fn() } as never, }); @@ -1248,6 +1295,7 @@ describe("Discord native plugin command dispatch", () => { channelConfig: null, mediaLocalRoots: [], preferFollowUp: true, + pluginCommandDispatch: { kind: "non-plugin" }, suppressReplies: true, log: { error: vi.fn() } as never, }); @@ -1262,7 +1310,6 @@ describe("Discord native plugin command dispatch", () => { interaction.followUp .mockResolvedValueOnce({ ok: true }) .mockRejectedValueOnce({ discordCode: 10062, message: "Unknown interaction" }); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); runtimeModuleMocks.dispatchReplyWithDispatcher.mockImplementation(async (params: unknown) => { const dispatcherOptions = ( params as { @@ -1315,7 +1362,6 @@ describe("Discord native plugin command dispatch", () => { discordCode: 10062, message: "Unknown interaction", }); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); nativeCommandRuntime.dispatchChannelInboundTurn = async (plan) => { const reportSuppressed = (suppressedKind: "block" | "final" | "tool") => plan.delivery.onDelivered?.( @@ -1386,7 +1432,6 @@ describe("Discord native plugin command dispatch", () => { ])("keeps an accepted final visible alongside $label", async ({ outcomes }) => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); for (const outcome of outcomes) { if (outcome === "accepted") { interaction.followUp.mockResolvedValueOnce({ ok: true }); @@ -1454,7 +1499,6 @@ describe("Discord native plugin command dispatch", () => { interaction.followUp .mockResolvedValueOnce({ ok: true }) .mockRejectedValueOnce(new Error("provider connection failed")); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); runtimeModuleMocks.dispatchReplyWithDispatcher.mockImplementation(async (params: unknown) => { const dispatcherOptions = ( params as { @@ -1491,7 +1535,6 @@ describe("Discord native plugin command dispatch", () => { it("does not warn when dispatch reports a queued final without visible counts", async () => { const cfg = createConfig(); const interaction = createInteraction(); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({ counts: { final: 0, block: 0, tool: 0 }, queuedFinal: true, @@ -1516,23 +1559,11 @@ describe("Discord native plugin command dispatch", () => { acceptsArgs: false, }; const interaction = createInteraction(); - const pluginMatch = { - command: { - name: "cron_jobs", - description: "List cron jobs", - pluginId: "cron-jobs", - acceptsArgs: false, - handler: vi.fn().mockResolvedValue({ text: "" }), - }, - args: undefined, - }; - - runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never); - runtimeModuleMocks.executePluginCommand.mockResolvedValue({}); + runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({}); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue( {} as never, ); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createMockPluginNativeCommand(cfg, commandSpec); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -1549,23 +1580,11 @@ describe("Discord native plugin command dispatch", () => { acceptsArgs: false, }; const interaction = createInteraction(); - const pluginMatch = { - command: { - name: "cron_jobs", - description: "List cron jobs", - pluginId: "cron-jobs", - acceptsArgs: false, - handler: vi.fn().mockResolvedValue({ suppressReply: true }), - }, - args: undefined, - }; - - runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never); - runtimeModuleMocks.executePluginCommand.mockResolvedValue({ suppressReply: true }); + runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ suppressReply: true }); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue( {} as never, ); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createMockPluginNativeCommand(cfg, commandSpec); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -1611,22 +1630,10 @@ describe("Discord native plugin command dispatch", () => { guildId: "345678901234567890", guildName: "Test Guild", }); - const pluginMatch = { - command: { - name: "cron_jobs", - description: "List cron jobs", - pluginId: "cron-jobs", - acceptsArgs: false, - handler: vi.fn().mockResolvedValue({ text: "jobs" }), - }, - args: undefined, - }; - - runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "direct plugin output", }); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createMockPluginNativeCommand(cfg, commandSpec); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -1691,22 +1698,10 @@ describe("Discord native plugin command dispatch", () => { return null; }, ); - const pluginMatch = { - command: { - name: "cron_jobs", - description: "List cron jobs", - pluginId: "cron-jobs", - acceptsArgs: false, - handler: vi.fn().mockResolvedValue({ text: "jobs" }), - }, - args: undefined, - }; - - runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never); - const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({ + const executeSpy = runtimeModuleMocks.pluginCommandHandler.mockResolvedValue({ text: "direct plugin output", }); - const command = await createNativeCommand(cfg, commandSpec); + const command = await createMockPluginNativeCommand(cfg, commandSpec); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); @@ -1780,7 +1775,6 @@ describe("Discord native plugin command dispatch", () => { sessionKey: `agent:qwen:discord:channel:${channelId}`, agentId: "qwen", }); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher; const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession; const command = await createStatusCommand(cfg); @@ -1853,7 +1847,6 @@ describe("Discord native plugin command dispatch", () => { }), ); nativeCommandRuntime.resolveDiscordNativeInteractionRouteState = resolveRouteState; - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); const dispatchSpy = createDispatchSpy(); const command = await createNativeCommand(cfg, { name: "new", @@ -1883,7 +1876,6 @@ describe("Discord native plugin command dispatch", () => { sessionKey: "agent:codex:acp:binding:discord:default:recovery", agentId: "codex", }); - runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); const dispatchSpy = createDispatchSpy(); const command = await createNativeCommand(cfg, { name: "new", diff --git a/extensions/discord/src/monitor/native-command.runtime.ts b/extensions/discord/src/monitor/native-command.runtime.ts index 7bba7b83f6db..2cda989ce3c6 100644 --- a/extensions/discord/src/monitor/native-command.runtime.ts +++ b/extensions/discord/src/monitor/native-command.runtime.ts @@ -1,13 +1,10 @@ import { dispatchChannelInboundTurn } from "openclaw/plugin-sdk/channel-inbound"; // Discord plugin module implements native command behavior. import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime"; -import * as pluginRuntime from "openclaw/plugin-sdk/plugin-runtime"; import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; import { resolveDiscordNativeInteractionRouteState } from "./native-command-route.js"; export const nativeCommandRuntime = { - matchPluginCommand: pluginRuntime.matchPluginCommand, - executePluginCommand: pluginRuntime.executePluginCommand, dispatchChannelInboundTurn, resolveDirectStatusReplyForSession, resolveDiscordNativeInteractionRouteState, diff --git a/extensions/discord/src/monitor/native-command.status-direct.test.ts b/extensions/discord/src/monitor/native-command.status-direct.test.ts index d7267741fc26..d63c8766fc94 100644 --- a/extensions/discord/src/monitor/native-command.status-direct.test.ts +++ b/extensions/discord/src/monitor/native-command.status-direct.test.ts @@ -82,13 +82,25 @@ function createConfig(params?: { requireMention?: boolean }): OpenClawConfig { } as OpenClawConfig; } -async function createStatusCommand(cfg: OpenClawConfig) { +async function createStatusCommand(cfg: OpenClawConfig, pluginExecute?: ReturnType) { return createDiscordNativeCommand({ command: { name: "status", description: "Status", acceptsArgs: false, - }, + ...(pluginExecute + ? { + requireAuth: true, + prepareDispatch: () => ({ + kind: "plugin" as const, + invocation: { + runtime: { execute: pluginExecute }, + selection: Object.freeze({}), + }, + }), + } + : {}), + } as never, cfg, discordConfig: cfg.channels?.discord ?? {}, accountId: "default", @@ -182,8 +194,6 @@ describe("discord native /status", () => { fileName: "status.png", }); nativeCommandRuntime.dispatchChannelInboundTurn = dispatchChannelInboundTurnForTest; - nativeCommandRuntime.matchPluginCommand = (() => - null) as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand; setDefaultRouteState(); }); @@ -206,20 +216,8 @@ describe("discord native /status", () => { it("prioritizes direct status replies over matching plugin commands", async () => { const executePluginCommand = vi.fn(async () => ({ text: "plugin status" })); - nativeCommandRuntime.matchPluginCommand = (() => ({ - command: { - name: "status", - description: "Plugin status", - pluginId: "status-plugin", - acceptsArgs: false, - handler: async () => ({ text: "plugin status" }), - }, - args: undefined, - })) as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand; - nativeCommandRuntime.executePluginCommand = - executePluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").executePluginCommand; const cfg = createConfig(); - const command = await createStatusCommand(cfg); + const command = await createStatusCommand(cfg, executePluginCommand); const interaction = createInteraction(); await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); diff --git a/extensions/discord/src/monitor/native-command.ts b/extensions/discord/src/monitor/native-command.ts index 6226dd40ee55..485d2e64b65f 100644 --- a/extensions/discord/src/monitor/native-command.ts +++ b/extensions/discord/src/monitor/native-command.ts @@ -15,6 +15,10 @@ import { type ChatCommandDefinition, type NativeCommandSpec, } from "openclaw/plugin-sdk/native-command-registry"; +import type { + PluginCommandCatalogDecision, + PluginCommandNativeCandidate, +} from "openclaw/plugin-sdk/plugin-command-runtime"; import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking"; import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { createSubsystemLogger, logVerbose } from "openclaw/plugin-sdk/runtime-env"; @@ -87,8 +91,10 @@ import type { ThreadBindingManager } from "./thread-bindings.js"; const log = createSubsystemLogger("discord/native-command"); +const NON_PLUGIN_COMMAND_DISPATCH = Object.freeze({ kind: "non-plugin" as const }); + export function createDiscordNativeCommand(params: { - command: NativeCommandSpec; + command: NativeCommandSpec | PluginCommandNativeCandidate; cfg: OpenClawConfig; discordConfig: DiscordConfig; accountId: string; @@ -106,14 +112,13 @@ export function createDiscordNativeCommand(params: { threadBindings, } = params; const fallbackCommandDefinition = createNativeCommandDefinition(command); - const pluginCommandMatch = nativeCommandRuntime.matchPluginCommand(`/${command.name}`); - const commandDefinition = - pluginCommandMatch !== null - ? fallbackCommandDefinition - : (findCommandByNativeName(command.name, "discord", { - includeBundledChannelFallback: false, - }) ?? fallbackCommandDefinition); - const argDefinitions = commandDefinition.args ?? command.args; + const pluginCommandCandidate = "prepareDispatch" in command ? command : undefined; + const commandDefinition = pluginCommandCandidate + ? fallbackCommandDefinition + : (findCommandByNativeName(command.name, "discord", { + includeBundledChannelFallback: false, + }) ?? fallbackCommandDefinition); + const argDefinitions = commandDefinition.args ?? ("args" in command ? command.args : undefined); const resolveCurrentConfig = () => getRuntimeConfigSnapshot() ?? cfg; const commandOptions = buildDiscordCommandOptions({ command: commandDefinition, @@ -125,7 +130,7 @@ export function createDiscordNativeCommand(params: { cfg: resolveCurrentConfig(), discordConfig, accountId, - skipCommandOwnerAllowFrom: pluginCommandMatch !== null, + skipCommandOwnerAllowFrom: pluginCommandCandidate !== undefined, }), resolveChoiceContext: async (interaction) => resolveDiscordNativeChoiceContext({ @@ -181,6 +186,9 @@ export function createDiscordNativeCommand(params: { } satisfies DiscordCommandArgs) : undefined; const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw); + const preparedPluginCommand = pluginCommandCandidate?.prepareDispatch( + commandArgsWithRaw?.raw, + ); await dispatchDiscordCommandInteraction({ interaction, prompt, @@ -195,6 +203,7 @@ export function createDiscordNativeCommand(params: { preferFollowUp: true, threadBindings, responseEphemeral: ephemeralDefault, + pluginCommandDispatch: preparedPluginCommand ?? NON_PLUGIN_COMMAND_DISPATCH, }); } })(); @@ -213,6 +222,7 @@ async function dispatchDiscordCommandInteraction(params: { threadBindings: ThreadBindingManager; responseEphemeral?: boolean; suppressReplies?: boolean; + pluginCommandDispatch: PluginCommandCatalogDecision; }): Promise { const { interaction, @@ -460,13 +470,12 @@ async function dispatchDiscordCommandInteraction(params: { } } - const pluginMatch = nativeCommandRuntime.matchPluginCommand(prompt); if ( commandOwnerAllowFrom && !commandOwnerAccessAllowed && !commandsAllowFromAccess.allowed && commandName !== "status" && - !pluginMatch + params.pluginCommandDispatch.kind !== "plugin" ) { await respond("You are not authorized to use this command.", { ephemeral: true }); return { accepted: false }; @@ -545,7 +554,7 @@ async function dispatchDiscordCommandInteraction(params: { return { accepted: true }; } - if (pluginMatch && commandName !== "status") { + if (params.pluginCommandDispatch.kind === "plugin" && commandName !== "status") { if (suppressReplies) { await settleDiscordInteractionWithoutVisibleReply(interaction); return { accepted: true }; @@ -562,9 +571,7 @@ async function dispatchDiscordCommandInteraction(params: { agentId: pluginCommandAgentId, sessionKey: effectiveRoute.sessionKey, }); - const pluginReply = await nativeCommandRuntime.executePluginCommand({ - command: pluginMatch.command, - args: pluginMatch.args, + const pluginReply = await params.pluginCommandDispatch.execute({ senderId: sender.id, channel: "discord", channelId, @@ -716,6 +723,7 @@ async function dispatchDiscordCommandInteraction(params: { responseEphemeral, suppressReplies, log, + pluginCommandDispatch: params.pluginCommandDispatch, }); return { accepted: dispatched, effectiveRoute, hiddenFinalReply }; diff --git a/extensions/discord/src/monitor/provider-runtime.ts b/extensions/discord/src/monitor/provider-runtime.ts index f6cd1b5573df..09739b9e7245 100644 --- a/extensions/discord/src/monitor/provider-runtime.ts +++ b/extensions/discord/src/monitor/provider-runtime.ts @@ -11,7 +11,6 @@ import { resolveDiscordAccount } from "../accounts.js"; import { Client } from "../internal/discord.js"; import { probeDiscordApplicationId } from "../probe.js"; import { createDiscordNativeCommand } from "./native-command.js"; -import type { GetPluginCommandSpecs } from "./provider.commands.js"; import { runDiscordGatewayLifecycle } from "./provider.lifecycle.js"; type DiscordVoiceRuntimeModule = typeof import("../voice/manager.runtime.js"); @@ -53,7 +52,6 @@ export const discordProviderRuntime = { loadDiscordVoiceRuntime, loadDiscordProviderSessionRuntime, createClient: (...args: ConstructorParameters) => new Client(...args), - getPluginCommandSpecs: undefined as GetPluginCommandSpecs | undefined, resolveDiscordAccount, resolveNativeCommandsEnabled, resolveNativeSkillsEnabled, diff --git a/extensions/discord/src/monitor/provider.commands.test.ts b/extensions/discord/src/monitor/provider.commands.test.ts index ad2ef9e9a9c1..afc87dec09ad 100644 --- a/extensions/discord/src/monitor/provider.commands.test.ts +++ b/extensions/discord/src/monitor/provider.commands.test.ts @@ -1,6 +1,7 @@ import { listNativeCommandSpecsForConfig as listRealNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/command-auth-native"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { NativeCommandSpec } from "openclaw/plugin-sdk/native-command-registry"; +import { registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; import { createTestRegistry, resetPluginRuntimeStateForTest, @@ -13,6 +14,26 @@ import { discordSetupPlugin } from "../channel.setup.js"; import { DISCORD_VOICE_COMMAND_SPEC } from "../voice/command.js"; import { resolveDiscordProviderCommandSpecs } from "./provider.commands.js"; +const retainNativeCatalog = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/plugin-command-runtime", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + createPluginCommandRuntime: () => { + const runtime = actual.createPluginCommandRuntime(); + return { + ...runtime, + retainNativeCatalog: (provider: string) => { + retainNativeCatalog(provider); + runtime.retainNativeCatalog(provider); + }, + }; + }, + }; +}); + type ResolverParams = Parameters[0]; type SkillCommands = ReturnType>; @@ -53,11 +74,22 @@ function createResolverHarness( })), ], ); - const getPluginCommandSpecs = vi.fn(() => options.pluginCommandSpecs ?? []); + setActivePluginRegistry(createTestRegistry()); + for (const spec of options.pluginCommandSpecs ?? []) { + expect( + registerPluginCommand(`test-${spec.name}`, { + name: spec.name, + description: spec.description, + descriptionLocalizations: spec.descriptionLocalizations, + acceptsArgs: spec.acceptsArgs, + channels: ["discord"], + handler: async () => ({ text: "ok" }), + }), + ).toEqual({ ok: true }); + } return { error, - getPluginCommandSpecs, listNativeCommandSpecsForConfig, listSkillCommandsForAgents, log, @@ -71,7 +103,6 @@ function createResolverHarness( maxDiscordCommands: options.maxDiscordCommands ?? 3, listSkillCommandsForAgents, listNativeCommandSpecsForConfig, - getPluginCommandSpecs, }), }; } @@ -79,6 +110,7 @@ function createResolverHarness( describe("resolveDiscordProviderCommandSpecs", () => { beforeEach(() => { resetPluginRuntimeStateForTest(); + retainNativeCatalog.mockClear(); }); afterEach(() => { @@ -109,15 +141,13 @@ describe("resolveDiscordProviderCommandSpecs", () => { "skill-only", "plugin-unique", ]); - expect(resolved.commandSpecs[2]).toEqual({ + expect(resolved.commandSpecs[2]).toMatchObject({ name: "skill-only", description: "Plugin skill alias", descriptionLocalizations: { de: "Plugin-Fertigkeitsalias" }, acceptsArgs: false, }); expect(harness.error).not.toHaveBeenCalled(); - expect(harness.getPluginCommandSpecs).toHaveBeenCalledOnce(); - expect(harness.getPluginCommandSpecs).toHaveBeenCalledWith("discord", { config: cfg }); expect(harness.listNativeCommandSpecsForConfig).toHaveBeenCalledTimes(2); expect(harness.log).toHaveBeenCalledOnce(); expect(harness.log).toHaveBeenCalledWith( @@ -125,6 +155,8 @@ describe("resolveDiscordProviderCommandSpecs", () => { "5 commands exceed the 4-command Discord limit; removing per-skill commands and keeping /skill.", ), ); + expect(retainNativeCatalog).toHaveBeenCalledOnce(); + expect(retainNativeCatalog).toHaveBeenCalledWith("discord"); }); it("logs a final built-in collision once when command overflow retries without skills", async () => { @@ -151,8 +183,6 @@ describe("resolveDiscordProviderCommandSpecs", () => { 'discord: plugin command "/built-in" duplicates an existing native command. Skipping.', ), ); - expect(harness.getPluginCommandSpecs).toHaveBeenCalledOnce(); - expect(harness.getPluginCommandSpecs).toHaveBeenCalledWith("discord", { config: cfg }); expect(harness.listNativeCommandSpecsForConfig).toHaveBeenCalledTimes(2); }); @@ -191,7 +221,7 @@ describe("resolveDiscordProviderCommandSpecs", () => { expect(harness.error).toHaveBeenCalledWith( danger('discord: plugin command "/vc" duplicates an existing native command. Skipping.'), ); - expect(harness.getPluginCommandSpecs).toHaveBeenCalledOnce(); + expect(retainNativeCatalog).not.toHaveBeenCalled(); }); it("keeps a skill named vc from shadowing or duplicating voice", async () => { @@ -247,7 +277,6 @@ describe("resolveDiscordProviderCommandSpecs", () => { voiceEnabled: false, maxDiscordCommands: uniqueCount, listSkillCommandsForAgents: vi.fn(() => [voiceSkill]), - getPluginCommandSpecs: vi.fn(() => []), }); expect(resolved.skillCommands).toEqual([voiceSkill]); diff --git a/extensions/discord/src/monitor/provider.commands.ts b/extensions/discord/src/monitor/provider.commands.ts index bfe1ee943766..7f6f25ed6fee 100644 --- a/extensions/discord/src/monitor/provider.commands.ts +++ b/extensions/discord/src/monitor/provider.commands.ts @@ -4,21 +4,23 @@ import { listSkillCommandsForAgents, } from "openclaw/plugin-sdk/command-auth-native"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; +import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { mergeNativeCommandSpecs, type NativeCommandSpec, } from "openclaw/plugin-sdk/native-command-registry"; +import type { + PluginCommandNativeCandidate, + PluginCommandRuntime, +} from "openclaw/plugin-sdk/plugin-command-runtime"; import { danger, warn, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { DISCORD_VOICE_COMMAND_SPEC } from "../voice/command.js"; -export type GetPluginCommandSpecs = - typeof import("openclaw/plugin-sdk/plugin-runtime").getPluginCommandSpecs; +export type DiscordProviderCommandSpec = NativeCommandSpec | PluginCommandNativeCandidate; -const loadPluginCommandSpecs = createLazyRuntimeNamedExport( - () => import("openclaw/plugin-sdk/plugin-runtime"), - "getPluginCommandSpecs", +const loadPluginCommandRuntime = createLazyRuntimeModule( + () => import("openclaw/plugin-sdk/plugin-command-runtime"), ); export async function resolveDiscordProviderCommandSpecs(params: { @@ -30,20 +32,19 @@ export async function resolveDiscordProviderCommandSpecs(params: { maxDiscordCommands?: number; listSkillCommandsForAgents?: typeof listSkillCommandsForAgents; listNativeCommandSpecsForConfig?: typeof listNativeCommandSpecsForConfig; - getPluginCommandSpecs?: GetPluginCommandSpecs; }): Promise<{ skillCommands: ReturnType; - commandSpecs: NativeCommandSpec[]; + commandSpecs: DiscordProviderCommandSpec[]; }> { const listSkillCommands = params.listSkillCommandsForAgents ?? listSkillCommandsForAgents; const listNativeCommandSpecs = params.listNativeCommandSpecsForConfig ?? listNativeCommandSpecsForConfig; const maxDiscordCommands = params.maxDiscordCommands ?? 100; - const pluginCommandSpecs = params.nativeEnabled - ? (params.getPluginCommandSpecs ?? (await loadPluginCommandSpecs()))("discord", { - config: params.cfg, - }) - : []; + let pluginCommandRuntime: PluginCommandRuntime | undefined; + if (params.nativeEnabled) { + pluginCommandRuntime = (await loadPluginCommandRuntime()).createPluginCommandRuntime(); + } + const pluginCommandSpecs = pluginCommandRuntime?.listNativeCandidates("discord") ?? []; const onCollision = (normalizedName: string) => { params.runtime.error?.( danger( @@ -85,7 +86,7 @@ export async function resolveDiscordProviderCommandSpecs(params: { params.nativeEnabled && params.nativeSkillsEnabled ? listSkillCommands({ cfg: params.cfg }) : []; - let commandSpecs = params.nativeEnabled + let commandSpecs: DiscordProviderCommandSpec[] = params.nativeEnabled ? mergePluginCommandSpecs(listPrimaryCommandSpecs(skillCommands), (normalizedName) => provisionalCollisions.push(normalizedName), ) @@ -115,5 +116,8 @@ export async function resolveDiscordProviderCommandSpecs(params: { ), ); } + if (commandSpecs.some((command) => "prepareDispatch" in command)) { + pluginCommandRuntime?.retainNativeCatalog("discord"); + } return { skillCommands, commandSpecs }; } diff --git a/extensions/discord/src/monitor/provider.interactions.ts b/extensions/discord/src/monitor/provider.interactions.ts index 4a706978af88..29c17edce3d0 100644 --- a/extensions/discord/src/monitor/provider.interactions.ts +++ b/extensions/discord/src/monitor/provider.interactions.ts @@ -3,7 +3,6 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract"; import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context"; import type { DiscordAccountConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import type { NativeCommandSpec } from "openclaw/plugin-sdk/native-command-registry"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { createDiscordActivityButton } from "../activities/interaction.js"; import { @@ -27,6 +26,7 @@ import { createDiscordModelPickerFallbackSelect, createDiscordNativeCommand, } from "./native-command.js"; +import type { DiscordProviderCommandSpec } from "./provider.commands.js"; import { createDiscordQuestionButton } from "./questions.js"; import type { ThreadBindingManager } from "./thread-bindings.types.js"; @@ -38,7 +38,7 @@ export function createDiscordProviderInteractionSurface(params: { accountId: string; applicationId?: string; token: string; - commandSpecs: NativeCommandSpec[]; + commandSpecs: DiscordProviderCommandSpec[]; nativeEnabled: boolean; voiceEnabled: boolean; groupPolicy: "open" | "disabled" | "allowlist"; diff --git a/extensions/discord/src/monitor/provider.test-support.ts b/extensions/discord/src/monitor/provider.test-support.ts index 8daadad71ffa..1b920a16b230 100644 --- a/extensions/discord/src/monitor/provider.test-support.ts +++ b/extensions/discord/src/monitor/provider.test-support.ts @@ -26,9 +26,6 @@ export const discordProviderTestSupport = { setCreateClient(mock: typeof discordProviderRuntime.createClient) { discordProviderRuntime.createClient = mock; }, - setGetPluginCommandSpecs(mock: typeof discordProviderRuntime.getPluginCommandSpecs) { - discordProviderRuntime.getPluginCommandSpecs = mock; - }, setResolveDiscordAccount(mock: typeof discordProviderRuntime.resolveDiscordAccount) { discordProviderRuntime.resolveDiscordAccount = mock; }, diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index cb6a6d6bf271..ede3833b64e4 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -3,6 +3,10 @@ import { EventEmitter } from "node:events"; import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract"; import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { + createEmptyPluginRegistry, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/plugin-test-runtime"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { RateLimitError } from "../internal/discord.js"; import { @@ -29,7 +33,6 @@ const { createNoopThreadBindingManagerMock, createThreadBindingManagerMock, getAcpSessionStatusMock, - getPluginCommandSpecsMock, isNativeCommandsExplicitlyDisabledMock, isVerboseMock, listNativeCommandSpecsForConfigMock, @@ -208,9 +211,6 @@ describe("monitorDiscordProvider", () => { }; beforeAll(async () => { - vi.doMock("openclaw/plugin-sdk/plugin-runtime", () => ({ - getPluginCommandSpecs: getPluginCommandSpecsMock, - })); vi.doMock("../accounts.js", () => ({ resolveDiscordAccount: (...args: Parameters) => resolveDiscordAccountMock(...args), @@ -313,9 +313,7 @@ describe("monitorDiscordProvider", () => { clientGetPluginMock(name) ?? pluginRegistry.find((entry) => entry.id === name)?.plugin, } as never; }); - providerTesting.setGetPluginCommandSpecs((provider?: string) => - getPluginCommandSpecsMock(provider), - ); + setActivePluginRegistry(createEmptyPluginRegistry()); providerTesting.setResolveDiscordAccount( (...args) => resolveDiscordAccountMock(...args) as never, ); @@ -1111,7 +1109,6 @@ describe("monitorDiscordProvider", () => { }); expect(listNativeCommandSpecsForConfigMock).not.toHaveBeenCalled(); - expect(getPluginCommandSpecsMock).not.toHaveBeenCalled(); expect(clientDeployCommandsMock).not.toHaveBeenCalled(); expectMockLogNotContains(runtime.log, "cleared native commands"); }); diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index f509f50e3f0b..a1276d4338a8 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -252,7 +252,6 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { voiceEnabled, listSkillCommandsForAgents: discordProviderRuntime.listSkillCommandsForAgents, listNativeCommandSpecsForConfig: discordProviderRuntime.listNativeCommandSpecsForConfig, - getPluginCommandSpecs: discordProviderRuntime.getPluginCommandSpecs, }); const voiceManagerRef: { current: DiscordVoiceManager | null } = { current: null }; const threadBindings = threadBindingsEnabled diff --git a/extensions/discord/src/test-support/provider.test-support.ts b/extensions/discord/src/test-support/provider.test-support.ts index abf0815a2045..8493d5e105bb 100644 --- a/extensions/discord/src/test-support/provider.test-support.ts +++ b/extensions/discord/src/test-support/provider.test-support.ts @@ -10,12 +10,6 @@ type NativeCommandSpecMock = { acceptsArgs: boolean; }; -type PluginCommandSpecMock = { - name: string; - description: string; - acceptsArgs: boolean; -}; - type ProviderMonitorTestMocks = { clientDeployCommandsMock: Mock<(options?: { mode?: string }) => Promise>; clientFetchUserMock: Mock<(target: string) => Promise<{ id: string }>>; @@ -44,7 +38,6 @@ type ProviderMonitorTestMocks = { signal?: AbortSignal; }) => Promise<{ state: string }> >; - getPluginCommandSpecsMock: Mock<(provider?: string) => PluginCommandSpecMock[]>; listNativeCommandSpecsForConfigMock: Mock< ( cfg?: unknown, @@ -130,7 +123,6 @@ const providerMonitorTestMocks: ProviderMonitorTestMocks = vi.hoisted(() => { state: "idle", }), ), - getPluginCommandSpecsMock: vi.fn<(provider?: string) => PluginCommandSpecMock[]>(() => []), listNativeCommandSpecsForConfigMock: vi.fn< ( cfg?: unknown, @@ -180,7 +172,6 @@ const { reconcileAcpThreadBindingsOnStartupMock, createdBindingManagers, getAcpSessionStatusMock, - getPluginCommandSpecsMock, listNativeCommandSpecsForConfigMock, listSkillCommandsForAgentsMock, monitorLifecycleMock, @@ -244,7 +235,6 @@ export function resetDiscordProviderMonitorMocks(params?: { }); createdBindingManagers.length = 0; getAcpSessionStatusMock.mockClear().mockResolvedValue({ state: "idle" }); - getPluginCommandSpecsMock.mockClear().mockReturnValue([]); listNativeCommandSpecsForConfigMock .mockClear() .mockReturnValue( diff --git a/extensions/slack/src/monitor/slash-plugin-commands.runtime.ts b/extensions/slack/src/monitor/slash-plugin-commands.runtime.ts deleted file mode 100644 index 4fb3f4663201..000000000000 --- a/extensions/slack/src/monitor/slash-plugin-commands.runtime.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Slack plugin module implements slash plugin commands behavior. -export { listProviderPluginCommandSpecs } from "openclaw/plugin-sdk/command-auth-native"; diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index 77817157d60e..d4e01c3a5ac8 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -3,6 +3,13 @@ import type { ChatCommandDefinition } from "openclaw/plugin-sdk/command-auth-nat import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import type { NativeCommandSpec } from "openclaw/plugin-sdk/native-command-registry"; +import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; +import { + createEmptyPluginRegistry, + getActivePluginRegistry, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/plugin-test-runtime"; +import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot, @@ -117,9 +124,34 @@ const slashCommandFixtures = vi.hoisted(() => { }); const pluginCommandFixtures = vi.hoisted(() => ({ - specs: [] as NativeCommandSpec[], + specs: [] as Array< + NativeCommandSpec & { + channels?: string[]; + execute?: (args?: string) => Promise<{ text: string }>; + } + >, })); +const retainNativeCatalog = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/plugin-command-runtime", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + createPluginCommandRuntime: () => { + const runtime = actual.createPluginCommandRuntime(); + return { + ...runtime, + retainNativeCatalog: (provider: string) => { + retainNativeCatalog(provider); + runtime.retainNativeCatalog(provider); + }, + }; + }, + }; +}); + const skillCommandFixtures = vi.hoisted(() => ({ commands: [] as Array<{ name: string; skillName: string; description: string }>, })); @@ -152,16 +184,6 @@ vi.mock("./slash-commands.runtime.js", async () => { }; }); -vi.mock("./slash-plugin-commands.runtime.js", async () => { - const actual = await vi.importActual( - "./slash-plugin-commands.runtime.js", - ); - return { - ...actual, - listProviderPluginCommandSpecs: () => pluginCommandFixtures.specs, - }; -}); - vi.mock("./slash-skill-commands.runtime.js", async () => { const actual = await vi.importActual( "./slash-skill-commands.runtime.js", @@ -181,21 +203,40 @@ const { registerSlackMonitorSlashCommands } = (await import("./slash.js")) as { }; const { dispatchMock } = getSlackSlashMocks(); +setActivePluginRegistry(createEmptyPluginRegistry()); beforeEach(() => { pluginCommandFixtures.specs = []; skillCommandFixtures.commands = []; clearRuntimeConfigSnapshot(); resetSlackSlashMocks(); + clearPluginCommands(); + retainNativeCatalog.mockClear(); }); afterEach(() => { pluginCommandFixtures.specs = []; skillCommandFixtures.commands = []; clearRuntimeConfigSnapshot(); + clearPluginCommands(); }); async function registerCommands(ctx: unknown, account: unknown, trackEvent?: () => void) { + const registry = getActivePluginRegistry(); + for (const spec of pluginCommandFixtures.specs) { + if (registry?.commands.some((entry) => entry.command.name === spec.name)) { + continue; + } + expect( + registerPluginCommand(`test-${spec.name}`, { + name: spec.name, + description: spec.description, + acceptsArgs: spec.acceptsArgs, + channels: spec.channels, + handler: async ({ args }) => (spec.execute ? await spec.execute(args) : { text: "plugin" }), + }), + ).toEqual({ ok: true }); + } return await registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never, @@ -358,6 +399,16 @@ async function runCommandHandler(handler: (args: unknown) => Promise) { return { respond, ack }; } +function setAsyncDispatchMock( + implementation: (params: { replyOptions?: Record }) => Promise, +) { + ( + dispatchMock as unknown as { + mockImplementation: (callback: typeof implementation) => unknown; + } + ).mockImplementation(implementation); +} + function expectArgMenuLayout(respond: ReturnType): { type: string; elements?: Array<{ type?: string; action_id?: string; confirm?: unknown }>; @@ -739,6 +790,9 @@ describe("Slack native command argument menus", () => { }); it("prefers the configured slash command over native commands", async () => { + pluginCommandFixtures.specs = [ + { name: "slackplugin", description: "Plugin command", acceptsArgs: false }, + ]; const configuredHarness = createArgMenusHarness(); ( configuredHarness.ctx as { @@ -754,6 +808,7 @@ describe("Slack native command argument menus", () => { ), ).toBe(true); expect(configuredHarness.commands.has("/usage")).toBe(false); + expect(retainNativeCatalog).not.toHaveBeenCalled(); }); it("does not register native argument handlers for a configured slash command", async () => { @@ -810,6 +865,101 @@ describe("Slack native command argument menus", () => { ); expect(runtimeLog).not.toHaveBeenCalled(); expect(runtimeError).not.toHaveBeenCalled(); + expect(retainNativeCatalog).toHaveBeenCalledOnce(); + expect(retainNativeCatalog).toHaveBeenCalledWith("slack"); + }); + + it("executes the exact selected plugin candidate with its native arguments", async () => { + const execute = vi.fn(async (args?: string) => ({ text: `plugin:${args}` })); + pluginCommandFixtures.specs = [ + { + name: "slackplugin", + description: "Unique plugin command", + acceptsArgs: true, + execute, + }, + ]; + setAsyncDispatchMock( + async (params) => await dispatchReplyWithBufferedBlockDispatcher(params as never), + ); + const pluginHarness = createArgMenusHarness(); + await registerCommands(pluginHarness.ctx, pluginHarness.account); + const handler = requireHandler(pluginHarness.commands, "/slackplugin", "plugin command"); + + await handler({ + command: createSlashCommand({ text: "now please" }), + ack: vi.fn().mockResolvedValue(undefined), + respond: vi.fn().mockResolvedValue(undefined), + }); + + expect(execute).toHaveBeenCalledWith("now please"); + }); + + it.each(["login", "reportlong"])( + "does not execute a plugin skipped behind the primary /%s command", + async (name) => { + const execute = vi.fn(async () => ({ text: "wrong owner" })); + pluginCommandFixtures.specs = [ + { name, description: "Skipped plugin", acceptsArgs: false, execute }, + ]; + setAsyncDispatchMock( + async (params) => await dispatchReplyWithBufferedBlockDispatcher(params as never), + ); + const collisionHarness = createArgMenusHarness(); + await registerCommands(collisionHarness.ctx, collisionHarness.account); + const handler = requireHandler(collisionHarness.commands, `/${name}`, `${name} command`); + + await runCommandHandler(handler); + + expect(execute).not.toHaveBeenCalled(); + expect(retainNativeCatalog).not.toHaveBeenCalled(); + }, + ); + + it("filters a same-name plugin owned by another channel", async () => { + const execute = vi.fn(async () => ({ text: "wrong channel" })); + pluginCommandFixtures.specs = [ + { + name: "reportlong", + description: "Telegram-only plugin", + acceptsArgs: false, + channels: ["telegram"], + execute, + }, + ]; + const channelHarness = createArgMenusHarness(); + await registerCommands(channelHarness.ctx, channelHarness.account); + + await runCommandHandler( + requireHandler(channelHarness.commands, "/reportlong", "report command"), + ); + + expect(execute).not.toHaveBeenCalled(); + }); + + it.each([ + ["same name", "skill-only"], + ["dash/underscore", "foo-bar"], + ])("keeps the selected route skill for a %s plugin collision", async (_label, pluginName) => { + const skillName = pluginName === "foo-bar" ? "foo_bar" : pluginName; + skillCommandFixtures.commands = [ + { name: skillName, skillName: "Selected Skill", description: "Selected skill" }, + ]; + const execute = vi.fn(async () => ({ text: "wrong owner" })); + pluginCommandFixtures.specs = [ + { name: pluginName, description: "Colliding plugin", acceptsArgs: false, execute }, + ]; + const skillHarness = createArgMenusHarness({ commands: { native: true, nativeSkills: true } }); + (skillHarness.account as { config: OpenClawConfig }).config = { + commands: { native: true, nativeSkills: true }, + }; + await registerCommands(skillHarness.ctx, skillHarness.account); + + await runCommandHandler( + requireHandler(skillHarness.commands, `/${skillName}`, "skill command"), + ); + + expect(execute).not.toHaveBeenCalled(); }); it("deduplicates a skill after the Slack status native rename", async () => { diff --git a/extensions/slack/src/monitor/slash.ts b/extensions/slack/src/monitor/slash.ts index 351d18f26afb..49bd3572018b 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -32,6 +32,11 @@ import { mergeNativeCommandSpecs, type NativeCommandSpec, } from "openclaw/plugin-sdk/native-command-registry"; +import type { + PluginCommandCatalogDecision, + PluginCommandNativeCandidate, + PluginCommandReplyOptions, +} from "openclaw/plugin-sdk/plugin-command-runtime"; import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { danger, logVerbose, warn } from "openclaw/plugin-sdk/runtime-env"; @@ -97,13 +102,12 @@ const loadSlashDispatchRuntime = createLazyRuntimeModule( () => import("./slash-dispatch.runtime.js"), ); -const loadSlackPluginCommandsRuntime = createLazyRuntimeModule( - () => import("./slash-plugin-commands.runtime.js"), -); - const loadSlashSkillCommandsRuntime = createLazyRuntimeModule( () => import("./slash-skill-commands.runtime.js"), ); +const loadPluginCommandRuntime = createLazyRuntimeModule( + () => import("openclaw/plugin-sdk/plugin-command-runtime"), +); function resolveSlackCommandMenuModelContext(params: { cfg: SlackMonitorContext["cfg"]; @@ -388,6 +392,11 @@ type SlackCommandRegistration = | { mode: "native" } | { mode: "disabled" }; +type SlackNativeCommandSpec = NativeCommandSpec | PluginCommandNativeCandidate; +const NON_PLUGIN_COMMAND_DISPATCH = Object.freeze({ + kind: "non-plugin" as const, +}) satisfies PluginCommandCatalogDecision; + export async function registerSlackMonitorSlashCommands(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; @@ -433,6 +442,7 @@ export async function registerSlackMonitorSlashCommands(params: { prompt: string; commandArgs?: CommandArgs; commandDefinition?: ChatCommandDefinition; + pluginCommandReplyOptions?: PluginCommandReplyOptions; }) => { const { command, @@ -443,6 +453,7 @@ export async function registerSlackMonitorSlashCommands(params: { prompt, commandArgs, commandDefinition, + pluginCommandReplyOptions, } = p; const responseBudget = p.responseTransport === "web-api" @@ -910,6 +921,7 @@ export async function registerSlackMonitorSlashCommands(params: { }, replyOptions: { skillFilter: channelConfig?.skills, + ...pluginCommandReplyOptions, }, }); } catch (err) { @@ -923,8 +935,14 @@ export async function registerSlackMonitorSlashCommands(params: { } }; - let nativeCommands: NativeCommandSpec[] = []; + let nativeCommands: SlackNativeCommandSpec[] = []; let slashCommandsRuntime: typeof import("./slash-commands.runtime.js") | null = null; + let pluginCommandRuntimeModule: + | typeof import("openclaw/plugin-sdk/plugin-command-runtime") + | null = null; + let pluginCommandRuntime: + | import("openclaw/plugin-sdk/plugin-command-runtime").PluginCommandRuntime + | null = null; if ( registration.mode === "disabled" && resolveNativeCommandsEnabled({ @@ -945,10 +963,11 @@ export async function registerSlackMonitorSlashCommands(params: { skillCommands, provider: "slack", }); - const { listProviderPluginCommandSpecs } = await loadSlackPluginCommandsRuntime(); + pluginCommandRuntimeModule = await loadPluginCommandRuntime(); + pluginCommandRuntime = pluginCommandRuntimeModule.createPluginCommandRuntime(); nativeCommands = mergeNativeCommandSpecs({ primary: nativeCommands, - secondary: listProviderPluginCommandSpecs("slack"), + secondary: pluginCommandRuntime.listNativeCandidates("slack"), }); registration = nativeCommands.length > 0 ? { mode: "native" } : { mode: "disabled" }; } @@ -979,10 +998,11 @@ export async function registerSlackMonitorSlashCommands(params: { }, ); } else if (registration.mode === "native") { - if (!slashCommandsRuntime) { - throw new Error("Missing commands runtime for native Slack commands."); + if (!slashCommandsRuntime || !pluginCommandRuntimeModule || !pluginCommandRuntime) { + throw new Error("Missing command runtimes for native Slack commands."); } for (const command of nativeCommands) { + const pluginCommandCandidate = "prepareDispatch" in command ? command : undefined; ctx.app.command(`/${command.name}`, async (args: SlackCommandHandlerArgs) => { const { command: cmd, ack, respond, body } = args; const eventScope = resolveEventScope(args); @@ -990,11 +1010,12 @@ export async function registerSlackMonitorSlashCommands(params: { await ack({ text: "This Slack workspace is unavailable.", response_type: "ephemeral" }); return; } - const commandDefinition = slashCommandsRuntime.findCommandByNativeName( - command.name, - "slack", - ); + const commandDefinition = pluginCommandCandidate + ? undefined + : slashCommandsRuntime.findCommandByNativeName(command.name, "slack"); const rawText = cmd.text?.trim() ?? ""; + const pluginCommandDispatch = + pluginCommandCandidate?.prepareDispatch(rawText) ?? NON_PLUGIN_COMMAND_DISPATCH; const commandArgs = commandDefinition ? slashCommandsRuntime.parseCommandArgs(commandDefinition, rawText) : rawText @@ -1019,9 +1040,15 @@ export async function registerSlackMonitorSlashCommands(params: { prompt, commandArgs, commandDefinition: commandDefinition ?? undefined, + pluginCommandReplyOptions: { + [pluginCommandRuntimeModule.PLUGIN_COMMAND_DISPATCH]: pluginCommandDispatch, + }, }); }); } + if (nativeCommands.some((command) => "prepareDispatch" in command)) { + pluginCommandRuntime.retainNativeCatalog("slack"); + } } else { logVerbose("slack: slash commands disabled"); } @@ -1207,6 +1234,9 @@ export async function registerSlackMonitorSlashCommands(params: { prompt, commandArgs, commandDefinition: commandDefinition ?? undefined, + pluginCommandReplyOptions: pluginCommandRuntimeModule + ? { [pluginCommandRuntimeModule.PLUGIN_COMMAND_DISPATCH]: NON_PLUGIN_COMMAND_DISPATCH } + : undefined, }); }); }; diff --git a/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts b/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts index c35fb0ded9f8..09fab5c3879e 100644 --- a/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts @@ -77,7 +77,7 @@ describeTelegramDispatch("dispatchTelegramMessage delivery-basics", () => { const outbound = expectRecordFields(mockCallArg(deliverInboundReplyWithMessageSendContext), { channel: "telegram", - to: "123", + to: "telegram:123", accountId: "default", info: { kind: "final" }, replyToMode: "first", diff --git a/extensions/telegram/src/bot-native-command-deps.runtime.ts b/extensions/telegram/src/bot-native-command-deps.runtime.ts index b665dedbd2ce..36f0a8520714 100644 --- a/extensions/telegram/src/bot-native-command-deps.runtime.ts +++ b/extensions/telegram/src/bot-native-command-deps.runtime.ts @@ -1,6 +1,5 @@ import { dispatchChannelInboundTurn } from "openclaw/plugin-sdk/channel-inbound"; import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime"; -import { getPluginCommandSpecs } from "openclaw/plugin-sdk/plugin-runtime"; // Telegram plugin module implements bot native command deps behavior. import type { ModelsAuthLoginFlowOptions, @@ -21,7 +20,6 @@ export type TelegramNativeCommandDeps = Pick< | "syncTelegramMenuCommands" > & { dispatchChannelInboundTurn?: typeof dispatchChannelInboundTurn; - getPluginCommandSpecs?: typeof getPluginCommandSpecs; runModelsAuthLoginFlow?: (opts: ModelsAuthLoginFlowOptions) => Promise; sendMessageTelegram: typeof import("./send.js").sendMessageTelegram; }; @@ -44,9 +42,6 @@ export const defaultTelegramNativeCommandDeps: TelegramNativeCommandDeps & { get syncTelegramMenuCommands() { return syncTelegramMenuCommands; }, - get getPluginCommandSpecs() { - return getPluginCommandSpecs; - }, async runModelsAuthLoginFlow(opts) { const { runModelsAuthLoginFlow } = await import("openclaw/plugin-sdk/provider-auth-login-flow-runtime"); diff --git a/extensions/telegram/src/bot-native-command-menu.ts b/extensions/telegram/src/bot-native-command-menu.ts index b54ff4efac5f..301ce6e998a5 100644 --- a/extensions/telegram/src/bot-native-command-menu.ts +++ b/extensions/telegram/src/bot-native-command-menu.ts @@ -46,6 +46,9 @@ type TelegramPluginCommandSpec = { descriptionLocalizations?: Record; }; +type TelegramSelectedPluginMenuCommand = + TelegramMenuCommand & { spec: TSpec }; + const TELEGRAM_COMMAND_MENU_SCOPES: readonly TelegramCommandMenuScope[] = [ { label: "default" }, { label: "all_group_chats", options: { scope: { type: "all_group_chats" } } }, @@ -182,12 +185,17 @@ function formatTelegramCommandRetrySuccessLog(params: { ); } -export function buildPluginTelegramMenuCommands(params: { - specs: TelegramPluginCommandSpec[]; +export function buildPluginTelegramMenuCommands(params: { + specs: readonly TSpec[]; existingCommands: Set; -}): { commands: TelegramMenuCommand[]; issues: string[] } { +}): { + commands: TelegramMenuCommand[]; + selectedCommands: TelegramSelectedPluginMenuCommand[]; + issues: string[]; +} { const { specs, existingCommands } = params; const commands: TelegramMenuCommand[] = []; + const selectedCommands: TelegramSelectedPluginMenuCommand[] = []; const issues: string[] = []; const pluginCommandNames = new Set(); @@ -238,14 +246,20 @@ export function buildPluginTelegramMenuCommands(params: { } pluginCommandNames.add(normalized); existingCommands.add(normalized); - const menuCommand: TelegramMenuCommand = { command: normalized, description }; + const menuCommand: TelegramSelectedPluginMenuCommand = { + command: normalized, + description, + spec, + }; if (spec.descriptionLocalizations) { menuCommand.descriptionLocalizations = spec.descriptionLocalizations; } - commands.push(menuCommand); + const { spec: _spec, ...displayCommand } = menuCommand; + commands.push(displayCommand); + selectedCommands.push(menuCommand); } - return { commands, issues }; + return { commands, selectedCommands, issues }; } export function buildCappedTelegramMenuCommands(params: { diff --git a/extensions/telegram/src/bot-native-commands.login.test.ts b/extensions/telegram/src/bot-native-commands.login.test.ts index a3e2d8fd0b16..a1ae41c28a1c 100644 --- a/extensions/telegram/src/bot-native-commands.login.test.ts +++ b/extensions/telegram/src/bot-native-commands.login.test.ts @@ -1,25 +1,55 @@ // Tests Telegram native Codex login command behavior. +import { + createEmptyPluginRegistry, + withPluginRuntimeRegistryScope, +} from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import type { ModelsAuthLoginFlowOptions } from "openclaw/plugin-sdk/provider-auth-login-flow-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createTelegramGroupCommandContext } from "./bot-native-commands.fixture-test-support.js"; +import { registerTelegramNativeCommands } from "./bot-native-commands.js"; import { createCommandBot, createNativeCommandTestParams, createPrivateCommandContext, resetNativeCommandMenuMocks, - waitForRegisteredCommands, } from "./bot-native-commands.menu-test-support.js"; import { telegramBotInfoForTest } from "./bot.create-telegram-bot.test-support.js"; import { resetTelegramForumFlagCacheForTest } from "./bot/helpers.js"; -import { resetPluginCommandMocks } from "./test-support/plugin-command.js"; -let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands; +vi.mock("./bot-native-commands.runtime.js", () => ({ + ensureConfiguredBindingRouteReady: vi.fn(async () => ({ ok: true })), + finalizeInboundContext: vi.fn((ctx: unknown) => ctx), + getAgentScopedMediaLocalRoots: vi.fn(() => []), + getSessionEntry: vi.fn(() => undefined), + recordInboundSessionMetaSafe: vi.fn(async () => undefined), + resolveChunkMode: vi.fn(() => "length"), + resolveThreadSessionKeys: vi.fn( + ({ + baseSessionKey, + parentSessionKey, + }: { + baseSessionKey: string; + parentSessionKey?: string; + }) => ({ + sessionKey: baseSessionKey, + parentSessionKey, + }), + ), +})); +vi.mock("openclaw/plugin-sdk/session-store-runtime", () => ({ + formatSqliteSessionFileMarker: vi.fn(() => "sqlite:test"), + getSessionEntry: vi.fn(() => undefined), + resolveStorePath: vi.fn(() => "/tmp/openclaw-login-test.sqlite"), + updateSessionStoreEntry: vi.fn(async () => undefined), +})); type LoginFlowMock = ReturnType; +let loginAccountIndex = 0; + function registerLoginCommand(params: { cfg: OpenClawConfig; loginFlow: LoginFlowMock; @@ -28,7 +58,9 @@ function registerLoginCommand(params: { runtime?: RuntimeEnv; }) { const botHarness = createCommandBot(); + const accountId = `login-test-${++loginAccountIndex}`; const nativeParams = createNativeCommandTestParams(params.cfg, { + accountId, bot: botHarness.bot, allowFrom: params.allowFrom ?? ["200"], ...(params.abortSignal @@ -45,20 +77,25 @@ function registerLoginCommand(params: { const result = await botHarness.bot.api.sendMessage(100, text, {}); return { messageId: String(result.message_id), chatId: "100" }; }); - const nativeCommandCallbackDispatcher = registerTelegramNativeCommands({ - ...nativeParams, - telegramDeps: { - ...nativeParams.telegramDeps, - runModelsAuthLoginFlow: params.loginFlow, - sendMessageTelegram, - } as never, - }); + const nativeCommandCallbackDispatcher = withPluginRuntimeRegistryScope( + createEmptyPluginRegistry(), + () => + registerTelegramNativeCommands({ + ...nativeParams, + telegramDeps: { + ...nativeParams.telegramDeps, + runModelsAuthLoginFlow: params.loginFlow, + sendMessageTelegram, + } as never, + }), + ); const handler = botHarness.commandHandlers.get("login"); if (!handler) { throw new Error("expected login command handler to be registered"); } return { ...botHarness, + accountId, handler, nativeCommandCallbackDispatcher, sendMessageTelegram, @@ -66,21 +103,15 @@ function registerLoginCommand(params: { } describe("registerTelegramNativeCommands /login", () => { - beforeAll(async () => { - ({ registerTelegramNativeCommands } = await import("./bot-native-commands.js")); - }); - beforeEach(() => { resetTelegramForumFlagCacheForTest(); resetNativeCommandMenuMocks(); - resetPluginCommandMocks(); }); it("handles /login codex by sending the device code before login completes", async () => { + let loginParams: ModelsAuthLoginFlowOptions | undefined; const loginFlow = vi.fn(async (params: ModelsAuthLoginFlowOptions) => { - expect(params.provider).toBe("openai"); - expect(params.method).toBe("device-code"); - expect(params.agent).toBe("main"); + loginParams = params; await params.prompter.deviceCode?.({ title: "OpenAI Codex device code", code: "ABCD-EFGH", @@ -107,18 +138,16 @@ describe("registerTelegramNativeCommands /login", () => { loginFlow, }); - const registeredCommands = await waitForRegisteredCommands(setMyCommands); + expect(setMyCommands).toHaveBeenCalledOnce(); + const registeredCommands = setMyCommands.mock.calls[0]?.[0]; expect(registeredCommands).toContainEqual({ command: "login", description: "Pair Codex login.", }); await handler(createPrivateCommandContext({ match: "codex", userId: 200 })); - await vi.waitFor(() => - expect(sendMessage.mock.calls.map((call) => String(call[1]))).toContain( - "Codex login complete. Try your request again now.", - ), - ); + expect(loginParams).toMatchObject({ provider: "openai", method: "device-code", agent: "main" }); + await vi.waitFor(() => expect(sendMessage).toHaveBeenCalledTimes(2), { timeout: 5_000 }); const texts = sendMessage.mock.calls.map((call) => String(call[1])); expect(texts[0]).toContain("URL: https://auth.openai.com/codex/device"); @@ -478,7 +507,7 @@ describe("registerTelegramNativeCommands /login", () => { profiles: [{ profileId: "openai:codex", provider: "openai", mode: "oauth" }], }; }); - const { handler, sendMessage, sendMessageTelegram } = registerLoginCommand({ + const { accountId, handler, sendMessage, sendMessageTelegram } = registerLoginCommand({ cfg: { commands: { native: true, ownerAllowFrom: ["200"] }, agents: { list: [{ id: "main", default: true }] }, @@ -498,7 +527,7 @@ describe("registerTelegramNativeCommands /login", () => { expect(sendMessageTelegram).toHaveBeenCalledWith( "telegram:100", "Codex login complete. Try your request again now.", - expect.objectContaining({ accountId: "default", token: "token" }), + expect.objectContaining({ accountId, token: "token" }), ), ); expect(sendMessage).toHaveBeenCalledTimes(1); diff --git a/extensions/telegram/src/bot-native-commands.registry.test.ts b/extensions/telegram/src/bot-native-commands.registry.test.ts index 9d2b2ca07dac..f49f3652249e 100644 --- a/extensions/telegram/src/bot-native-commands.registry.test.ts +++ b/extensions/telegram/src/bot-native-commands.registry.test.ts @@ -1,10 +1,34 @@ +import { + createEmptyPluginRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/channel-test-helpers"; // Telegram tests cover bot native commands.registry plugin behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const retainNativeCatalog = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/plugin-command-runtime", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + createPluginCommandRuntime: () => { + const runtime = actual.createPluginCommandRuntime(); + return { + ...runtime, + retainNativeCatalog: (provider: string) => { + retainNativeCatalog(provider); + runtime.retainNativeCatalog(provider); + }, + }; + }, + }; +}); let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands; -let setActivePluginRegistry: typeof import("openclaw/plugin-sdk/plugin-test-runtime").setActivePluginRegistry; let createCommandBot: typeof import("./bot-native-commands.menu-test-support.js").createCommandBot; let createNativeCommandTestParams: typeof import("./bot-native-commands.menu-test-support.js").createNativeCommandTestParams; let createPrivateCommandContext: typeof import("./bot-native-commands.menu-test-support.js").createPrivateCommandContext; @@ -14,63 +38,42 @@ let resetNativeCommandMenuMocks: typeof import("./bot-native-commands.menu-test- let waitForRegisteredCommands: typeof import("./bot-native-commands.menu-test-support.js").waitForRegisteredCommands; function createTelegramPluginRegistry() { - return { - plugins: [], - tools: [], - hooks: [], - typedHooks: [], - channels: [ - { - pluginId: "telegram", - source: "test", - plugin: { - id: "telegram", - meta: { - id: "telegram", - label: "Telegram", - selectionLabel: "Telegram", - docsPath: "/channels/telegram", - blurb: "test stub.", - }, - capabilities: { chatTypes: ["direct"] }, - config: { - listAccountIds: () => ["default"], - resolveAccount: () => ({}), - }, - commands: { - nativeCommandsAutoEnabled: true, - }, - }, + const registry = createEmptyPluginRegistry(); + registry.channels.push({ + pluginId: "telegram", + source: "test", + plugin: { + id: "telegram", + meta: { + id: "telegram", + label: "Telegram", + selectionLabel: "Telegram", + docsPath: "/channels/telegram", + blurb: "test stub.", }, - ], - channelSetups: [ - { - pluginId: "telegram", - source: "test", - enabled: true, - plugin: { - id: "telegram", - }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), }, - ], - providers: [], - speechProviders: [], - mediaUnderstandingProviders: [], - imageGenerationProviders: [], - videoGenerationProviders: [], - webFetchProviders: [], - webSearchProviders: [], - migrationProviders: [], - gatewayHandlers: {}, - httpRoutes: [], - cliRegistrars: [], - services: [], - commands: [], - conversationBindingResolvedHandlers: [], - diagnostics: [], - }; + commands: { + nativeCommandsAutoEnabled: true, + }, + }, + } as never); + registry.channelSetups.push({ + pluginId: "telegram", + source: "test", + enabled: true, + plugin: { + id: "telegram", + }, + } as never); + return registry; } +let activePluginRegistry: ReturnType; + function registerPairPluginCommand(params?: { nativeNames?: { telegram?: string; discord?: string }; nativeProgressMessages?: { telegram?: string; default?: string }; @@ -150,7 +153,9 @@ function mockCall(mock: { mock: { calls: unknown[][] } }, index: number): unknow describe("registerTelegramNativeCommands real plugin registry", () => { beforeAll(async () => { - ({ setActivePluginRegistry } = await import("openclaw/plugin-sdk/plugin-test-runtime")); + resetPluginRuntimeStateForTest(); + activePluginRegistry = createTelegramPluginRegistry(); + setActivePluginRegistry(activePluginRegistry as never); ({ registerTelegramNativeCommands } = await import("./bot-native-commands.js")); ({ createCommandBot, @@ -164,9 +169,12 @@ describe("registerTelegramNativeCommands real plugin registry", () => { }); beforeEach(() => { - setActivePluginRegistry(createTelegramPluginRegistry() as never); + resetPluginRuntimeStateForTest(); + activePluginRegistry = createTelegramPluginRegistry(); + setActivePluginRegistry(activePluginRegistry as never); clearPluginCommands(); resetNativeCommandMenuMocks(); + retainNativeCatalog.mockClear(); }); afterEach(() => { @@ -269,21 +277,99 @@ describe("registerTelegramNativeCommands real plugin registry", () => { expect(sendMessage).not.toHaveBeenCalledWith(123, "Command not found."); }); - it("keeps real plugin command handlers available when native menu registration is disabled", () => { + it.each([ + ["transformed-first", ["foo-bar", "foo_bar"]], + ["exact-first", ["foo_bar", "foo-bar"]], + ] as const)("executes the exact normalized winner with %s discovery", async (_label, names) => { + const handlers = new Map>(); + for (const name of names) { + const handler = vi.fn(async () => ({ text: name })); + handlers.set(name, handler); + expect( + registerPluginCommand(`plugin-${name}`, { + name, + description: name, + channels: ["telegram"], + requireAuth: false, + handler, + }), + ).toEqual({ ok: true }); + } const { bot, commandHandlers, setMyCommands } = createCommandBot(); + registerTelegramNativeCommands({ ...createNativeCommandTestParams({}), bot }); + const registered = await waitForRegisteredCommands(setMyCommands); + expect(registered.filter((command) => command.command === "foo_bar")).toEqual([ + { command: "foo_bar", description: "foo_bar" }, + ]); - registerPairPluginCommand(); + await requireCommandHandler(commandHandlers, "foo_bar")(createPrivateCommandContext()); - registerTelegramNativeCommands({ - ...createNativeCommandTestParams({}, { accountId: "default" }), - bot, - nativeEnabled: false, - }); - - expect(setMyCommands).not.toHaveBeenCalled(); - expect(commandHandlers.has("pair")).toBe(true); + expectLastDeliveredReplyText("foo_bar"); + expect(handlers.get("foo_bar")).toHaveBeenCalledOnce(); + expect(handlers.get("foo-bar")).not.toHaveBeenCalled(); }); + it.each([ + ["telegram-first", ["foo-bar", "foo_bar"]], + ["discord-first", ["foo_bar", "foo-bar"]], + ] as const)("ignores a cross-channel exact shadow with %s discovery", async (_label, names) => { + const telegramHandler = vi.fn(async () => ({ text: "telegram-owner" })); + const discordHandler = vi.fn(async () => ({ text: "discord-owner" })); + for (const name of names) { + const telegram = name === "foo-bar"; + expect( + registerPluginCommand(telegram ? "telegram-owner" : "discord-owner", { + name, + description: name, + channels: [telegram ? "telegram" : "discord"], + requireAuth: false, + handler: telegram ? telegramHandler : discordHandler, + }), + ).toEqual({ ok: true }); + } + const { bot, commandHandlers, setMyCommands } = createCommandBot(); + registerTelegramNativeCommands({ ...createNativeCommandTestParams({}), bot }); + await waitForRegisteredCommands(setMyCommands); + + await requireCommandHandler(commandHandlers, "foo_bar")(createPrivateCommandContext()); + + expectLastDeliveredReplyText("telegram-owner"); + expect(telegramHandler).toHaveBeenCalledOnce(); + expect(discordHandler).not.toHaveBeenCalled(); + }); + + it.each([ + { command: "pair", channels: undefined, retained: true }, + { command: "discord-only", channels: ["discord"], retained: false }, + ])( + "registers only supported plugin handlers when native menu display is disabled: $command", + ({ command, channels, retained }) => { + const { bot, commandHandlers, setMyCommands } = createCommandBot(); + + expect( + registerPluginCommand("demo-plugin", { + name: command, + description: `${command} command`, + channels, + handler: async () => ({ text: "ok" }), + }), + ).toEqual({ ok: true }); + + registerTelegramNativeCommands({ + ...createNativeCommandTestParams({}, { accountId: "default" }), + bot, + nativeEnabled: false, + }); + + expect(setMyCommands).not.toHaveBeenCalled(); + expect(commandHandlers.has(command)).toBe(retained); + expect(retainNativeCatalog).toHaveBeenCalledTimes(retained ? 1 : 0); + if (retained) { + expect(retainNativeCatalog).toHaveBeenCalledWith("telegram"); + } + }, + ); + it("allows requireAuth:false plugin commands for unauthorized senders through the real registry", async () => { const { bot, commandHandlers, sendMessage, setMyCommands } = createCommandBot(); diff --git a/extensions/telegram/src/bot-native-commands.runtime.ts b/extensions/telegram/src/bot-native-commands.runtime.ts index 92a321bb0725..a42867b2565d 100644 --- a/extensions/telegram/src/bot-native-commands.runtime.ts +++ b/extensions/telegram/src/bot-native-commands.runtime.ts @@ -4,11 +4,6 @@ export { recordInboundSessionMetaSafe, } from "openclaw/plugin-sdk/conversation-runtime"; export { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime"; -export { - executePluginCommand, - getPluginCommandSpecs, - matchPluginCommand, -} from "openclaw/plugin-sdk/plugin-runtime"; export { finalizeInboundContext, resolveChunkMode, diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index 55db7debe204..d65686f24a6f 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -1,14 +1,19 @@ import { createChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound"; +import { + createEmptyPluginRegistry, + withPluginRuntimeRegistryScope, +} from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime"; +import { registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; import { resolveChunkMode } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; import type { SessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; // Telegram tests cover bot native commands.session meta plugin behavior. import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { TelegramNativeCommandDeps } from "./bot-native-command-deps.runtime.js"; import { createTelegramGroupCommandContext, @@ -42,7 +47,6 @@ type DeliverRepliesParams = Parameters[0]; type LoadModelCatalogFn = typeof import("openclaw/plugin-sdk/agent-runtime").loadModelCatalog; type ResolveDefaultModelForAgentFn = typeof import("openclaw/plugin-sdk/agent-runtime").resolveDefaultModelForAgent; -type MatchPluginCommandFn = typeof import("./bot-native-commands.runtime.js").matchPluginCommand; const dispatchReplyResult: DispatchReplyWithBufferedBlockDispatcherResult = { queuedFinal: false, @@ -80,8 +84,7 @@ const agentRuntimeMocks = vi.hoisted(() => ({ resolveDefaultModelForAgent: vi.fn(), })); const pluginRuntimeMocks = vi.hoisted(() => ({ - executePluginCommand: vi.fn(async () => ({ text: "ok" })), - matchPluginCommand: vi.fn(() => null), + executePluginCommand: vi.fn(async (_params?: unknown) => ({ text: "ok" })), })); const replyMocks = vi.hoisted(() => ({ dispatchReplyWithBufferedBlockDispatcher: vi.fn( @@ -258,12 +261,9 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async () => { vi.mock("./bot-native-commands.runtime.js", () => { return { ensureConfiguredBindingRouteReady: persistentBindingMocks.ensureConfiguredBindingRouteReady, - executePluginCommand: pluginRuntimeMocks.executePluginCommand, finalizeInboundContext: vi.fn((ctx: unknown) => ctx), getAgentScopedMediaLocalRoots, - getPluginCommandSpecs: vi.fn(() => []), getSessionEntry: sessionMocks.getSessionEntry, - matchPluginCommand: pluginRuntimeMocks.matchPluginCommand, recordInboundSessionMetaSafe: vi.fn( async (params: { cfg: OpenClawConfig; @@ -293,17 +293,6 @@ vi.mock("./bot-native-commands.runtime.js", () => { >, }; }); -vi.mock("openclaw/plugin-sdk/plugin-runtime", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/plugin-runtime", - ); - return { - ...actual, - getPluginCommandSpecs: vi.fn(() => []), - matchPluginCommand: pluginRuntimeMocks.matchPluginCommand, - executePluginCommand: pluginRuntimeMocks.executePluginCommand, - }; -}); vi.mock("./bot/delivery.js", () => ({ deliverReplies: deliveryMocks.deliverReplies, })); @@ -311,12 +300,14 @@ vi.mock("./bot/delivery.replies.js", () => ({ deliverReplies: deliveryMocks.deliverReplies, })); -let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands; +let activePluginRegistry: ReturnType; type TelegramCommandHandler = (ctx: unknown) => Promise; -type TelegramPluginCommandSpecs = ReturnType< - NonNullable ->; +type TelegramPluginCommandSpecs = Array<{ + name: string; + description: string; + acceptsArgs?: boolean; +}>; type TelegramLoginFlow = NonNullable; function registerAndResolveStatusHandler(params: { @@ -389,7 +380,6 @@ function registerAndResolveCommandHandlerBase(params: { dispatchChannelInboundTurn: dispatchChannelInboundTurnMock as unknown as NonNullable< TelegramNativeCommandDeps["dispatchChannelInboundTurn"] >, - getPluginCommandSpecs: vi.fn(() => pluginCommandSpecs ?? []), listSkillCommandsForAgents: vi.fn(() => []), syncTelegramMenuCommands: vi.fn(), sendMessageTelegram: vi.fn(async (_to, text) => { @@ -398,24 +388,35 @@ function registerAndResolveCommandHandlerBase(params: { }), ...(runModelsAuthLoginFlow ? { runModelsAuthLoginFlow } : {}), }; - registerTelegramNativeCommands({ - ...createNativeCommandTestParams({ - bot: { - api: { - setMyCommands: vi.fn().mockResolvedValue(undefined), - sendMessage, - }, - command: vi.fn((name: string, cb: TelegramCommandHandler) => { - commandHandlers.set(name, cb); + withPluginRuntimeRegistryScope(activePluginRegistry, () => { + for (const spec of pluginCommandSpecs ?? []) { + expect( + registerPluginCommand(`test-${spec.name}`, { + ...spec, + requireAuth: true, + handler: pluginRuntimeMocks.executePluginCommand, }), - } as unknown as NativeCommandTestParams["bot"], - cfg, - allowFrom, - groupAllowFrom, - telegramCfg, - resolveTelegramGroupConfig, - telegramDeps, - }), + ).toEqual({ ok: true }); + } + registerTelegramNativeCommands({ + ...createNativeCommandTestParams({ + bot: { + api: { + setMyCommands: vi.fn().mockResolvedValue(undefined), + sendMessage, + }, + command: vi.fn((name: string, cb: TelegramCommandHandler) => { + commandHandlers.set(name, cb); + }), + } as unknown as NativeCommandTestParams["bot"], + cfg, + allowFrom, + groupAllowFrom, + telegramCfg, + resolveTelegramGroupConfig, + telegramDeps, + }), + }); }); const handler = commandHandlers.get(commandName); @@ -696,7 +697,7 @@ function resetSessionMetaMocks() { sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined); sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json"); pluginRuntimeMocks.executePluginCommand.mockClear().mockResolvedValue({ text: "ok" }); - pluginRuntimeMocks.matchPluginCommand.mockClear().mockReturnValue(null); + activePluginRegistry = createEmptyPluginRegistry(); replyMocks.dispatchReplyWithBufferedBlockDispatcher .mockClear() .mockResolvedValue(dispatchReplyResult); @@ -706,23 +707,40 @@ function resetSessionMetaMocks() { deliveryMocks.deliverReplies.mockClear().mockResolvedValue({ delivered: true }); } -describe("registerTelegramNativeCommands — session metadata", () => { - beforeAll(async () => { - const commandModule = await import("./bot-native-commands.js"); - registerTelegramNativeCommands = commandModule.registerTelegramNativeCommands; - await import("./bot-native-commands.runtime.js"); - agentRuntimeMocks.resolveDefaultModelForAgent({ cfg: {}, agentId: "main" }); - }); +activePluginRegistry = createEmptyPluginRegistry(); +const { registerTelegramNativeCommands } = await import("./bot-native-commands.js"); +await import("./bot-native-commands.runtime.js"); +agentRuntimeMocks.resolveDefaultModelForAgent({ cfg: {}, agentId: "main" }); +resetSessionMetaMocks(); +const warmStatusHandler = registerAndResolveStatusHandler({ cfg: {} }); +await warmStatusHandler.handler(createTelegramPrivateCommandContext()); +describe("registerTelegramNativeCommands — session metadata", () => { beforeEach(resetSessionMetaMocks); it("calls recordSessionMetaFromInbound after a native slash command", async () => { + const shadowHandler = vi.fn(async () => ({ text: "wrong plugin" })); + activePluginRegistry.commands.push({ + pluginId: "shadow-plugin", + source: "test", + command: { + name: "status", + description: "Shadow status", + channels: ["telegram"], + requireAuth: false, + handler: shadowHandler, + }, + }); const cfg: OpenClawConfig = {}; const { handler } = registerAndResolveStatusHandler({ cfg }); await handler(createTelegramPrivateCommandContext()); expect(sessionMocks.recordSessionMetaFromInbound).toHaveBeenCalledTimes(1); + expect(shadowHandler).not.toHaveBeenCalled(); const turnPlan = dispatchChannelInboundTurnMock.mock.calls[0]?.[0]; + expect(turnPlan?.replyOptions?.[Symbol.for("openclaw.pluginCommandDispatch") as never]).toEqual( + { kind: "non-plugin" }, + ); const call = ( sessionMocks.recordSessionMetaFromInbound.mock.calls as unknown as Array< [{ sessionKey?: string; ctx?: { OriginatingChannel?: string; Provider?: string } }] @@ -1955,29 +1973,17 @@ describe("registerTelegramNativeCommands — session metadata", () => { }); const { handler } = registerAndResolveCommandHandler({ - commandName: "codex", + commandName: "plugin_meta", cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig, groupAllowFrom: ["-1001234567890"], pluginCommandSpecs: [ { - name: "codex", + name: "plugin_meta", description: "Codex", acceptsArgs: true, }, ] as TelegramPluginCommandSpecs, }); - pluginRuntimeMocks.matchPluginCommand.mockReturnValue({ - command: { - name: "codex", - description: "Codex", - handler: vi.fn(), - pluginId: "openclaw-codex-app-server", - pluginName: "Codex", - requireAuth: true, - }, - args: "bind --cwd /tmp/work", - }); - await handler( createTelegramTopicCommandContext({ match: "bind --cwd /tmp/work", threadId: 42 }), ); @@ -1987,7 +1993,6 @@ describe("registerTelegramNativeCommands — session metadata", () => { { sessionKey: "agent:main:telegram:group:-1001234567890:topic:42", sessionId: "sess-topic", - authProfileId: "openai:owner@example.com", messageThreadId: 42, }, "plugin command params", @@ -2020,7 +2025,7 @@ describe("registerTelegramNativeCommands — session metadata", () => { }; }); - const { handler } = registerAndResolveCommandHandler({ + const { handler, sendMessage } = registerAndResolveCommandHandler({ commandName: "login", cfg: { commands: { native: true, ownerAllowFrom: ["200"] }, @@ -2066,6 +2071,13 @@ describe("registerTelegramNativeCommands — session metadata", () => { authProfileOverrideSource: "user", authProfileOverrideCompactionCount: undefined, }); + await vi.waitFor(() => + expect(sendMessage).toHaveBeenCalledWith( + 100, + "Codex login complete. Try your request again now.", + {}, + ), + ); }); it("moves a session created while Telegram login is pending to the returned profile", async () => { @@ -2356,28 +2368,16 @@ describe("registerTelegramNativeCommands — session metadata", () => { }); const { handler } = registerAndResolveCommandHandler({ - commandName: "codex", + commandName: "plugin_meta", cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig, pluginCommandSpecs: [ { - name: "codex", + name: "plugin_meta", description: "Codex", acceptsArgs: true, }, ] as TelegramPluginCommandSpecs, }); - pluginRuntimeMocks.matchPluginCommand.mockReturnValue({ - command: { - name: "codex", - description: "Codex", - handler: vi.fn(), - pluginId: "openclaw-codex-app-server", - pluginName: "Codex", - requireAuth: true, - }, - args: "status", - }); - await handler(createTelegramPrivateCommandContext({ match: "status" })); expectRecordFields( @@ -2402,28 +2402,16 @@ describe("registerTelegramNativeCommands — session metadata", () => { }); const { handler } = registerAndResolveCommandHandler({ - commandName: "codex", + commandName: "plugin_meta", cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig, pluginCommandSpecs: [ { - name: "codex", + name: "plugin_meta", description: "Codex", acceptsArgs: true, }, ] as TelegramPluginCommandSpecs, }); - pluginRuntimeMocks.matchPluginCommand.mockReturnValue({ - command: { - name: "codex", - description: "Codex", - handler: vi.fn(), - pluginId: "openclaw-codex-app-server", - pluginName: "Codex", - requireAuth: true, - }, - args: "status", - }); - await handler(createTelegramPrivateCommandContext({ match: "status" })); expectRecordFields( @@ -2448,28 +2436,16 @@ describe("registerTelegramNativeCommands — session metadata", () => { }); const { handler } = registerAndResolveCommandHandler({ - commandName: "codex", + commandName: "plugin_meta", cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig, pluginCommandSpecs: [ { - name: "codex", + name: "plugin_meta", description: "Codex", acceptsArgs: true, }, ] as TelegramPluginCommandSpecs, }); - pluginRuntimeMocks.matchPluginCommand.mockReturnValue({ - command: { - name: "codex", - description: "Codex", - handler: vi.fn(), - pluginId: "openclaw-codex-app-server", - pluginName: "Codex", - requireAuth: true, - }, - args: "status", - }); - await handler(createTelegramPrivateCommandContext({ match: "status" })); expectRecordFields( @@ -2487,28 +2463,16 @@ describe("registerTelegramNativeCommands — session metadata", () => { pluginRuntimeMocks.executePluginCommand.mockResolvedValue(undefined as never); const { handler } = registerAndResolveCommandHandler({ - commandName: "codex", + commandName: "plugin_meta", cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig, pluginCommandSpecs: [ { - name: "codex", + name: "plugin_meta", description: "Codex", acceptsArgs: true, }, ] as TelegramPluginCommandSpecs, }); - pluginRuntimeMocks.matchPluginCommand.mockReturnValue({ - command: { - name: "codex", - description: "Codex", - handler: vi.fn(), - pluginId: "openclaw-codex-app-server", - pluginName: "Codex", - requireAuth: true, - }, - args: "status", - }); - await handler(createTelegramPrivateCommandContext({ match: "status" })); const deliveryCall = requireValue( diff --git a/extensions/telegram/src/bot-native-commands.skills-allowlist.test.ts b/extensions/telegram/src/bot-native-commands.skills-allowlist.test.ts index 9b98df55c59d..c85981e896c7 100644 --- a/extensions/telegram/src/bot-native-commands.skills-allowlist.test.ts +++ b/extensions/telegram/src/bot-native-commands.skills-allowlist.test.ts @@ -2,6 +2,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { + createEmptyPluginRegistry, + withPluginRuntimeRegistryScope, +} from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { listSkillCommandsForAgents as listActualSkillCommandsForAgents } from "openclaw/plugin-sdk/skill-commands-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -10,9 +14,7 @@ import { createNativeCommandTestParams, listSkillCommandsForAgents, resetNativeCommandMenuMocks, - waitForRegisteredCommands, } from "./bot-native-commands.menu-test-support.js"; -import { resetPluginCommandMocks } from "./test-support/plugin-command.js"; import { writeSkill } from "./test-support/write-skill.js"; const tempDirs: string[] = []; @@ -26,7 +28,6 @@ async function makeWorkspace(prefix: string) { describe("registerTelegramNativeCommands skill allowlist integration", () => { afterEach(async () => { resetNativeCommandMenuMocks(); - resetPluginCommandMocks(); await Promise.all( tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), ); @@ -65,23 +66,28 @@ describe("registerTelegramNativeCommands skill allowlist integration", () => { listActualSkillCommandsForAgents({ cfg: cfgLocal, agentIds }), ); - registerTelegramNativeCommands({ - ...createNativeCommandTestParams(cfg, { - bot: { - api: { - setMyCommands, - sendMessage: vi.fn().mockResolvedValue(undefined), - }, - command: vi.fn(), - } as unknown as Parameters[0]["bot"], - runtime: { log: vi.fn() } as unknown as Parameters< - typeof registerTelegramNativeCommands - >[0]["runtime"], - accountId: "bot-a", + withPluginRuntimeRegistryScope(createEmptyPluginRegistry(), () => + registerTelegramNativeCommands({ + ...createNativeCommandTestParams(cfg, { + bot: { + api: { + setMyCommands, + sendMessage: vi.fn().mockResolvedValue(undefined), + }, + command: vi.fn(), + } as unknown as Parameters[0]["bot"], + runtime: { log: vi.fn() } as unknown as Parameters< + typeof registerTelegramNativeCommands + >[0]["runtime"], + accountId: "bot-a", + }), }), - }); + ); - const registeredCommands = await waitForRegisteredCommands(setMyCommands); + expect(setMyCommands).toHaveBeenCalledOnce(); + const registeredCommands = (setMyCommands.mock.calls[0]?.[0] ?? []) as Array<{ + command: string; + }>; expect(registeredCommands.map((entry) => entry.command)).toContain("alpha_skill"); expect(registeredCommands.map((entry) => entry.command)).not.toContain("beta_skill"); diff --git a/extensions/telegram/src/bot-native-commands.test-helpers.ts b/extensions/telegram/src/bot-native-commands.test-helpers.ts index 06c2ec53e5b7..cc4e69ba3459 100644 --- a/extensions/telegram/src/bot-native-commands.test-helpers.ts +++ b/extensions/telegram/src/bot-native-commands.test-helpers.ts @@ -1,4 +1,9 @@ // Telegram helper module supports bot native commands helpers behavior. +import { + createEmptyPluginRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/channel-test-helpers"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { ChannelGroupPolicy } from "openclaw/plugin-sdk/config-contracts"; import type { @@ -12,13 +17,11 @@ import { vi } from "vitest"; import type { TelegramNativeCommandDeps } from "./bot-native-command-deps.runtime.js"; import { registerTelegramNativeCommands } from "./bot-native-commands.js"; +resetPluginRuntimeStateForTest(); +setActivePluginRegistry(createEmptyPluginRegistry()); + type RegisterTelegramNativeCommandsParams = Parameters[0]; -type GetPluginCommandSpecsFn = - typeof import("./bot-native-commands.runtime.js").getPluginCommandSpecs; -type MatchPluginCommandFn = typeof import("./bot-native-commands.runtime.js").matchPluginCommand; -type ExecutePluginCommandFn = - typeof import("./bot-native-commands.runtime.js").executePluginCommand; type DispatchReplyWithBufferedBlockDispatcherFn = typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").dispatchReplyWithBufferedBlockDispatcher; type DispatchReplyWithBufferedBlockDispatcherResult = Awaited< @@ -43,17 +46,6 @@ type NativeCommandHarness = { readChannelAllowFromStore: AnyAsyncMock; }; -const pluginCommandMocks = vi.hoisted(() => ({ - getPluginCommandSpecs: vi.fn(() => []), - matchPluginCommand: vi.fn(() => null), - executePluginCommand: vi.fn(async () => ({ text: "ok" })), -})); -vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({ - getPluginCommandSpecs: pluginCommandMocks.getPluginCommandSpecs, - matchPluginCommand: pluginCommandMocks.matchPluginCommand, - executePluginCommand: pluginCommandMocks.executePluginCommand, -})); - const replyPipelineMocks = vi.hoisted(() => { const dispatchReplyResult: DispatchReplyWithBufferedBlockDispatcherResult = { queuedFinal: false, @@ -113,9 +105,6 @@ const dispatchChannelInboundTurnForTest: TelegramNativeCommandDeps["dispatchChan }; vi.mock("./bot-native-commands.runtime.js", () => ({ - getPluginCommandSpecs: pluginCommandMocks.getPluginCommandSpecs, - matchPluginCommand: pluginCommandMocks.matchPluginCommand, - executePluginCommand: pluginCommandMocks.executePluginCommand, finalizeInboundContext: replyPipelineMocks.finalizeInboundContext, resolveChunkMode: replyPipelineMocks.resolveChunkMode, ensureConfiguredBindingRouteReady: replyPipelineMocks.ensureConfiguredBindingRouteReady, @@ -184,7 +173,6 @@ export function createNativeCommandsHarness(params?: { readChannelAllowFromStore: readChannelAllowFromStore as TelegramNativeCommandDeps["readChannelAllowFromStore"], dispatchChannelInboundTurn: dispatchChannelInboundTurnForTest, - getPluginCommandSpecs: pluginCommandMocks.getPluginCommandSpecs, listSkillCommandsForAgents: vi.fn(() => []), syncTelegramMenuCommands: vi.fn(), sendMessageTelegram: vi.fn(async (_to, text) => { diff --git a/extensions/telegram/src/bot-native-commands.test.ts b/extensions/telegram/src/bot-native-commands.test.ts index 55422c027009..89bd3d06ce28 100644 --- a/extensions/telegram/src/bot-native-commands.test.ts +++ b/extensions/telegram/src/bot-native-commands.test.ts @@ -1,8 +1,14 @@ +import { + createEmptyPluginRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/channel-test-helpers"; // Telegram tests cover bot native commands plugin behavior. import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts"; import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry"; +import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createCommandBot, createNativeCommandTestParams, @@ -16,10 +22,6 @@ import { } from "./bot-native-commands.menu-test-support.js"; import { resetTelegramForumFlagCacheForTest } from "./bot/helpers.js"; import { normalizeTelegramCommandName, TELEGRAM_COMMAND_NAME_PATTERN } from "./command-config.js"; -import { pluginCommandMocks, resetPluginCommandMocks } from "./test-support/plugin-command.js"; - -let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands; -let parseTelegramNativeCommandCallbackData: typeof import("./bot-native-commands.js").parseTelegramNativeCommandCallbackData; type CommandBotHarness = ReturnType; type TelegramInlineKeyboardReplyMarkup = { @@ -29,29 +31,45 @@ type PlugCommandHarnessParams = { botHarness?: CommandBotHarness; cfg?: OpenClawConfig; command?: Record; + acceptsArgs?: boolean; args?: string; result?: Record; registerOverrides?: Partial[0]>; }; -function primePlugCommand(params: PlugCommandHarnessParams = {}) { - pluginCommandMocks.getPluginCommandSpecs.mockReturnValue([ - { - name: "plug", - description: "Plugin command", - }, - ] as never); - pluginCommandMocks.matchPluginCommand.mockReturnValue({ - command: { - key: "plug", +const pluginCommandHandler = vi.fn(async (_ctx: Record) => ({ text: "ok" })); + +function registerTestPluginCommand(params: { + name: string; + description: string; + acceptsArgs?: boolean; + command?: Record; + result?: Record; +}) { + const result = params.result ?? { text: "ok" }; + expect( + registerPluginCommand(`test-${params.name}`, { + name: params.name, + description: params.description, + acceptsArgs: params.acceptsArgs, requireAuth: false, ...params.command, - }, - args: params.args, - } as never); - pluginCommandMocks.executePluginCommand.mockResolvedValue( - (params.result ?? { text: "ok" }) as never, - ); + handler: async (ctx) => { + await pluginCommandHandler(ctx as unknown as Record); + return result; + }, + }), + ).toEqual({ ok: true }); +} + +function primePlugCommand(params: PlugCommandHarnessParams = {}) { + registerTestPluginCommand({ + name: "plug", + description: "Plugin command", + acceptsArgs: params.acceptsArgs ?? true, + command: params.command, + result: params.result, + }); } function registerPlugCommand(params: PlugCommandHarnessParams = {}) { @@ -107,7 +125,7 @@ function firstDeliverRepliesParams() { function firstExecutePluginCommandParams() { return firstCallArg( - pluginCommandMocks.executePluginCommand as unknown as { + pluginCommandHandler as unknown as { mock: { calls: Array> }; }, ); @@ -122,16 +140,20 @@ function replyAt(params: Record, index = 0) { return reply; } -describe("registerTelegramNativeCommands", () => { - beforeAll(async () => { - ({ registerTelegramNativeCommands, parseTelegramNativeCommandCallbackData } = - await import("./bot-native-commands.js")); - }); +resetPluginRuntimeStateForTest(); +setActivePluginRegistry(createEmptyPluginRegistry()); +const { registerTelegramNativeCommands, parseTelegramNativeCommandCallbackData } = + await import("./bot-native-commands.js"); +registerTelegramNativeCommands(createNativeCommandTestParams({})); +describe("registerTelegramNativeCommands", () => { beforeEach(() => { resetTelegramForumFlagCacheForTest(); resetNativeCommandMenuMocks(); - resetPluginCommandMocks(); + resetPluginRuntimeStateForTest(); + setActivePluginRegistry(createEmptyPluginRegistry()); + clearPluginCommands(); + pluginCommandHandler.mockClear(); }); it("scopes skill commands when account binding exists", () => { @@ -213,10 +235,8 @@ describe("registerTelegramNativeCommands", () => { agents: { list: [{ id: "main", default: true }] }, }; listSkillCommandsForAgents.mockReturnValue(skillCommands); - pluginCommandMocks.getPluginCommandSpecs.mockReturnValue([ - { name: "zeta", description: "Zeta unchanged" }, - { name: "alpha", description: "Alpha unchanged" }, - ] as never); + registerTestPluginCommand({ name: "zeta", description: "Zeta unchanged" }); + registerTestPluginCommand({ name: "alpha", description: "Alpha unchanged" }); registerTelegramNativeCommands( createNativeCommandTestParams(cfg, { @@ -322,7 +342,7 @@ describe("registerTelegramNativeCommands", () => { expect(registeredHandlers).not.toContain("export-session"); }); - it("resolves plugin commands with the Telegram runtime config", () => { + it("resolves plugin commands from one registry-bound runtime", () => { const cfg: OpenClawConfig = { commands: { native: true }, channels: { @@ -332,20 +352,16 @@ describe("registerTelegramNativeCommands", () => { }, }; - registerTelegramNativeCommands(createNativeCommandTestParams(cfg)); - - expect(pluginCommandMocks.getPluginCommandSpecs).toHaveBeenCalledWith("telegram", { - config: cfg, - }); + registerTestPluginCommand({ name: "plug", description: "Plugin command" }); + const { bot, commandHandlers } = createCommandBot(); + registerTelegramNativeCommands(createNativeCommandTestParams(cfg, { bot })); + expect(commandHandlers.has("plug")).toBe(true); }); it("registers only Telegram-safe command names across native, custom, and plugin sources", async () => { const setMyCommands = vi.fn().mockResolvedValue(undefined); - pluginCommandMocks.getPluginCommandSpecs.mockReturnValue([ - { name: "plugin-status", description: "Plugin status" }, - { name: "plugin@bad", description: "Bad plugin command" }, - ] as never); + registerTestPluginCommand({ name: "plugin-status", description: "Plugin status" }); registerTelegramNativeCommands({ ...createNativeCommandTestParams({}), @@ -521,11 +537,10 @@ describe("registerTelegramNativeCommands", () => { }); it("replies to unmatched plugin commands in the originating forum topic", async () => { - const { handler, sendMessage } = registerPlugCommand(); - pluginCommandMocks.matchPluginCommand.mockReturnValue(null as never); + const { handler, sendMessage } = registerPlugCommand({ acceptsArgs: false }); await handler({ - match: "", + match: "unexpected", message: { message_id: 2, date: Math.floor(Date.now() / 1000), diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 2865747e2dc0..706d3fbbc0eb 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -40,6 +40,11 @@ import type { import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime"; +import { + createPluginCommandRuntime, + PLUGIN_COMMAND_DISPATCH, + type PluginCommandCatalogDecision, +} from "openclaw/plugin-sdk/plugin-command-runtime"; import { codexChannelLoginRuntime } from "openclaw/plugin-sdk/provider-auth-login-flow-runtime"; import { hasOutboundReplyContent } from "openclaw/plugin-sdk/reply-payload"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; @@ -124,6 +129,9 @@ import { getTopicName, resolveTopicNameCacheScope } from "./topic-name-cache.js" export { parseTelegramNativeCommandCallbackData } from "./native-command-callback-data.js"; const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again."; +const NON_PLUGIN_COMMAND_DISPATCH = Object.freeze({ + kind: "non-plugin", +}) satisfies PluginCommandCatalogDecision; const activeTelegramCodexLoginFlows = codexChannelLoginRuntime.createFlowRegistry(); type TelegramNativeCommandContext = Context & { match?: string }; @@ -259,15 +267,6 @@ function resolveTelegramCommandSessionFile(params: { }); } -function resolveTelegramProgressPlaceholder(command: { - nativeProgressMessages?: Partial> & { default?: string }; -}): string | null { - const text = - command.nativeProgressMessages?.telegram?.trim() ?? - command.nativeProgressMessages?.default?.trim(); - return text ? text : null; -} - async function resolveTelegramCommandTranscriptContext(params: { cfg: OpenClawConfig; agentId: string; @@ -942,10 +941,8 @@ export const registerTelegramNativeCommands = ({ agentIds: [boundRoute.agentId], }) : []; - const pluginCommandSpecs = - ( - telegramDeps.getPluginCommandSpecs ?? defaultTelegramNativeCommandDeps.getPluginCommandSpecs - )?.("telegram", { config: cfg }) ?? []; + const pluginCommandRuntime = createPluginCommandRuntime(); + const pluginCommandSpecs = pluginCommandRuntime.listNativeCandidates("telegram"); const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram" }) : []; @@ -1212,7 +1209,7 @@ export const registerTelegramNativeCommands = ({ rawText: string, ) => Promise) | undefined; - if (nativeCommandsToHandle.length > 0 || pluginCatalog.commands.length > 0) { + if (nativeCommandsToHandle.length > 0 || pluginCatalog.selectedCommands.length > 0) { for (const command of nativeCommandsToHandle) { const normalizedCommandName = normalizeTelegramCommandName(command.name); const commandDefinition = findCommandByNativeName(command.name, "telegram"); @@ -1847,6 +1844,7 @@ export const registerTelegramNativeCommands = ({ replyOptions: { skillFilter, disableBlockStreaming, + [PLUGIN_COMMAND_DISPATCH]: NON_PLUGIN_COMMAND_DISPATCH, }, }; const turnResult = await ( @@ -1889,7 +1887,7 @@ export const registerTelegramNativeCommands = ({ } } - for (const pluginCommand of pluginCatalog.commands) { + for (const pluginCommand of pluginCatalog.selectedCommands) { bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => { const msg = ctx.message; if (!msg) { @@ -1910,9 +1908,9 @@ export const registerTelegramNativeCommands = ({ const { threadParams } = await resolveTelegramNativeCommandThreadContext({ msg, bot }); const rawText = ctx.match?.trim() ?? ""; const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`; - const nativeCommandRuntime = await loadTelegramNativeCommandRuntime(); - const match = nativeCommandRuntime.matchPluginCommand(commandBody); - if (!match) { + const candidate = pluginCommand.spec; + const pluginCommandDispatch = candidate.prepareDispatch(rawText); + if (pluginCommandDispatch.kind === "non-plugin") { await withTelegramApiErrorLogging({ operation: "sendMessage", runtime, @@ -1920,6 +1918,7 @@ export const registerTelegramNativeCommands = ({ }); return; } + const nativeCommandRuntime = await loadTelegramNativeCommandRuntime(); const auth = await resolveTelegramCommandAuth({ msg, bot, @@ -1931,7 +1930,7 @@ export const registerTelegramNativeCommands = ({ groupAllowFrom: turnSettings.groupAllowFrom, resolveGroupPolicy, resolveTelegramGroupConfig, - requireAuth: match.command.requireAuth !== false, + requireAuth: candidate.requireAuth, }); if (!auth) { return; @@ -1990,7 +1989,7 @@ export const registerTelegramNativeCommands = ({ const { deliverReplies, emitTelegramMessageSentHooks } = await loadTelegramNativeCommandDeliveryRuntime(); let progressMessageId: number | undefined; - const progressPlaceholder = resolveTelegramProgressPlaceholder(match.command); + const progressPlaceholder = candidate.progressMessage; if (progressPlaceholder) { try { @@ -2021,9 +2020,7 @@ export const registerTelegramNativeCommands = ({ }); const result = normalizeTelegramNativeReplyPayload( - await nativeCommandRuntime.executePluginCommand({ - command: match.command, - args: match.args, + await pluginCommandDispatch.execute({ senderId, channel: "telegram", isAuthorizedSender: commandAuthorized, @@ -2116,6 +2113,9 @@ export const registerTelegramNativeCommands = ({ }); }); } + if (pluginCatalog.selectedCommands.length > 0) { + pluginCommandRuntime.retainNativeCatalog("telegram"); + } } if (!handleLoginCallback) { diff --git a/extensions/telegram/src/test-support/plugin-command.ts b/extensions/telegram/src/test-support/plugin-command.ts deleted file mode 100644 index 8c33a8b19a02..000000000000 --- a/extensions/telegram/src/test-support/plugin-command.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Telegram plugin module implements plugin command behavior. -import { vi } from "vitest"; - -export const pluginCommandMocks = { - getPluginCommandSpecs: vi.fn(() => []), - matchPluginCommand: vi.fn(() => null), - executePluginCommand: vi.fn(async () => ({ text: "ok" })), -}; - -vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({ - getPluginCommandSpecs: pluginCommandMocks.getPluginCommandSpecs, - matchPluginCommand: pluginCommandMocks.matchPluginCommand, - executePluginCommand: pluginCommandMocks.executePluginCommand, -})); - -export function resetPluginCommandMocks() { - pluginCommandMocks.getPluginCommandSpecs.mockClear(); - pluginCommandMocks.getPluginCommandSpecs.mockReturnValue([]); - pluginCommandMocks.matchPluginCommand.mockClear(); - pluginCommandMocks.matchPluginCommand.mockReturnValue(null); - pluginCommandMocks.executePluginCommand.mockClear(); - pluginCommandMocks.executePluginCommand.mockResolvedValue({ text: "ok" }); -} diff --git a/package.json b/package.json index 0f2a6ddfeded..6d74ba904f36 100644 --- a/package.json +++ b/package.json @@ -734,6 +734,10 @@ "./plugin-sdk/tts-runtime": { "default": "./dist/plugin-sdk/tts-runtime.js" }, + "./plugin-sdk/plugin-command-runtime": { + "types": "./dist/plugin-sdk/plugin-command-runtime.d.ts", + "default": "./dist/plugin-sdk/plugin-command-runtime.js" + }, "./plugin-sdk/plugin-runtime": { "types": "./dist/plugin-sdk/plugin-runtime.d.ts", "default": "./dist/plugin-sdk/plugin-runtime.js" diff --git a/scripts/lib/plugin-sdk-doc-metadata.ts b/scripts/lib/plugin-sdk-doc-metadata.ts index 628e75355ddb..5a3bbbaf8e2c 100644 --- a/scripts/lib/plugin-sdk-doc-metadata.ts +++ b/scripts/lib/plugin-sdk-doc-metadata.ts @@ -84,6 +84,9 @@ export const pluginSdkDocMetadata = { "runtime-store": { category: "runtime", }, + "plugin-command-runtime": { + category: "runtime", + }, "session-store-runtime": { category: "runtime", }, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 8b6a34e9d7da..811729352b0f 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -98,6 +98,7 @@ "speech-core", "speech-settings", "tts-runtime", + "plugin-command-runtime", "plugin-runtime", "channel-secret-basic-runtime", "channel-secret-runtime", diff --git a/scripts/plugin-sdk-surface-report.mts b/scripts/plugin-sdk-surface-report.mts index 282200cf69bd..149999b07cf9 100644 --- a/scripts/plugin-sdk-surface-report.mts +++ b/scripts/plugin-sdk-surface-report.mts @@ -187,7 +187,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +1: dependency-light agent scope helpers for doctor migration enumeration. // +1: dependency-light channel streaming config readers for doctor closures // (realtime-voice-activation is private-local and not counted here). - 151, + // +1: registry-bound plugin command planning and exact selected execution. + 152, env, ), publicExports: readPluginSdkSurfaceBudgetEnv( @@ -252,10 +253,11 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +3: session-catalog terminal-start provider request and Gateway params/result contracts. // +1: worker desktop endpoint contract for desktop-capable worker leases. // +1: native command spec merger through the native-command-registry facade. + // +8: focused plugin command runtime factory, dispatch symbol, and six readonly contracts. // -2: remove unused WhatsApp-specific ack policy exports from channel-feedback. // -7: retire unused and duplicate inbound-dispatch compatibility exports. // +7: restore still-existing deprecated inbound-dispatch compatibility re-exports. - 4848, + 4856, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -309,10 +311,12 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env // +4: focused agent scope functions for doctor migration enumeration. // +3: channel streaming config reader functions and session-agent scope resolver. // +1: native command spec merger through the native-command-registry facade. + // +1: focused registry-bound plugin command runtime factory. // -1: remove the unused WhatsApp-specific ack policy helper. // -10: collapse inbound-dispatch callable aliases and wrappers. // +7: restore still-existing deprecated inbound-dispatch callable re-exports. - 2918, + // -3: keep the generic plugin-command reply carrier opaque and non-callable. + 2919, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/auto-reply/reply/commands-plugin.test.ts b/src/auto-reply/reply/commands-plugin.test.ts index 54f445bec635..138b147beff0 100644 --- a/src/auto-reply/reply/commands-plugin.test.ts +++ b/src/auto-reply/reply/commands-plugin.test.ts @@ -1,19 +1,43 @@ // Tests plugin command dispatch and plugin-scoped command aliases. import { expectDefined } from "@openclaw/normalization-core"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { parseSqliteSessionFileMarker } from "../../config/sessions/legacy-sqlite-marker.js"; +import { registerPluginCommandInRegistry } from "../../plugins/command-registration.js"; +import { + PLUGIN_COMMAND_DISPATCH, + type PluginCommandExecutionReplyOptions, +} from "../../plugins/plugin-command-runtime.js"; +import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; +import type { PluginRegistry } from "../../plugins/registry-types.js"; +import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; +import type { PluginCommandContext, PluginCommandResult } from "../../plugins/types.js"; import { resolveIncognitoOpenClawAgentSqlitePath } from "../../state/openclaw-agent-db.js"; import { handlePluginCommand } from "./commands-plugin.js"; import type { HandleCommandsParams } from "./commands-types.js"; +import { shouldBypassPluginOwnedBindingForCommand } from "./dispatch-from-config.plugin-binding.js"; -const matchPluginCommandMock = vi.hoisted(() => vi.fn()); -const executePluginCommandMock = vi.hoisted(() => vi.fn()); +let registry: PluginRegistry; -vi.mock("../../plugins/commands.js", () => ({ - matchPluginCommand: matchPluginCommandMock, - executePluginCommand: executePluginCommandMock, -})); +function registerTestCommand( + result: PluginCommandResult = { text: "from plugin" }, + overrides: Partial[2]> = {}, +) { + const handler = vi.fn(async (_ctx: PluginCommandContext) => result); + expect( + registerPluginCommandInRegistry(registry, "test-plugin", { + name: "card", + description: "Card command", + handler, + ...overrides, + }), + ).toEqual({ ok: true }); + return handler; +} + +function firstCommandContext(handler: ReturnType) { + return expectDefined(handler.mock.calls[0]?.[0], "plugin command handler context"); +} function buildPluginParams( commandBodyNormalized: string, @@ -47,15 +71,14 @@ function buildPluginParams( describe("handlePluginCommand", () => { beforeEach(() => { - vi.clearAllMocks(); + resetPluginRuntimeStateForTest(); + registry = createEmptyPluginRegistry(); + setActivePluginRegistry(registry); }); + afterEach(() => resetPluginRuntimeStateForTest()); it("dispatches registered plugin commands with gateway scopes and session metadata", async () => { - matchPluginCommandMock.mockReturnValue({ - command: { name: "card" }, - args: "", - }); - executePluginCommandMock.mockResolvedValue({ text: "from plugin" }); + const handler = registerTestCommand(); const result = await handlePluginCommand( buildPluginParams("/card", { @@ -67,22 +90,8 @@ describe("handlePluginCommand", () => { expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toBe("from plugin"); - expect(executePluginCommandMock).toHaveBeenCalledTimes(1); - const [commandParams] = expectDefined( - ( - executePluginCommandMock.mock.calls as unknown as Array< - [ - { - gatewayClientScopes?: string[]; - sessionKey?: string; - sessionId?: string; - commandBody?: string; - }, - ] - > - )[0], - "(executePluginCommandMock.mock.calls as unknown as Array<\n [\n {\n gatewayClientScopes?: string[];\n sessionKey?: string;\n sessionId?: string;\n commandBody?: string;\n },\n ]\n >)[0] test invariant", - ); + expect(handler).toHaveBeenCalledTimes(1); + const commandParams = firstCommandContext(handler); expect(commandParams.gatewayClientScopes).toEqual(["operator.write", "operator.pairing"]); expect(commandParams.sessionKey).toBe("agent:main:whatsapp:direct:test-user"); expect(commandParams.sessionId).toBe("session-plugin-command"); @@ -90,11 +99,7 @@ describe("handlePluginCommand", () => { }); it("prefers the target session entry from sessionStore for plugin command metadata", async () => { - matchPluginCommandMock.mockReturnValue({ - command: { name: "card" }, - args: "", - }); - executePluginCommandMock.mockResolvedValue({ text: "from plugin" }); + const handler = registerTestCommand(); const params = buildPluginParams("/card", { commands: { text: true }, @@ -118,23 +123,8 @@ describe("handlePluginCommand", () => { await handlePluginCommand(params, true); - expect(executePluginCommandMock).toHaveBeenCalledTimes(1); - const [commandParams] = expectDefined( - ( - executePluginCommandMock.mock.calls as unknown as Array< - [ - { - agentId?: string; - authProfileId?: string; - sessionId?: string; - sessionFile?: string; - sessionTarget?: { agentId?: string; sessionId?: string; sessionKey?: string }; - }, - ] - > - )[0], - "(executePluginCommandMock.mock.calls as unknown as Array<\n [{ authProfileId?: string; sessionId?: string; sessionFile?: string }]\n >)[0] test invariant", - ); + expect(handler).toHaveBeenCalledTimes(1); + const commandParams = firstCommandContext(handler); expect(commandParams.agentId).toBe("target"); expect(commandParams.sessionId).toBe("target-session"); expect(commandParams.sessionTarget).toMatchObject({ @@ -146,15 +136,10 @@ describe("handlePluginCommand", () => { agentId: "target", sessionId: "target-session", }); - expect(commandParams.authProfileId).toBe("openai:owner@example.com"); }); it("uses the process-local transcript store for incognito plugin commands", async () => { - matchPluginCommandMock.mockReturnValue({ - command: { name: "card" }, - args: "", - }); - executePluginCommandMock.mockResolvedValue({ text: "from plugin" }); + const handler = registerTestCommand(); const params = buildPluginParams("/card", { commands: { text: true }, @@ -173,12 +158,7 @@ describe("handlePluginCommand", () => { await handlePluginCommand(params, true); - const [commandParams] = expectDefined( - executePluginCommandMock.mock.calls[0] as unknown as [ - { sessionFile?: string; sessionTarget?: { storePath?: string } }, - ], - "plugin command invocation", - ); + const commandParams = firstCommandContext(handler); const expectedStorePath = resolveIncognitoOpenClawAgentSqlitePath({ agentId: "main" }); expect(commandParams.sessionTarget?.storePath).toBe(expectedStorePath); expect(parseSqliteSessionFileMarker(commandParams.sessionFile)?.storePath).toBe( @@ -187,11 +167,7 @@ describe("handlePluginCommand", () => { }); it("keeps the current agent for unqualified global session keys", async () => { - matchPluginCommandMock.mockReturnValue({ - command: { name: "card" }, - args: "", - }); - executePluginCommandMock.mockResolvedValue({ text: "from plugin" }); + const handler = registerTestCommand(); const params = buildPluginParams("/card", { commands: { text: true }, @@ -202,12 +178,7 @@ describe("handlePluginCommand", () => { await handlePluginCommand(params, true); - const [commandParams] = expectDefined( - executePluginCommandMock.mock.calls[0] as unknown as [ - { sessionTarget?: { agentId?: string; storePath?: string } }, - ], - "plugin command invocation", - ); + const commandParams = firstCommandContext(handler); expect(commandParams.sessionTarget).toMatchObject({ agentId: "other", storePath: "/tmp/durable/other/sessions.json", @@ -215,11 +186,7 @@ describe("handlePluginCommand", () => { }); it("continues the agent without leaking continueAgent into the reply payload", async () => { - matchPluginCommandMock.mockReturnValue({ - command: { name: "card" }, - args: "", - }); - executePluginCommandMock.mockResolvedValue({ + registerTestCommand({ text: "from plugin", continueAgent: true, }); @@ -239,27 +206,18 @@ describe("handlePluginCommand", () => { }); it("enforces requiredScopes through the command handler path", async () => { - const actualCommands = await vi.importActual( - "../../plugins/commands.js", - ); const handler = vi.fn().mockResolvedValue({ text: "approved", continueAgent: true, }); - const command = { - pluginId: "approval-plugin", - pluginName: "Approval Plugin", - pluginRoot: "/tmp/approval-plugin", - name: "approve-deploy", - description: "Approve deployment", - requiredScopes: ["operator.approvals"], - handler, - }; - matchPluginCommandMock.mockReturnValue({ - command, - args: "", - }); - executePluginCommandMock.mockImplementation(actualCommands.executePluginCommand); + expect( + registerPluginCommandInRegistry(registry, "approval-plugin", { + name: "approve-deploy", + description: "Approve deployment", + requiredScopes: ["operator.approvals"], + handler, + }), + ).toEqual({ ok: true }); const denied = await handlePluginCommand( buildPluginParams("/approve-deploy", { @@ -289,4 +247,55 @@ describe("handlePluginCommand", () => { }); expect(handler).toHaveBeenCalledTimes(1); }); + + it("carries one binding selection into dispatch without rematching a replacement registry", async () => { + const originalHandler = registerTestCommand(); + const replyOptions: NonNullable & + PluginCommandExecutionReplyOptions = {}; + const cfg = { commands: { text: true } } as OpenClawConfig; + expect( + shouldBypassPluginOwnedBindingForCommand( + { + Body: "/card", + CommandAuthorized: true, + CommandSource: "text", + Provider: "whatsapp", + Surface: "whatsapp", + } as never, + cfg, + replyOptions, + ), + ).toBe(true); + expect(replyOptions[PLUGIN_COMMAND_DISPATCH]?.kind).toBe("plugin"); + + const replacement = createEmptyPluginRegistry(); + const replacementHandler = vi.fn(async () => ({ text: "replacement" })); + expect( + registerPluginCommandInRegistry(replacement, "replacement", { + name: "card", + description: "Replacement card", + handler: replacementHandler, + }), + ).toEqual({ ok: true }); + setActivePluginRegistry(replacement); + const params = buildPluginParams("/card", cfg); + params.opts = replyOptions; + + const result = await handlePluginCommand(params, true); + + expect(result?.reply?.text).toContain("registry changed"); + expect(originalHandler).not.toHaveBeenCalled(); + expect(replacementHandler).not.toHaveBeenCalled(); + }); + + it("treats an explicit non-plugin catalog winner as terminal for plugin matching", async () => { + const handler = registerTestCommand(); + const params = buildPluginParams("/card", { commands: { text: true } } as OpenClawConfig); + params.opts = { + [PLUGIN_COMMAND_DISPATCH]: { kind: "non-plugin" }, + } as NonNullable & PluginCommandExecutionReplyOptions; + + await expect(handlePluginCommand(params, true)).resolves.toBeNull(); + expect(handler).not.toHaveBeenCalled(); + }); }); diff --git a/src/auto-reply/reply/commands-plugin.ts b/src/auto-reply/reply/commands-plugin.ts index 1dd06ce8567a..a5eec384fccb 100644 --- a/src/auto-reply/reply/commands-plugin.ts +++ b/src/auto-reply/reply/commands-plugin.ts @@ -10,7 +10,13 @@ import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { formatSqliteSessionFileMarker } from "../../config/sessions/legacy-sqlite-marker.js"; import { resolveStorePath } from "../../config/sessions/paths.js"; import { resolveSessionStorePathForScope } from "../../config/sessions/session-store-path.js"; -import { matchPluginCommand, executePluginCommand } from "../../plugins/commands.js"; +import { + createPluginCommandRuntime, + executePluginCommandDispatch, + matchPluginCommandInvocation, + PLUGIN_COMMAND_DISPATCH, + type PluginCommandExecutionReplyOptions, +} from "../../plugins/plugin-command-runtime.js"; import { DEFAULT_AGENT_ID, isUnscopedSessionKeySentinel } from "../../routing/session-key.js"; import type { CommandHandler, CommandHandlerResult } from "./commands-types.js"; @@ -49,16 +55,26 @@ export const handlePluginCommand: CommandHandler = async ( return null; } - // Try to match a plugin command - const match = matchPluginCommand(command.commandBodyNormalized, { channel: command.channel }); - if (!match) { + const planned = (params.opts as PluginCommandExecutionReplyOptions | undefined)?.[ + PLUGIN_COMMAND_DISPATCH + ]; + if (planned?.kind === "non-plugin") { + return null; + } + if (!planned && !command.commandBodyNormalized.trim().startsWith("/")) { + return null; + } + const dispatch = + planned?.kind === "plugin" + ? planned + : matchPluginCommandInvocation(createPluginCommandRuntime(), command.commandBodyNormalized, { + channel: command.channel, + })?.dispatch; + if (!dispatch) { return null; } - // Execute the plugin command (always returns a result) - const result = await executePluginCommand({ - command: match.command, - args: match.args, + const result = await executePluginCommandDispatch(dispatch, { senderId: command.senderId, channel: command.channel, channelId: command.channelId, diff --git a/src/auto-reply/reply/dispatch-from-config.gather.ts b/src/auto-reply/reply/dispatch-from-config.gather.ts index 22d7280f49c2..77c2c1ff5a35 100644 --- a/src/auto-reply/reply/dispatch-from-config.gather.ts +++ b/src/auto-reply/reply/dispatch-from-config.gather.ts @@ -64,7 +64,11 @@ export async function gatherDispatchRequest( const ctx = isFinalizedInboundContext(params.ctx) ? params.ctx : finalizeInboundContext(params.ctx); - const normalizedParams = ctx === params.ctx ? params : { ...params, ctx }; + const normalizedParams: DispatchFromConfigParams = { + ...params, + ctx, + replyOptions: { ...params.replyOptions }, + }; const state = { params: normalizedParams, messageAuditTerminal, @@ -346,7 +350,7 @@ export async function gatherDispatchRequest( initialDispatchReplyOperation, messageAuditTerminal, operationSessionStoreEntry, - replyOptions: params.replyOptions, + replyOptions: normalizedParams.replyOptions, resolveOperationExpectedSessionId, routeThreadId, }); diff --git a/src/auto-reply/reply/dispatch-from-config.plugin-binding.ts b/src/auto-reply/reply/dispatch-from-config.plugin-binding.ts index 5d8d4e6c540c..b72c18083405 100644 --- a/src/auto-reply/reply/dispatch-from-config.plugin-binding.ts +++ b/src/auto-reply/reply/dispatch-from-config.plugin-binding.ts @@ -1,6 +1,12 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { matchPluginCommand } from "../../plugins/commands.js"; +import { + createPluginCommandRuntime, + matchPluginCommandInvocation, + PLUGIN_COMMAND_DISPATCH, + type PluginCommandCatalogDecision, + type PluginCommandExecutionReplyOptions, +} from "../../plugins/plugin-command-runtime.js"; import { isNativeCommandTurn, resolveCommandTurnContext } from "../command-turn-context.js"; import { findCommandByNativeName, @@ -15,6 +21,7 @@ import { isExplicitSourceReplyCommand } from "./source-reply-delivery-mode.js"; export function shouldBypassPluginOwnedBindingForCommand( ctx: FinalizedRuntimeMsgContext, cfg: OpenClawConfig, + replyOptions?: PluginCommandExecutionReplyOptions, ): boolean { // Command authorization is a trust boundary. Reject malformed runtime context // before command-turn normalization can coerce a truthy value. @@ -52,11 +59,20 @@ export function shouldBypassPluginOwnedBindingForCommand( if (!commandBody.startsWith("/")) { return false; } - if ( - matchPluginCommand(commandBody, { - channel: normalizeOptionalString(ctx.Surface ?? ctx.Provider), - }) - ) { + const planned = replyOptions?.[PLUGIN_COMMAND_DISPATCH]; + if (planned) { + return true; + } + const channel = normalizeOptionalString(ctx.Surface ?? ctx.Provider) ?? ""; + const match = matchPluginCommandInvocation(createPluginCommandRuntime(), commandBody, { + channel, + }); + if (match) { + if (replyOptions) { + (replyOptions as { [PLUGIN_COMMAND_DISPATCH]?: PluginCommandCatalogDecision })[ + PLUGIN_COMMAND_DISPATCH + ] = match.dispatch; + } return true; } if (!isExplicitSourceReplyCommand(ctx, cfg)) { diff --git a/src/auto-reply/reply/dispatch-from-config.prepare-operation.ts b/src/auto-reply/reply/dispatch-from-config.prepare-operation.ts index 729bf6e018a1..7d04f1efcb6d 100644 --- a/src/auto-reply/reply/dispatch-from-config.prepare-operation.ts +++ b/src/auto-reply/reply/dispatch-from-config.prepare-operation.ts @@ -11,6 +11,7 @@ import { markPluginBindingFallbackNoticeShown, } from "../../plugins/conversation-binding.js"; import { getGlobalPluginRegistry } from "../../plugins/hook-runner-global.js"; +import type { PluginCommandExecutionReplyOptions } from "../../plugins/plugin-command-runtime.js"; import { resolveCommandAuthorization } from "../command-auth.js"; import type { ReplyPayload } from "../reply-payload.js"; import { DispatchReplyOperationAbortedError } from "./dispatch-from-config.abort.js"; @@ -166,7 +167,14 @@ export async function prepareDispatchOperation(state: PrepareDispatchOperationCo return { status: "complete" as const, result: finishReplyOperationAbortedDispatch() }; } touchConversationBindingRecord(pluginOwnedBinding.bindingId); - if (shouldBypassPluginOwnedBindingForCommand(ctx, cfg)) { + params.replyOptions ??= {}; + if ( + shouldBypassPluginOwnedBindingForCommand( + ctx, + cfg, + params.replyOptions as PluginCommandExecutionReplyOptions, + ) + ) { logVerbose( `plugin-bound inbound command escaped plugin binding (plugin=${pluginOwnedBinding.pluginId} session=${sessionKey ?? "unknown"}); falling through to command processing`, ); diff --git a/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts b/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts index b25c9e9fc966..c589f0f9721b 100644 --- a/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts @@ -2,10 +2,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createDeferred } from "../../../test/helpers/promise.js"; import { clearAgentHarnesses } from "../../agents/harness/registry.js"; -import { - OutboundDeliveryError, - PlatformMessageNotDispatchedError, -} from "../../infra/outbound/deliver-types.js"; import type { PluginHookReplyDispatchResult } from "../../plugins/hooks.test-fixtures.js"; import { getPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js"; import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; @@ -221,90 +217,6 @@ describe("dispatchReplyFromConfig reply_dispatch hook", () => { }); }); - it("clears pending final delivery after final dispatch succeeds", async () => { - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("durable reply", { - context: { source: "heartbeat" }, - }), - }; - sessionStoreMocks.loadSessionStore.mockClear(); - mocks.routeReply.mockResolvedValue({ ok: true, delivered: true, messageId: "mock" }); - - const deliver = vi.fn().mockResolvedValue(undefined); - const dispatcher = createReplyDispatcher({ deliver }); - const result = await dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => ({ text: "durable reply" }), - }); - await dispatcher.waitForIdle(); - await vi.waitFor(() => { - expect(sessionStoreMocks.updateSessionEntry).toHaveBeenCalledOnce(); - }); - - expect(result.queuedFinal).toBe(true); - expect(sessionStoreMocks.loadSessionStoreEntry).toHaveBeenCalledWith({ - agentId: "test", - storePath: "/tmp/mock-sessions.json", - sessionKey: "agent:test:session", - readConsistency: "latest", - clone: false, - }); - expect(sessionStoreMocks.loadSessionStore).not.toHaveBeenCalled(); - expect(deliver).toHaveBeenCalledOnce(); - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toBeUndefined(); - }); - - it("clears pending final delivery when abort fires after a successful final send (#89115)", async () => { - // Regression for #89115: an abort that lands after the final reply has - // shipped (here, during sendFinalReply) must still clear the pending-final - // bookkeeping — otherwise pendingFinalDelivery stays true and the get-reply - // redelivery short-circuit silently blocks every later inbound. - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("durable reply", { - context: { source: "heartbeat" }, - intentId: "intent-89115", - }), - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const abortController = new AbortController(); - const deliver = vi.fn().mockResolvedValue(undefined); - const dispatcher = createReplyDispatcher({ deliver }); - const sendFinalReply = dispatcher.sendFinalReply.bind(dispatcher); - vi.spyOn(dispatcher, "sendFinalReply").mockImplementation((payload) => { - const queued = sendFinalReply(payload); - abortController.abort(); - return queued; - }); - - const result = await withReplyDispatcher({ - dispatcher, - run: () => - dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyOptions: { abortSignal: abortController.signal }, - replyResolver: async () => ({ text: "durable reply" }), - }), - }); - - // Abort landed after delivery: the run is still surfaced as aborted - // (queuedFinal:false), but the pending-final state is fully cleared. - expect(dispatcher.sendFinalReply).toHaveBeenCalledOnce(); - expect(deliver).toHaveBeenCalledOnce(); - expect(result.queuedFinal).toBe(false); - expect(sessionStoreMocks.updateSessionEntry).toHaveBeenCalledOnce(); - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toBeUndefined(); - }); - it("preserves pending final delivery when final dispatch fails", async () => { hookMocks.runner.hasHooks.mockReturnValue(false); sessionStoreMocks.currentEntry = { @@ -387,59 +299,6 @@ describe("dispatchReplyFromConfig reply_dispatch hook", () => { } }); - it("clears pending final delivery when a later queued final succeeds", async () => { - vi.useFakeTimers(); - try { - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("durable reply"), - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const hookStarted = createDeferred(); - const deliver = vi.fn().mockResolvedValue(undefined); - let hookCalls = 0; - const dispatcher = createReplyDispatcher({ - deliver, - beforeDeliver: (payload) => { - hookCalls += 1; - if (hookCalls === 1) { - hookStarted.resolve(); - return new Promise(() => {}); - } - return payload; - }, - }); - - const resultPromise = withReplyDispatcher({ - dispatcher, - run: () => - dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => [{ text: "first" }, { text: "durable reply" }], - }), - }); - await hookStarted.promise; - await vi.advanceTimersByTimeAsync(15_000); - await resultPromise; - - expect(deliver).toHaveBeenCalledOnce(); - expect(deliver).toHaveBeenCalledWith( - expect.objectContaining({ text: "durable reply" }), - expect.objectContaining({ kind: "final" }), - ); - expect(dispatcher.getFailedCounts?.()).toEqual({ tool: 0, block: 0, final: 1 }); - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toBeUndefined(); - expect(vi.getTimerCount()).toBe(0); - } finally { - vi.useRealTimers(); - } - }); - it("preserves the durable final when an earlier auxiliary final succeeds", async () => { vi.useFakeTimers(); try { @@ -494,119 +353,6 @@ describe("dispatchReplyFromConfig reply_dispatch hook", () => { } }); - it("narrows combined retry text to finals that failed before transport", async () => { - vi.useFakeTimers(); - try { - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("auxiliary\n\ndurable reply"), - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const hookStarted = createDeferred(); - let hookCalls = 0; - const dispatcher = createReplyDispatcher({ - deliver: vi.fn().mockResolvedValue(undefined), - beforeDeliver: (payload) => { - hookCalls += 1; - if (hookCalls === 2) { - hookStarted.resolve(); - return new Promise(() => {}); - } - return payload; - }, - }); - - const resultPromise = withReplyDispatcher({ - dispatcher, - run: () => - dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => [{ text: "auxiliary" }, { text: "durable reply" }], - }), - }); - await hookStarted.promise; - await vi.advanceTimersByTimeAsync(15_000); - await resultPromise; - - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toEqual( - pendingFinalDelivery("durable reply"), - ); - expect(vi.getTimerCount()).toBe(0); - } finally { - vi.useRealTimers(); - } - }); - - it("narrows heartbeat-normalized retry text using its originating payloads", async () => { - vi.useFakeTimers(); - try { - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("auxiliary durable reply", { - intentId: "heartbeat-intent", - }), - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const hookStarted = createDeferred(); - let hookCalls = 0; - const dispatcher = createReplyDispatcher({ - deliver: vi.fn().mockResolvedValue(undefined), - beforeDeliver: (payload) => { - hookCalls += 1; - if (hookCalls === 2) { - hookStarted.resolve(); - return new Promise(() => {}); - } - return payload; - }, - }); - - const resultPromise = withReplyDispatcher({ - dispatcher, - run: () => - dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => [ - setReplyPayloadMetadata( - { text: "auxiliary" }, - { - pendingFinalDeliveryIntentId: "heartbeat-intent", - pendingFinalDeliveryRetryText: "auxiliary", - }, - ), - setReplyPayloadMetadata( - { text: "durable reply" }, - { - pendingFinalDeliveryIntentId: "heartbeat-intent", - pendingFinalDeliveryRetryText: "durable reply", - }, - ), - ], - }), - }); - await hookStarted.promise; - await vi.advanceTimersByTimeAsync(15_000); - await resultPromise; - - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toEqual( - pendingFinalDelivery("durable reply", { intentId: "heartbeat-intent" }), - ); - expect(vi.getTimerCount()).toBe(0); - } finally { - vi.useRealTimers(); - } - }); - it("does not let an older settlement rewrite a newer pending-final intent", async () => { vi.useFakeTimers(); try { @@ -661,104 +407,6 @@ describe("dispatchReplyFromConfig reply_dispatch hook", () => { } }); - const createNoSendFailure = (retryable = true) => - new PlatformMessageNotDispatchedError("offline", { cause: new Error("offline"), retryable }); - const wrapDeliveryFailure = (cause: unknown) => - new OutboundDeliveryError("delivery failed", { cause }); - const refused = Object.assign(new Error(), { - code: "ECONNREFUSED", - syscall: "connect", - }); - const createPartialDelivery = () => - Object.assign(new Error("partial delivery", { cause: createNoSendFailure() }), { - code: "CHANNEL_PARTIAL_DELIVERY", - deliveryResult: { visibleReplySent: true }, - }); - - it.each([ - ["direct retryable provider proof", createNoSendFailure(), true], - ["wrapped retryable provider proof", wrapDeliveryFailure(createNoSendFailure()), true], - ["wrapped pre-connect ECONNREFUSED proof", wrapDeliveryFailure(refused), true], - ["permanent provider rejection", createNoSendFailure(false), false], - [ - "partial outbound delivery", - Object.assign(wrapDeliveryFailure(createNoSendFailure()), { sentBeforeError: true }), - false, - ], - ["nested partial envelope", new Error("partial", { cause: createPartialDelivery() }), false], - ["aggregate partial envelope", new AggregateError([createPartialDelivery()]), false], - ["observer-attached delivery evidence", createNoSendFailure(), false], - ["ambiguous transport failure", new Error("transport failed"), false], - ] as const)("reconciles pending final delivery after %s", async (name, error, preserve) => { - hookMocks.runner.hasHooks.mockReturnValue(false); - const pending = pendingFinalDelivery("recoverable final reply"); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pending, - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const dispatcher = createReplyDispatcher({ - deliver: async () => { - throw error; - }, - onError: () => { - if (name.startsWith("observer")) { - Object.assign(error, { visibleReplySent: true }); - } - }, - }); - await withReplyDispatcher({ - dispatcher, - run: () => - dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => ({ text: "recoverable final reply" }), - }), - }); - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toEqual( - preserve ? pending : undefined, - ); - }); - - it("clears pending final delivery after intentional pre-delivery cancellation", async () => { - hookMocks.runner.hasHooks.mockReturnValue(false); - sessionStoreMocks.currentEntry = { - sessionKey: "agent:test:session", - pendingFinalDelivery: pendingFinalDelivery("policy-suppressed reply"), - }; - sessionStoreMocks.resolveSessionStoreEntry.mockReturnValue({ - existing: sessionStoreMocks.currentEntry, - }); - const deliver = vi.fn().mockResolvedValue(undefined); - const dispatcher = createReplyDispatcher({ - deliver, - beforeDeliver: () => null, - }); - - const result = await dispatchReplyFromConfig({ - ctx: createHookCtx(), - cfg: emptyConfig, - dispatcher, - replyResolver: async () => ({ text: "policy-suppressed reply" }), - }); - await dispatcher.waitForIdle(); - await vi.waitFor(() => { - expect(sessionStoreMocks.updateSessionEntry).toHaveBeenCalledOnce(); - }); - - expect(result.queuedFinal).toBe(true); - expect(deliver).not.toHaveBeenCalled(); - // createHookCtx's "private" chat type is undirected, so the cancelled final - // does not trigger a fallback attempt. - expect(dispatcher.getCancelledCounts?.()).toEqual({ tool: 0, block: 0, final: 1 }); - expect(dispatcher.getFailedCounts?.()).toEqual({ tool: 0, block: 0, final: 0 }); - expect(sessionStoreMocks.currentEntry?.pendingFinalDelivery).toBeUndefined(); - }); - it("delivers a generated final reply before queued follow-up admission", async () => { hookMocks.runner.hasHooks.mockReturnValue(false); const dispatcher = createDispatcher(); diff --git a/src/auto-reply/reply/dispatch-from-config.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.test-harness.ts index 0870a2ef1f22..5d33dfe0471a 100644 --- a/src/auto-reply/reply/dispatch-from-config.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.test-harness.ts @@ -291,23 +291,23 @@ export function firstRouteReplyCall(): Record { export function installThreadingTestPlugin(params: { defaultAccountId?: string; id: string }) { const plugin = createChannelTestPluginBase({ id: params.id }); const defaultAccountId = params.defaultAccountId; - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: params.id, - source: "test", - plugin: { - ...plugin, - config: defaultAccountId - ? { ...plugin.config, defaultAccountId: () => defaultAccountId } - : plugin.config, - threading: { - resolveReplyToMode: () => "all", - }, + const registry = createTestRegistry([ + { + pluginId: params.id, + source: "test", + plugin: { + ...plugin, + config: defaultAccountId + ? { ...plugin.config, defaultAccountId: () => defaultAccountId } + : plugin.config, + threading: { + resolveReplyToMode: () => "all", }, }, - ]), - ); + }, + ]); + setActivePluginRegistry(registry); + runtimePluginMocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(registry); } export function installCaptionedVoiceTestPlugin(id: string) { @@ -318,15 +318,15 @@ export function installCaptionedVoiceTestPlugin(id: string) { tts: { voice: { synthesisTarget: "voice-note", captionedFinalText: true } }, }, }); - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: id, - source: "test", - plugin, - }, - ]), - ); + const registry = createTestRegistry([ + { + pluginId: id, + source: "test", + plugin, + }, + ]); + setActivePluginRegistry(registry); + runtimePluginMocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(registry); } export function requireToolResultHandler( @@ -615,6 +615,13 @@ export const describe1BeforeEach0 = () => { }; export const describe2BeforeEach0 = () => { + // This suite swaps global registries between cases; keep each request on a live + // snapshot so later retirement cannot invalidate the dispatch under test. + const activeRegistry = getActivePluginRegistry(); + runtimePluginMocks.loadAgentRuntimePluginRegistryHandle.mockReset(); + runtimePluginMocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue( + activeRegistry ? { ...activeRegistry } : createTestRegistry([]), + ); resetInboundDedupe(); // Same routeReply reset as the sibling suite setups: queued once-values and // persistent overrides must not leak between tests. diff --git a/src/auto-reply/reply/get-reply.types.ts b/src/auto-reply/reply/get-reply.types.ts index 7c3a837c9e09..442f6879e494 100644 --- a/src/auto-reply/reply/get-reply.types.ts +++ b/src/auto-reply/reply/get-reply.types.ts @@ -2,6 +2,7 @@ import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/lo import type { SessionToolOverrides } from "../../config/sessions/types.js"; // Shared get-reply type contracts for command, directive, and runtime layers. import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { PluginCommandReplyOptions } from "../../plugins/plugin-command-dispatch-contract.js"; import type { GetReplyOptions } from "../get-reply-options.types.js"; import type { ReplyPayload } from "../reply-payload.js"; import type { MsgContext } from "../templating.js"; @@ -38,6 +39,7 @@ type InternalReplySessionOptions = { }; export type InternalGetReplyOptions = GetReplyOptions & + PluginCommandReplyOptions & InternalReplySessionOptions & ReplyOptionsWithOperationRunState & ReplyOptionsWithAdmissionTicket; diff --git a/src/auto-reply/reply/provider-dispatcher.types.ts b/src/auto-reply/reply/provider-dispatcher.types.ts index eb2e7387af32..65dc61df0522 100644 --- a/src/auto-reply/reply/provider-dispatcher.types.ts +++ b/src/auto-reply/reply/provider-dispatcher.types.ts @@ -1,5 +1,6 @@ // Shared provider dispatch type contracts for reply runtime execution. import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { PluginCommandReplyOptions } from "../../plugins/plugin-command-dispatch-contract.js"; import type { GetReplyOptions } from "../get-reply-options.types.js"; import type { FinalizedMsgContext, MsgContext } from "../templating.js"; import type { DispatchFromConfigResult } from "./dispatch-from-config.types.js"; @@ -10,7 +11,7 @@ import type { } from "./reply-dispatcher.js"; type DispatchReplyContext = MsgContext | FinalizedMsgContext; -type DispatchReplyOptions = Omit; +type DispatchReplyOptions = Omit & PluginCommandReplyOptions; /** Buffered block dispatcher entry point used by provider reply flows. */ export type DispatchReplyWithBufferedBlockDispatcher = (params: { diff --git a/src/channels/turn/types.ts b/src/channels/turn/types.ts index 5f873a6381ef..2ebf46e51fad 100644 --- a/src/channels/turn/types.ts +++ b/src/channels/turn/types.ts @@ -26,6 +26,7 @@ import type { OutboundDeliveryQueuePolicy, } from "../../infra/outbound/deliver.js"; import type { MediaFact } from "../../media/media-facts.js"; +import type { PluginCommandReplyOptions } from "../../plugins/plugin-command-dispatch-contract.js"; import type { InboundEventKind } from "../inbound-event/kind.js"; import type { CreateChannelReplyPipelineParams } from "../message/reply-pipeline.js"; import type { MessageReceipt } from "../message/types.js"; @@ -292,6 +293,9 @@ export type ChannelTurnDroppedHistoryOptions = { /** Dispatcher options excluding delivery hooks owned by the channel turn adapter. */ type ChannelTurnDispatcherOptions = Omit; +/** Reply options plus the opaque native command ownership decision carried by channel turns. */ +type ChannelTurnReplyOptions = Omit & PluginCommandReplyOptions; + /** Reply pipeline options excluding cfg/agent/channel identity supplied by the turn. */ type ChannelTurnReplyPipelineOptions = Omit< CreateChannelReplyPipelineParams, @@ -314,7 +318,7 @@ export type AssembledChannelTurn = { replyPipeline?: ChannelTurnReplyPipelineOptions; dispatcherOptions?: ChannelTurnDispatcherOptions; toolsAllow?: string[]; - replyOptions?: Omit; + replyOptions?: ChannelTurnReplyOptions; replyResolver?: GetReplyFromConfig; sessionInitRetry?: { delaysMs: readonly number[]; diff --git a/src/gateway/channel-health-monitor.test.ts b/src/gateway/channel-health-monitor.test.ts index c590f7bcb0f4..60c8a4e6b46b 100644 --- a/src/gateway/channel-health-monitor.test.ts +++ b/src/gateway/channel-health-monitor.test.ts @@ -12,6 +12,7 @@ import type { ChannelManager } from "./server-channels.js"; function createMockChannelManager(overrides?: Partial): ChannelManager { return { getRuntimeSnapshot: vi.fn(() => ({ channels: {}, channelAccounts: {} })), + getPluginCommandCatalogAccounts: vi.fn(() => new Map()), startChannels: vi.fn(async () => {}), startChannel: vi.fn(async () => {}), stopChannel: vi.fn(async () => {}), diff --git a/src/gateway/server-channels.test.ts b/src/gateway/server-channels.test.ts index 940f8bb502cf..5b8f567229db 100644 --- a/src/gateway/server-channels.test.ts +++ b/src/gateway/server-channels.test.ts @@ -20,6 +20,8 @@ import { type SubsystemLogger, runtimeForLogger, } from "../logging/subsystem.js"; +import { registerPluginCommandInRegistry } from "../plugins/command-registration.js"; +import { createPluginCommandRuntime } from "../plugins/plugin-command-runtime.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { createRuntimeChannel } from "../plugins/runtime/runtime-channel.js"; @@ -1350,6 +1352,9 @@ describe("server-channels auto restart", () => { let startCount = 0; const startAccount = vi.fn(async ({ abortSignal }: { abortSignal: AbortSignal }) => { startCount += 1; + const commandRuntime = createPluginCommandRuntime(); + expect(commandRuntime.listNativeCandidates("discord")).toHaveLength(1); + commandRuntime.retainNativeCatalog("discord"); abortSignal.addEventListener("abort", () => {}, { once: true }); if (startCount === 1) { await releaseFirstTask.promise; @@ -1362,6 +1367,14 @@ describe("server-channels auto restart", () => { startAccount, }), ); + expect( + registerPluginCommandInRegistry(getActivePluginRegistry()!, "catalog-owner", { + name: "catalog", + description: "Catalog command", + channels: ["discord"], + handler: async () => ({ text: "ok" }), + }), + ).toEqual({ ok: true }); const manager = createManager(); await manager.startChannels(); @@ -1374,6 +1387,9 @@ describe("server-channels auto restart", () => { await manager.startChannel("discord", DEFAULT_ACCOUNT_ID); await manager.startChannel("discord", DEFAULT_ACCOUNT_ID); expect(startAccount).toHaveBeenCalledTimes(2); + expect(manager.getPluginCommandCatalogAccounts().get("discord")).toEqual( + new Set([DEFAULT_ACCOUNT_ID]), + ); releaseFirstTask.resolve(); await flushMicrotasks(); @@ -1383,6 +1399,9 @@ describe("server-channels auto restart", () => { expect(account?.running).toBe(true); expect(account?.restartPending).toBe(false); expect(account?.lastError).toBeNull(); + expect(manager.getPluginCommandCatalogAccounts().get("discord")).toEqual( + new Set([DEFAULT_ACCOUNT_ID]), + ); expect(hoisted.sleepWithAbort).not.toHaveBeenCalled(); }); @@ -2058,6 +2077,56 @@ describe("server-channels auto restart", () => { expect(startAccount).toHaveBeenCalledTimes(1); }); + it("reports only running accounts that retained a real plugin command catalog", async () => { + const startAccount = vi.fn( + async ({ accountId, abortSignal }: { accountId: string; abortSignal: AbortSignal }) => { + if (accountId === "catalog") { + const commandRuntime = createPluginCommandRuntime(); + expect(commandRuntime.listNativeCandidates("discord")).toHaveLength(1); + commandRuntime.retainNativeCatalog("discord"); + } + await new Promise((resolve) => { + abortSignal.addEventListener("abort", () => resolve(), { once: true }); + }); + }, + ); + installTestRegistry( + createTestPlugin({ + startAccount, + listAccountIds: () => ["catalog", "plain"], + }), + ); + expect( + registerPluginCommandInRegistry(getActivePluginRegistry()!, "catalog-owner", { + name: "catalog", + description: "Catalog command", + channels: ["discord"], + handler: async () => ({ text: "ok" }), + }), + ).toEqual({ ok: true }); + const manager = createManager(); + + await manager.startChannels(); + await waitForMicrotaskCondition( + () => startAccount.mock.calls.length === 2, + "expected both account tasks to start", + ); + + const reported = manager.getPluginCommandCatalogAccounts(); + expect(reported).toEqual(new Map([["discord", new Set(["catalog"])]])); + (reported.get("discord") as Set).clear(); + expect(manager.getPluginCommandCatalogAccounts()).toEqual( + new Map([["discord", new Set(["catalog"])]]), + ); + + await manager.stopChannel("discord", "plain"); + expect(manager.getPluginCommandCatalogAccounts()).toEqual( + new Map([["discord", new Set(["catalog"])]]), + ); + await manager.stopChannel("discord", "catalog"); + expect(manager.getPluginCommandCatalogAccounts()).toEqual(new Map()); + }); + it("cancels a pending startup when the account is stopped mid-boot", async () => { const startupGate = createDeferred(); const isConfigured = vi.fn(async () => { diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index 55c64baae552..a868aeb42a2c 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -29,6 +29,7 @@ import { type SubsystemLogger, } from "../logging/subsystem.js"; import { withPluginHttpRouteRegistry } from "../plugins/http-registry.js"; +import { withPluginCommandAccountStartScope } from "../plugins/plugin-command-account-start-scope.js"; import type { PluginRegistry } from "../plugins/registry.js"; import type { PluginRuntimeChannel } from "../plugins/runtime/types-channel.js"; import { resolveAccountEntry, resolveNormalizedAccountEntry } from "../routing/account-lookup.js"; @@ -72,6 +73,9 @@ function waitForChannelStartupHandoff(): Promise { type ChannelRuntimeStore = { aborts: Map; + // The account task's controller is the ownership token: late predecessor cleanup + // must not clear a catalog retained by its replacement. + pluginCommandCatalogOwners: Map; starting: Map>; stops: Map; tasks: Map>; @@ -132,6 +136,7 @@ type GatewayStartupTrace = { function createRuntimeStore(): ChannelRuntimeStore { return { aborts: new Map(), + pluginCommandCatalogOwners: new Map(), starting: new Map(), stops: new Map(), tasks: new Map(), @@ -251,6 +256,7 @@ async function waitForDeferredAccountStart( export type ChannelManager = { getRuntimeSnapshot: () => ChannelRuntimeSnapshot; + getPluginCommandCatalogAccounts: () => ReadonlyMap>; startChannels: () => Promise; startChannel: ( channel: ChannelId, @@ -299,6 +305,16 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage ); const restartKey = (channelId: ChannelId, accountId: string) => `${channelId}:${accountId}`; + const clearPluginCommandCatalogOwner = ( + store: ChannelRuntimeStore, + accountId: string, + owner?: AbortController, + ): void => { + if (owner && store.pluginCommandCatalogOwners.get(accountId) !== owner) { + return; + } + store.pluginCommandCatalogOwners.delete(accountId); + }; const ensureChannelLog = (channelId: ChannelId): SubsystemLogger => { channelLogs[channelId] ??= createSubsystemLogger("channels").child(channelId); return channelLogs[channelId]; @@ -467,6 +483,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage continue; } store.runtimes.delete(id); + store.pluginCommandCatalogOwners.delete(id); restarts.delete(restartKey(channelId, id)); manuallyStopped.delete(restartKey(channelId, id)); recoveryStartRequested.delete(restartKey(channelId, id)); @@ -582,7 +599,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage // cannot race into duplicate provider boots for the same account. const abort = new AbortController(); store.aborts.set(id, abort); + clearPluginCommandCatalogOwner(store, id); let handedOffTask = false; + let startAccountLifetimeActive = false; const log = ensureChannelLog(channelId); const runtime = ensureChannelRuntime(channelId); let scopedChannelRuntime: { @@ -785,25 +804,41 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage const startedAt = Date.now(); const recordDuration = () => { channelRunDurationMs = Date.now() - startedAt; + startAccountLifetimeActive = false; + clearPluginCommandCatalogOwner(store, id, abort); + }; + const retainCatalog = () => { + if ( + !startAccountLifetimeActive || + abort.signal.aborted || + store.aborts.get(id) !== abort || + !isCurrentTask() + ) { + return; + } + store.pluginCommandCatalogOwners.set(id, abort); }; try { - return withGatewayNativeApprovalRuntime(opts.getNativeApprovalRuntime?.(), () => - startAccount({ - cfg, - accountId: id, - account, - runtime, - abortSignal: abort.signal, - log, - getStatus: () => getRuntime(channelId, id), - setStatus: (next) => - isCurrentTask() - ? setRuntimeFromTaskStatus(channelId, id, next, abort.signal) - : getRuntime(channelId, id), - invalidateDirectoryCache: () => - resetDirectoryCache({ cfg, channel: channelId, accountId: id }), - ...(channelRuntimeForTask ? { channelRuntime: channelRuntimeForTask } : {}), - }), + startAccountLifetimeActive = true; + return withPluginCommandAccountStartScope({ channelId, retainCatalog }, () => + withGatewayNativeApprovalRuntime(opts.getNativeApprovalRuntime?.(), () => + startAccount({ + cfg, + accountId: id, + account, + runtime, + abortSignal: abort.signal, + log, + getStatus: () => getRuntime(channelId, id), + setStatus: (next) => + isCurrentTask() + ? setRuntimeFromTaskStatus(channelId, id, next, abort.signal) + : getRuntime(channelId, id), + invalidateDirectoryCache: () => + resetDirectoryCache({ cfg, channel: channelId, accountId: id }), + ...(channelRuntimeForTask ? { channelRuntime: channelRuntimeForTask } : {}), + }), + ), ).finally(recordDuration); } catch (error) { recordDuration(); @@ -1142,6 +1177,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage ...stoppedPatch, }); } else { + clearPluginCommandCatalogOwner(store, id, abort); setStoppedRuntime(channelId, id, stoppedPatch); recoveryStopTimedOut.add(rKey); } @@ -1149,6 +1185,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage } recoveryStopTimedOut.delete(rKey); recoveryStartRequested.delete(rKey); + clearPluginCommandCatalogOwner(store, id, abort); if (store.aborts.get(id) === abort) { store.aborts.delete(id); } @@ -1304,6 +1341,13 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage return { channels, channelAccounts }; }; + const getPluginCommandCatalogAccounts = (): ReadonlyMap> => + new Map( + [...channelStores] + .filter(([, store]) => store.pluginCommandCatalogOwners.size > 0) + .map(([channelId, store]) => [channelId, new Set(store.pluginCommandCatalogOwners.keys())]), + ); + const isManuallyStoppedFlag = (channelId: ChannelId, accountId: string): boolean => { return manuallyStopped.has(restartKey(channelId, accountId)); }; @@ -1318,6 +1362,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage return { getRuntimeSnapshot, + getPluginCommandCatalogAccounts, startChannels, startChannel, stopChannel, diff --git a/src/gateway/server-core-runtime.ts b/src/gateway/server-core-runtime.ts index 0ab8939b783f..ced5482249f6 100644 --- a/src/gateway/server-core-runtime.ts +++ b/src/gateway/server-core-runtime.ts @@ -391,7 +391,10 @@ export async function startGatewayCoreRuntime(input: { const reloadAttachedGatewayPlugins = async (params: { nextConfig: OpenClawConfig; changedPaths: readonly string[]; - beforeReplace: (channels: ReadonlySet) => Promise; + beforeReplace: ( + channels: ReadonlySet, + accounts?: ReadonlyMap>, + ) => Promise; commitRuntime: () => Promise; env: NodeJS.ProcessEnv; isAborted?: () => boolean; @@ -473,7 +476,10 @@ export async function startGatewayCoreRuntime(input: { channelsToStopBeforeReplace.add(channelId); } } - await params.beforeReplace(channelsToStopBeforeReplace); + await params.beforeReplace( + channelsToStopBeforeReplace, + channelManager.getPluginCommandCatalogAccounts(), + ); // If an in-process restart signalled abort during beforeReplace, // stop before any plugin metadata/runtime side effects continue. if (params.isAborted?.()) { diff --git a/src/gateway/server-methods/session-catalog-entry-snapshot.test.ts b/src/gateway/server-methods/session-catalog-entry-snapshot.test.ts index a900ad50cd13..fcca99db571b 100644 --- a/src/gateway/server-methods/session-catalog-entry-snapshot.test.ts +++ b/src/gateway/server-methods/session-catalog-entry-snapshot.test.ts @@ -26,6 +26,7 @@ const hoisted = vi.hoisted(() => ({ vi.mock("../../plugins/runtime.js", () => ({ getActivePluginRegistry: () => hoisted.activeRegistry, + requireActivePluginRegistry: () => hoisted.activeRegistry, })); vi.mock("../../config/sessions/session-accessor.js", async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/gateway/server-methods/session-catalog.test.ts b/src/gateway/server-methods/session-catalog.test.ts index 01afa03a4db4..7b5ef008f6e1 100644 --- a/src/gateway/server-methods/session-catalog.test.ts +++ b/src/gateway/server-methods/session-catalog.test.ts @@ -39,6 +39,7 @@ const conversationBindingMocks = vi.hoisted(() => ({ vi.mock("../../plugins/runtime.js", () => ({ getActivePluginRegistry: () => hoisted.activeRegistry, + requireActivePluginRegistry: () => hoisted.activeRegistry, })); vi.mock("../../sessions/session-state-events.js", () => ({ diff --git a/src/gateway/server-reload-channel-restart.ts b/src/gateway/server-reload-channel-restart.ts index 09fcdbe53073..a92f309d6d87 100644 --- a/src/gateway/server-reload-channel-restart.ts +++ b/src/gateway/server-reload-channel-restart.ts @@ -1,12 +1,28 @@ import { getChannelPlugin } from "../channels/plugins/index.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; +import { requireActivePluginChannelRegistry } from "../plugins/runtime.js"; +import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js"; import { runOutsideGatewayRootWorkAdmission } from "../process/gateway-work-admission.js"; import type { ChannelKind } from "./config-reload-plan.js"; import type { GatewayReloadPlan } from "./config-reload.js"; import type { GatewayReloadHandlerParams } from "./server-reload-contracts.js"; import { collectChannelOperationFailures } from "./server-reload-utils.js"; +export function startGatewayChannelFromActiveRegistry( + params: Pick, + channel: ChannelKind, + accountId?: string, +): Promise { + return withPluginRuntimeRegistryScope(requireActivePluginChannelRegistry(), () => + runOutsideGatewayRootWorkAdmission(() => + accountId === undefined + ? params.startChannel(channel) + : params.startChannel(channel, accountId), + ), + ); +} + export async function restartGatewayChannels(options: { params: GatewayReloadHandlerParams; plan: GatewayReloadPlan; @@ -15,6 +31,7 @@ export async function restartGatewayChannels(options: { restartChannelAccounts: ReadonlyMap>; activePluginChannelsAfterReload: ReadonlySet | null; channelsStoppedBeforePluginReload: Set; + accountsStoppedBeforePluginReload: ReadonlyMap>; shouldSkipChannelRestart: boolean; skipChannelRestartLogMessage: string; pluginReloadAborted: boolean; @@ -32,6 +49,7 @@ export async function restartGatewayChannels(options: { restartChannelAccounts, activePluginChannelsAfterReload, channelsStoppedBeforePluginReload, + accountsStoppedBeforePluginReload, shouldSkipChannelRestart, skipChannelRestartLogMessage, pluginReloadAborted, @@ -41,6 +59,8 @@ export async function restartGatewayChannels(options: { logSuppressedChannelRestart, scheduleRecoveryRestart, } = options; + const wasStoppedBeforePluginReload = (channel: ChannelKind, accountId: string) => + accountsStoppedBeforePluginReload.get(channel)?.has(accountId) === true; // Suppressed and normal reloads share fallback selection so stale account // ids always reach the wholesale path that evicts their old runtime. const collectChannelAccountTargets = (): Array<[ChannelKind, string]> => { @@ -97,7 +117,9 @@ export async function restartGatewayChannels(options: { params.logChannels.info( `stopping ${channel} account ${accountId} before suppressed hot reload`, ); - await params.stopChannel(channel, accountId, { manual: false }); + if (!wasStoppedBeforePluginReload(channel, accountId)) { + await params.stopChannel(channel, accountId, { manual: false }); + } } catch (err) { accountStopFailures.push(`${channel}[${accountId}]`); params.logChannels.error( @@ -141,11 +163,13 @@ export async function restartGatewayChannels(options: { for (const [channel, accountId] of accountRestarts) { try { params.logChannels.info(`restarting ${channel} account ${accountId}`); - await params.stopChannel(channel, accountId, { manual: false }); + if (!wasStoppedBeforePluginReload(channel, accountId)) { + await params.stopChannel(channel, accountId, { manual: false }); + } if (isLifecycleReloadAborted()) { continue; } - await runOutsideGatewayRootWorkAdmission(() => params.startChannel(channel, accountId)); + await startGatewayChannelFromActiveRegistry(params, channel, accountId); } catch (err) { accountRestartFailures.push(`${channel}[${accountId}]`); params.logChannels.error( @@ -164,7 +188,7 @@ export async function restartGatewayChannels(options: { if (isLifecycleReloadAborted()) { return; } - await runOutsideGatewayRootWorkAdmission(() => params.startChannel(name)); + await startGatewayChannelFromActiveRegistry(params, name); }; const restartFailures = await collectChannelOperationFailures({ channels: channelsToRestart, diff --git a/src/gateway/server-reload-contracts.ts b/src/gateway/server-reload-contracts.ts index e483a2cd91f9..78378491313e 100644 --- a/src/gateway/server-reload-contracts.ts +++ b/src/gateway/server-reload-contracts.ts @@ -150,7 +150,10 @@ export type GatewayReloadHandlerParams = { reloadPlugins: (params: { nextConfig: OpenClawConfig; changedPaths: readonly string[]; - beforeReplace: (channels: ReadonlySet) => Promise; + beforeReplace: ( + channels: ReadonlySet, + accounts?: ReadonlyMap>, + ) => Promise; commitRuntime: () => Promise; env: NodeJS.ProcessEnv; isAborted?: () => boolean; diff --git a/src/gateway/server-reload-handlers.test.ts b/src/gateway/server-reload-handlers.test.ts index a2adb11e24a6..42e1bf839556 100644 --- a/src/gateway/server-reload-handlers.test.ts +++ b/src/gateway/server-reload-handlers.test.ts @@ -31,7 +31,13 @@ import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck, } from "../infra/restart.js"; +import { registerPluginCommandInRegistry } from "../plugins/command-registration.js"; +import { + createPluginCommandRuntime, + type PluginCommandCatalogDecision, +} from "../plugins/plugin-command-runtime.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; +import { withPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; import { enqueueCommandInLane, getCommandLaneSnapshot, @@ -5011,6 +5017,110 @@ describe("gateway plugin hot reload handlers", () => { expect(handlers.setState).toHaveBeenCalledTimes(1); }); + it("restarts only the account retaining a command catalog on plugin replacement", async () => { + const discordPlugin = createChannelTestPluginBase({ + id: "discord", + config: { + listAccountIds: () => ["catalog-account", "disabled-account"], + resolveAccount: (_cfg, accountId) => ({ accountId }), + }, + }); + const createDiscordRegistry = () => + createTestRegistry([{ pluginId: "discord", plugin: discordPlugin, source: "test" }]); + const oldRegistry = createDiscordRegistry(); + const nextRegistry = createDiscordRegistry(); + const oldHandler = vi.fn(async () => ({ text: "old" })); + const nextHandler = vi.fn(async () => ({ text: "next" })); + for (const [registry, handler] of [ + [oldRegistry, oldHandler], + [nextRegistry, nextHandler], + ] as const) { + expect( + registerPluginCommandInRegistry(registry, "command-owner", { + name: "refresh", + description: "Refresh", + channels: ["discord"], + handler, + }), + ).toEqual({ ok: true }); + } + setActivePluginRegistry(oldRegistry); + const staleDispatch = createPluginCommandRuntime() + .listNativeCandidates("discord")[0]! + .prepareDispatch(); + expect(staleDispatch.kind).toBe("plugin"); + + const events: string[] = []; + let restartedDispatch: PluginCommandCatalogDecision | undefined; + const handlers = createReloadHandlersForTest( + undefined, + { + stop: vi.fn(async (channel, accountId) => { + events.push(`stop:${channel}:${accountId ?? "all"}`); + }), + start: vi.fn(async (channel, accountId) => { + events.push(`start:${channel}:${accountId ?? "all"}`); + restartedDispatch = createPluginCommandRuntime() + .listNativeCandidates("discord")[0]! + .prepareDispatch(); + }), + }, + vi.fn(async (params): Promise => { + const beforeReplace = params.beforeReplace as ( + channels: ReadonlySet, + accounts?: ReadonlyMap>, + ) => Promise; + await beforeReplace(new Set(), new Map([["discord", new Set(["catalog-account"])]])); + await params.commitRuntime(); + setActivePluginRegistry(nextRegistry); + events.push("registry:next"); + return makePluginReloadResult({ activeChannels: new Set(["discord"]) }); + }), + ); + + await withPluginRuntimeGatewayRequestScope( + { + isWebchatConnect: () => false, + pluginRegistry: oldRegistry, + }, + () => + handlers.applyHotReload( + createPluginReloadPlan(), + { plugins: { enabled: true } }, + { publish: async (commit) => await commit(), isCurrent: () => true }, + ), + ); + + expect(events).toEqual([ + "stop:discord:catalog-account", + "registry:next", + "start:discord:catalog-account", + ]); + if (staleDispatch.kind === "plugin") { + await expect( + staleDispatch.execute({ + channel: "discord", + isAuthorizedSender: true, + commandBody: "/refresh", + config: {}, + }), + ).resolves.toMatchObject({ text: expect.stringContaining("registry changed") }); + } + expect(restartedDispatch?.kind).toBe("plugin"); + if (restartedDispatch?.kind === "plugin") { + await expect( + restartedDispatch.execute({ + channel: "discord", + isAuthorizedSender: true, + commandBody: "/refresh", + config: {}, + }), + ).resolves.toEqual({ text: "next" }); + } + expect(oldHandler).not.toHaveBeenCalled(); + expect(nextHandler).toHaveBeenCalledOnce(); + }); + it("keeps a committed plugin generation when a later channel restart fails", async () => { await withGatewayRestartSignal(async (signalSpy) => { const logReload = { info: vi.fn(), warn: vi.fn() }; @@ -5165,17 +5275,23 @@ describe("gateway plugin hot reload handlers", () => { }, ); - it("restarts pre-stopped channels when runtime publication fails", async () => { + it("restarts pre-stopped channel targets when runtime publication fails", async () => { const events: string[] = []; const publish = vi.fn(async () => { throw new Error("publication failed"); }); const reloadPlugins = vi.fn( async (params: { - beforeReplace: (channels: ReadonlySet) => Promise; + beforeReplace: ( + channels: ReadonlySet, + accounts?: ReadonlyMap>, + ) => Promise; commitRuntime: () => Promise; }): Promise => { - await params.beforeReplace(new Set(["discord"])); + await params.beforeReplace( + new Set(["discord"]), + new Map([["slack", new Set(["catalog-account"])]]), + ); await params.commitRuntime(); return makePluginReloadResult({ activeChannels: new Set(["discord"]) }); }, @@ -5183,11 +5299,11 @@ describe("gateway plugin hot reload handlers", () => { const handlers = createReloadHandlersForTest( undefined, { - stop: vi.fn(async (channel) => { - events.push(`stop:${channel}`); + stop: vi.fn(async (channel, accountId) => { + events.push(`stop:${channel}:${accountId ?? "all"}`); }), - start: vi.fn(async (channel) => { - events.push(`start:${channel}`); + start: vi.fn(async (channel, accountId) => { + events.push(`start:${channel}:${accountId ?? "all"}`); }), }, reloadPlugins, @@ -5201,18 +5317,26 @@ describe("gateway plugin hot reload handlers", () => { ), ).rejects.toThrow("publication failed"); - expect(events).toEqual(["stop:discord", "start:discord"]); + expect(events).toEqual([ + "stop:slack:catalog-account", + "stop:discord:all", + "start:slack:catalog-account", + "start:discord:all", + ]); expect(handlers.setState).not.toHaveBeenCalled(); }); - it("restarts pre-stopped channels when plugin replacement is cancelled", async () => { + it("restarts pre-stopped account targets when plugin replacement is cancelled", async () => { const events: string[] = []; const reloadPlugins = vi.fn( async (params: { - beforeReplace: (channels: ReadonlySet) => Promise; + beforeReplace: ( + channels: ReadonlySet, + accounts?: ReadonlyMap>, + ) => Promise; isAborted?: () => boolean; }): Promise => { - await params.beforeReplace(new Set(["discord"])); + await params.beforeReplace(new Set(), new Map([["discord", new Set(["catalog-account"])]])); expect(params.isAborted?.()).toBe(false); return makePluginReloadResult({ cancelled: true }); }, @@ -5220,11 +5344,11 @@ describe("gateway plugin hot reload handlers", () => { const handlers = createReloadHandlersForTest( undefined, { - stop: vi.fn(async (channel) => { - events.push(`stop:${channel}`); + stop: vi.fn(async (channel, accountId) => { + events.push(`stop:${channel}:${accountId ?? "all"}`); }), - start: vi.fn(async (channel) => { - events.push(`start:${channel}`); + start: vi.fn(async (channel, accountId) => { + events.push(`start:${channel}:${accountId ?? "all"}`); }), }, reloadPlugins, @@ -5234,7 +5358,7 @@ describe("gateway plugin hot reload handlers", () => { handlers.applyHotReload(createPluginReloadPlan(), { plugins: { enabled: true } }), ).rejects.toThrow("config hot reload cancelled by config supersession or in-process restart"); - expect(events).toEqual(["stop:discord", "start:discord"]); + expect(events).toEqual(["stop:discord:catalog-account", "start:discord:catalog-account"]); expect(handlers.setState).not.toHaveBeenCalled(); }); diff --git a/src/gateway/server-reload-hot.ts b/src/gateway/server-reload-hot.ts index 4508b1ef9927..4ec3234774d3 100644 --- a/src/gateway/server-reload-hot.ts +++ b/src/gateway/server-reload-hot.ts @@ -13,7 +13,6 @@ import { isTruthyEnvValue } from "../infra/env.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resetDirectoryCache } from "../infra/outbound/target-resolver.js"; import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js"; -import { runOutsideGatewayRootWorkAdmission } from "../process/gateway-work-admission.js"; import type { ChannelKind } from "./config-reload-plan.js"; import { shouldRefreshContextWindowCache, @@ -24,7 +23,10 @@ import { commitHooksConfigReload, resolveHooksConfig } from "./hooks.js"; import { buildGatewayCronService } from "./server-cron.js"; import { applyGatewayLaneConcurrency, resolveGatewayLaneConcurrency } from "./server-lanes.js"; import { createGatewayActiveWorkTracker } from "./server-reload-active-work.js"; -import { restartGatewayChannels } from "./server-reload-channel-restart.js"; +import { + restartGatewayChannels, + startGatewayChannelFromActiveRegistry, +} from "./server-reload-channel-restart.js"; import { GatewayHotReloadCancelledError, GatewayHotReloadRecoveryError, @@ -126,9 +128,14 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) resetDirectoryCache(); const channelsToRestart = new Set(plan.restartChannels); - const restartChannelAccounts = - plan.restartChannelAccounts ?? new Map>(); + const restartChannelAccounts = new Map>( + [...(plan.restartChannelAccounts ?? [])].map(([channel, accountIds]) => [ + channel, + new Set(accountIds), + ]), + ); const channelsStoppedBeforePluginReload = new Set(); + const accountsStoppedBeforePluginReload = new Map>(); let activePluginChannelsAfterReload: ReadonlySet | null = null; let pluginReloadAborted = false; const isLifecycleReloadAborted = () => isGatewayReloadGenerationAborted(myGeneration); @@ -330,12 +337,33 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) } }; if (plan.reloadPlugins) { + const restartStoppedPluginAccounts = async (reason: string): Promise => { + const failures: string[] = []; + for (const [channel, accountIds] of accountsStoppedBeforePluginReload) { + for (const accountId of accountIds) { + try { + params.logChannels.info(`restarting ${channel} account ${accountId} after ${reason}`); + await startGatewayChannelFromActiveRegistry(params, channel, accountId); + accountIds.delete(accountId); + } catch (err) { + failures.push(`${channel}[${accountId}]`); + params.logChannels.error( + `failed to restart ${channel} account ${accountId} after ${reason}: ${formatErrorMessage(err)}`, + ); + } + } + if (accountIds.size === 0) { + accountsStoppedBeforePluginReload.delete(channel); + } + } + return failures; + }; const restartStoppedPluginChannels = async (reason: string) => await collectChannelOperationFailures({ channels: [...channelsStoppedBeforePluginReload], run: async (channel) => { params.logChannels.info(`restarting ${channel} channel after ${reason}`); - await runOutsideGatewayRootWorkAdmission(() => params.startChannel(channel)); + await startGatewayChannelFromActiveRegistry(params, channel); channelsStoppedBeforePluginReload.delete(channel); }, onFailure: (channel, err) => { @@ -344,17 +372,37 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) ); }, }); - const failPluginChannelRollback = (reason: string, failures: ChannelKind[]): never => { + const rollbackStoppedPluginTargets = async (reason: string): Promise => [ + ...(await restartStoppedPluginAccounts(reason)), + ...(await restartStoppedPluginChannels(reason)), + ]; + const failPluginChannelRollback = (reason: string, failures: string[]): never => { const error = new Error( `plugin reload cancellation rollback failed for: ${failures.join(", ")}`, ); scheduleRecoveryRestart(`plugin channel rollback after ${reason}`, error); throw error; }; - const stopChannelsBeforePluginReplace = async (channels: ReadonlySet) => { + const stopChannelsBeforePluginReplace = async ( + channels: ReadonlySet, + accounts: ReadonlyMap> = new Map(), + ) => { for (const channel of channels) { channelsToRestart.add(channel); } + for (const [channel, accountIds] of accounts) { + if (channelsToRestart.has(channel)) { + continue; + } + let restartAccountIds = restartChannelAccounts.get(channel); + if (!restartAccountIds) { + restartAccountIds = new Set(); + restartChannelAccounts.set(channel, restartAccountIds); + } + for (const accountId of accountIds) { + restartAccountIds.add(accountId); + } + } const targets = channelReloadTargets(); if (targets.size === 0 || shouldSkipChannelRestart) { return; @@ -366,7 +414,42 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) pluginReloadAborted = true; return; } - const stopFailures = await collectChannelOperationFailures({ + const accountStopFailures: string[] = []; + for (const [channel, accountIds] of accounts) { + if (channelsToRestart.has(channel)) { + continue; + } + for (const accountId of accountIds) { + if (isPluginReloadAborted()) { + pluginReloadAborted = true; + break; + } + let stoppedAccountIds = accountsStoppedBeforePluginReload.get(channel); + if (!stoppedAccountIds) { + stoppedAccountIds = new Set(); + accountsStoppedBeforePluginReload.set(channel, stoppedAccountIds); + } + if (stoppedAccountIds.has(accountId)) { + continue; + } + stoppedAccountIds.add(accountId); + try { + params.logChannels.info( + `stopping ${channel} account ${accountId} before plugin reload`, + ); + await params.stopChannel(channel, accountId, { manual: false }); + if (isPluginReloadAborted()) { + pluginReloadAborted = true; + } + } catch (err) { + accountStopFailures.push(`${channel}[${accountId}]`); + params.logChannels.error( + `failed to stop ${channel} account ${accountId} before plugin reload: ${formatErrorMessage(err)}`, + ); + } + } + } + const channelStopFailures = await collectChannelOperationFailures({ channels: channelsToRestart, run: async (channel) => { if (isPluginReloadAborted()) { @@ -396,7 +479,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) if (isLifecycleReloadAborted()) { return; } - const rollbackFailures = await restartStoppedPluginChannels( + const rollbackFailures = await rollbackStoppedPluginTargets( "cancelled plugin reload pre-stop", ); if (rollbackFailures.length > 0) { @@ -404,8 +487,9 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) } return; } + const stopFailures = [...accountStopFailures, ...channelStopFailures]; if (stopFailures.length > 0) { - const rollbackFailures = await restartStoppedPluginChannels( + const rollbackFailures = await rollbackStoppedPluginTargets( "failed plugin reload pre-stop", ); if (rollbackFailures.length > 0) { @@ -429,7 +513,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) }); } catch (err) { if (!runtimeCommitted) { - const rollbackFailures = await restartStoppedPluginChannels( + const rollbackFailures = await rollbackStoppedPluginTargets( "failed plugin runtime publication", ); if (rollbackFailures.length > 0) { @@ -443,7 +527,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) if (pluginReloadResult.cancelled) { pluginReloadAborted = true; if (!isLifecycleReloadAborted()) { - const rollbackFailures = await restartStoppedPluginChannels( + const rollbackFailures = await rollbackStoppedPluginTargets( "cancelled plugin runtime publication", ); if (rollbackFailures.length > 0) { @@ -566,6 +650,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams) restartChannelAccounts, activePluginChannelsAfterReload, channelsStoppedBeforePluginReload, + accountsStoppedBeforePluginReload, shouldSkipChannelRestart, skipChannelRestartLogMessage: "skipping channel reload (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)", diff --git a/src/gateway/server/readiness.test.ts b/src/gateway/server/readiness.test.ts index e15e525fc396..caefbdf66a61 100644 --- a/src/gateway/server/readiness.test.ts +++ b/src/gateway/server/readiness.test.ts @@ -30,6 +30,7 @@ function snapshotWith( function createManager(snapshot: ChannelRuntimeSnapshot): ChannelManager { return { getRuntimeSnapshot: vi.fn(() => snapshot), + getPluginCommandCatalogAccounts: vi.fn(() => new Map()), startChannels: vi.fn(), startChannel: vi.fn(), stopChannel: vi.fn(), diff --git a/src/infra/outbound/deliver.queue-integration.test.ts b/src/infra/outbound/deliver.queue-integration.test.ts index 3d631304c223..214dee7791d2 100644 --- a/src/infra/outbound/deliver.queue-integration.test.ts +++ b/src/infra/outbound/deliver.queue-integration.test.ts @@ -886,7 +886,7 @@ describe("deliverOutboundPayloads queue integration: mid-batch failure with send expect(sendMatrix).toHaveBeenCalledOnce(); }); - it("advances queued entry to unknown_after_send when a later payload fails after an earlier one succeeded", async () => { + it("advances queued entry to unknown_after_send before a later payload fails", async () => { let sendCount = 0; let stateBeforeSecondSend: string | undefined; const sendMatrix = vi.fn(async () => { diff --git a/src/plugin-sdk/channel-test-helpers.ts b/src/plugin-sdk/channel-test-helpers.ts index 5f5d1537a505..e9f5b77e918a 100644 --- a/src/plugin-sdk/channel-test-helpers.ts +++ b/src/plugin-sdk/channel-test-helpers.ts @@ -18,6 +18,7 @@ export { resetPluginRuntimeStateForTest, resetGlobalHookRunner, setActivePluginRegistry, + withPluginRuntimeRegistryScope, type PluginHookRegistration, } from "./test-helpers/outbound-delivery.js"; export { diff --git a/src/plugin-sdk/plugin-command-runtime.ts b/src/plugin-sdk/plugin-command-runtime.ts new file mode 100644 index 000000000000..be7bcb9eefc6 --- /dev/null +++ b/src/plugin-sdk/plugin-command-runtime.ts @@ -0,0 +1,13 @@ +/** Focused registry-bound plugin command planning and execution contract. */ +export { + createPluginCommandRuntime, + PLUGIN_COMMAND_DISPATCH, +} from "../plugins/plugin-command-runtime.js"; +export type { + PluginCommandCatalogDecision, + PluginCommandDispatch, + PluginCommandDispatchContext, + PluginCommandNativeCandidate, + PluginCommandReplyOptions, + PluginCommandRuntime, +} from "../plugins/plugin-command-runtime.js"; diff --git a/src/plugin-sdk/test-helpers/outbound-delivery.ts b/src/plugin-sdk/test-helpers/outbound-delivery.ts index d46e39f88a28..b7e97eea8ee0 100644 --- a/src/plugin-sdk/test-helpers/outbound-delivery.ts +++ b/src/plugin-sdk/test-helpers/outbound-delivery.ts @@ -7,4 +7,5 @@ export { addTestHook } from "../../plugins/hooks.test-helpers.js"; export type { PluginHookRegistration } from "../../plugins/hook-types.js"; export { createEmptyPluginRegistry } from "../../plugins/registry.js"; export { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; +export { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js"; export { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; diff --git a/src/plugins/command-execution-lock.ts b/src/plugins/command-execution-lock.ts new file mode 100644 index 000000000000..0832fe4763d1 --- /dev/null +++ b/src/plugins/command-execution-lock.ts @@ -0,0 +1,90 @@ +/** Per-registry command execution admission and retirement drain. */ +import { AsyncLocalStorage } from "node:async_hooks"; +import { isPluginRegistryRetired } from "./registry-lifecycle.js"; +import type { PluginRegistry } from "./registry-types.js"; + +type PluginCommandExecutionState = { + count: number; + waiters: Array<() => void>; +}; + +type PluginCommandExecutionToken = { + registry: PluginRegistry; + active: boolean; +}; + +const executionStates = new WeakMap(); +const executionContext = new AsyncLocalStorage>(); + +function getExecutionState(registry: PluginRegistry): PluginCommandExecutionState { + const existing = executionStates.get(registry); + if (existing) { + return existing; + } + const created = { count: 0, waiters: [] }; + executionStates.set(registry, created); + return created; +} + +export function getPluginCommandExecutionCount(registry: PluginRegistry): number { + return executionStates.get(registry)?.count ?? 0; +} + +function beginPluginCommandExecution(registry: PluginRegistry): boolean { + if (isPluginRegistryRetired(registry)) { + return false; + } + getExecutionState(registry).count += 1; + return true; +} + +function endPluginCommandExecution(registry: PluginRegistry): void { + const state = getExecutionState(registry); + if (state.count <= 0) { + throw new Error("Plugin command execution lock is unbalanced."); + } + state.count -= 1; + if (state.count !== 0) { + return; + } + const waiters = state.waiters.splice(0); + for (const resolve of waiters) { + resolve(); + } +} + +export function isPluginCommandExecutionActiveHere(registry: PluginRegistry): boolean { + return [...(executionContext.getStore() ?? [])].some( + (token) => token.registry === registry && token.active, + ); +} + +export async function withPluginCommandExecution( + registry: PluginRegistry, + run: () => T | Promise, +): Promise<{ admitted: true; value: T } | { admitted: false }> { + if (!beginPluginCommandExecution(registry)) { + return { admitted: false }; + } + const token: PluginCommandExecutionToken = { registry, active: true }; + const active = new Set( + [...(executionContext.getStore() ?? [])].filter((inherited) => inherited.registry !== registry), + ); + active.add(token); + try { + return { admitted: true, value: await executionContext.run(active, run) }; + } finally { + token.active = false; + endPluginCommandExecution(registry); + } +} + +export async function waitForPluginCommandExecutions(registry: PluginRegistry): Promise { + const state = getExecutionState(registry); + if (state.count === 0) { + return; + } + await new Promise((resolve) => { + state.waiters.push(resolve); + }); +} diff --git a/src/plugins/command-registration.ts b/src/plugins/command-registration.ts index b18aba1ad0d1..43a82c8618c2 100644 --- a/src/plugins/command-registration.ts +++ b/src/plugins/command-registration.ts @@ -7,13 +7,10 @@ import { isOperatorScope } from "../gateway/operator-scopes.js"; import { logVerbose } from "../globals.js"; import { isRecord } from "../utils.js"; import { normalizeAgentPromptSurfaceKind } from "./agent-prompt-surface-kind.js"; +import { getPluginCommandExecutionCount } from "./command-execution-lock.js"; import { clearPluginCommands } from "./command-registry-state.js"; import type { PluginRegistry } from "./registry-types.js"; -import { - getActivePluginGatewayCommandRegistry, - getPluginRegistrationContext, - requireActivePluginRegistry, -} from "./runtime.js"; +import { getPluginRegistrationContext, requireActivePluginRegistry } from "./runtime.js"; import { AGENT_PROMPT_SURFACE_KINDS, type AgentPromptGuidance, @@ -284,7 +281,7 @@ function normalizeAgentPromptGuidance( }); } -export function listPluginInvocationKeys(command: OpenClawPluginCommandDefinition): string[] { +function listPluginInvocationKeys(command: OpenClawPluginCommandDefinition): string[] { const keys = new Set(); const push = (value: string | undefined) => { const normalized = normalizeOptionalLowercaseString(value); @@ -304,19 +301,6 @@ export function listPluginInvocationKeys(command: OpenClawPluginCommandDefinitio return [...keys]; } -export function pluginCommandSupportsChannel( - command: OpenClawPluginCommandDefinition, - channel?: string, -): boolean { - if (!command.channels || command.channels.length === 0 || !channel) { - return true; - } - const normalizedChannel = normalizeLowercaseStringOrEmpty(channel); - return command.channels.some( - (entry) => normalizeLowercaseStringOrEmpty(entry) === normalizedChannel, - ); -} - export function registerPluginCommand( pluginId: string, command: OpenClawPluginCommandDefinition, @@ -329,7 +313,7 @@ export function registerPluginCommand( ): CommandRegistrationResult { const context = getPluginRegistrationContext(); return registerPluginCommandInRegistry( - context?.registry ?? getActivePluginGatewayCommandRegistry() ?? requireActivePluginRegistry(), + context?.registry ?? requireActivePluginRegistry(), context?.pluginId ?? pluginId, command, opts, @@ -343,7 +327,7 @@ export function registerPluginCommandInRegistry( opts?: Parameters[2], ): CommandRegistrationResult { // Prevent registration while commands are being processed - if (registry.commandRegistryLocked) { + if (getPluginCommandExecutionCount(registry) > 0) { return { ok: false, error: "Cannot register commands while processing is in progress" }; } if (command.ownership === "reserved") { diff --git a/src/plugins/command-registry-state.ts b/src/plugins/command-registry-state.ts index bfac7d7c96e9..ce09b7843a6b 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -1,7 +1,8 @@ // Stores plugin command registry state for the current process lifecycle. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeAgentPromptSurfaceKind } from "./agent-prompt-surface-kind.js"; -import { getActivePluginGatewayCommandRegistry, requireActivePluginRegistry } from "./runtime.js"; +import { listRegisteredPluginCommands } from "./plugin-command-registry.js"; +import { requireActivePluginRegistry } from "./runtime.js"; import type { AgentPromptGuidance, AgentPromptSurfaceKind, @@ -15,28 +16,21 @@ export type RegisteredPluginCommand = OpenClawPluginCommandDefinition & { trustedOwnerStatusExposure?: true; }; -const getCommandRegistry = () => - getActivePluginGatewayCommandRegistry() ?? requireActivePluginRegistry(); - const getPluginCommandMap = () => new Map( - getCommandRegistry().commands.map((entry) => [ - `/${normalizeOptionalLowercaseString(entry.command.name) ?? ""}`, - { - ...entry.command, - pluginId: entry.pluginId, - pluginName: entry.pluginName, - pluginRoot: entry.rootDir, - trustedOwnerStatusExposure: entry.trustedOwnerStatusExposure, - }, + listRegisteredPluginCommands(resolveCompatibilityPluginCommandRegistry()).map((command) => [ + `/${normalizeOptionalLowercaseString(command.name) ?? ""}`, + command, ]), ); +export const resolveCompatibilityPluginCommandRegistry = requireActivePluginRegistry; + export const pluginCommands = new Proxy(new Map(), { get(_target, property) { if (property === "clear") { return () => { - getCommandRegistry().commands.length = 0; + resolveCompatibilityPluginCommandRegistry().commands.length = 0; }; } const map = getPluginCommandMap(); @@ -45,10 +39,6 @@ export const pluginCommands = new Proxy(new Map }, }); -export function setPluginCommandRegistryLocked(locked: boolean): void { - getCommandRegistry().commandRegistryLocked = locked; -} - export function clearPluginCommands(): void { pluginCommands.clear(); } diff --git a/src/plugins/command-specs.ts b/src/plugins/command-specs.ts index e5ee42a93694..bf493fa67e00 100644 --- a/src/plugins/command-specs.ts +++ b/src/plugins/command-specs.ts @@ -3,8 +3,11 @@ import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/s import { getLoadedChannelPlugin } from "../channels/plugins/index.js"; import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only-command-defaults.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { pluginCommandSupportsChannel } from "./command-registration.js"; import { pluginCommands } from "./command-registry-state.js"; +import { + pluginCommandSupportsChannel, + projectPluginCommandNativeMetadata, +} from "./plugin-command-metadata.js"; import type { PluginCommandRegistration } from "./registry-types.js"; import type { OpenClawPluginCommandDefinition } from "./types.js"; @@ -22,23 +25,6 @@ type PluginCommandEntrySpec = { nativeName?: string; }; -function resolvePluginNativeName( - command: OpenClawPluginCommandDefinition, - provider?: string, -): string { - const providerName = normalizeOptionalLowercaseString(provider); - const providerOverride = providerName ? command.nativeNames?.[providerName] : undefined; - if (typeof providerOverride === "string" && providerOverride.trim()) { - return providerOverride.trim(); - } - const defaultOverride = command.nativeNames?.default; - if (typeof defaultOverride === "string" && defaultOverride.trim()) { - return defaultOverride.trim(); - } - const fallbackName = command.name.trim(); - return fallbackName || command.name; -} - function resolvePluginTextName(command: OpenClawPluginCommandDefinition): string { const name = command.name.trim(); return name || command.name; @@ -125,18 +111,19 @@ function serializePluginCommandSpec( descriptionLocalizations?: Record; acceptsArgs: boolean; } { + const metadata = projectPluginCommandNativeMetadata(cmd, provider); const spec: { name: string; description: string; descriptionLocalizations?: Record; acceptsArgs: boolean; } = { - name: resolvePluginNativeName(cmd, provider), - description: cmd.description.trim(), - acceptsArgs: cmd.acceptsArgs ?? false, + name: metadata.name, + description: metadata.description, + acceptsArgs: metadata.acceptsArgs, }; - if (cmd.descriptionLocalizations) { - spec.descriptionLocalizations = cmd.descriptionLocalizations; + if (metadata.descriptionLocalizations) { + spec.descriptionLocalizations = { ...metadata.descriptionLocalizations }; } return spec; } @@ -149,7 +136,9 @@ function serializePluginCommandEntrySpec( if (!pluginCommandSupportsChannel(cmd, provider)) { return null; } - const nativeName = nativeCommandsEnabled ? resolvePluginNativeName(cmd, provider) : undefined; + const nativeName = nativeCommandsEnabled + ? projectPluginCommandNativeMetadata(cmd, provider).name + : undefined; return { name: resolvePluginTextName(cmd), description: cmd.description.trim(), diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index e052308a3ab0..79104ff06b84 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -14,6 +14,7 @@ import { import { createEmptyPluginRegistry } from "./registry-empty.js"; import { createPluginRegistry } from "./registry.js"; import { setActivePluginRegistry, withPluginRegistrationContext } from "./runtime.js"; +import { withPluginRuntimeRegistryScope } from "./runtime/gateway-request-scope.js"; import type { PluginRuntime } from "./runtime/types.js"; import { createBundledPluginRecord } from "./status.test-fixtures.js"; @@ -423,6 +424,49 @@ describe("registerPluginCommand", () => { expect(listRegisteredPluginAgentPromptGuidance()).toEqual(["Use /demo_cmd for demo routing."]); }); + it("prefers a request-scoped registry over ambient compatibility state", async () => { + const ambientHandler = vi.fn(async () => ({ text: "ambient" })); + const scopedHandler = vi.fn(async () => ({ text: "scoped" })); + expect( + registerPluginCommand("ambient", { + name: "same", + description: "Ambient command", + agentPromptGuidance: ["Ambient guidance"], + handler: ambientHandler, + }), + ).toEqual({ ok: true }); + const scoped = createEmptyPluginRegistry(); + + await withPluginRuntimeRegistryScope(scoped, async () => { + expect( + registerPluginCommand("scoped", { + name: "same", + description: "Scoped command", + agentPromptGuidance: ["Scoped guidance"], + handler: scopedHandler, + }), + ).toEqual({ ok: true }); + expect(listProviderPluginCommandSpecs().map((entry) => entry.description)).toEqual([ + "Scoped command", + ]); + expect(listRegisteredPluginAgentPromptGuidance()).toEqual(["Scoped guidance"]); + const match = matchPluginCommand("/same"); + expect(match?.command.pluginId).toBe("scoped"); + await executePluginCommand({ + command: match!.command, + senderId: "user-1", + channel: "telegram", + isAuthorizedSender: true, + commandBody: "/same", + config: {}, + }); + }); + + expect(scopedHandler).toHaveBeenCalledOnce(); + expect(ambientHandler).not.toHaveBeenCalled(); + expect(listRegisteredPluginAgentPromptGuidance()).toEqual(["Ambient guidance"]); + }); + it.each([ ["zeta-plugin", "alpha-plugin"], ["alpha-plugin", "zeta-plugin"], diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index f468d9c88032..8596566ed1c9 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -1,218 +1,41 @@ /** * Plugin Command Registry * - * Manages commands registered by plugins that bypass the LLM agent. - * These commands are processed before built-in commands and before agent invocation. + * Compatibility wrappers for plugin command registration, matching, and execution. */ - -import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; -import { resolveBoundAgentIdForSession } from "../agents/session-agent-binding.js"; -import { resolveConversationBindingContext } from "../channels/conversation-binding-context.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { ADMIN_SCOPE, isOperatorScope } from "../gateway/operator-scopes.js"; -import { logVerbose } from "../globals.js"; +import { clearPluginCommands, registerPluginCommand } from "./command-registration.js"; import { - clearPluginCommands, - isReservedCommandName, - listPluginInvocationKeys, - pluginCommandSupportsChannel, - registerPluginCommand, -} from "./command-registration.js"; -import { - canExposeSenderIsOwner, - isTrustedReservedCommandOwner, listRegisteredPluginAgentPromptGuidance, pluginCommands, - setPluginCommandRegistryLocked, + resolveCompatibilityPluginCommandRegistry, type RegisteredPluginCommand, } from "./command-registry-state.js"; import { - detachPluginConversationBinding, - getCurrentPluginConversationBinding, - requestPluginConversationBinding, -} from "./conversation-binding.js"; -import { getActivePluginChannelRegistry } from "./runtime.js"; -import type { - OpenClawPluginCommandDefinition, - PluginCommandContext, - PluginCommandResult, -} from "./types.js"; - -// Maximum allowed length for command arguments (defense in depth) -const MAX_ARGS_LENGTH = 4096; + executeRegisteredPluginCommand, + type PluginCommandExecutionParams, +} from "./plugin-command-execution.js"; +import { matchRegisteredPluginCommand } from "./plugin-command-matcher.js"; +import { listRegisteredPluginCommands } from "./plugin-command-registry.js"; +import type { PluginCommandContext, PluginCommandResult } from "./types.js"; export { clearPluginCommands, listRegisteredPluginAgentPromptGuidance, registerPluginCommand }; -/** - * Check if a command body matches a registered plugin command. - * Returns the command definition and parsed args if matched. - * - * Note: If a command has `acceptsArgs: false` and the user provides arguments, - * the command will not match. This allows the message to fall through to - * built-in handlers or the agent. Document this behavior to plugin authors. - */ +/** Match one compatibility command invocation against the current command registry. */ export function matchPluginCommand( commandBody: string, options: { channel?: string } = {}, ): { command: RegisteredPluginCommand; args?: string } | null { - const trimmed = commandBody.trim(); - if (!trimmed.startsWith("/")) { - return null; - } - - // Accept whitespace after the slash so `/ pair qr` keeps `/pair` ownership. - const commandMatch = trimmed.match(/^\/\s*([^\s]+)(?:\s+([\s\S]*))?$/); - if (!commandMatch) { - return null; - } - const commandName = `/${commandMatch[1]}`; - const args = commandMatch[2]?.trim(); - - const key = normalizeLowercaseStringOrEmpty(commandName); - const alternateKeys = [key]; - if (key.includes("_")) { - alternateKeys.push(key.replace(/_/g, "-")); - } - if (key.includes("-")) { - alternateKeys.push(key.replace(/-/g, "_")); - } - const command = - alternateKeys - .map( - (candidateKey) => - pluginCommands.get(candidateKey) ?? - Array.from(pluginCommands.values()).find((candidate) => - listPluginInvocationNames(candidate).includes(candidateKey), - ), - ) - .filter((candidate) => candidate && pluginCommandSupportsChannel(candidate, options.channel)) - .find(Boolean) ?? null; - - if (!command) { - return null; - } - - // If command doesn't accept args but args were provided, don't match - if (args && !command.acceptsArgs) { - return null; - } - - return { command, args: args || undefined }; -} - -/** - * Sanitize command arguments to prevent injection attacks. - * Removes control characters and enforces length limits. - */ -function sanitizeArgs(args: string | undefined): string | undefined { - if (!args) { - return undefined; - } - - // Remove control characters (except newlines and tabs which may be intentional) - let sanitized = ""; - for (const char of truncateUtf16Safe(args, MAX_ARGS_LENGTH)) { - const code = char.charCodeAt(0); - const isControl = (code <= 0x1f && code !== 0x09 && code !== 0x0a) || code === 0x7f; - if (!isControl) { - sanitized += char; - } - } - return sanitized; -} - -function resolveBindingConversationFromCommand(params: { - config?: OpenClawConfig; - channel: string; - senderId?: string; - from?: string; - to?: string; - originatingTo?: string; - accountId?: string; - messageThreadId?: string | number; - threadParentId?: string; -}): { - channel: string; - accountId: string; - conversationId: string; - parentConversationId?: string; - threadId?: string | number; -} | null { - const channelPlugin = getActivePluginChannelRegistry()?.channels.find( - (entry) => entry.plugin.id === params.channel, - )?.plugin; - if (!channelPlugin?.bindings?.resolveCommandConversation) { - return null; - } - return resolveConversationBindingContext({ - cfg: params.config ?? ({} as OpenClawConfig), - channel: params.channel, - accountId: params.accountId, - threadId: params.messageThreadId, - threadParentId: params.threadParentId, - senderId: params.senderId, - originatingTo: params.originatingTo ?? params.from, - commandTo: params.to, - fallbackTo: params.to ?? params.from, + const registry = resolveCompatibilityPluginCommandRegistry(); + return matchRegisteredPluginCommand({ + commands: listRegisteredPluginCommands(registry), + commandBody, + channel: options.channel, + aliasScope: { kind: "all" }, }); } -type PluginCommandRuntimeLlm = NonNullable["llm"]; -type PluginCommandLlmCompleteParams = Parameters< - NonNullable["complete"] ->[0]; - -function buildPluginCommandRuntimeContext(params: { - command: RegisteredPluginCommand; - config: OpenClawConfig; - agentId?: string; - sessionKey?: string; - authProfileId?: string; -}): PluginCommandContext["runtimeContext"] { - const sessionKey = params.sessionKey?.trim(); - const agentId = resolveBoundAgentIdForSession({ - config: params.config, - agentId: params.agentId, - sessionKey, - }); - if (!sessionKey && !agentId) { - return undefined; - } - return { - llm: { - complete: async (request: PluginCommandLlmCompleteParams) => { - const { createRuntimeLlm } = await import("./runtime/runtime-llm.runtime.js"); - return await createRuntimeLlm({ - getConfig: () => params.config, - authority: { - caller: { - kind: "plugin", - id: params.command.pluginId, - name: params.command.pluginName, - }, - pluginIdForPolicy: params.command.pluginId, - requiresBoundAgent: true, - ...(sessionKey ? { sessionKey } : {}), - ...(agentId ? { agentId } : {}), - ...(params.authProfileId ? { preferredProfile: params.authProfileId } : {}), - allowAgentIdOverride: false, - allowModelOverride: false, - allowComplete: true, - }, - }).complete(request); - }, - }, - }; -} - -/** - * Execute a plugin command handler. - * - * Note: Plugin authors should still validate and sanitize ctx.args for their - * specific use case. This function provides basic defense-in-depth sanitization. - */ -export async function executePluginCommand(params: { +export function executePluginCommand(params: { command: RegisteredPluginCommand; args?: string; senderId?: string; @@ -240,206 +63,24 @@ export async function executePluginCommand(params: { diagnosticsUploadApproved?: PluginCommandContext["diagnosticsUploadApproved"]; diagnosticsPreviewOnly?: PluginCommandContext["diagnosticsPreviewOnly"]; diagnosticsPrivateRouted?: PluginCommandContext["diagnosticsPrivateRouted"]; -}): Promise { - const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params; - - // Check authorization - if (!pluginCommandSupportsChannel(command, channel)) { - logVerbose(`Plugin command /${command.name} skipped on unsupported channel ${channel}`); - return { continueAgent: true }; - } - const requireAuth = command.requireAuth !== false; // Default to true - if (requireAuth && !isAuthorizedSender) { - logVerbose( - `Plugin command /${command.name} blocked: unauthorized sender ${senderId || ""}`, - ); - return { text: "⚠️ This command requires authorization." }; - } - if (command.requiredScopes !== undefined && !Array.isArray(command.requiredScopes)) { - logVerbose(`Plugin command /${command.name} blocked: invalid requiredScopes configuration`); - return { text: "⚠️ This command has invalid gateway scope configuration." }; - } - const requiredScopes = command.requiredScopes ?? []; - const unknownScope = (requiredScopes as readonly unknown[]).find( - (scope) => !isOperatorScope(scope), - ); - if (unknownScope) { - logVerbose(`Plugin command /${command.name} blocked: unknown gateway scope`); - return { text: "⚠️ This command has invalid gateway scope configuration." }; - } - if (requiredScopes.length > 0) { - const senderIsOwner = params.senderIsOwner === true; - const scopes = Array.isArray(params.gatewayClientScopes) - ? new Set(params.gatewayClientScopes) - : undefined; - const hasGatewayScopeContext = scopes !== undefined; - const hasAdmin = scopes?.has(ADMIN_SCOPE) === true; - const missingScope = scopes - ? requiredScopes.find((scope) => !hasAdmin && !scopes.has(scope)) - : requiredScopes[0]; - if (missingScope && (hasGatewayScopeContext || !senderIsOwner)) { - logVerbose(`Plugin command /${command.name} blocked: missing gateway scope ${missingScope}`); - return { text: `⚠️ This command requires gateway scope: ${missingScope}.` }; - } - } - - // Sanitize args before passing to handler - const sanitizedArgs = sanitizeArgs(args); - const bindingConversation = resolveBindingConversationFromCommand({ - config, - channel, - senderId, - from: params.from, - to: params.to, - originatingTo: params.originatingTo, - accountId: params.accountId, - messageThreadId: params.messageThreadId, - threadParentId: params.threadParentId, - }); - const effectiveAccountId = bindingConversation?.accountId ?? params.accountId; - const senderIsOwnerForCommand = - canExposeSenderIsOwner(command) || - (isTrustedReservedCommandOwner(command) && - command.ownership === "reserved" && - isReservedCommandName(command.name) && - command.pluginId === normalizeLowercaseStringOrEmpty(command.name)) - ? params.senderIsOwner - : undefined; - const diagnosticsPrivateRoutedForCommand = - isTrustedReservedCommandOwner(command) && - command.ownership === "reserved" && - isReservedCommandName(command.name) && - command.pluginId === normalizeLowercaseStringOrEmpty(command.name) - ? params.diagnosticsPrivateRouted - : undefined; - const diagnosticsUploadApprovedForCommand = - isTrustedReservedCommandOwner(command) && - command.ownership === "reserved" && - isReservedCommandName(command.name) && - command.pluginId === normalizeLowercaseStringOrEmpty(command.name) - ? params.diagnosticsUploadApproved - : undefined; - const diagnosticsPreviewOnlyForCommand = - isTrustedReservedCommandOwner(command) && - command.ownership === "reserved" && - isReservedCommandName(command.name) && - command.pluginId === normalizeLowercaseStringOrEmpty(command.name) - ? params.diagnosticsPreviewOnly - : undefined; - - const ctx: PluginCommandContext = { - senderId, - channel, - channelId: params.channelId, - isAuthorizedSender, - ...(senderIsOwnerForCommand === undefined ? {} : { senderIsOwner: senderIsOwnerForCommand }), - gatewayClientScopes: params.gatewayClientScopes, - agentId: params.agentId, - sessionKey: params.sessionKey, - sessionId: params.sessionId, - sessionTarget: params.sessionTarget, - sessionFile: params.sessionFile, - args: sanitizedArgs, - commandBody, - config, - from: params.from, - to: params.to, - accountId: effectiveAccountId, - messageThreadId: params.messageThreadId, - threadParentId: params.threadParentId, - diagnosticsSessions: params.diagnosticsSessions, - runtimeContext: buildPluginCommandRuntimeContext({ - command, - config, - agentId: params.agentId, - sessionKey: params.sessionKey, - authProfileId: params.authProfileId, - }), - ...(diagnosticsUploadApprovedForCommand === undefined - ? {} - : { diagnosticsUploadApproved: diagnosticsUploadApprovedForCommand }), - ...(diagnosticsPreviewOnlyForCommand === undefined - ? {} - : { diagnosticsPreviewOnly: diagnosticsPreviewOnlyForCommand }), - ...(diagnosticsPrivateRoutedForCommand === undefined - ? {} - : { diagnosticsPrivateRouted: diagnosticsPrivateRoutedForCommand }), - requestConversationBinding: async (bindingParams) => { - if (!command.pluginRoot || !bindingConversation) { - return { - status: "error", - message: "This command cannot bind the current conversation.", - }; - } - return requestPluginConversationBinding({ - pluginId: command.pluginId, - pluginName: command.pluginName, - pluginRoot: command.pluginRoot, - requestedBySenderId: senderId, - conversation: bindingConversation, - binding: bindingParams, - }); - }, - detachConversationBinding: async () => { - if (!command.pluginRoot || !bindingConversation) { - return { removed: false }; - } - return detachPluginConversationBinding({ - pluginRoot: command.pluginRoot, - conversation: bindingConversation, - }); - }, - getCurrentConversationBinding: async () => { - if (!command.pluginRoot || !bindingConversation) { - return null; - } - return getCurrentPluginConversationBinding({ - pluginRoot: command.pluginRoot, - conversation: bindingConversation, - }); - }, - }; - - // Lock registry during execution to prevent concurrent modifications - setPluginCommandRegistryLocked(true); - try { - const result = await command.handler(ctx); - logVerbose( - `Plugin command /${command.name} executed successfully for ${senderId || "unknown"}`, - ); - if (!result || typeof result !== "object") { - logVerbose(`Plugin command /${command.name} returned no reply payload`); - return {}; - } - return result; - } catch (err) { - const error = err as Error; - logVerbose(`Plugin command /${command.name} error: ${error.message}`); - // Don't leak internal error details - return a safe generic message - return { text: "⚠️ Command failed. Please try again later." }; - } finally { - setPluginCommandRegistryLocked(false); - } +}): Promise; +export async function executePluginCommand( + params: PluginCommandExecutionParams, +): Promise { + return await executeRegisteredPluginCommand(resolveCompatibilityPluginCommandRegistry(), params); } -/** - * List all registered plugin commands. - * Used for /help and /commands output. - */ +/** List registered plugin commands for help and command discovery. */ export function listPluginCommands(): Array<{ name: string; description: string; pluginId: string; acceptsArgs: boolean; }> { - return Array.from(pluginCommands.values()).map((cmd) => ({ - name: cmd.name, - description: cmd.description, - pluginId: cmd.pluginId, - acceptsArgs: cmd.acceptsArgs ?? false, + return Array.from(pluginCommands.values()).map((command) => ({ + name: command.name, + description: command.description, + pluginId: command.pluginId, + acceptsArgs: command.acceptsArgs ?? false, })); } - -function listPluginInvocationNames(command: OpenClawPluginCommandDefinition): string[] { - return listPluginInvocationKeys(command); -} diff --git a/src/plugins/plugin-command-account-start-scope.ts b/src/plugins/plugin-command-account-start-scope.ts new file mode 100644 index 000000000000..68aa229d1e34 --- /dev/null +++ b/src/plugins/plugin-command-account-start-scope.ts @@ -0,0 +1,34 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { resolveGlobalSingleton } from "../shared/global-singleton.js"; + +type PluginCommandAccountStartScope = Readonly<{ + channelId: string; + retainCatalog: () => void; +}>; + +const PLUGIN_COMMAND_ACCOUNT_START_SCOPE_KEY: unique symbol = Symbol.for( + "openclaw.pluginCommandAccountStartScope", +); + +const pluginCommandAccountStartScope = resolveGlobalSingleton< + AsyncLocalStorage +>( + PLUGIN_COMMAND_ACCOUNT_START_SCOPE_KEY, + () => new AsyncLocalStorage(), +); + +/** Runs one channel account startup lifetime with its catalog-retention owner. */ +export function withPluginCommandAccountStartScope( + scope: PluginCommandAccountStartScope, + run: () => T, +): T { + return pluginCommandAccountStartScope.run(scope, run); +} + +/** Marks the current account only when its startup channel matches the catalog provider. */ +export function retainPluginCommandCatalogForCurrentAccount(channelId: string): void { + const scope = pluginCommandAccountStartScope.getStore(); + if (scope?.channelId === channelId) { + scope.retainCatalog(); + } +} diff --git a/src/plugins/plugin-command-dispatch-contract.ts b/src/plugins/plugin-command-dispatch-contract.ts new file mode 100644 index 000000000000..fd3be2b24c90 --- /dev/null +++ b/src/plugins/plugin-command-dispatch-contract.ts @@ -0,0 +1,8 @@ +/** Lightweight reply-option contract for prepared plugin command ownership. */ +export const PLUGIN_COMMAND_DISPATCH: unique symbol = Symbol.for( + "openclaw.pluginCommandDispatch", +) as never; + +export type PluginCommandReplyOptions = Readonly<{ + [PLUGIN_COMMAND_DISPATCH]?: Readonly<{ kind: "plugin" | "non-plugin" }>; +}>; diff --git a/src/plugins/plugin-command-execution.ts b/src/plugins/plugin-command-execution.ts new file mode 100644 index 000000000000..76436ef4cbe8 --- /dev/null +++ b/src/plugins/plugin-command-execution.ts @@ -0,0 +1,299 @@ +/** Exact-registry plugin command execution shared by focused and compatibility runtimes. */ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; +import { resolveBoundAgentIdForSession } from "../agents/session-agent-binding.js"; +import { resolveConversationBindingContext } from "../channels/conversation-binding-context.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { ADMIN_SCOPE, isOperatorScope } from "../gateway/operator-scopes.js"; +import { logVerbose } from "../globals.js"; +import { withPluginCommandExecution } from "./command-execution-lock.js"; +import { isReservedCommandName } from "./command-registration.js"; +import { + canExposeSenderIsOwner, + isTrustedReservedCommandOwner, + type RegisteredPluginCommand, +} from "./command-registry-state.js"; +import { + detachPluginConversationBinding, + getCurrentPluginConversationBinding, + requestPluginConversationBinding, +} from "./conversation-binding.js"; +import { pluginCommandSupportsChannel } from "./plugin-command-metadata.js"; +import type { PluginRegistry } from "./registry-types.js"; +import { withPluginRuntimeRegistryScope } from "./runtime/gateway-request-scope.js"; +import type { PluginCommandContext, PluginCommandResult } from "./types.js"; + +const MAX_ARGS_LENGTH = 4096; + +export type PluginCommandExecutionParams = { + command: RegisteredPluginCommand; + args?: string; + senderId?: string; + channel: string; + channelId?: PluginCommandContext["channelId"]; + isAuthorizedSender: boolean; + senderIsOwner?: boolean; + gatewayClientScopes?: PluginCommandContext["gatewayClientScopes"]; + agentId?: string; + sessionKey?: PluginCommandContext["sessionKey"]; + sessionId?: PluginCommandContext["sessionId"]; + sessionTarget?: PluginCommandContext["sessionTarget"]; + sessionFile?: PluginCommandContext["sessionFile"]; + authProfileId?: string; + commandBody: string; + config: OpenClawConfig; + from?: PluginCommandContext["from"]; + to?: PluginCommandContext["to"]; + originatingTo?: string; + accountId?: PluginCommandContext["accountId"]; + messageThreadId?: PluginCommandContext["messageThreadId"]; + threadParentId?: PluginCommandContext["threadParentId"]; + diagnosticsSessions?: PluginCommandContext["diagnosticsSessions"]; + diagnosticsUploadApproved?: PluginCommandContext["diagnosticsUploadApproved"]; + diagnosticsPreviewOnly?: PluginCommandContext["diagnosticsPreviewOnly"]; + diagnosticsPrivateRouted?: PluginCommandContext["diagnosticsPrivateRouted"]; +}; + +function sanitizeArgs(args: string | undefined): string | undefined { + if (!args) { + return undefined; + } + let sanitized = ""; + for (const char of truncateUtf16Safe(args, MAX_ARGS_LENGTH)) { + const code = char.charCodeAt(0); + const isControl = (code <= 0x1f && code !== 0x09 && code !== 0x0a) || code === 0x7f; + if (!isControl) { + sanitized += char; + } + } + return sanitized; +} + +function resolveBindingConversation(params: { + registry: PluginRegistry; + config?: OpenClawConfig; + channel: string; + senderId?: string; + from?: string; + to?: string; + originatingTo?: string; + accountId?: string; + messageThreadId?: string | number; + threadParentId?: string; +}) { + const channelPlugin = params.registry.channels.find( + (entry) => entry.plugin.id === params.channel, + )?.plugin; + if (!channelPlugin?.bindings?.resolveCommandConversation) { + return null; + } + return resolveConversationBindingContext({ + cfg: params.config ?? ({} as OpenClawConfig), + channel: params.channel, + accountId: params.accountId, + threadId: params.messageThreadId, + threadParentId: params.threadParentId, + senderId: params.senderId, + originatingTo: params.originatingTo ?? params.from, + commandTo: params.to, + fallbackTo: params.to ?? params.from, + }); +} + +type PluginCommandRuntimeLlm = NonNullable["llm"]; +type PluginCommandLlmCompleteParams = Parameters< + NonNullable["complete"] +>[0]; + +function buildRuntimeContext(params: { + command: RegisteredPluginCommand; + config: OpenClawConfig; + agentId?: string; + sessionKey?: string; + authProfileId?: string; +}): PluginCommandContext["runtimeContext"] { + const sessionKey = params.sessionKey?.trim(); + const agentId = resolveBoundAgentIdForSession({ + config: params.config, + agentId: params.agentId, + sessionKey, + }); + if (!sessionKey && !agentId) { + return undefined; + } + return { + llm: { + complete: async (request: PluginCommandLlmCompleteParams) => { + const { createRuntimeLlm } = await import("./runtime/runtime-llm.runtime.js"); + return await createRuntimeLlm({ + getConfig: () => params.config, + authority: { + caller: { + kind: "plugin", + id: params.command.pluginId, + name: params.command.pluginName, + }, + pluginIdForPolicy: params.command.pluginId, + requiresBoundAgent: true, + ...(sessionKey ? { sessionKey } : {}), + ...(agentId ? { agentId } : {}), + ...(params.authProfileId ? { preferredProfile: params.authProfileId } : {}), + allowAgentIdOverride: false, + allowModelOverride: false, + allowComplete: true, + }, + }).complete(request); + }, + }, + }; +} + +export async function executeRegisteredPluginCommand( + registry: PluginRegistry, + params: PluginCommandExecutionParams, +): Promise { + const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params; + if (!pluginCommandSupportsChannel(command, channel)) { + logVerbose(`Plugin command /${command.name} skipped on unsupported channel ${channel}`); + return { continueAgent: true }; + } + if (command.requireAuth !== false && !isAuthorizedSender) { + logVerbose( + `Plugin command /${command.name} blocked: unauthorized sender ${senderId || ""}`, + ); + return { text: "⚠️ This command requires authorization." }; + } + if (command.requiredScopes !== undefined && !Array.isArray(command.requiredScopes)) { + logVerbose(`Plugin command /${command.name} blocked: invalid requiredScopes configuration`); + return { text: "⚠️ This command has invalid gateway scope configuration." }; + } + const requiredScopes = command.requiredScopes ?? []; + const unknownScope = (requiredScopes as readonly unknown[]).find( + (scope) => !isOperatorScope(scope), + ); + if (unknownScope) { + logVerbose(`Plugin command /${command.name} blocked: unknown gateway scope`); + return { text: "⚠️ This command has invalid gateway scope configuration." }; + } + if (requiredScopes.length > 0) { + const scopes = Array.isArray(params.gatewayClientScopes) + ? new Set(params.gatewayClientScopes) + : undefined; + const hasAdmin = scopes?.has(ADMIN_SCOPE) === true; + const missingScope = scopes + ? requiredScopes.find((scope) => !hasAdmin && !scopes.has(scope)) + : requiredScopes[0]; + if (missingScope && (scopes !== undefined || params.senderIsOwner !== true)) { + logVerbose(`Plugin command /${command.name} blocked: missing gateway scope ${missingScope}`); + return { text: `⚠️ This command requires gateway scope: ${missingScope}.` }; + } + } + + const bindingConversation = resolveBindingConversation({ + registry, + config, + channel, + senderId, + from: params.from, + to: params.to, + originatingTo: params.originatingTo, + accountId: params.accountId, + messageThreadId: params.messageThreadId, + threadParentId: params.threadParentId, + }); + const trustedReservedOwner = + isTrustedReservedCommandOwner(command) && + command.ownership === "reserved" && + isReservedCommandName(command.name) && + command.pluginId === normalizeLowercaseStringOrEmpty(command.name); + const senderIsOwner = + canExposeSenderIsOwner(command) || trustedReservedOwner ? params.senderIsOwner : undefined; + const ctx: PluginCommandContext = { + senderId, + channel, + channelId: params.channelId, + isAuthorizedSender, + ...(senderIsOwner === undefined ? {} : { senderIsOwner }), + gatewayClientScopes: params.gatewayClientScopes, + agentId: params.agentId, + sessionKey: params.sessionKey, + sessionId: params.sessionId, + sessionTarget: params.sessionTarget, + sessionFile: params.sessionFile, + args: sanitizeArgs(args), + commandBody, + config, + from: params.from, + to: params.to, + accountId: bindingConversation?.accountId ?? params.accountId, + messageThreadId: params.messageThreadId, + threadParentId: params.threadParentId, + diagnosticsSessions: params.diagnosticsSessions, + runtimeContext: buildRuntimeContext({ + command, + config, + agentId: params.agentId, + sessionKey: params.sessionKey, + authProfileId: params.authProfileId, + }), + ...(trustedReservedOwner && params.diagnosticsUploadApproved !== undefined + ? { diagnosticsUploadApproved: params.diagnosticsUploadApproved } + : {}), + ...(trustedReservedOwner && params.diagnosticsPreviewOnly !== undefined + ? { diagnosticsPreviewOnly: params.diagnosticsPreviewOnly } + : {}), + ...(trustedReservedOwner && params.diagnosticsPrivateRouted !== undefined + ? { diagnosticsPrivateRouted: params.diagnosticsPrivateRouted } + : {}), + requestConversationBinding: async (bindingParams) => { + if (!command.pluginRoot || !bindingConversation) { + return { status: "error", message: "This command cannot bind the current conversation." }; + } + return requestPluginConversationBinding({ + pluginId: command.pluginId, + pluginName: command.pluginName, + pluginRoot: command.pluginRoot, + requestedBySenderId: senderId, + conversation: bindingConversation, + binding: bindingParams, + }); + }, + detachConversationBinding: async () => + command.pluginRoot && bindingConversation + ? detachPluginConversationBinding({ + pluginRoot: command.pluginRoot, + conversation: bindingConversation, + }) + : { removed: false }, + getCurrentConversationBinding: async () => + command.pluginRoot && bindingConversation + ? getCurrentPluginConversationBinding({ + pluginRoot: command.pluginRoot, + conversation: bindingConversation, + }) + : null, + }; + + try { + const execution = await withPluginCommandExecution(registry, () => + withPluginRuntimeRegistryScope(registry, () => command.handler(ctx)), + ); + if (!execution.admitted) { + return { + text: "⚠️ This command is no longer available after the plugin registry changed. Please try again.", + }; + } + const result = execution.value; + logVerbose( + `Plugin command /${command.name} executed successfully for ${senderId || "unknown"}`, + ); + if (!result || typeof result !== "object") { + logVerbose(`Plugin command /${command.name} returned no reply payload`); + return {}; + } + return result; + } catch (error) { + logVerbose(`Plugin command /${command.name} error: ${(error as Error).message}`); + return { text: "⚠️ Command failed. Please try again later." }; + } +} diff --git a/src/plugins/plugin-command-matcher.ts b/src/plugins/plugin-command-matcher.ts new file mode 100644 index 000000000000..c21a01ac1a1e --- /dev/null +++ b/src/plugins/plugin-command-matcher.ts @@ -0,0 +1,75 @@ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "@openclaw/normalization-core/string-coerce"; +import type { RegisteredPluginCommand } from "./command-registry-state.js"; +import { pluginCommandSupportsChannel } from "./plugin-command-metadata.js"; + +type PluginCommandAliasScope = { kind: "all" } | { kind: "provider"; provider: string }; + +function listInvocationKeys( + command: RegisteredPluginCommand, + aliasScope: PluginCommandAliasScope, +): string[] { + const keys = new Set(); + const add = (value: string | undefined) => { + const normalized = normalizeOptionalLowercaseString(value); + if (normalized) { + keys.add(`/${normalized}`); + } + }; + add(command.name); + if (aliasScope.kind === "all") { + for (const alias of Object.values(command.nativeNames ?? {})) { + if (typeof alias === "string") { + add(alias); + } + } + return [...keys]; + } + const provider = normalizeOptionalLowercaseString(aliasScope.provider); + const providerAlias = provider ? command.nativeNames?.[provider] : undefined; + add(typeof providerAlias === "string" ? providerAlias : command.nativeNames?.default); + return [...keys]; +} + +export function matchRegisteredPluginCommand(params: { + commands: readonly RegisteredPluginCommand[]; + commandBody: string; + channel?: string; + aliasScope: PluginCommandAliasScope; +}): { command: RegisteredPluginCommand; args?: string } | null { + const trimmed = params.commandBody.trim(); + if (!trimmed.startsWith("/")) { + return null; + } + const commandMatch = trimmed.match(/^\/\s*([^\s]+)(?:\s+([\s\S]*))?$/); + if (!commandMatch) { + return null; + } + const key = normalizeLowercaseStringOrEmpty(`/${commandMatch[1]}`); + const alternateKeys = [key]; + if (key.includes("_")) { + alternateKeys.push(key.replace(/_/g, "-")); + } + if (key.includes("-")) { + alternateKeys.push(key.replace(/-/g, "_")); + } + const command = alternateKeys + .map((candidateKey) => + params.commands.find( + (candidate) => + pluginCommandSupportsChannel(candidate, params.channel) && + listInvocationKeys(candidate, params.aliasScope).includes(candidateKey), + ), + ) + .find((candidate): candidate is RegisteredPluginCommand => candidate !== undefined); + if (!command) { + return null; + } + const args = commandMatch[2]?.trim(); + if (args && !command.acceptsArgs) { + return null; + } + return { command, args: args || undefined }; +} diff --git a/src/plugins/plugin-command-metadata.ts b/src/plugins/plugin-command-metadata.ts new file mode 100644 index 000000000000..7452f6424656 --- /dev/null +++ b/src/plugins/plugin-command-metadata.ts @@ -0,0 +1,61 @@ +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; +import type { OpenClawPluginCommandDefinition } from "./types.js"; + +type PluginCommandNativeMetadata = Readonly<{ + name: string; + description: string; + descriptionLocalizations?: Readonly>; + acceptsArgs: boolean; + requireAuth: boolean; + progressMessage?: string; +}>; + +export function pluginCommandSupportsChannel( + command: OpenClawPluginCommandDefinition, + channel?: string, +): boolean { + if (!command.channels || command.channels.length === 0 || !channel) { + return true; + } + const normalizedChannel = normalizeOptionalLowercaseString(channel); + return command.channels.some( + (entry) => normalizeOptionalLowercaseString(entry) === normalizedChannel, + ); +} + +/** Projects the safe provider-native metadata shared by catalog and runtime surfaces. */ +export function projectPluginCommandNativeMetadata( + command: OpenClawPluginCommandDefinition, + provider?: string, +): PluginCommandNativeMetadata { + const normalizedProvider = normalizeOptionalLowercaseString(provider); + const providerName = normalizedProvider ? command.nativeNames?.[normalizedProvider] : undefined; + const defaultName = command.nativeNames?.default; + const name = + typeof providerName === "string" && providerName.trim() + ? providerName.trim() + : typeof defaultName === "string" && defaultName.trim() + ? defaultName.trim() + : command.name.trim() || command.name; + const providerProgress = normalizedProvider + ? command.nativeProgressMessages?.[normalizedProvider] + : undefined; + const defaultProgress = command.nativeProgressMessages?.default; + const progressMessage = + typeof providerProgress === "string" && providerProgress.trim() + ? providerProgress.trim() + : typeof defaultProgress === "string" && defaultProgress.trim() + ? defaultProgress.trim() + : undefined; + const descriptionLocalizations = command.descriptionLocalizations + ? Object.freeze({ ...command.descriptionLocalizations }) + : undefined; + return Object.freeze({ + name, + description: command.description.trim(), + ...(descriptionLocalizations ? { descriptionLocalizations } : {}), + acceptsArgs: command.acceptsArgs ?? false, + requireAuth: command.requireAuth !== false, + ...(progressMessage ? { progressMessage } : {}), + }); +} diff --git a/src/plugins/plugin-command-registry.ts b/src/plugins/plugin-command-registry.ts new file mode 100644 index 000000000000..997b93eea13d --- /dev/null +++ b/src/plugins/plugin-command-registry.ts @@ -0,0 +1,24 @@ +/** Exact registry projection helpers for plugin command runtimes. */ +import type { PluginRegistry } from "./registry-types.js"; +import { getPluginRegistryState } from "./runtime-state.js"; +import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-scope.js"; + +export function resolveSelectedPluginCommandRegistry(): PluginRegistry | null { + const state = getPluginRegistryState(); + return ( + state?.registrationContext?.registry ?? + getPluginRuntimeGatewayRequestScope()?.pluginRegistry ?? + state?.activeRegistry ?? + null + ); +} + +export function listRegisteredPluginCommands(registry: PluginRegistry) { + return registry.commands.map((entry) => ({ + ...entry.command, + pluginId: entry.pluginId, + pluginName: entry.pluginName, + pluginRoot: entry.rootDir, + trustedOwnerStatusExposure: entry.trustedOwnerStatusExposure, + })); +} diff --git a/src/plugins/plugin-command-runtime.test.ts b/src/plugins/plugin-command-runtime.test.ts new file mode 100644 index 000000000000..dfa054956abf --- /dev/null +++ b/src/plugins/plugin-command-runtime.test.ts @@ -0,0 +1,447 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +const cleanupReplacedPluginHostRegistry = vi.hoisted(() => vi.fn(async () => {})); + +vi.mock("./host-hook-cleanup.js", () => ({ cleanupReplacedPluginHostRegistry })); + +import { getPluginCommandExecutionCount } from "./command-execution-lock.js"; +import { registerPluginCommandInRegistry } from "./command-registration.js"; +import { withPluginCommandAccountStartScope } from "./plugin-command-account-start-scope.js"; +import { + createPluginCommandRuntime, + executePluginCommandDispatch, + matchPluginCommandInvocation, + type PluginCommandDispatch, +} from "./plugin-command-runtime.js"; +import { createEmptyPluginRegistry } from "./registry-empty.js"; +import { markPluginRegistryRetired } from "./registry-lifecycle.js"; +import { + clearActivePluginRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "./runtime.js"; +import { withPluginRuntimeRegistryScope } from "./runtime/gateway-request-scope.js"; + +const executionContext = { + senderId: "user-1", + channel: "telegram", + isAuthorizedSender: true, + commandBody: "/demo", + config: {}, +} as const; + +function registerCommand( + registry: ReturnType, + params: { + pluginId: string; + name: string; + channels?: string[]; + nativeNames?: Record; + acceptsArgs?: boolean; + handler: (args?: string) => Promise<{ text: string }>; + }, +) { + const result = registerPluginCommandInRegistry(registry, params.pluginId, { + name: params.name, + description: `${params.pluginId} command`, + channels: params.channels, + nativeNames: params.nativeNames, + acceptsArgs: params.acceptsArgs, + handler: async (ctx) => await params.handler(ctx.args), + }); + expect(result).toEqual({ ok: true }); +} + +function requirePluginDispatch( + candidate: ReturnType< + ReturnType["listNativeCandidates"] + >[number], + args?: string, +) { + const dispatch = candidate.prepareDispatch(args); + expect(dispatch.kind).toBe("plugin"); + if (dispatch.kind !== "plugin") { + throw new Error("expected plugin command dispatch"); + } + return dispatch; +} + +afterEach(() => { + cleanupReplacedPluginHostRegistry.mockClear(); + resetPluginRuntimeStateForTest(); +}); + +describe("plugin command runtime", () => { + it("binds the request-scoped registry and scopes provider aliases", async () => { + const ambient = createEmptyPluginRegistry(); + const scoped = createEmptyPluginRegistry(); + const ambientHandler = vi.fn(async () => ({ text: "ambient" })); + const scopedHandler = vi.fn(async (args?: string) => ({ text: `scoped:${args}` })); + registerCommand(ambient, { + pluginId: "ambient", + name: "demo", + handler: ambientHandler, + }); + registerCommand(scoped, { + pluginId: "scoped", + name: "demo", + channels: ["discord"], + nativeNames: { discord: "discord-demo" }, + acceptsArgs: true, + handler: scopedHandler, + }); + setActivePluginRegistry(ambient); + + await withPluginRuntimeRegistryScope(scoped, async () => { + const runtime = createPluginCommandRuntime(); + expect(runtime.listNativeCandidates("telegram")).toEqual([]); + const candidates = runtime.listNativeCandidates("discord"); + expect(candidates.map((candidate) => candidate.name)).toEqual(["discord-demo"]); + expect( + matchPluginCommandInvocation(runtime, "/discord-demo hi", { channel: "telegram" }), + ).toBeNull(); + const match = matchPluginCommandInvocation(runtime, "/discord-demo hi", { + channel: "discord", + }); + expect(match?.dispatch.kind).toBe("plugin"); + if (!match) { + throw new Error("expected scoped command match"); + } + const result = await match.dispatch.execute({ + ...executionContext, + channel: "discord", + commandBody: "/discord-demo hi", + }); + expect(result).toEqual({ text: "scoped:hi" }); + }); + expect(scopedHandler).toHaveBeenCalledOnce(); + expect(ambientHandler).not.toHaveBeenCalled(); + }); + + it("rejects forged, cross-runtime, wrong-channel, and retired selections", async () => { + const registry = createEmptyPluginRegistry(); + const handler = vi.fn(async () => ({ text: "ok" })); + registerCommand(registry, { pluginId: "demo", name: "demo", handler }); + setActivePluginRegistry(registry); + const firstRuntime = createPluginCommandRuntime(); + const secondRuntime = createPluginCommandRuntime(); + const dispatch = requirePluginDispatch(firstRuntime.listNativeCandidates("telegram")[0]!); + const secondDispatch = requirePluginDispatch( + secondRuntime.listNativeCandidates("telegram")[0]!, + ); + const forged = Object.freeze({ + kind: "plugin", + execute: dispatch.execute, + }) as PluginCommandDispatch; + + await expect(executePluginCommandDispatch(forged, executionContext)).resolves.toMatchObject({ + text: expect.stringContaining("no longer valid"), + }); + await expect(dispatch.execute.call(secondDispatch, executionContext)).resolves.toMatchObject({ + text: expect.stringContaining("no longer valid"), + }); + await expect( + dispatch.execute({ + ...executionContext, + channel: "discord", + }), + ).resolves.toMatchObject({ text: expect.stringContaining("no longer valid") }); + + markPluginRegistryRetired(registry); + await expect(dispatch.execute(executionContext)).resolves.toMatchObject({ + text: expect.stringContaining("registry changed"), + }); + expect(handler).not.toHaveBeenCalled(); + }); + + it("keeps overlapping executions locked until both handlers settle", async () => { + const registry = createEmptyPluginRegistry(); + const releases: Array<() => void> = []; + registerCommand(registry, { + pluginId: "slow", + name: "slow", + handler: async () => { + await new Promise((resolve) => { + releases.push(resolve); + }); + return { text: "done" }; + }, + }); + setActivePluginRegistry(registry); + const runtime = createPluginCommandRuntime(); + const candidate = runtime.listNativeCandidates("telegram")[0]!; + const first = requirePluginDispatch(candidate); + const second = requirePluginDispatch(candidate); + const firstRun = first.execute(executionContext); + const secondRun = second.execute(executionContext); + await vi.waitFor(() => expect(getPluginCommandExecutionCount(registry)).toBe(2)); + + expect( + registerPluginCommandInRegistry(registry, "blocked", { + name: "blocked", + description: "blocked", + handler: async () => ({ text: "blocked" }), + }), + ).toMatchObject({ ok: false }); + releases.shift()?.(); + await vi.waitFor(() => expect(getPluginCommandExecutionCount(registry)).toBe(1)); + expect( + registerPluginCommandInRegistry(registry, "still-blocked", { + name: "still-blocked", + description: "still blocked", + handler: async () => ({ text: "blocked" }), + }), + ).toMatchObject({ ok: false }); + releases.shift()?.(); + await Promise.all([firstRun, secondRun]); + expect(getPluginCommandExecutionCount(registry)).toBe(0); + expect( + registerPluginCommandInRegistry(registry, "ready", { + name: "ready", + description: "ready", + handler: async () => ({ text: "ready" }), + }), + ).toEqual({ ok: true }); + }); + + it("admits an invocation before retirement but rejects later starts", async () => { + const registry = createEmptyPluginRegistry(); + let release!: () => void; + const entered = new Promise((resolveEntered) => { + registerCommand(registry, { + pluginId: "slow", + name: "slow", + handler: async () => { + resolveEntered(); + await new Promise((resolve) => { + release = resolve; + }); + return { text: "finished" }; + }, + }); + }); + setActivePluginRegistry(registry); + const runtime = createPluginCommandRuntime(); + const candidate = runtime.listNativeCandidates("telegram")[0]!; + const admitted = requirePluginDispatch(candidate); + const late = requirePluginDispatch(candidate); + const running = admitted.execute(executionContext); + await entered; + markPluginRegistryRetired(registry); + await expect(late.execute(executionContext)).resolves.toMatchObject({ + text: expect.stringContaining("registry changed"), + }); + release(); + await expect(running).resolves.toEqual({ text: "finished" }); + expect(getPluginCommandExecutionCount(registry)).toBe(0); + }); + + it("does not prepare arguments for commands that reject them", () => { + const registry = createEmptyPluginRegistry(); + registerCommand(registry, { + pluginId: "demo", + name: "demo", + handler: async () => ({ text: "ok" }), + }); + setActivePluginRegistry(registry); + const candidate = createPluginCommandRuntime().listNativeCandidates("telegram")[0]!; + expect(candidate.prepareDispatch("unexpected")).toEqual({ kind: "non-plugin" }); + }); + + it("retains only a supported provider in its matching account startup scope", () => { + const registry = createEmptyPluginRegistry(); + registerCommand(registry, { + pluginId: "demo", + name: "demo", + channels: ["telegram"], + handler: async () => ({ text: "ok" }), + }); + setActivePluginRegistry(registry); + const runtime = createPluginCommandRuntime(); + const retainCatalog = vi.fn(); + + runtime.retainNativeCatalog("telegram"); + withPluginCommandAccountStartScope({ channelId: "telegram", retainCatalog }, () => { + runtime.retainNativeCatalog("discord"); + runtime.retainNativeCatalog("telegram"); + }); + + expect(retainCatalog).toHaveBeenCalledOnce(); + markPluginRegistryRetired(registry); + expect(() => runtime.retainNativeCatalog("telegram")).toThrow("retired registry generation"); + }); + + it("defers full registry cleanup until an admitted command settles", async () => { + const registry = createEmptyPluginRegistry(); + registry.plugins.push({ status: "loaded" } as never); + let release!: () => void; + let entered!: () => void; + const started = new Promise((resolve) => { + entered = resolve; + }); + registerCommand(registry, { + pluginId: "slow", + name: "slow", + handler: async () => { + entered(); + await new Promise((resolve) => { + release = resolve; + }); + return { text: "done" }; + }, + }); + setActivePluginRegistry(registry); + const dispatch = requirePluginDispatch( + createPluginCommandRuntime().listNativeCandidates("telegram")[0]!, + ); + const running = dispatch.execute(executionContext); + await started; + let clearSettled = false; + const clearing = clearActivePluginRegistry().then(() => { + clearSettled = true; + }); + await Promise.resolve(); + expect(clearSettled).toBe(false); + expect(cleanupReplacedPluginHostRegistry).not.toHaveBeenCalled(); + release(); + await expect(running).resolves.toEqual({ text: "done" }); + await clearing; + expect(cleanupReplacedPluginHostRegistry).toHaveBeenCalledOnce(); + }); + + it("lets repeated command-triggered clears return without deadlocking their drain", async () => { + const registry = createEmptyPluginRegistry(); + registry.plugins.push({ status: "loaded" } as never); + registerCommand(registry, { + pluginId: "clear", + name: "clear", + handler: async () => { + await clearActivePluginRegistry(); + await clearActivePluginRegistry(); + return { text: "cleared" }; + }, + }); + setActivePluginRegistry(registry); + const dispatch = requirePluginDispatch( + createPluginCommandRuntime().listNativeCandidates("telegram")[0]!, + ); + await expect(dispatch.execute(executionContext)).resolves.toEqual({ text: "cleared" }); + await clearActivePluginRegistry(); + expect(cleanupReplacedPluginHostRegistry).toHaveBeenCalledOnce(); + }); + + it("awaits cleanup from detached handler context after execution settles", async () => { + const registry = createEmptyPluginRegistry(); + registry.plugins.push({ status: "loaded" } as never); + let releaseDetached!: () => void; + const detachedGate = new Promise((resolve) => { + releaseDetached = resolve; + }); + let releaseCleanup!: () => void; + cleanupReplacedPluginHostRegistry.mockImplementationOnce( + async () => + await new Promise((resolve) => { + releaseCleanup = resolve; + }), + ); + let detachedClear!: Promise; + registerCommand(registry, { + pluginId: "detached", + name: "detached", + handler: () => { + detachedClear = (async () => { + await detachedGate; + await clearActivePluginRegistry(); + })(); + return Promise.resolve({ text: "scheduled" }); + }, + }); + setActivePluginRegistry(registry); + const dispatch = requirePluginDispatch( + createPluginCommandRuntime().listNativeCandidates("telegram")[0]!, + ); + + await expect(dispatch.execute(executionContext)).resolves.toEqual({ text: "scheduled" }); + expect(getPluginCommandExecutionCount(registry)).toBe(0); + releaseDetached(); + await vi.waitFor(() => expect(cleanupReplacedPluginHostRegistry).toHaveBeenCalledOnce()); + let clearSettled = false; + void detachedClear.then(() => { + clearSettled = true; + }); + await Promise.resolve(); + expect(clearSettled).toBe(false); + releaseCleanup(); + await detachedClear; + expect(clearSettled).toBe(true); + }); + + it("does not reuse an outer admission for detached nested handler cleanup", async () => { + const registry = createEmptyPluginRegistry(); + registry.plugins.push({ status: "loaded" } as never); + let releaseDetached!: () => void; + const detachedGate = new Promise((resolve) => { + releaseDetached = resolve; + }); + let detachedClear!: Promise; + registerCommand(registry, { + pluginId: "inner", + name: "inner", + handler: () => { + detachedClear = (async () => { + await detachedGate; + await clearActivePluginRegistry(); + })(); + return Promise.resolve({ text: "inner" }); + }, + }); + let releaseOuter!: () => void; + const outerGate = new Promise((resolve) => { + releaseOuter = resolve; + }); + let outerHolding!: () => void; + const outerHoldingGate = new Promise((resolve) => { + outerHolding = resolve; + }); + const innerDispatchRef: { current?: PluginCommandDispatch } = {}; + registerCommand(registry, { + pluginId: "outer", + name: "outer", + handler: async () => { + await innerDispatchRef.current!.execute({ ...executionContext, commandBody: "/inner" }); + outerHolding(); + await outerGate; + return { text: "outer" }; + }, + }); + setActivePluginRegistry(registry); + const candidates = createPluginCommandRuntime().listNativeCandidates("telegram"); + innerDispatchRef.current = requirePluginDispatch( + candidates.find((candidate) => candidate.name === "inner")!, + ); + const outerDispatch = requirePluginDispatch( + candidates.find((candidate) => candidate.name === "outer")!, + ); + + const running = outerDispatch.execute({ ...executionContext, commandBody: "/outer" }); + await outerHoldingGate; + expect(getPluginCommandExecutionCount(registry)).toBe(1); + let clearSettled = false; + void detachedClear.then(() => { + clearSettled = true; + }); + releaseDetached(); + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + expect(clearSettled).toBe(false); + releaseOuter(); + await expect(running).resolves.toEqual({ text: "outer" }); + await detachedClear; + expect(clearSettled).toBe(true); + }); + + it("fails factory creation when no registry generation exists", () => { + resetPluginRuntimeStateForTest(); + expect(() => createPluginCommandRuntime()).toThrow("requires an active or request-scoped"); + }); +}); diff --git a/src/plugins/plugin-command-runtime.ts b/src/plugins/plugin-command-runtime.ts new file mode 100644 index 000000000000..d5dd48465450 --- /dev/null +++ b/src/plugins/plugin-command-runtime.ts @@ -0,0 +1,264 @@ +/** Registry-bound plugin command selection and execution for native/channel surfaces. */ +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { RegisteredPluginCommand } from "./command-registry-state.js"; +import { retainPluginCommandCatalogForCurrentAccount } from "./plugin-command-account-start-scope.js"; +import { + PLUGIN_COMMAND_DISPATCH, + type PluginCommandReplyOptions, +} from "./plugin-command-dispatch-contract.js"; +import { matchRegisteredPluginCommand } from "./plugin-command-matcher.js"; +import { + pluginCommandSupportsChannel, + projectPluginCommandNativeMetadata, +} from "./plugin-command-metadata.js"; +import { + listRegisteredPluginCommands, + resolveSelectedPluginCommandRegistry, +} from "./plugin-command-registry.js"; +import { isPluginRegistryRetired } from "./registry-lifecycle.js"; +import type { PluginRegistry } from "./registry-types.js"; +import type { PluginCommandContext, PluginCommandResult } from "./types.js"; + +export { PLUGIN_COMMAND_DISPATCH }; +export type { PluginCommandReplyOptions }; + +declare const pluginCommandDispatchBrand: unique symbol; + +export type PluginCommandDispatchContext = Readonly<{ + senderId?: string; + channel: string; + channelId?: PluginCommandContext["channelId"]; + isAuthorizedSender: boolean; + senderIsOwner?: boolean; + gatewayClientScopes?: PluginCommandContext["gatewayClientScopes"]; + agentId?: string; + sessionKey?: PluginCommandContext["sessionKey"]; + sessionId?: PluginCommandContext["sessionId"]; + sessionTarget?: PluginCommandContext["sessionTarget"]; + sessionFile?: PluginCommandContext["sessionFile"]; + authProfileId?: string; + commandBody: string; + config: OpenClawConfig; + from?: PluginCommandContext["from"]; + to?: PluginCommandContext["to"]; + originatingTo?: string; + accountId?: PluginCommandContext["accountId"]; + messageThreadId?: PluginCommandContext["messageThreadId"]; + threadParentId?: PluginCommandContext["threadParentId"]; + diagnosticsSessions?: PluginCommandContext["diagnosticsSessions"]; + diagnosticsUploadApproved?: PluginCommandContext["diagnosticsUploadApproved"]; + diagnosticsPreviewOnly?: PluginCommandContext["diagnosticsPreviewOnly"]; + diagnosticsPrivateRouted?: PluginCommandContext["diagnosticsPrivateRouted"]; +}>; + +/** Opaque capability bound to one selected command in one registry generation. */ +export type PluginCommandDispatch = Readonly<{ + kind: "plugin"; + execute: (context: PluginCommandDispatchContext) => Promise; + [pluginCommandDispatchBrand]: true; +}>; + +export type PluginCommandCatalogDecision = PluginCommandDispatch | Readonly<{ kind: "non-plugin" }>; + +/** Internal reply-pipeline view after the opaque catalog decision has been validated. */ +export type PluginCommandExecutionReplyOptions = Readonly<{ + [PLUGIN_COMMAND_DISPATCH]?: PluginCommandCatalogDecision; +}>; + +export type PluginCommandNativeCandidate = Readonly<{ + name: string; + description: string; + descriptionLocalizations?: Readonly>; + acceptsArgs: boolean; + requireAuth: boolean; + progressMessage?: string; + prepareDispatch: (rawArgs?: string) => PluginCommandCatalogDecision; +}>; + +type PluginCommandInvocationMatch = Readonly<{ + dispatch: PluginCommandDispatch; + acceptsArgs: boolean; + requireAuth: boolean; + progressMessage?: string; +}>; + +export type PluginCommandRuntime = Readonly<{ + listNativeCandidates: (provider: string) => readonly PluginCommandNativeCandidate[]; + retainNativeCatalog: (provider: string) => void; +}>; + +type PluginCommandRuntimeState = Readonly<{ + registry: PluginRegistry; + commands: readonly RegisteredPluginCommand[]; +}>; + +type SelectedCommand = { + runtime: PluginCommandRuntime; + registry: PluginRegistry; + channel: string; + command: RegisteredPluginCommand; + args?: string; +}; + +const dispatchSelections = new WeakMap(); +const runtimeStates = new WeakMap(); + +const INVALID_SELECTION_REPLY = { + text: "⚠️ This command selection is no longer valid. Please try again.", +} as const; +const RETIRED_SELECTION_REPLY = { + text: "⚠️ This command is no longer available after the plugin registry changed. Please try again.", +} as const; + +function createSelectedPluginCommandDispatch( + runtime: PluginCommandRuntime, + state: PluginCommandRuntimeState, + command: RegisteredPluginCommand, + channel: string, + args?: string, +): PluginCommandDispatch { + const dispatch = Object.freeze({ + kind: "plugin" as const, + async execute(this: PluginCommandDispatch, context: PluginCommandDispatchContext) { + if (this !== dispatch) { + return { ...INVALID_SELECTION_REPLY }; + } + return await executeSelectedPluginCommand(runtime, dispatch, context); + }, + }) as PluginCommandDispatch; + dispatchSelections.set(dispatch as object, { + runtime, + registry: state.registry, + command, + channel: normalizeOptionalLowercaseString(channel) ?? "", + ...(args?.trim() ? { args: args.trim() } : {}), + }); + return dispatch; +} + +async function executeSelectedPluginCommand( + runtime: PluginCommandRuntime | undefined, + dispatch: PluginCommandDispatch, + context: PluginCommandDispatchContext, +): Promise { + const selected = dispatchSelections.get(dispatch as object); + if (!selected || (runtime && selected.runtime !== runtime)) { + return { ...INVALID_SELECTION_REPLY }; + } + if (isPluginRegistryRetired(selected.registry)) { + return { ...RETIRED_SELECTION_REPLY }; + } + const channel = normalizeOptionalLowercaseString(context.channel) ?? ""; + if (selected.channel !== channel) { + return { ...INVALID_SELECTION_REPLY }; + } + const { executeRegisteredPluginCommand } = await import("./plugin-command-execution.js"); + if (isPluginRegistryRetired(selected.registry)) { + return { ...RETIRED_SELECTION_REPLY }; + } + return await executeRegisteredPluginCommand(selected.registry, { + ...context, + args: selected.args, + command: selected.command, + }); +} + +/** Validates and executes a dispatch carried through the core reply pipeline. */ +export async function executePluginCommandDispatch( + dispatch: PluginCommandDispatch, + context: PluginCommandDispatchContext, +): Promise { + return await executeSelectedPluginCommand(undefined, dispatch, context); +} + +/** Creates one command runtime bound permanently to the current scoped registry generation. */ +export function createPluginCommandRuntime(): PluginCommandRuntime { + const registry = resolveSelectedPluginCommandRegistry(); + if (!registry) { + throw new Error("Plugin command runtime requires an active or request-scoped registry."); + } + const state: PluginCommandRuntimeState = Object.freeze({ + registry, + commands: Object.freeze(listRegisteredPluginCommands(registry)), + }); + + const assertCurrent = () => { + if (isPluginRegistryRetired(state.registry)) { + throw new Error("Plugin command runtime is bound to a retired registry generation."); + } + }; + + const runtime: PluginCommandRuntime = Object.freeze({ + listNativeCandidates(provider: string): readonly PluginCommandNativeCandidate[] { + assertCurrent(); + const channel = normalizeOptionalLowercaseString(provider) ?? ""; + return Object.freeze( + state.commands + .filter((command) => pluginCommandSupportsChannel(command, channel)) + .map((command) => { + const metadata = projectPluginCommandNativeMetadata(command, channel); + return Object.freeze({ + ...metadata, + prepareDispatch(rawArgs?: string): PluginCommandCatalogDecision { + const args = rawArgs?.trim(); + if (args && !command.acceptsArgs) { + return Object.freeze({ kind: "non-plugin" as const }); + } + return createSelectedPluginCommandDispatch(runtime, state, command, channel, args); + }, + }); + }), + ); + }, + retainNativeCatalog(provider: string): void { + assertCurrent(); + const channel = normalizeOptionalLowercaseString(provider) ?? ""; + if (!state.commands.some((command) => pluginCommandSupportsChannel(command, channel))) { + return; + } + retainPluginCommandCatalogForCurrentAccount(channel); + }, + }); + runtimeStates.set(runtime, state); + return runtime; +} + +/** Core-only text matcher that returns a dispatch from the same bound runtime. */ +export function matchPluginCommandInvocation( + runtime: PluginCommandRuntime, + commandBody: string, + options: { channel: string; provider?: string }, +): PluginCommandInvocationMatch | null { + const state = runtimeStates.get(runtime); + if (!state) { + return null; + } + if (isPluginRegistryRetired(state.registry)) { + throw new Error("Plugin command runtime is bound to a retired registry generation."); + } + const channel = normalizeOptionalLowercaseString(options.channel) ?? ""; + const provider = normalizeOptionalLowercaseString(options.provider) ?? channel; + const match = matchRegisteredPluginCommand({ + commands: state.commands, + commandBody, + channel, + aliasScope: { kind: "provider", provider }, + }); + if (!match) { + return null; + } + const metadata = projectPluginCommandNativeMetadata(match.command, provider); + return Object.freeze({ + dispatch: createSelectedPluginCommandDispatch( + runtime, + state, + match.command, + channel, + match.args, + ), + acceptsArgs: metadata.acceptsArgs, + requireAuth: metadata.requireAuth, + ...(metadata.progressMessage ? { progressMessage: metadata.progressMessage } : {}), + }); +} diff --git a/src/plugins/registry-empty.ts b/src/plugins/registry-empty.ts index 215ac998d990..03ac792939c0 100644 --- a/src/plugins/registry-empty.ts +++ b/src/plugins/registry-empty.ts @@ -42,7 +42,6 @@ export function createEmptyPluginRegistry(): PluginRegistry { memoryPromptSupplements: [], sessionDiscussionProviders: new Map(), contextEngines: new Map(), - commandRegistryLocked: false, gatewayHandlers: {}, gatewayMethodDescriptors: [], dashboardDataBindings: new Map(), diff --git a/src/plugins/registry-types.ts b/src/plugins/registry-types.ts index 160bf97fbbf5..8d4b0f5a58fa 100644 --- a/src/plugins/registry-types.ts +++ b/src/plugins/registry-types.ts @@ -550,7 +550,6 @@ export type PluginRegistry = { memoryPromptSupplements: MemoryPromptSupplementRegistration[]; sessionDiscussionProviders: Map; contextEngines: Map; - commandRegistryLocked: boolean; gatewayHandlers: GatewayRequestHandlers; gatewayMethodDescriptors: GatewayMethodDescriptor[]; dashboardDataBindings: Map; diff --git a/src/plugins/runtime-state.ts b/src/plugins/runtime-state.ts index f244e0e9e19e..be9e89303285 100644 --- a/src/plugins/runtime-state.ts +++ b/src/plugins/runtime-state.ts @@ -15,6 +15,8 @@ export type RegistryState = { runtimeSubagentMode: "default" | "explicit" | "gateway-bindable"; importedPluginIds: Set; registrationContext?: { registry: PluginRegistry; pluginId: string }; + commandRegistryClearTail?: Promise; + commandRegistryClearRegistries?: Map; }; type GlobalRegistryState = typeof globalThis & { diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index 2ec1bfe4be85..f1fb246a34cc 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -2,6 +2,11 @@ import { onAgentEvent } from "../infra/agent-events.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { drainGlobalSingletonLifecycleState } from "../shared/global-singleton.js"; +import { + getPluginCommandExecutionCount, + isPluginCommandExecutionActiveHere, + waitForPluginCommandExecutions, +} from "./command-execution-lock.js"; import { clearPluginHostRuntimeState, dispatchPluginAgentEventSubscriptions, @@ -84,11 +89,15 @@ function cleanupRetiredPluginHostRegistry(previousRegistry: PluginRegistry): voi if (!registryHasPluginHostCleanupWork(previousRegistry)) { return; } - void cleanupPreviousPluginHostRegistry({ - previousRegistry, - }).catch((error: unknown) => { - log.warn(`plugin host registry cleanup failed: ${String(error)}`); - }); + const cleanup = () => + cleanupPreviousPluginHostRegistry({ previousRegistry }).catch((error: unknown) => { + log.warn(`plugin host registry cleanup failed: ${String(error)}`); + }); + if (getPluginCommandExecutionCount(previousRegistry) > 0) { + void waitForPluginCommandExecutions(previousRegistry).then(cleanup); + return; + } + void cleanup(); } function retirePluginRegistryIfUnused(registry: PluginRegistry | null): boolean { @@ -374,17 +383,51 @@ function clearActivePluginRegistryState(): PluginRegistry | null { export async function clearActivePluginRegistry(): Promise { const previousRegistry = clearActivePluginRegistryState(); - try { - if (registryHasPluginHostCleanupWork(previousRegistry)) { - await cleanupPreviousPluginHostRegistry({ previousRegistry: previousRegistry! }); - } - } finally { - try { - await drainGlobalSingletonLifecycleState("plugin-registry"); - } finally { - clearPluginHostRuntimeState(); - } + const clearVersion = state.activeVersion; + const clearRegistries = (state.commandRegistryClearRegistries ??= new Map()); + if (previousRegistry) { + clearRegistries.set(previousRegistry, (clearRegistries.get(previousRegistry) ?? 0) + 1); } + const previousTail = state.commandRegistryClearTail ?? Promise.resolve(); + const completion = previousTail + .catch(() => undefined) + .then(async () => { + try { + if (previousRegistry) { + await waitForPluginCommandExecutions(previousRegistry); + if (registryHasPluginHostCleanupWork(previousRegistry)) { + await cleanupPreviousPluginHostRegistry({ previousRegistry }); + } + } + } finally { + // A handler-triggered clear may publish a successor before its own drain settles. + // Never let the retired generation's tail erase that successor's host state. + if (state.activeRegistry === null && state.activeVersion === clearVersion) { + try { + await drainGlobalSingletonLifecycleState("plugin-registry"); + } finally { + clearPluginHostRuntimeState(); + } + } + } + }) + .finally(() => { + if (previousRegistry) { + const remaining = (clearRegistries.get(previousRegistry) ?? 1) - 1; + if (remaining === 0) { + clearRegistries.delete(previousRegistry); + } else { + clearRegistries.set(previousRegistry, remaining); + } + } + }); + state.commandRegistryClearTail = completion.catch((error: unknown) => { + log.warn(`plugin registry clear failed: ${String(error)}`); + }); + if ([...clearRegistries.keys()].some(isPluginCommandExecutionActiveHere)) { + return; + } + await completion; } export function resetPluginRuntimeStateForTest(): void {