fix(agents): keep media generation tasks fresh

This commit is contained in:
Peter Steinberger
2026-04-28 10:59:34 +01:00
parent 9da76c4255
commit 1fcf0a422f
5 changed files with 111 additions and 32 deletions

View File

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

View File

@@ -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<T>(params: {
handle: MediaGenerationTaskHandle | null;
progressSummary: string;
eventSummary?: string;
run: () => Promise<T>;
}): Promise<T> {
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;

View File

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

View File

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

View File

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