From 1fcf0a422f9b53c42dffc8defe46852b799391f3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 10:59:34 +0100 Subject: [PATCH] fix(agents): keep media generation tasks fresh --- CHANGELOG.md | 1 + .../tools/media-generate-background-shared.ts | 25 +++++++++++ src/agents/tools/music-generate-tool.ts | 32 ++++++++------ .../tools/video-generate-background.test.ts | 43 ++++++++++++++++++- src/agents/tools/video-generate-tool.ts | 42 ++++++++++-------- 5 files changed, 111 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09cc00ffbd2..e805e2b52c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Tasks/media: infer agent ownership for session-scoped task records so `/tasks` agent-local fallback includes session-backed `video_generate` and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc. +- Agents/media: keep long-running `video_generate` and `music_generate` tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc. - Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc. - Slack/auto-reply: keep fully consumed text reset triggers such as `new session` out of `BodyForAgent` after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana. - Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc. diff --git a/src/agents/tools/media-generate-background-shared.ts b/src/agents/tools/media-generate-background-shared.ts index 9655b3bf93c..5b279024a0c 100644 --- a/src/agents/tools/media-generate-background-shared.ts +++ b/src/agents/tools/media-generate-background-shared.ts @@ -18,6 +18,7 @@ import { formatAgentInternalEventsForPrompt, type AgentInternalEvent } from "../ import { deliverSubagentAnnouncement } from "../subagent-announce-delivery.js"; const log = createSubsystemLogger("agents/tools/media-generate-background-shared"); +const MEDIA_GENERATION_TASK_KEEPALIVE_INTERVAL_MS = 60_000; export type MediaGenerationTaskHandle = { taskId: string; @@ -133,6 +134,30 @@ export function recordMediaGenerationTaskProgress(params: { }); } +export async function withMediaGenerationTaskKeepalive(params: { + handle: MediaGenerationTaskHandle | null; + progressSummary: string; + eventSummary?: string; + run: () => Promise; +}): Promise { + if (!params.handle) { + return await params.run(); + } + const interval = setInterval(() => { + recordMediaGenerationTaskProgress({ + handle: params.handle, + progressSummary: params.progressSummary, + eventSummary: params.eventSummary, + }); + }, MEDIA_GENERATION_TASK_KEEPALIVE_INTERVAL_MS); + interval.unref?.(); + try { + return await params.run(); + } finally { + clearInterval(interval); + } +} + export function completeMediaGenerationTaskRun(params: { handle: MediaGenerationTaskHandle | null; provider: string; diff --git a/src/agents/tools/music-generate-tool.ts b/src/agents/tools/music-generate-tool.ts index df2f25f03f8..f2052ab2b19 100644 --- a/src/agents/tools/music-generate-tool.ts +++ b/src/agents/tools/music-generate-tool.ts @@ -28,6 +28,7 @@ import type { DeliveryContext } from "../../utils/delivery-context.js"; import { buildTimeoutAbortSignal } from "../../utils/fetch-timeout.js"; import { ToolInputError, readNumberParam, readStringParam } from "./common.js"; import { decodeDataUrl } from "./image-tool.helpers.js"; +import { withMediaGenerationTaskKeepalive } from "./media-generate-background-shared.js"; import { applyMusicGenerationModelConfigDefaults, buildMediaReferenceDetails, @@ -586,19 +587,24 @@ export function createMusicGenerateTool(options?: { if (shouldDetach) { scheduleBackgroundWork(async () => { try { - const executed = await executeMusicGenerationJob({ - effectiveCfg, - prompt, - agentDir: options?.agentDir, - model, - lyrics, - instrumental, - durationSeconds, - format, - filename, - loadedReferenceImages, - taskHandle, - timeoutMs, + const executed = await withMediaGenerationTaskKeepalive({ + handle: taskHandle, + progressSummary: "Generating music", + run: () => + executeMusicGenerationJob({ + effectiveCfg, + prompt, + agentDir: options?.agentDir, + model, + lyrics, + instrumental, + durationSeconds, + format, + filename, + loadedReferenceImages, + taskHandle, + timeoutMs, + }), }); completeMusicGenerationTaskRun({ handle: taskHandle, diff --git a/src/agents/tools/video-generate-background.test.ts b/src/agents/tools/video-generate-background.test.ts index 480b32dc62f..60912274731 100644 --- a/src/agents/tools/video-generate-background.test.ts +++ b/src/agents/tools/video-generate-background.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js"; import { announceDeliveryMocks, @@ -21,6 +21,7 @@ const { recordVideoGenerationTaskProgress, wakeVideoGenerationTaskCompletion, } = await import("./video-generate-background.js"); +const { withMediaGenerationTaskKeepalive } = await import("./media-generate-background-shared.js"); describe("video generate background helpers", () => { beforeEach(() => { @@ -31,6 +32,10 @@ describe("video generate background helpers", () => { }); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("creates a running task with queued progress text", () => { taskExecutorMocks.createRunningTaskRun.mockReturnValue({ taskId: "task-123", @@ -77,6 +82,42 @@ describe("video generate background helpers", () => { }); }); + it("keeps long-running media tasks fresh while provider work is pending", async () => { + vi.useFakeTimers(); + let resolveRun!: (value: string) => void; + const runPromise = new Promise((resolve) => { + resolveRun = resolve; + }); + const task = withMediaGenerationTaskKeepalive({ + handle: { + taskId: "task-123", + runId: "tool:video_generate:abc", + requesterSessionKey: "agent:main:discord:direct:123", + taskLabel: "friendly lobster surfing", + }, + progressSummary: "Generating video", + run: () => runPromise, + }); + + await vi.advanceTimersByTimeAsync(60_000); + + expectRecordedTaskProgress({ + taskExecutorMocks, + runId: "tool:video_generate:abc", + progressSummary: "Generating video", + }); + + resolveRun("done"); + await expect(task).resolves.toBe("done"); + const callsAfterCompletion = taskExecutorMocks.recordTaskRunProgressByRunId.mock.calls.length; + + await vi.advanceTimersByTimeAsync(60_000); + + expect(taskExecutorMocks.recordTaskRunProgressByRunId).toHaveBeenCalledTimes( + callsAfterCompletion, + ); + }); + it("queues a completion event by default when direct send is disabled", async () => { announceDeliveryMocks.deliverSubagentAnnouncement.mockResolvedValue({ delivered: true, diff --git a/src/agents/tools/video-generate-tool.ts b/src/agents/tools/video-generate-tool.ts index b8fc823f820..e18c85ebddf 100644 --- a/src/agents/tools/video-generate-tool.ts +++ b/src/agents/tools/video-generate-tool.ts @@ -31,6 +31,7 @@ import type { } from "../../video-generation/types.js"; import { ToolInputError, readNumberParam, readStringParam } from "./common.js"; import { decodeDataUrl } from "./image-tool.helpers.js"; +import { withMediaGenerationTaskKeepalive } from "./media-generate-background-shared.js"; import { applyVideoGenerationModelConfigDefaults, buildMediaReferenceDetails, @@ -982,24 +983,29 @@ export function createVideoGenerateTool(options?: { if (shouldDetach) { scheduleBackgroundWork(async () => { try { - const executed = await executeVideoGenerationJob({ - effectiveCfg, - prompt, - agentDir: options?.agentDir, - model, - size, - aspectRatio, - resolution, - durationSeconds, - audio, - watermark, - filename, - loadedReferenceImages, - loadedReferenceVideos, - loadedReferenceAudios, - taskHandle, - providerOptions, - timeoutMs, + const executed = await withMediaGenerationTaskKeepalive({ + handle: taskHandle, + progressSummary: "Generating video", + run: () => + executeVideoGenerationJob({ + effectiveCfg, + prompt, + agentDir: options?.agentDir, + model, + size, + aspectRatio, + resolution, + durationSeconds, + audio, + watermark, + filename, + loadedReferenceImages, + loadedReferenceVideos, + loadedReferenceAudios, + taskHandle, + providerOptions, + timeoutMs, + }), }); completeVideoGenerationTaskRun({ handle: taskHandle,