From f1ce679929c115def8d4f53e90e5481c41e63d6c Mon Sep 17 00:00:00 2001 From: Harold Hunt Date: Thu, 19 Mar 2026 22:23:21 -0400 Subject: [PATCH] Discord: reconcile native commands without restart churn (#46597) Merged via squash. Prepared head SHA: 37090daad4b99171a55962101d9998fd452e2739 Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com> Reviewed-by: @huntharo --- CHANGELOG.md | 1 + extensions/discord/package.json | 2 +- .../discord/src/monitor/provider.test.ts | 26 ++++++++++++++++ extensions/discord/src/monitor/provider.ts | 4 ++- pnpm-lock.yaml | 31 ++++++++++++++++--- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7928c21129d..70652051c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -181,6 +181,7 @@ Docs: https://docs.openclaw.ai - Plugins/message discovery: require `ChannelMessageActionAdapter.describeMessageTool(...)` for shared `message` tool discovery. The legacy `listActions`, `getCapabilities`, and `getToolSchema` adapter methods are removed. Plugin authors should migrate message discovery to `describeMessageTool(...)` and keep channel-specific action runtime code inside the owning plugin package. Thanks @gumadeiras. - Exec/env sandbox: block build-tool JVM injection (`MAVEN_OPTS`, `SBT_OPTS`, `GRADLE_OPTS`, `ANT_OPTS`), glibc tunable exploitation (`GLIBC_TUNABLES`), and .NET dependency resolution hijack (`DOTNET_ADDITIONAL_DEPS`) from the host exec environment, and restrict Gradle init script redirect (`GRADLE_USER_HOME`) as an override-only block so user-configured Gradle homes still propagate. (#49702) - Plugins/Matrix: add a new Matrix plugin backed by the official `matrix-js-sdk`. If you are upgrading from the previous public Matrix plugin, follow the migration guide: https://docs.openclaw.ai/install/migrating-matrix Thanks @gumadeiras. +- Discord/commands: switch native command deployment to Carbon reconcile by default so Discord restarts stop churning slash commands through OpenClaw’s local deploy path. (#46597) Thanks @huntharo and @thewilloftheshadow. ## 2026.3.13 diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 33adc17e6da..589ceed8d21 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -4,7 +4,7 @@ "description": "OpenClaw Discord channel plugin", "type": "module", "dependencies": { - "@buape/carbon": "0.0.0-beta-20260216184201", + "@buape/carbon": "0.0.0-beta-20260317045421", "@discordjs/voice": "^0.19.2", "discord-api-types": "^0.38.42", "https-proxy-agent": "^8.0.0", diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 23c4b394379..2772790878b 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -88,11 +88,25 @@ describe("monitorDiscordProvider", () => { const getConstructedEventQueue = (): { listenerTimeout?: number } | undefined => { expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1); const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as { + commandDeploymentMode?: string; eventQueue?: { listenerTimeout?: number }; }; return opts.eventQueue; }; + const getConstructedClientOptions = (): { + commandDeploymentMode?: string; + eventQueue?: { listenerTimeout?: number }; + } => { + expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1); + return ( + (clientConstructorOptionsMock.mock.calls[0]?.[0] as { + commandDeploymentMode?: string; + eventQueue?: { listenerTimeout?: number }; + }) ?? {} + ); + }; + const getHealthProbe = () => { expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1); const firstCall = reconcileAcpThreadBindingsOnStartupMock.mock.calls.at(0) as @@ -539,6 +553,18 @@ describe("monitorDiscordProvider", () => { ); }); + it("configures Carbon reconcile deployment by default", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + expect(clientHandleDeployRequestMock).toHaveBeenCalledTimes(1); + expect(getConstructedClientOptions().commandDeploymentMode).toBe("reconcile"); + }); + it("reports connected status on startup and shutdown", async () => { const { monitorDiscordProvider } = await import("./provider.js"); const setStatus = vi.fn(); diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index 9c766334964..55293357763 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -306,6 +306,7 @@ async function deployDiscordCommands(params: { // errors like Discord 30034 fail fast and don't wedge the provider. restClient.options.queueRequests = false; } + params.runtime.log?.("discord: native commands using Carbon reconcile path"); for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { await params.client.handleDeployRequest(); @@ -762,6 +763,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { baseUrl: "http://localhost", deploySecret: "a", clientId: applicationId, + commandDeploymentMode: "reconcile", publicKey: "a", token, autoDeploy: false, @@ -805,7 +807,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { phase: "deploy-commands:start", startAt: startupStartedAt, gateway: lifecycleGateway, - details: `native=${nativeEnabled ? "on" : "off"} commandCount=${commands.length}`, + details: `native=${nativeEnabled ? "on" : "off"} reconcile=on commandCount=${commands.length}`, }); await deployDiscordCommands({ client, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70e7586716b..f0d503f2346 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,8 +309,8 @@ importers: extensions/discord: dependencies: '@buape/carbon': - specifier: 0.0.0-beta-20260216184201 - version: 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1) + specifier: 0.0.0-beta-20260317045421 + version: 0.0.0-beta-20260317045421(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1) '@discordjs/voice': specifier: ^0.19.2 version: 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1) @@ -991,6 +991,9 @@ packages: '@buape/carbon@0.0.0-beta-20260216184201': resolution: {integrity: sha512-u5mgYcigfPVqT7D9gVTGd+3YSflTreQmrWog7ORbb0z5w9eT8ft4rJOdw9fGwr75zMu9kXpSBaAcY2eZoJFSdA==} + '@buape/carbon@0.0.0-beta-20260317045421': + resolution: {integrity: sha512-yM+r5iSxA/iG8CZ2VhK+EkcBQV+y45WLgF7kuczt2Ul1yixjXSCCcM80GppsklfUv7pqM4Dui+7w1WB3f5p7Kg==} + '@cacheable/memory@2.0.7': resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==} @@ -7494,7 +7497,27 @@ snapshots: dependencies: css-tree: 3.2.1 - '@buape/carbon@0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)': + '@buape/carbon@0.0.0-beta-20260216184201(hono@4.12.8)(opusscript@0.1.1)': + dependencies: + '@types/node': 25.5.0 + discord-api-types: 0.38.37 + optionalDependencies: + '@cloudflare/workers-types': 4.20260120.0 + '@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1) + '@hono/node-server': 1.19.10(hono@4.12.8) + '@types/bun': 1.3.9 + '@types/ws': 8.18.1 + ws: 8.19.0 + transitivePeerDependencies: + - '@discordjs/opus' + - bufferutil + - ffmpeg-static + - hono + - node-opus + - opusscript + - utf-8-validate + + '@buape/carbon@0.0.0-beta-20260317045421(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)': dependencies: '@types/node': 25.5.0 discord-api-types: 0.38.37 @@ -12415,7 +12438,7 @@ snapshots: dependencies: '@agentclientprotocol/sdk': 0.16.1(zod@4.3.6) '@aws-sdk/client-bedrock': 3.1009.0 - '@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1) + '@buape/carbon': 0.0.0-beta-20260216184201(hono@4.12.8)(opusscript@0.1.1) '@clack/prompts': 1.1.0 '@discordjs/voice': 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1) '@grammyjs/runner': 2.0.3(grammy@1.41.1)