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
This commit is contained in:
Peter Steinberger
2026-08-10 19:30:47 -07:00
committed by GitHub
parent bdd35432dd
commit d6f70a96cb
83 changed files with 3214 additions and 1821 deletions
+105 -96
View File
@@ -53,15 +53,15 @@
{"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise<AbortAndDrainEmbeddedAgentRunResult>;","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<OpenClawAgentToolResult>; };","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<AgentToolResult<unknown>>; };","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<void>;","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<TResult = unknown> = OpenClawAgentToolResult<TResult>;","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<typeof buildAfterTurnRuntimeContext>[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<typeof buildAfterTurnRuntimeContextFromUsage>[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<typeof buildAfterTurnRuntimeContext>[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<typeof buildAfterTurnRuntimeContextFromUsage>[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<string>; capabilityToolNames?: Iterable<string>; }): 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<OpenClawAgentToolResult>; };","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<AgentToolResult<unknown>>; };","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<void>;","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<void>;","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<void>;","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<AgentEventPayload, \"seq\" | \"ts\">): 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<AgentHarnessBeforeAgentFinalizeOutcome>;","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<void>;","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<QuestionWaitAnswerResult>;","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<QuestionWaitAnswerResult>;","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<HookOutcome>;","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<TAccountEntry extends object>(params: { accounts?: Record<string, TAccountEntry>; accountId: string; fields: string[]; isValueSet?: (value: unknown) => boolean; markClearedOnFieldPresence?: boolean; }): { nextAccounts?: Record<string, TAccountEntry>; 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<Record<StreamingHintKey, StreamingHintValue>>; retry?: boolean; }): HintMap;","entrypoint":"channel-core","exportName":"createChannelConfigUiHints","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"b8e27bea2add3eaafac7074cbd44fcf2acea30a1ac125c3551925e8cd0b3378d","declaration":"export function createChatChannelPlugin<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<TResolvedAccount>; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin<TResolvedAccount, Probe, Audit>;","entrypoint":"channel-core","exportName":"createChatChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"0f2201e81ac7e4578b83d7973dd5e5e6d5c82e8d115f3786a8e10b5eab604b76","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"d0215218f4367a9b95487036431f4fc5453ebb39960ec6deccde5acb1cab032c","declaration":"export function defineChannelPluginEntry<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","entrypoint":"channel-core","exportName":"defineChannelPluginEntry","importSpecifier":"openclaw/plugin-sdk/channel-core","kind":"function","recordType":"export"}
{"closureHash":"0e0a9f6b22c433bdb1165e7707b1f9a1ddac1a544a8add00b503048cf157921f","declaration":"export function defineSetupPluginEntry<TPlugin>(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<ResolvedAccount = any, Probe = unknown, Audit = unknown> = ChannelPlugin<ResolvedAccount, Probe, Audit>;","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<TConfig extends DmPolicyAccountConfig>(params: CreateChannelDmPolicyParams<TConfig>): ChannelSetupDmPolicy & { promptAllowFrom: NonNullable<ChannelSetupDmPolicy[\"promptAllowFrom\"]>; };","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<TPlugin = ChannelPlugin>({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions<TPlugin>): BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"e33981a9f791ee1e487060346c997b305dd6bc26eb5c1524de6baa0b198595b4","declaration":"export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"d53f1d525e8942681d9d38c92211fabd88c571b284998e667e1920cfbc4991f5","declaration":"export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({ id, name, description, importMetaUrl, plugin, outbound, secrets, configSchema, runtime, accountInspect, features, registerCliMetadata, registerFull, registerCapabilities, }: DefineBundledChannelEntryOptions<TPlugin>): BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"2224da993e2c72c18cf17c5f2fa43ca5e95a7ab823c453dd51d07bcd07ff92c4","declaration":"export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({ importMetaUrl, plugin, secrets, runtime, legacyStateMigrations, legacySessionSurface, registerSetupRuntime, features, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"defineBundledChannelSetupEntry","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"function","recordType":"export"}
{"closureHash":"cd150287d7e286e7a0d7d6caeb05d0a1efa63b5f351f75f69ba932510a108afd","declaration":"export function loadBundledEntryExportSync<T>(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<TPlugin = ChannelPlugin> = BundledChannelEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"e52edea0d5c41728b7e7703483f93656ae6d55bd09cff9ca481ffad8d9058666","declaration":"export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = BundledChannelEntryContract<TPlugin>;","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<TPlugin = ChannelPlugin> = BundledChannelSetupEntryContract<TPlugin>;","entrypoint":"channel-entry-contract","exportName":"BundledChannelSetupEntryContract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract","kind":"type","recordType":"export"}
{"closureHash":"4ae358d564c9980c4bee18ce79fb5807dfd0de0b75ec18586b43b469227c7377","declaration":"export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = BundledChannelSetupEntryContract<TPlugin>;","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<T>(params: InboundDebounceCreateParams<T>): { enqueue: (item: T) => Promise<void>; flushKey: (key: string) => Promise<void>; cancelKey: (key: string) => boolean; drain: () => Promise<void>; };","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<Pick<ResolvedInboundDirectDmAccess, \"access\"> | ResolvedInboundDirectDmAccess>; issuePairingChallenge?: (params: { senderId: string; reply: (text: string) => Promise<void>; }) => Promise<void>; onBlocked?: (params: { senderId: string; reason: string; reasonCode: DmGroupAccessReasonCode; }) => void; }): (input: { senderId: string; reply: (text: string) => Promise<void>; }) => 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<ChannelTurnResult>;","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<ChannelTurnResult>;\nexport function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan): Promise<ChannelTurnResult>;","entrypoint":"channel-inbound","exportName":"dispatchChannelInboundTurn","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"e088ddef111b171ef5d034d9de6062cc551d6775412ddc8cfa7b72ca8f31c62d","declaration":"export function dispatchChannelInboundReply(params: AssembledInboundReply): Promise<ChannelTurnResult>;","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<ChannelTurnResult>;\nexport function dispatchChannelInboundTurn(params: ChannelInboundTurnPlan): Promise<ChannelTurnResult>;","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<T extends Record<string, unknown>>(params: FinalizeChannelInboundContextAsyncParams<T>): Promise<FinalizeChannelInboundContextResult<T>>;\nexport function finalizeChannelInboundContext<T extends Record<string, unknown>>(params: FinalizeChannelInboundContextParams<T>): FinalizeChannelInboundContextResult<T>;","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<ChannelFeedbackReflectionResult>;","entrypoint":"channel-inbound","exportName":"runChannelFeedbackReflection","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"channel-inbound","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"4cd63266d379acc2cdf9aba02f50b8ceb55500cf820787c370fc98d173fb1950","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"channel-inbound","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"function","recordType":"export"}
{"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply<TDispatchResult>(params: PreparedChannelTurn<TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","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<TRaw, TDispatchResult = DispatchFromConfigResult> = ChannelInboundEventRunnerParams<TRaw, TDispatchResult>;","entrypoint":"channel-inbound","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"}
{"closureHash":"809fd66291f793b3d61ddd275ed153cb2050b1d7a31767ef577489a20ee582d0","declaration":"export type ChannelInboundEventRunnerParams<TRaw, TDispatchResult = DispatchFromConfigResult> = ChannelInboundEventRunnerParams<TRaw, TDispatchResult>;","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<TOwnership extends \"core\" | \"provider_message_sending\" = \"core\"> = ChannelInboundTurnPlan<TOwnership>;","entrypoint":"channel-inbound","exportName":"ChannelInboundTurnPlan","importSpecifier":"openclaw/plugin-sdk/channel-inbound","kind":"type","recordType":"export"}
{"closureHash":"70e8655f976ef1cea98653416b9c454aa64b4151e9824d238dbdc8799b477839","declaration":"export type ChannelInboundTurnPlan<TOwnership extends \"core\" | \"provider_message_sending\" = \"core\"> = ChannelInboundTurnPlan<TOwnership>;","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<typeof issuePairingChallenge>[0][\"upsertPairingRequest\"]; }): (challenge: Omit<Parameters<typeof issuePairingChallenge>[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<ChannelPairingAdapter[\"notifyApproval\"]>;","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<ChannelPairingAdapter[\"normalizeAllowEntry\"]>;","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> | 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<string[]>;","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<ResolvedAccount = any, Probe = unknown, Audit = unknown> = ChannelPlugin<ResolvedAccount, Probe, Audit>;","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<string | number> | 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<TResolvedAccount extends { accountId?: string | null; }, Probe = unknown, Audit = unknown>(params: { base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>; security?: ChannelSecurityAdapter<TResolvedAccount> | ChatChannelSecurityOptions<TResolvedAccount>; pairing?: ChannelPairingAdapter | ChatChannelPairingOptions; threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>; outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions; }): ChannelPlugin<TResolvedAccount, Probe, Audit>;","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<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","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<TPlugin>({ id, name, description, plugin, configSchema, setRuntime, registerCliMetadata, registerFull, registerCapabilities, }: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin>;","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<TPlugin>(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<ContextEngine[\"compact\"]>[0]): Promise<CompactResult>;","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<string>;\nexport function tryReadSecretFileSync(filePath: string | undefined, label: string, options: FsSafeSecretFileReadOptions | undefined, diagnostic: { configPath: string; }): CredentialResult<string>;","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<ChannelTurnResult>;","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<RecordInboundSessionAndDispatchReplyParams, \"deliver\" | \"durable\" | \"onRecordError\" | \"onDispatchError\" | \"replyOptions\">): Promise<void>;","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<ChannelTurnResult>;","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<RecordInboundSessionAndDispatchReplyParams, \"deliver\" | \"durable\" | \"onRecordError\" | \"onDispatchError\" | \"replyOptions\">): Promise<void>;","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<ChannelTurnVisibleDeliverySignals, \"fallbackDelivered\" | \"deliverySummaryDelivered\">): 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<void>;","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<void>;","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<ReplyDispatchKind, number>;","entrypoint":"inbound-reply-dispatch","exportName":"resolveInboundReplyDispatchCounts","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"9d503d05e408ddd1a1c373e3c4485b1f9acff4c240fdc241840a2cfa8b55c3a7","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"4cd63266d379acc2cdf9aba02f50b8ceb55500cf820787c370fc98d173fb1950","declaration":"export function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: RunChannelTurnParams<TRaw, TDispatchResult, ChannelProviderOwnedMessageSendingDeliveryAdapter>): Promise<ChannelTurnResult<TDispatchResult>>;\nexport function runChannelInboundEvent<TRaw, TDispatchResult = DispatchFromConfigResult>(params: ChannelInboundEventRunnerParams<TRaw, TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","entrypoint":"inbound-reply-dispatch","exportName":"runChannelInboundEvent","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"function","recordType":"export"}
{"closureHash":"2c68496a174872b31aaf1ac8fad2789deb79d490c5ffbcb157325f1c95d1b51f","declaration":"export function runPreparedInboundReply<TDispatchResult>(params: PreparedChannelTurn<TDispatchResult>): Promise<ChannelTurnResult<TDispatchResult>>;","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<TRaw, TDispatchResult = DispatchFromConfigResult> = ChannelInboundEventRunnerParams<TRaw, TDispatchResult>;","entrypoint":"inbound-reply-dispatch","exportName":"ChannelInboundEventRunnerParams","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch","kind":"type","recordType":"export"}
{"closureHash":"809fd66291f793b3d61ddd275ed153cb2050b1d7a31767ef577489a20ee582d0","declaration":"export type ChannelInboundEventRunnerParams<TRaw, TDispatchResult = DispatchFromConfigResult> = ChannelInboundEventRunnerParams<TRaw, TDispatchResult>;","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<unknown>;","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<unknown>;","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<string>; };","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<void>; };","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<void>; };","entrypoint":"meeting-runtime","exportName":"createMeetingRealtimeEngineBindings","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"9124db05923060c467edb149bd8abdc8be1d00d00f867cf84600972923a58489","declaration":"export function createMeetingSession<TTransport extends string, TMode extends string, TToolPolicy extends string>(params: { platform: MeetingPlatformRuntimeMetadata; config: { realtime: { provider?: string; voiceProvider?: string; transcriptionProvider?: string; model?: string; toolPolicy: TToolPolicy; }; }; resolved: MeetingResolvedJoin<TTransport, TMode>; createdAt: string; }): MeetingSessionRecord<TTransport, TMode, { enabled: boolean; strategy: string; provider: string | undefined; model: string | undefined; transcriptionProvider: string | undefined; toolPolicy: TToolPolicy; }>;","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<MeetingVoiceCallGatewayClient>; }): 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<MeetingVoiceCallGatewayClient>; }): 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<void>;","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<MeetingVoiceCallStatusResult>;","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<MeetingVoiceCallJoinResult>;","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<MeetingVoiceCallJoinResult>;","entrypoint":"meeting-runtime","exportName":"joinMeetingViaVoiceCallGateway","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"bac9621cd025754fbcd4c21409b06eb0503405db81f35cac993aaf0b881ab3e1","declaration":"export function leaveMeetingWithBrowser<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; 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<Session extends MeetingBrowserJoinSession<Mode>, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; 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<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; callBrowser: MeetingBrowserRequestCaller; finalize: boolean; meetingUrl: string; meetingSessionId: string; tab: MeetingBrowserTab; timeoutMs: number; }): Promise<Transcript>;","entrypoint":"meeting-runtime","exportName":"readMeetingTranscriptWithBrowser","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"0b42493cc0197096c4847c2d6e149c5581580abdda448d07eb1c6d77a775bc4a","declaration":"export function recoverMeetingBrowserTab<Session, Mode extends string, Health extends MeetingBrowserHealth & { browserTitle?: string; browserUrl?: string; notes?: string[]; }, Transcript extends MeetingTranscriptSnapshot>(params: { adapter: BrowserAdapter<Session, Mode, Health, Transcript>; 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<MeetingBrowserRequestCaller>;","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<string>;","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<MeetingBrowserNodeInfo>;","entrypoint":"meeting-runtime","exportName":"resolveMeetingBrowserNodeInfo","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"function","recordType":"export"}
{"closureHash":"da54392a9712ff0a2ee42a3fcd532e68ff7edac473f17a00d6965f9cde07b160","declaration":"export function resolveLocalMeetingBrowserRequest(runtime: PluginRuntime): Promise<MeetingBrowserRequestCaller>;","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<string>;","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<MeetingBrowserNodeInfo>;","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<void>;","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<MeetingRealtimeAudioEngineHandle>;","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<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","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<MeetingRealtimeAudioEngineHandle>;","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<void>; }): Promise<MeetingRealtimeAudioEngineHandle>;","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<THealth extends MeetingBrowserHealth> = MeetingSessionRuntimeHandles<THealth>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeHandles","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"aa8054fc2c442891197e89bdad61e79999ea15d813c4b127fb6150607b9e67e7","declaration":"export type MeetingSessionRuntimeJoinContext<TSession extends MeetingSessionRecord<TTransport, TMode>, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth, TTab extends MeetingBrowserTab> = MeetingSessionRuntimeJoinContext<TSession, TTransport, TMode, THealth, TTab>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeJoinContext","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"325241724edd38da439c5df03d225e2a7cb8e56d817e98ed7c37850656d3744b","declaration":"export type MeetingSessionRuntimeMessages<TSpeechBlockedReason extends string> = MeetingSessionRuntimeMessages<TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeMessages","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"9d1dd094ee987d5333be26e2e99f53337907101c4da2f9b2ce36d5d16ce9ed77","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","entrypoint":"meeting-runtime","exportName":"MeetingSessionRuntimeOptions","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"type","recordType":"export"}
{"closureHash":"0bc52ea7f7d9ac4512e80911a3504280d3a41bae58631fe09a591dd91f118985","declaration":"export type MeetingSessionRuntimeOptions<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> = MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>;","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<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\n}","entrypoint":"meeting-runtime","exportName":"MeetingPlatformAdapter","importSpecifier":"openclaw/plugin-sdk/meeting-runtime","kind":"interface","recordType":"export"}
{"closureHash":"15b888a20bea3b0304902ccc345c44bfb3b1b8c5b6c19c8a37596c6cb1f63c2d","declaration":"export interface MeetingPlatformAdapter<Session, Mode extends string, Health extends MeetingBrowserHealth, Transcript extends MeetingTranscriptSnapshot, CreateParams = never, CreateResult = never, DialInParams = never, DialInPlan = never> extends MeetingPlatformAdapterContract<Session, Mode, Health, Transcript, CreateParams, CreateResult, DialInParams, DialInPlan> {\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<void>;\n writeOutput(audio: Buffer): Promise<void>;\n clearOutput(): Promise<void>;\n dispose(): Promise<void>;\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<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\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<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): 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<MeetingSessionLeaveResult<TSession>>;\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<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\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<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\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<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): 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<TSession extends MeetingSessionRecord<TTransport, TMode>, TRequest, TTransport extends string, TMode extends string, THealth extends MeetingBrowserHealth<TManualReason, TSpeechBlockedReason>, TTab extends MeetingBrowserTab, TManualReason extends string, TSpeechBlockedReason extends string> {\n readonly #sessions: Map<string, TSession>;\n readonly #sessionLeaves: Map<string, Promise<MeetingSessionLeaveResult<TSession>>>;\n readonly #sessionCleanup: MeetingSessionCleanupTracker;\n readonly #meetingLock: MeetingSessionJoinLock;\n readonly #sessionStops: Map<string, () => Promise<void>>;\n readonly #sessionSpeakers: Map<string, (instructions?: string) => void>;\n readonly #sessionHealth: Map<string, () => Partial<THealth>>;\n readonly #durableTranscripts: MeetingSessionDurableTranscripts<TSession>;\n readonly #transcriptStore: MeetingSessionTranscriptStore<TSession>;\n constructor(private readonly options: MeetingSessionRuntimeOptions<TSession, TRequest, TTransport, TMode, THealth, TTab, TManualReason, TSpeechBlockedReason>);\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<TranscriptsStartResult>;\n async stopTranscriptSource(request: TranscriptStopRequest): Promise<TranscriptsStopResult>;\n isReusableSession(session: TSession, resolved: MeetingResolvedJoin<TTransport, TMode>): 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<MeetingSessionLeaveResult<TSession>>;\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<boolean>;\n hasHealthHandle(sessionId: string): boolean;\n refreshHealth(sessionId?: string): void;\n async refreshBrowserHealth(session: TSession, options: {\n force?: boolean;\n readOnly?: boolean;\n }): Promise<void>;\n async refreshCaptionHealth(session: TSession): Promise<void>;\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<TTransport, TMode>): Promise<{\n session: TSession;\n spoken?: boolean;\n }>;\n async #leaveUnlocked(sessionId: string, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\n async #leaveSession(session: TSession, options?: {\n keepBrowserTab?: boolean;\n }): Promise<MeetingSessionLeaveResult<TSession>>;\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<boolean>;\n async #rollbackFailedJoinSession(session: TSession): Promise<void>;\n async #settleRetainedBrowserTabsAfterFailure(retained: Array<{\n session: TSession;\n tab: TTab;\n }>): Promise<void>;\n #attachRuntimeHandles(session: TSession, handles: MeetingSessionRuntimeHandles<THealth>): 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<PluginCommandResult>; [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<Record<string, string>>; 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<PluginCommandResult>;","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<string, string>; 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<LazyServiceModule>; loadOverrideModule?: (specifier: string) => Promise<LazyServiceModule>; startExportNames: string[]; stopExportNames?: string[]; }): Promise<LazyPluginServiceHandle | null>;","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<TContext = unknown, TChannel extends string = string, TResult = PluginInteractiveHandlerResult> = PluginInteractiveRegistration<TContext, TChannel, TResult>;","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<ModelCatalogEntry[]>;","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<typeof resolvePluginProviders>[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<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): 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<typeof resolvePluginProviders>[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<PluginMetadataSnapshot, \"owners\" | \"manifestRegistry\" | \"byPluginId\">; }): 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<string, string> | undefined; requestBody?: BodyInit | Buffer | string | null; response: Response; transport?: \"http\" | \"sse\"; flowId?: string; meta?: Record<string, unknown>; }, 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<T extends Record<string, unknown>>(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<string | null>;","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<void>; }): Promise<DispatchInboundResult>;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessage","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"}
{"closureHash":"491045cedc08b713771d0546174b0ae54f63992fa505cb830e1190dc3a6d9943","declaration":"export function dispatchInboundMessageWithBufferedDispatcher(params: BufferedInboundDispatcherParams): Promise<DispatchInboundResult>;","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<DispatchInboundResult>;","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<void>; }): Promise<DispatchInboundResult>;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessage","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"}
{"closureHash":"7e381c648547325fa1411a95399241208425027e0146ec663b8457f24dc2a33d","declaration":"export function dispatchInboundMessageWithBufferedDispatcher(params: BufferedInboundDispatcherParams): Promise<DispatchInboundResult>;","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<DispatchInboundResult>;","entrypoint":"reply-runtime","exportName":"dispatchInboundMessageWithDispatcher","importSpecifier":"openclaw/plugin-sdk/reply-runtime","kind":"function","recordType":"export"}
{"closureHash":"d75cb355b23e89fd45f6948ffa5df92b066be54cfd71e78f7db7a3419bfa7114","declaration":"export function finalizeInboundContext<T extends Record<string, unknown>>(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<string | null>;","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<ReplyPayload | ReplyPayload[] | undefined>;","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<T>(errorMessage: string): { setRuntime: (next: T) => void; clearRuntime: () => void; tryGetRuntime: () => T | null; getRuntime: () => T; };\nexport function createPluginRuntimeStore<T>(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<void>;","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<string>;","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<SessionsCatalogReadResult>; sessionId: string; sessionKey: string; agentId: string; cwd?: string; config: OpenClawConfig; }): Promise<void>;","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<string, string>;","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<string, string>;","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<TScope extends string | number, TId extends string | number> = ScopedExpiringIdCache<TScope, TId>;","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<TConfigSchema extends TSchema | undefined = undefined>(definition: DefineToolPluginOptions<TConfigSchema>): DefinedToolPluginEntry;","entrypoint":"tool-plugin","exportName":"defineToolPlugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"function","recordType":"export"}
{"closureHash":"fb5203d8d0fb7f790e856e8bd91f095ef09bfc4e07c847051815b511eec89b3e","declaration":"export function defineToolPlugin<TConfigSchema extends TSchema | undefined = undefined>(definition: DefineToolPluginOptions<TConfigSchema>): 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<TConfigSchema extends TSchema | undefined = undefined> = DefineToolPluginOptions<TConfigSchema>;","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<TConfig> = ToolPluginFactoryContext<TConfig>;","entrypoint":"tool-plugin","exportName":"ToolPluginFactoryContext","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"396ce432df15713c3d8293d12c91941da5c10ca2ba2b09d9e31166d541769cf1","declaration":"export type DefineToolPluginOptions<TConfigSchema extends TSchema | undefined = undefined> = DefineToolPluginOptions<TConfigSchema>;","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<TConfig> = ToolPluginFactoryContext<TConfig>;","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<TConfig, TParamsSchema extends TSchema> = ToolPluginToolDefinition<TConfig, TParamsSchema>;","entrypoint":"tool-plugin","exportName":"ToolPluginToolDefinition","importSpecifier":"openclaw/plugin-sdk/tool-plugin","kind":"type","recordType":"export"}
{"closureHash":"b55fee969fe9db614f96a26d4e3bcb563448ce20a5b96341ce9bca98b5d60bb1","declaration":"export type ToolPluginToolDefinition<TConfig, TParamsSchema extends TSchema> = ToolPluginToolDefinition<TConfig, TParamsSchema>;","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<TDetails>(payload: TDetails): AgentToolResult<TDetails>;","entrypoint":"tool-results","exportName":"jsonResult","importSpecifier":"openclaw/plugin-sdk/tool-results","kind":"function","recordType":"export"}
{"closureHash":"b9aefc637a694c74e028e92006004bef18185ed569af48ac43d36b646b0af2f3","declaration":"export function textResult<TDetails>(text: string, details: TDetails): AgentToolResult<TDetails>;","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<string>;","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<T extends { path: string; }>(targetsByPath: Map<string, T[]>, target: T, opts?: RegisterWebhookTargetOptions<T>): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTarget","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"20864ae868c6f3a5e205e36cf187ba0dc317b863f08eab544292311f797a14c6","declaration":"export function registerWebhookTargetWithPluginRoute<T extends { path: string; }>(params: { targetsByPath: Map<string, T[]>; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions<T>[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget<T>;","entrypoint":"webhook-ingress","exportName":"registerWebhookTargetWithPluginRoute","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"function","recordType":"export"}
{"closureHash":"742e865c4abaf46d2b6b6c7ff40f54547c34cf1a61ec51c94eeb6d9358396f70","declaration":"export function registerWebhookTargetWithPluginRoute<T extends { path: string; }>(params: { targetsByPath: Map<string, T[]>; target: T; route: RegisterWebhookPluginRouteOptions; onLastPathTargetRemoved?: RegisterWebhookTargetOptions<T>[\"onLastPathTargetRemoved\"]; }): RegisteredWebhookTarget<T>;","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<T>(targets: readonly T[], isMatch: (target: T) => boolean): WebhookTargetMatchResult<T>;","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<T extends {\n path: string;\n}> = RegisterWebhookTargetOptions<T>;","entrypoint":"webhook-ingress","exportName":"RegisterWebhookTargetOptions","importSpecifier":"openclaw/plugin-sdk/webhook-ingress","kind":"type","recordType":"export"}
{"closureHash":"964c2bfd8f268c411ee7b982c9de02c207f4bec402dd62c9f998a59752bb9d1f","declaration":"export type RegisteredWebhookTarget<T> = RegisteredWebhookTarget<T>;","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"}
+27
View File
@@ -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.
</Step>
<Step title="Wire the entry point">
+1
View File
@@ -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` |
@@ -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<ChannelInboundTurnPlan["replyOptions"]>;
type ReplyOptions = Omit<GetReplyOptions, "onBlockReply">;
type CallbackPayload<K extends keyof ReplyOptions> =
NonNullable<ReplyOptions[K]> extends (...args: infer Args) => unknown ? Args[0] : never;
type DraftPreview = ReturnType<typeof createDiscordDraftPreviewController>;
@@ -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<typeof createSubsystemLogger>;
pluginCommandDispatch: PluginCommandCatalogDecision;
}): Promise<DispatchDiscordNativeAgentReplyResult> {
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,
},
@@ -124,6 +124,7 @@ async function handleDiscordCommandArgInteraction(params: {
preferFollowUp: true,
threadBindings: ctx.threadBindings,
responseEphemeral: resolveDiscordSlashCommandConfig(ctx.discordConfig?.slashCommand).ephemeral,
pluginCommandDispatch: { kind: "non-plugin" },
});
}
@@ -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 = {
@@ -65,6 +65,7 @@ export async function applyDiscordModelPickerSelection(params: {
preferFollowUp: true,
threadBindings: params.threadBindings,
suppressReplies: true,
pluginCommandDispatch: { kind: "non-plugin" },
}),
12000,
);
@@ -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 () => {
@@ -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<void> }).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<unknown> }) => 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<void> }).run(
createInteraction() as unknown,
);
expect(pluginHandler).not.toHaveBeenCalled();
const dispatchParams = requireRecord(
firstMockArg(runtimeModuleMocks.dispatchReplyWithDispatcher, "core dispatch"),
"core dispatch",
);
expect(
(dispatchParams.replyOptions as Record<PropertyKey, unknown>)[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<void> }).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<void> }).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<void> }).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<void> }).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<void> }).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<void> }).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<void> }).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",
@@ -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,
@@ -82,13 +82,25 @@ function createConfig(params?: { requireMention?: boolean }): OpenClawConfig {
} as OpenClawConfig;
}
async function createStatusCommand(cfg: OpenClawConfig) {
async function createStatusCommand(cfg: OpenClawConfig, pluginExecute?: ReturnType<typeof vi.fn>) {
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<void> }).run(interaction as unknown);
@@ -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<DispatchDiscordCommandInteractionResult> {
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 };
@@ -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<typeof Client>) => new Client(...args),
getPluginCommandSpecs: undefined as GetPluginCommandSpecs | undefined,
resolveDiscordAccount,
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
@@ -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<typeof import("openclaw/plugin-sdk/plugin-command-runtime")>();
return {
...actual,
createPluginCommandRuntime: () => {
const runtime = actual.createPluginCommandRuntime();
return {
...runtime,
retainNativeCatalog: (provider: string) => {
retainNativeCatalog(provider);
runtime.retainNativeCatalog(provider);
},
};
},
};
});
type ResolverParams = Parameters<typeof resolveDiscordProviderCommandSpecs>[0];
type SkillCommands = ReturnType<NonNullable<ResolverParams["listSkillCommandsForAgents"]>>;
@@ -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]);
@@ -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<typeof listSkillCommandsForAgents>;
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 };
}
@@ -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";
@@ -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;
},
@@ -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<typeof resolveDiscordAccountMock>) =>
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");
});
@@ -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
@@ -10,12 +10,6 @@ type NativeCommandSpecMock = {
acceptsArgs: boolean;
};
type PluginCommandSpecMock = {
name: string;
description: string;
acceptsArgs: boolean;
};
type ProviderMonitorTestMocks = {
clientDeployCommandsMock: Mock<(options?: { mode?: string }) => Promise<void>>;
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(
@@ -1,2 +0,0 @@
// Slack plugin module implements slash plugin commands behavior.
export { listProviderPluginCommandSpecs } from "openclaw/plugin-sdk/command-auth-native";
+161 -11
View File
@@ -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<typeof import("openclaw/plugin-sdk/plugin-command-runtime")>();
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<typeof import("./slash-plugin-commands.runtime.js")>(
"./slash-plugin-commands.runtime.js",
);
return {
...actual,
listProviderPluginCommandSpecs: () => pluginCommandFixtures.specs,
};
});
vi.mock("./slash-skill-commands.runtime.js", async () => {
const actual = await vi.importActual<typeof import("./slash-skill-commands.runtime.js")>(
"./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<void>) {
return { respond, ack };
}
function setAsyncDispatchMock(
implementation: (params: { replyOptions?: Record<PropertyKey, unknown> }) => Promise<unknown>,
) {
(
dispatchMock as unknown as {
mockImplementation: (callback: typeof implementation) => unknown;
}
).mockImplementation(implementation);
}
function expectArgMenuLayout(respond: ReturnType<typeof vi.fn>): {
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 () => {
+43 -13
View File
@@ -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,
});
});
};
@@ -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",
@@ -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<ModelsAuthLoginFlowResult>;
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");
@@ -46,6 +46,9 @@ type TelegramPluginCommandSpec = {
descriptionLocalizations?: Record<string, string>;
};
type TelegramSelectedPluginMenuCommand<TSpec extends TelegramPluginCommandSpec> =
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<TSpec extends TelegramPluginCommandSpec>(params: {
specs: readonly TSpec[];
existingCommands: Set<string>;
}): { commands: TelegramMenuCommand[]; issues: string[] } {
}): {
commands: TelegramMenuCommand[];
selectedCommands: TelegramSelectedPluginMenuCommand<TSpec>[];
issues: string[];
} {
const { specs, existingCommands } = params;
const commands: TelegramMenuCommand[] = [];
const selectedCommands: TelegramSelectedPluginMenuCommand<TSpec>[] = [];
const issues: string[] = [];
const pluginCommandNames = new Set<string>();
@@ -238,14 +246,20 @@ export function buildPluginTelegramMenuCommands(params: {
}
pluginCommandNames.add(normalized);
existingCommands.add(normalized);
const menuCommand: TelegramMenuCommand = { command: normalized, description };
const menuCommand: TelegramSelectedPluginMenuCommand<TSpec> = {
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: {
@@ -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<typeof vi.fn>;
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);
@@ -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<typeof import("openclaw/plugin-sdk/plugin-command-runtime")>();
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<typeof createTelegramPluginRegistry>;
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<string, ReturnType<typeof vi.fn>>();
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();
@@ -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,
@@ -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<DeliverRepliesFn>[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<ResolveDefaultModelForAgentFn>(),
}));
const pluginRuntimeMocks = vi.hoisted(() => ({
executePluginCommand: vi.fn(async () => ({ text: "ok" })),
matchPluginCommand: vi.fn<MatchPluginCommandFn>(() => null),
executePluginCommand: vi.fn(async (_params?: unknown) => ({ text: "ok" })),
}));
const replyMocks = vi.hoisted(() => ({
dispatchReplyWithBufferedBlockDispatcher: vi.fn<DispatchReplyWithBufferedBlockDispatcherFn>(
@@ -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<typeof import("openclaw/plugin-sdk/plugin-runtime")>(
"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<typeof createEmptyPluginRegistry>;
type TelegramCommandHandler = (ctx: unknown) => Promise<void>;
type TelegramPluginCommandSpecs = ReturnType<
NonNullable<TelegramNativeCommandDeps["getPluginCommandSpecs"]>
>;
type TelegramPluginCommandSpecs = Array<{
name: string;
description: string;
acceptsArgs?: boolean;
}>;
type TelegramLoginFlow = NonNullable<TelegramNativeCommandDeps["runModelsAuthLoginFlow"]>;
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(
@@ -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<typeof registerTelegramNativeCommands>[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<typeof registerTelegramNativeCommands>[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");
@@ -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<typeof registerTelegramNativeCommands>[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<GetPluginCommandSpecsFn>(() => []),
matchPluginCommand: vi.fn<MatchPluginCommandFn>(() => null),
executePluginCommand: vi.fn<ExecutePluginCommandFn>(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) => {
@@ -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<typeof createCommandBot>;
type TelegramInlineKeyboardReplyMarkup = {
@@ -29,29 +31,45 @@ type PlugCommandHarnessParams = {
botHarness?: CommandBotHarness;
cfg?: OpenClawConfig;
command?: Record<string, unknown>;
acceptsArgs?: boolean;
args?: string;
result?: Record<string, unknown>;
registerOverrides?: Partial<Parameters<typeof registerTelegramNativeCommands>[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<string, unknown>) => ({ text: "ok" }));
function registerTestPluginCommand(params: {
name: string;
description: string;
acceptsArgs?: boolean;
command?: Record<string, unknown>;
result?: Record<string, unknown>;
}) {
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<string, unknown>);
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<Array<unknown>> };
},
);
@@ -122,16 +140,20 @@ function replyAt(params: Record<string, unknown>, 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),
+23 -23
View File
@@ -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<Record<string, string>> & { 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<boolean>)
| 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) {
@@ -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" });
}
+4
View File
@@ -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"
+3
View File
@@ -84,6 +84,9 @@ export const pluginSdkDocMetadata = {
"runtime-store": {
category: "runtime",
},
"plugin-command-runtime": {
category: "runtime",
},
"session-store-runtime": {
category: "runtime",
},
+1
View File
@@ -98,6 +98,7 @@
"speech-core",
"speech-settings",
"tts-runtime",
"plugin-command-runtime",
"plugin-runtime",
"channel-secret-basic-runtime",
"channel-secret-runtime",
+7 -3
View File
@@ -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(
+105 -96
View File
@@ -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<Parameters<typeof registerPluginCommandInRegistry>[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<typeof registerTestCommand>) {
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<typeof import("../../plugins/commands.js")>(
"../../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<HandleCommandsParams["opts"]> &
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<HandleCommandsParams["opts"]> & PluginCommandExecutionReplyOptions;
await expect(handlePluginCommand(params, true)).resolves.toBeNull();
expect(handler).not.toHaveBeenCalled();
});
});
+24 -8
View File
@@ -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,
@@ -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,
});
@@ -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)) {
@@ -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`,
);
@@ -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<never>(() => {});
}
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<never>(() => {});
}
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<never>(() => {});
}
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();
@@ -291,23 +291,23 @@ export function firstRouteReplyCall(): Record<string, unknown> {
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.
+2
View File
@@ -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;
@@ -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<GetReplyOptions, "onBlockReply">;
type DispatchReplyOptions = Omit<GetReplyOptions, "onBlockReply"> & PluginCommandReplyOptions;
/** Buffered block dispatcher entry point used by provider reply flows. */
export type DispatchReplyWithBufferedBlockDispatcher = (params: {
+5 -1
View File
@@ -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<ReplyDispatcherWithTypingOptions, "deliver" | "onError">;
/** Reply options plus the opaque native command ownership decision carried by channel turns. */
type ChannelTurnReplyOptions = Omit<GetReplyOptions, "onBlockReply"> & 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<GetReplyOptions, "onBlockReply">;
replyOptions?: ChannelTurnReplyOptions;
replyResolver?: GetReplyFromConfig;
sessionInitRetry?: {
delaysMs: readonly number[];
@@ -12,6 +12,7 @@ import type { ChannelManager } from "./server-channels.js";
function createMockChannelManager(overrides?: Partial<ChannelManager>): ChannelManager {
return {
getRuntimeSnapshot: vi.fn(() => ({ channels: {}, channelAccounts: {} })),
getPluginCommandCatalogAccounts: vi.fn(() => new Map()),
startChannels: vi.fn(async () => {}),
startChannel: vi.fn(async () => {}),
stopChannel: vi.fn(async () => {}),
+69
View File
@@ -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<void>((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<string>).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 () => {
+62 -17
View File
@@ -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<void> {
type ChannelRuntimeStore = {
aborts: Map<string, AbortController>;
// The account task's controller is the ownership token: late predecessor cleanup
// must not clear a catalog retained by its replacement.
pluginCommandCatalogOwners: Map<string, AbortController>;
starting: Map<string, Promise<void>>;
stops: Map<string, ChannelAccountStopState>;
tasks: Map<string, Promise<unknown>>;
@@ -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<ChannelId, ReadonlySet<string>>;
startChannels: () => Promise<void>;
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<ChannelId, ReadonlySet<string>> =>
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,
+8 -2
View File
@@ -391,7 +391,10 @@ export async function startGatewayCoreRuntime(input: {
const reloadAttachedGatewayPlugins = async (params: {
nextConfig: OpenClawConfig;
changedPaths: readonly string[];
beforeReplace: (channels: ReadonlySet<ChannelId>) => Promise<void>;
beforeReplace: (
channels: ReadonlySet<ChannelId>,
accounts?: ReadonlyMap<ChannelId, ReadonlySet<string>>,
) => Promise<void>;
commitRuntime: () => Promise<void>;
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?.()) {
@@ -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<typeof import("../../config/sessions/session-accessor.js")>();
@@ -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", () => ({
+28 -4
View File
@@ -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<GatewayReloadHandlerParams, "startChannel">,
channel: ChannelKind,
accountId?: string,
): Promise<void> {
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<ChannelKind, Set<string>>;
activePluginChannelsAfterReload: ReadonlySet<ChannelKind> | null;
channelsStoppedBeforePluginReload: Set<ChannelKind>;
accountsStoppedBeforePluginReload: ReadonlyMap<ChannelKind, ReadonlySet<string>>;
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,
+4 -1
View File
@@ -150,7 +150,10 @@ export type GatewayReloadHandlerParams = {
reloadPlugins: (params: {
nextConfig: OpenClawConfig;
changedPaths: readonly string[];
beforeReplace: (channels: ReadonlySet<ChannelKind>) => Promise<void>;
beforeReplace: (
channels: ReadonlySet<ChannelKind>,
accounts?: ReadonlyMap<ChannelKind, ReadonlySet<string>>,
) => Promise<void>;
commitRuntime: () => Promise<void>;
env: NodeJS.ProcessEnv;
isAborted?: () => boolean;
+140 -16
View File
@@ -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<GatewayPluginReloadResult> => {
const beforeReplace = params.beforeReplace as (
channels: ReadonlySet<ChannelKind>,
accounts?: ReadonlyMap<ChannelKind, ReadonlySet<string>>,
) => Promise<void>;
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<ChannelKind>) => Promise<void>;
beforeReplace: (
channels: ReadonlySet<ChannelKind>,
accounts?: ReadonlyMap<ChannelKind, ReadonlySet<string>>,
) => Promise<void>;
commitRuntime: () => Promise<void>;
}): Promise<GatewayPluginReloadResult> => {
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<ChannelKind>) => Promise<void>;
beforeReplace: (
channels: ReadonlySet<ChannelKind>,
accounts?: ReadonlyMap<ChannelKind, ReadonlySet<string>>,
) => Promise<void>;
isAborted?: () => boolean;
}): Promise<GatewayPluginReloadResult> => {
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();
});
+97 -12
View File
@@ -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<ChannelKind, Set<string>>();
const restartChannelAccounts = new Map<ChannelKind, Set<string>>(
[...(plan.restartChannelAccounts ?? [])].map(([channel, accountIds]) => [
channel,
new Set(accountIds),
]),
);
const channelsStoppedBeforePluginReload = new Set<ChannelKind>();
const accountsStoppedBeforePluginReload = new Map<ChannelKind, Set<string>>();
let activePluginChannelsAfterReload: ReadonlySet<ChannelKind> | 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<string[]> => {
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<string[]> => [
...(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<ChannelKind>) => {
const stopChannelsBeforePluginReplace = async (
channels: ReadonlySet<ChannelKind>,
accounts: ReadonlyMap<ChannelKind, ReadonlySet<string>> = 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)",
+1
View File
@@ -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(),
@@ -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 () => {
+1
View File
@@ -18,6 +18,7 @@ export {
resetPluginRuntimeStateForTest,
resetGlobalHookRunner,
setActivePluginRegistry,
withPluginRuntimeRegistryScope,
type PluginHookRegistration,
} from "./test-helpers/outbound-delivery.js";
export {
+13
View File
@@ -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";
@@ -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";
+90
View File
@@ -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<PluginRegistry, PluginCommandExecutionState>();
const executionContext = new AsyncLocalStorage<ReadonlySet<PluginCommandExecutionToken>>();
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<T>(
registry: PluginRegistry,
run: () => T | Promise<T>,
): 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<void> {
const state = getExecutionState(registry);
if (state.count === 0) {
return;
}
await new Promise<void>((resolve) => {
state.waiters.push(resolve);
});
}
+5 -21
View File
@@ -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<string>();
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<typeof registerPluginCommand>[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") {
+8 -18
View File
@@ -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<string, RegisteredPluginCommand>(), {
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<string, RegisteredPluginCommand>
},
});
export function setPluginCommandRegistryLocked(locked: boolean): void {
getCommandRegistry().commandRegistryLocked = locked;
}
export function clearPluginCommands(): void {
pluginCommands.clear();
}
+13 -24
View File
@@ -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<string, string>;
acceptsArgs: boolean;
} {
const metadata = projectPluginCommandNativeMetadata(cmd, provider);
const spec: {
name: string;
description: string;
descriptionLocalizations?: Record<string, string>;
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(),
+44
View File
@@ -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"],
+28 -387
View File
@@ -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<PluginCommandContext["runtimeContext"]>["llm"];
type PluginCommandLlmCompleteParams = Parameters<
NonNullable<PluginCommandRuntimeLlm>["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<PluginCommandResult> {
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 || "<unknown>"}`,
);
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<PluginCommandResult>;
export async function executePluginCommand(
params: PluginCommandExecutionParams,
): Promise<PluginCommandResult> {
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);
}
@@ -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<PluginCommandAccountStartScope>
>(
PLUGIN_COMMAND_ACCOUNT_START_SCOPE_KEY,
() => new AsyncLocalStorage<PluginCommandAccountStartScope>(),
);
/** Runs one channel account startup lifetime with its catalog-retention owner. */
export function withPluginCommandAccountStartScope<T>(
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();
}
}
@@ -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" }>;
}>;
+299
View File
@@ -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<PluginCommandContext["runtimeContext"]>["llm"];
type PluginCommandLlmCompleteParams = Parameters<
NonNullable<PluginCommandRuntimeLlm>["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<PluginCommandResult> {
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 || "<unknown>"}`,
);
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." };
}
}
+75
View File
@@ -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<string>();
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 };
}
+61
View File
@@ -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<Record<string, string>>;
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 } : {}),
});
}
+24
View File
@@ -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,
}));
}
+447
View File
@@ -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<typeof createEmptyPluginRegistry>,
params: {
pluginId: string;
name: string;
channels?: string[];
nativeNames?: Record<string, string>;
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<typeof createPluginCommandRuntime>["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<void>((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<void>((resolveEntered) => {
registerCommand(registry, {
pluginId: "slow",
name: "slow",
handler: async () => {
resolveEntered();
await new Promise<void>((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<void>((resolve) => {
entered = resolve;
});
registerCommand(registry, {
pluginId: "slow",
name: "slow",
handler: async () => {
entered();
await new Promise<void>((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<void>((resolve) => {
releaseDetached = resolve;
});
let releaseCleanup!: () => void;
cleanupReplacedPluginHostRegistry.mockImplementationOnce(
async () =>
await new Promise<void>((resolve) => {
releaseCleanup = resolve;
}),
);
let detachedClear!: Promise<void>;
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<void>((resolve) => {
releaseDetached = resolve;
});
let detachedClear!: Promise<void>;
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<void>((resolve) => {
releaseOuter = resolve;
});
let outerHolding!: () => void;
const outerHoldingGate = new Promise<void>((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<void>((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");
});
});
+264
View File
@@ -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<PluginCommandResult>;
[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<Record<string, string>>;
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<object, SelectedCommand>();
const runtimeStates = new WeakMap<PluginCommandRuntime, PluginCommandRuntimeState>();
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<PluginCommandResult> {
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<PluginCommandResult> {
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 } : {}),
});
}
-1
View File
@@ -42,7 +42,6 @@ export function createEmptyPluginRegistry(): PluginRegistry {
memoryPromptSupplements: [],
sessionDiscussionProviders: new Map(),
contextEngines: new Map(),
commandRegistryLocked: false,
gatewayHandlers: {},
gatewayMethodDescriptors: [],
dashboardDataBindings: new Map(),
-1
View File
@@ -550,7 +550,6 @@ export type PluginRegistry = {
memoryPromptSupplements: MemoryPromptSupplementRegistration[];
sessionDiscussionProviders: Map<string, PluginSessionDiscussionRegistration>;
contextEngines: Map<string, ContextEngineRegistration>;
commandRegistryLocked: boolean;
gatewayHandlers: GatewayRequestHandlers;
gatewayMethodDescriptors: GatewayMethodDescriptor[];
dashboardDataBindings: Map<string, PluginDashboardDataBindingRegistration>;
+2
View File
@@ -15,6 +15,8 @@ export type RegistryState = {
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable";
importedPluginIds: Set<string>;
registrationContext?: { registry: PluginRegistry; pluginId: string };
commandRegistryClearTail?: Promise<void>;
commandRegistryClearRegistries?: Map<PluginRegistry, number>;
};
type GlobalRegistryState = typeof globalThis & {
+58 -15
View File
@@ -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<void> {
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 {