fix(discord): preserve native command session keys

This commit is contained in:
Peter Steinberger 2026-03-08 01:03:08 +00:00
parent cf1c2cc208
commit bc91ae9ca0
3 changed files with 57 additions and 1 deletions

View File

@ -325,6 +325,7 @@ Docs: https://docs.openclaw.ai
- Telegram/DM streaming transport parity: use message preview transport for all DM streaming lanes so final delivery can edit the active preview instead of sending duplicate finals. Landed from contributor PR #38906 by @gambletan. Thanks @gambletan.
- Telegram/send retry safety: retry non-idempotent send paths only for pre-connect failures and make custom retry predicates strict, preventing ambiguous reconnect retries from sending duplicate messages. Landed from contributor PR #34238 by @hal-crackbot. Thanks @hal-crackbot.
- Discord/DM session-key normalization: rewrite legacy `discord:dm:*` and phantom direct-message `discord:channel:<user>` session keys to `discord:direct:*` when the sender matches, so multi-agent Discord DMs stop falling into empty channel-shaped sessions and resume replying correctly.
- Discord/native slash session fallback: treat empty configured bound-session keys as missing so `/status` and other native commands fall back to the routed slash session and routed channel session instead of blanking Discord session keys in normal channel bindings.
## 2026.3.2

View File

@ -230,6 +230,61 @@ describe("Discord native plugin command dispatch", () => {
expectBoundSessionDispatch(dispatchSpy, boundSessionKey);
});
it("falls back to the routed slash and channel session keys when no bound session exists", async () => {
const guildId = "1459246755253325866";
const channelId = "1478836151241412759";
const cfg = {
commands: {
useAccessGroups: false,
},
bindings: [
{
agentId: "qwen",
match: {
channel: "discord",
accountId: "default",
peer: { kind: "channel", id: channelId },
guildId,
},
},
],
channels: {
discord: {
guilds: {
[guildId]: {
channels: {
[channelId]: { allow: true, requireMention: false },
},
},
},
},
},
} as OpenClawConfig;
const command = createStatusCommand(cfg);
const interaction = createInteraction({
channelType: ChannelType.GuildText,
channelId,
guildId,
guildName: "Ops",
});
vi.spyOn(pluginCommandsModule, "matchPluginCommand").mockReturnValue(null);
const dispatchSpy = createDispatchSpy();
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as {
ctx?: { SessionKey?: string; CommandTargetSessionKey?: string };
};
expect(dispatchCall.ctx?.SessionKey).toBe("agent:qwen:discord:slash:owner");
expect(dispatchCall.ctx?.CommandTargetSessionKey).toBe(
"agent:qwen:discord:channel:1478836151241412759",
);
expect(persistentBindingMocks.resolveConfiguredAcpBindingRecord).toHaveBeenCalledTimes(1);
expect(persistentBindingMocks.ensureConfiguredAcpBindingSession).not.toHaveBeenCalled();
});
it("routes Discord DM native slash commands through configured ACP bindings", async () => {
const channelId = "dm-1";
const boundSessionKey = "agent:codex:acp:binding:discord:default:dmfeedface";

View File

@ -1644,7 +1644,7 @@ async function dispatchDiscordCommandInteraction(params: {
return;
}
}
const configuredBoundSessionKey = configuredRoute?.boundSessionKey ?? "";
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
const effectiveRoute = boundSessionKey