From e61efb6cd8a79915041652238be00efbf50b0aa5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 28 Sep 2026 22:46:22 -0700 Subject: [PATCH] fix(agents): media generation blocks channel turns sharing the main session (#160998) Pass the durable runSessionKey to image/video/music tools so shared-main channel sessions detach media generation again (regression from #159179), and wait for the terminal image task state in the image-generation-lifecycle e2e. --- src/agents/openclaw-tools.ts | 6 +---- src/agents/openclaw-tools.tts-config.test.ts | 11 +++++---- .../image-generation-lifecycle.e2e.test.ts | 23 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index a8c27904da22..f1a221f7906a 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -146,10 +146,6 @@ export function createOpenClawTools(options?: OpenClawToolsOptions): AnyAgentToo }); const trimmedRunSessionKey = options?.runSessionKey?.trim(); const requesterSessionKey = trimmedRunSessionKey || options?.agentSessionKey; - const mediaGenerationAgentSessionKey = - trimmedRunSessionKey && isCronRunSessionKey(trimmedRunSessionKey) - ? trimmedRunSessionKey - : options?.agentSessionKey; const imageTool = options?.agentDir && resolveImageToolFactoryAvailable({ @@ -182,7 +178,7 @@ export function createOpenClawTools(options?: OpenClawToolsOptions): AnyAgentToo config: options?.config, agentDir: options?.agentDir, authProfileStore: options?.authProfileStore, - agentSessionKey: mediaGenerationAgentSessionKey, + agentSessionKey: requesterSessionKey, requesterAgentId: sessionAgentId, requesterOrigin: widgetPresentation.deliveryContext ?? undefined, workspaceDir, diff --git a/src/agents/openclaw-tools.tts-config.test.ts b/src/agents/openclaw-tools.tts-config.test.ts index 5f50c0c7069f..2adde6170cb6 100644 --- a/src/agents/openclaw-tools.tts-config.test.ts +++ b/src/agents/openclaw-tools.tts-config.test.ts @@ -180,16 +180,19 @@ describe("createOpenClawTools context wiring", () => { expect(onYield).not.toHaveBeenCalled(); }); - it("uses the isolated cron run key for background media completions", () => { + it.each([ + ["agent:main:cron:daily-media", "agent:main:cron:daily-media:run:run-123"], + ["agent:main:qa-channel:default:direct:media-requester", "agent:main:main"], + ])("uses the durable run key for background media from %s", (agentSessionKey, runSessionKey) => { createTools({ config: mediaConfig, - agentSessionKey: "agent:main:cron:daily-media", - runSessionKey: "agent:main:cron:daily-media:run:run-123", + agentSessionKey, + runSessionKey, onYield: vi.fn(), }); for (const factory of [mocks.image, mocks.video, mocks.music]) { expect(factory).toHaveBeenCalledWith( - expect.objectContaining({ agentSessionKey: "agent:main:cron:daily-media:run:run-123" }), + expect.objectContaining({ agentSessionKey: runSessionKey }), ); } }); diff --git a/test/e2e/qa-lab/media/image-generation-lifecycle.e2e.test.ts b/test/e2e/qa-lab/media/image-generation-lifecycle.e2e.test.ts index ec869f5c51d1..14d85be9e295 100644 --- a/test/e2e/qa-lab/media/image-generation-lifecycle.e2e.test.ts +++ b/test/e2e/qa-lab/media/image-generation-lifecycle.e2e.test.ts @@ -21,6 +21,8 @@ const IMAGE_MODEL_REF = "openai/gpt-image-1"; const REQUEST_TEXT = "Image generation check IMAGE_TASK_LIFECYCLE: generate the QA lighthouse image."; const CONVERSATION = { id: "image-generation-lifecycle", kind: "direct" as const }; +// QA direct chats share the agent main session, which owns the media task. +const REQUESTER_SESSION_KEY = "agent:qa:main"; async function readRequestBody(request: IncomingMessage) { const chunks: Buffer[] = []; @@ -156,6 +158,17 @@ async function waitForToolOutput(baseUrl: string, needle: string) { return matched as MockOpenAiRequestSnapshot; } +async function readImageTaskStatus(gateway: { + call: (method: string, params: unknown) => Promise; +}) { + const result = (await gateway.call("tools.invoke", { + name: "image_generate", + args: { action: "status" }, + sessionKey: REQUESTER_SESSION_KEY, + })) as { ok?: boolean; output?: unknown; result?: unknown }; + return JSON.stringify(result); +} + describe("image generation task lifecycle through QA-channel", () => { const cleanups: Array<() => Promise> = []; @@ -225,6 +238,7 @@ describe("image generation task lifecycle through QA-channel", () => { )?.[1]; expect(runningReceipt).toEqual(expect.any(String)); expect(imageProvider.requests).toHaveLength(1); + expect(await readImageTaskStatus(gateway)).toContain(runningReceipt); imageProvider.release(); await vi.waitFor( () => { @@ -263,6 +277,15 @@ describe("image generation task lifecycle through QA-channel", () => { ); expect(completionReentry.allInputText).toContain("sourceChannel=internal"); + // Completion delivery precedes the terminal task state; wait for it before the next duplicate. + await vi.waitFor( + async () => + expect(await readImageTaskStatus(gateway)).toContain( + "No active image generation task is currently running", + ), + { interval: 50, timeout: 30_000 }, + ); + await sendExactRequest(); const completedDuplicate = await waitForToolOutput(mock.baseUrl, "recently succeeded"); expect(completedDuplicate.toolOutput).toContain(runningReceipt);