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.
This commit is contained in:
Peter Steinberger 2026-09-28 22:46:22 -07:00 • committed by GitHub
parent 8634a33466
commit e61efb6cd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 9 deletions

View file

@ -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,

View file

@ -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 }),
);
}
});

View file

@ -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<unknown>;
}) {
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<void>> = [];
@ -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);