























@@ -17,6 +17,7 @@ const enqueueFollowupRunMock = vi.fn();
1717const scheduleFollowupDrainMock = vi.fn();
1818const refreshQueuedFollowupSessionMock = vi.fn();
1919const resolveOutboundAttachmentFromUrlMock = vi.fn();
20+const createReplyMediaPathNormalizerRuntimeMock = vi.fn();
20212122vi.mock("../../agents/model-fallback.js", () => ({
2223runWithModelFallback: (params: {
@@ -52,6 +53,18 @@ vi.mock("../../media/outbound-attachment.js", () => ({
5253resolveOutboundAttachmentFromUrlMock(...args),
5354}));
545556+// Spy on the .runtime import path used by agent-runner-execution.ts so we can assert
57+// that the fix prevents a second normalizer from being created inside runAgentTurnWithFallback.
58+vi.mock("./reply-media-paths.runtime.js", async (importOriginal) => {
59+const mod = await importOriginal<typeof import("./reply-media-paths.runtime.js")>();
60+return {
61+createReplyMediaPathNormalizer: (...args: Parameters<typeof mod.createReplyMediaPathNormalizer>) => {
62+createReplyMediaPathNormalizerRuntimeMock(...args);
63+return mod.createReplyMediaPathNormalizer(...args);
64+},
65+};
66+});
67+5568let runReplyAgent: typeof import("./agent-runner.js").runReplyAgent;
56695770describe("runReplyAgent media path normalization", () => {
@@ -73,6 +86,7 @@ describe("runReplyAgent media path normalization", () => {
7386scheduleFollowupDrainMock.mockReset();
7487refreshQueuedFollowupSessionMock.mockReset();
7588resolveOutboundAttachmentFromUrlMock.mockReset();
89+createReplyMediaPathNormalizerRuntimeMock.mockReset();
7690vi.stubEnv("OPENCLAW_TEST_FAST", "1");
7791resolveOutboundAttachmentFromUrlMock.mockImplementation(async (mediaUrl: string) => ({
7892path: path.join("/tmp/outbound-media", path.basename(mediaUrl)),
@@ -160,4 +174,69 @@ describe("runReplyAgent media path normalization", () => {
160174}),
161175);
162176});
177+178+it("does not create a second normalizer inside runAgentTurnWithFallback when onBlockReply is provided", async () => {
179+// Regression test for openclaw/openclaw#68056.
180+// Before the fix, runAgentTurnWithFallback always called createReplyMediaPathNormalizer
181+// from reply-media-paths.runtime.js to build its own normalizer instance — separate from
182+// the one agent-runner.ts created and passed to buildReplyPayloads. Two separate
183+// persistedMediaBySource caches meant the same source could be persisted twice (two UUID
184+// outbound files, two WhatsApp sends).
185+//
186+// After the fix, agent-runner.ts passes its normalizer into runAgentTurnWithFallback, so
187+// the .runtime import path is never called from inside that function.
188+runEmbeddedPiAgentMock.mockResolvedValue({
189+payloads: [],
190+meta: {
191+agentMeta: {
192+sessionId: "session",
193+provider: "anthropic",
194+model: "claude",
195+},
196+},
197+});
198+199+await runReplyAgent({
200+commandBody: "generate chart",
201+followupRun: createMockFollowupRun({
202+prompt: "generate chart",
203+run: {
204+agentId: "main",
205+agentDir: "/tmp/agent",
206+messageProvider: "whatsapp",
207+workspaceDir: "/tmp/workspace",
208+},
209+}) as unknown as FollowupRun,
210+queueKey: "main",
211+resolvedQueue: { mode: "interrupt" } as QueueSettings,
212+shouldSteer: false,
213+shouldFollowup: false,
214+isActive: false,
215+isStreaming: false,
216+typing: createMockTypingController(),
217+sessionCtx: {
218+Provider: "whatsapp",
219+Surface: "whatsapp",
220+To: "chat-1",
221+OriginatingTo: "chat-1",
222+AccountId: "default",
223+MessageSid: "msg-1",
224+} as unknown as TemplateContext,
225+defaultModel: "anthropic/claude",
226+resolvedVerboseLevel: "off",
227+isNewSession: false,
228+blockStreamingEnabled: false,
229+resolvedBlockStreamingBreak: "message_end",
230+shouldInjectGroupIntro: false,
231+typingMode: "instant",
232+opts: {
233+onBlockReply: vi.fn(),
234+},
235+});
236+237+// The .runtime import is only used by agent-runner-execution.ts. After the fix,
238+// runAgentTurnWithFallback receives the normalizer from the caller and never
239+// calls createReplyMediaPathNormalizer itself.
240+expect(createReplyMediaPathNormalizerRuntimeMock).not.toHaveBeenCalled();
241+});
163242});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。