fix(discord): collapse cron announce text

This commit is contained in:
Peter Steinberger
2026-04-25 06:22:24 +01:00
parent ee3c32c103
commit 576c6c240f
6 changed files with 47 additions and 1 deletions

View File

@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Discord/cron: deliver text-only isolated cron and heartbeat announce output from the canonical final assistant text once, avoiding duplicate Discord posts when streamed block payloads and the final answer contain the same content. Fixes #71406. Thanks @alexgross21.
- Control UI/WebChat: hide heartbeat prompts, `HEARTBEAT_OK` acknowledgments, and internal-only runtime context turns from visible chat history while leaving the underlying transcript intact. Fixes #71381. Thanks @gerald1950ggg-ai.
- Control UI/chat: keep optimistic user and assistant tail messages visible when a final history refresh briefly returns an older snapshot, preventing message cards from flash-disappearing until the next refresh. Fixes #71371. Thanks @WolvenRA.
- Talk/TTS: resolve configured extension speech providers from the active runtime registry before provider-list discovery, so Talk mode no longer rejects valid plugin speech providers as unsupported.

View File

@@ -94,6 +94,11 @@ When isolated cron runs orchestrate subagents, delivery also prefers the final
descendant output over stale parent interim text. If descendants are still
running, OpenClaw suppresses that partial parent update instead of announcing it.
For text-only Discord announce targets, OpenClaw sends the canonical final
assistant text once instead of replaying both streamed/intermediate text payloads
and the final answer. Media and structured Discord payloads are still delivered
as separate payloads so attachments and components are not dropped.
### Payload options for isolated jobs
- `--message`: prompt text (required for isolated)

View File

@@ -267,6 +267,9 @@ Now create some channels on your Discord server and start chatting. Your agent c
- Guild channels are isolated session keys (`agent:<agentId>:discord:channel:<channelId>`).
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
- Native slash commands run in isolated command sessions (`agent:<agentId>:discord:slash:<userId>`), while still carrying `CommandTargetSessionKey` to the routed conversation session.
- Text-only cron/heartbeat announce delivery to Discord uses the final
assistant-visible answer once. Media and structured component payloads remain
multi-message when the agent emits multiple deliverable payloads.
## Forum channels

View File

@@ -115,6 +115,10 @@ describe("discordPlugin outbound", () => {
expect(source).not.toContain('require("./channel-actions.js")');
});
it("prefers final assistant text for text-only cron announce delivery", () => {
expect(discordPlugin.outbound?.preferFinalAssistantVisibleText).toBe(true);
});
it("honors per-account replyToMode overrides", () => {
const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode;
if (!resolveReplyToMode) {

View File

@@ -789,6 +789,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
},
outbound: {
...discordOutbound,
preferFinalAssistantVisibleText: true,
shouldTreatDeliveredTextAsVisible: shouldTreatDiscordDeliveredTextAsVisible,
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload }) =>
shouldSuppressLocalDiscordExecApprovalPrompt({

View File

@@ -157,10 +157,14 @@ function resolveCoreChannelSender(
function createCliDelegatingOutbound(params: {
channel: CoreChannel;
deliveryMode?: ChannelOutboundAdapter["deliveryMode"];
preferFinalAssistantVisibleText?: boolean;
resolveTarget?: ChannelOutboundAdapter["resolveTarget"];
}): ChannelOutboundAdapter {
return {
deliveryMode: params.deliveryMode ?? "direct",
...(params.preferFinalAssistantVisibleText !== undefined
? { preferFinalAssistantVisibleText: params.preferFinalAssistantVisibleText }
: {}),
...(params.resolveTarget ? { resolveTarget: params.resolveTarget } : {}),
sendText: async ({ cfg, to, text, accountId, deps }) =>
withRequiredMessageId(
@@ -239,7 +243,10 @@ describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
pluginId: "discord",
plugin: createOutboundTestPlugin({
id: "discord",
outbound: createCliDelegatingOutbound({ channel: "discord" }),
outbound: createCliDelegatingOutbound({
channel: "discord",
preferFinalAssistantVisibleText: true,
}),
}),
source: "test",
},
@@ -283,6 +290,31 @@ describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
});
});
if (testCase.channel === "discord") {
it("collapses Discord text-only announce delivery to the final assistant text", async () => {
await expectCoreChannelAnnounceDelivery({
testCase,
payloads: [{ text: "Working on it..." }, { text: "Final weather summary" }],
meta: {
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
finalAssistantVisibleText: "Final weather summary",
},
},
assertSend: (sendFn) => {
expect(sendFn).toHaveBeenCalledTimes(1);
expect(sendFn).toHaveBeenCalledWith(
testCase.expectedTo,
"Final weather summary",
expect.any(Object),
);
},
});
});
continue;
}
it(`preserves multi-payload text-only announce delivery for ${testCase.name} even when final assistant text exists`, async () => {
await expectCoreChannelAnnounceDelivery({
testCase,