惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
Jina AI
Jina AI
罗磊的独立博客
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
腾讯CDC
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
I
InfoQ
月光博客
月光博客
博客园_首页
Vercel News
Vercel News
P
Proofpoint News Feed
GbyAI
GbyAI
Y
Y Combinator Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(discord): fail dropped final reply delivery · opencla...
Patrick-Eric · 2026-05-05 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai

6969

- Plugins/runtime-deps: include `json5` in the memory-core plugin runtime dependency set so packaged `memory_search` sandboxes can resolve generated OpenClaw runtime chunks that parse JSON5 config. Fixes #77461.

7070

- Codex harness: preserve app-server usage-limit reset details and deliver OpenClaw-owned runtime failure notices through tool-only source-reply mode, so Telegram and other chat channels tell users when Codex subscription limits or API failures block a turn instead of going silent. (#77557) Thanks @pashpashpash.

7171

- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.

72+

- Discord/replies: treat failed final reply delivery as a failed turn instead of counting it as a delivered automatic visible reply, so guild/channel turns no longer show done when the final message was dropped. Fixes #77520.

7273

- Discord: prefer IPv4 for Discord REST and gateway WebSocket startup paths so IPv4-only networks no longer stall before Gateway READY and inbound message dispatch. Fixes #77398; refs #77526. Thanks @Beandon13.

7374

- Channels/plugins: key bundled package-state probes, env/config presence, and read-only command defaults by channel id instead of manifest plugin id, preserving setup and native-command detection for channel plugins whose package id differs from the channel alias. Thanks @vincentkoc.

7475

- Docker: prune package-excluded plugin dist directories from runtime images unless the build explicitly opts that plugin in, so official external plugins such as Feishu stay install-on-demand instead of shipping partial metadata without compiled runtime output. Fixes #77424. Thanks @vincentkoc.

Original file line numberDiff line numberDiff line change

@@ -139,9 +139,11 @@ type DispatchInboundParams = {

139139

};

140140

const dispatchInboundMessage = vi.hoisted(() =>

141141

vi.fn<

142-

(

143-

params?: DispatchInboundParams,

144-

) => Promise<{ queuedFinal: boolean; counts: { final: number; tool: number; block: number } }>

142+

(params?: DispatchInboundParams) => Promise<{

143+

queuedFinal: boolean;

144+

counts: { final: number; tool: number; block: number };

145+

failedCounts?: { final?: number; tool?: number; block?: number };

146+

}>

145147

>(async (_params?: DispatchInboundParams) => ({

146148

queuedFinal: false,

147149

counts: { final: 0, tool: 0, block: 0 },

@@ -621,6 +623,22 @@ describe("processDiscordMessage ack reactions", () => {

621623

expect(emojis).not.toContain(DEFAULT_EMOJIS.coding);

622624

});

623625
626+

it("marks automatic visible replies as failed when final Discord delivery fails", async () => {

627+

dispatchInboundMessage.mockResolvedValueOnce({

628+

queuedFinal: false,

629+

counts: { final: 0, tool: 0, block: 0 },

630+

failedCounts: { final: 1 },

631+

});

632+
633+

const ctx = await createAutomaticSourceDeliveryContext();

634+
635+

await runProcessDiscordMessage(ctx);

636+
637+

const emojis = getReactionEmojis();

638+

expect(emojis).toContain(DEFAULT_EMOJIS.error);

639+

expect(emojis).not.toContain(DEFAULT_EMOJIS.done);

640+

});

641+
624642

it("can bind status reactions to an explicitly tracked reaction target", async () => {

625643

vi.useFakeTimers();

626644

dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {

Original file line numberDiff line numberDiff line change

@@ -802,6 +802,7 @@ export async function processDiscordMessage(

802802

markDispatchIdle();

803803

}

804804

}

805+

const finalDeliveryFailed = (dispatchResult?.failedCounts?.final ?? 0) > 0;

805806

if (statusReactionsActive) {

806807

if (dispatchAborted) {

807808

if (removeAckAfterReply) {

@@ -810,14 +811,18 @@ export async function processDiscordMessage(

810811

void statusReactions.restoreInitial();

811812

}

812813

} else {

813-

if (dispatchError) {

814+

if (dispatchError || finalDeliveryFailed) {

814815

await statusReactions.setError();

815816

} else {

816817

await statusReactions.setDone();

817818

}

818819

if (removeAckAfterReply) {

819820

void (async () => {

820-

await sleep(dispatchError ? DEFAULT_TIMING.errorHoldMs : DEFAULT_TIMING.doneHoldMs);

821+

await sleep(

822+

dispatchError || finalDeliveryFailed

823+

? DEFAULT_TIMING.errorHoldMs

824+

: DEFAULT_TIMING.doneHoldMs,

825+

);

821826

await statusReactions.clear();

822827

})();

823828

} else {

Original file line numberDiff line numberDiff line change

@@ -3,7 +3,9 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";

33

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

44

import type { RequestClient } from "../internal/discord.js";

55
6-

const deliverOutboundPayloadsMock = vi.hoisted(() => vi.fn(async () => []));

6+

const deliverOutboundPayloadsMock = vi.hoisted(() =>

7+

vi.fn(async () => [{ messageId: "msg-1", channelId: "channel-1" }]),

8+

);

79

const sendMessageDiscordMock = vi.hoisted(() => vi.fn());

810

const sendVoiceMessageDiscordMock = vi.hoisted(() => vi.fn());

911

@@ -57,7 +59,7 @@ describe("deliverDiscordReply", () => {

5759
5860

beforeEach(() => {

5961

deliverOutboundPayloadsMock.mockClear();

60-

deliverOutboundPayloadsMock.mockResolvedValue([]);

62+

deliverOutboundPayloadsMock.mockResolvedValue([{ messageId: "msg-1", channelId: "channel-1" }]);

6163

sendMessageDiscordMock.mockReset().mockResolvedValue({

6264

messageId: "msg-1",

6365

channelId: "channel-1",

@@ -105,6 +107,22 @@ describe("deliverDiscordReply", () => {

105107

);

106108

});

107109
110+

it("fails when shared outbound accepts a final reply but delivers no Discord message", async () => {

111+

deliverOutboundPayloadsMock.mockResolvedValueOnce([]);

112+
113+

await expect(

114+

deliverDiscordReply({

115+

replies: [{ text: "lost reply" }],

116+

target: "channel:101",

117+

token: "token",

118+

accountId: "default",

119+

runtime,

120+

cfg,

121+

textLimit: 2000,

122+

}),

123+

).rejects.toThrow("discord final reply produced no delivered message for channel:101");

124+

});

125+
108126

it("strips internal execution trace lines at the final Discord send boundary", async () => {

109127

await deliverDiscordReply({

110128

replies: [

Original file line numberDiff line numberDiff line change

@@ -181,7 +181,7 @@ export async function deliverDiscordReply(params: {

181181

return;

182182

}

183183
184-

await deliverOutboundPayloads({

184+

const results = await deliverOutboundPayloads({

185185

cfg: params.cfg,

186186

channel: "discord",

187187

to: delivery.to,

@@ -205,4 +205,7 @@ export async function deliverDiscordReply(params: {

205205

requesterAccountId: params.accountId,

206206

}),

207207

});

208+

if (results.length === 0) {

209+

throw new Error(`discord final reply produced no delivered message for ${delivery.to}`);

210+

}

208211

}

Original file line numberDiff line numberDiff line change

@@ -283,6 +283,36 @@ describe("withReplyDispatcher", () => {

283283

});

284284

});

285285
286+

it("reconciles queuedFinal and counts after dispatcher-side delivery failure", async () => {

287+

const dispatcher = {

288+

sendToolResult: () => true,

289+

sendBlockReply: () => true,

290+

sendFinalReply: () => true,

291+

getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),

292+

getCancelledCounts: () => ({ tool: 0, block: 0, final: 0 }),

293+

getFailedCounts: () => ({ tool: 0, block: 0, final: 1 }),

294+

markComplete: () => undefined,

295+

waitForIdle: async () => undefined,

296+

} satisfies ReplyDispatcher;

297+

hoisted.dispatchReplyFromConfigMock.mockResolvedValueOnce({

298+

queuedFinal: true,

299+

counts: { tool: 0, block: 0, final: 1 },

300+

});

301+
302+

const result = await dispatchInboundMessage({

303+

ctx: buildTestCtx(),

304+

cfg: {} as OpenClawConfig,

305+

dispatcher,

306+

replyResolver: async () => ({ text: "ok" }),

307+

});

308+
309+

expect(result).toEqual({

310+

queuedFinal: false,

311+

counts: { tool: 0, block: 0, final: 0 },

312+

failedCounts: { tool: 0, block: 0, final: 1 },

313+

});

314+

});

315+
286316

it("uses CommandTargetSessionKey for silent-reply policy on native command turns", async () => {

287317

hoisted.createReplyDispatcherWithTypingMock.mockReturnValueOnce({

288318

dispatcher: createDispatcher([]),

Original file line numberDiff line numberDiff line change

@@ -103,19 +103,36 @@ function finalizeDispatchResult(

103103

dispatcher: ReplyDispatcher,

104104

): DispatchFromConfigResult {

105105

const cancelledCounts = dispatcher.getCancelledCounts?.();

106-

if (!cancelledCounts) {

106+

const failedCounts = dispatcher.getFailedCounts?.();

107+

if (!cancelledCounts && !failedCounts) {

107108

return result;

108109

}

109110
111+

const resultCounts = {

112+

tool: result.counts?.tool ?? 0,

113+

block: result.counts?.block ?? 0,

114+

final: result.counts?.final ?? 0,

115+

};

110116

const counts = {

111-

tool: Math.max(0, result.counts.tool - cancelledCounts.tool),

112-

block: Math.max(0, result.counts.block - cancelledCounts.block),

113-

final: Math.max(0, result.counts.final - cancelledCounts.final),

117+

tool: Math.max(0, resultCounts.tool - (cancelledCounts?.tool ?? 0) - (failedCounts?.tool ?? 0)),

118+

block: Math.max(

119+

0,

120+

resultCounts.block - (cancelledCounts?.block ?? 0) - (failedCounts?.block ?? 0),

121+

),

122+

final: Math.max(

123+

0,

124+

resultCounts.final - (cancelledCounts?.final ?? 0) - (failedCounts?.final ?? 0),

125+

),

114126

};

127+

const hasFailedCounts =

128+

(failedCounts?.tool ?? 0) > 0 ||

129+

(failedCounts?.block ?? 0) > 0 ||

130+

(failedCounts?.final ?? 0) > 0;

115131

return {

116132

...result,

117133

queuedFinal: result.queuedFinal && counts.final > 0,

118134

counts,

135+

...(hasFailedCounts ? { failedCounts } : {}),

119136

};

120137

}

121138
Original file line numberDiff line numberDiff line change

@@ -8,6 +8,7 @@ import type { ReplyDispatchKind, ReplyDispatcher } from "./reply-dispatcher.type

88

export type DispatchFromConfigResult = {

99

queuedFinal: boolean;

1010

counts: Record<ReplyDispatchKind, number>;

11+

failedCounts?: Partial<Record<ReplyDispatchKind, number>>;

1112

sourceReplyDeliveryMode?: SourceReplyDeliveryMode;

1213

};

1314