mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(tasks): keep media tool runs live
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/media: register detached `video_generate` and `music_generate` tool run contexts until terminal status, so Discord-backed provider jobs stay live in `/tasks` instead of becoming `lost` when the parent chat run context disappears. Thanks @vincentkoc.
|
||||
- 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.
|
||||
- CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so `openclaw status --all` no longer reports a live gateway as unreachable after `missing scope: operator.read`. Fixes #49180; supersedes #47981. Thanks @openjay.
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
||||
import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { clearAgentRunContext, registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
|
||||
@@ -64,6 +65,13 @@ type WakeMediaGenerationTaskCompletionParams = {
|
||||
statsLine?: string;
|
||||
};
|
||||
|
||||
function touchMediaGenerationTaskRunContext(handle: MediaGenerationTaskHandle) {
|
||||
registerAgentRunContext(handle.runId, {
|
||||
sessionKey: handle.requesterSessionKey,
|
||||
lastActiveAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createMediaGenerationTaskRun(params: {
|
||||
sessionKey?: string;
|
||||
requesterOrigin?: DeliveryContext;
|
||||
@@ -98,13 +106,15 @@ export function createMediaGenerationTaskRun(params: {
|
||||
lastEventAt: Date.now(),
|
||||
progressSummary: params.queuedProgressSummary,
|
||||
});
|
||||
return {
|
||||
const handle = {
|
||||
taskId: task.taskId,
|
||||
runId,
|
||||
requesterSessionKey: sessionKey,
|
||||
requesterOrigin: params.requesterOrigin,
|
||||
taskLabel: params.prompt,
|
||||
};
|
||||
touchMediaGenerationTaskRunContext(handle);
|
||||
return handle;
|
||||
} catch (error) {
|
||||
log.warn("Failed to create media generation task ledger record", {
|
||||
sessionKey,
|
||||
@@ -124,6 +134,7 @@ export function recordMediaGenerationTaskProgress(params: {
|
||||
if (!params.handle) {
|
||||
return;
|
||||
}
|
||||
touchMediaGenerationTaskRunContext(params.handle);
|
||||
recordTaskRunProgressByRunId({
|
||||
runId: params.handle.runId,
|
||||
runtime: "cli",
|
||||
@@ -169,17 +180,21 @@ export function completeMediaGenerationTaskRun(params: {
|
||||
if (!params.handle) {
|
||||
return;
|
||||
}
|
||||
const endedAt = Date.now();
|
||||
const target = params.count === 1 ? params.paths[0] : `${params.count} files`;
|
||||
completeTaskRunByRunId({
|
||||
runId: params.handle.runId,
|
||||
runtime: "cli",
|
||||
sessionKey: params.handle.requesterSessionKey,
|
||||
endedAt,
|
||||
lastEventAt: endedAt,
|
||||
progressSummary: `Generated ${params.count} ${params.generatedLabel}${params.count === 1 ? "" : "s"}`,
|
||||
terminalSummary: `Generated ${params.count} ${params.generatedLabel}${params.count === 1 ? "" : "s"} with ${params.provider}/${params.model}${target ? ` -> ${target}` : ""}.`,
|
||||
});
|
||||
try {
|
||||
const endedAt = Date.now();
|
||||
const target = params.count === 1 ? params.paths[0] : `${params.count} files`;
|
||||
completeTaskRunByRunId({
|
||||
runId: params.handle.runId,
|
||||
runtime: "cli",
|
||||
sessionKey: params.handle.requesterSessionKey,
|
||||
endedAt,
|
||||
lastEventAt: endedAt,
|
||||
progressSummary: `Generated ${params.count} ${params.generatedLabel}${params.count === 1 ? "" : "s"}`,
|
||||
terminalSummary: `Generated ${params.count} ${params.generatedLabel}${params.count === 1 ? "" : "s"} with ${params.provider}/${params.model}${target ? ` -> ${target}` : ""}.`,
|
||||
});
|
||||
} finally {
|
||||
clearAgentRunContext(params.handle.runId);
|
||||
}
|
||||
}
|
||||
|
||||
export function failMediaGenerationTaskRun(params: {
|
||||
@@ -190,18 +205,22 @@ export function failMediaGenerationTaskRun(params: {
|
||||
if (!params.handle) {
|
||||
return;
|
||||
}
|
||||
const endedAt = Date.now();
|
||||
const errorText = formatErrorMessage(params.error);
|
||||
failTaskRunByRunId({
|
||||
runId: params.handle.runId,
|
||||
runtime: "cli",
|
||||
sessionKey: params.handle.requesterSessionKey,
|
||||
endedAt,
|
||||
lastEventAt: endedAt,
|
||||
error: errorText,
|
||||
progressSummary: params.progressSummary,
|
||||
terminalSummary: errorText,
|
||||
});
|
||||
try {
|
||||
const endedAt = Date.now();
|
||||
const errorText = formatErrorMessage(params.error);
|
||||
failTaskRunByRunId({
|
||||
runId: params.handle.runId,
|
||||
runtime: "cli",
|
||||
sessionKey: params.handle.requesterSessionKey,
|
||||
endedAt,
|
||||
lastEventAt: endedAt,
|
||||
error: errorText,
|
||||
progressSummary: params.progressSummary,
|
||||
terminalSummary: errorText,
|
||||
});
|
||||
} finally {
|
||||
clearAgentRunContext(params.handle.runId);
|
||||
}
|
||||
}
|
||||
|
||||
function buildMediaGenerationReplyInstruction(params: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getAgentRunContext, resetAgentRunContextForTest } from "../../infra/agent-events.js";
|
||||
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
|
||||
import {
|
||||
announceDeliveryMocks,
|
||||
@@ -18,6 +19,7 @@ vi.mock("../subagent-announce-delivery.js", () => announceDeliveryMocks);
|
||||
|
||||
const {
|
||||
createVideoGenerationTaskRun,
|
||||
failVideoGenerationTaskRun,
|
||||
recordVideoGenerationTaskProgress,
|
||||
wakeVideoGenerationTaskCompletion,
|
||||
} = await import("./video-generate-background.js");
|
||||
@@ -25,6 +27,7 @@ const { withMediaGenerationTaskKeepalive } = await import("./media-generate-back
|
||||
|
||||
describe("video generate background helpers", () => {
|
||||
beforeEach(() => {
|
||||
resetAgentRunContextForTest();
|
||||
resetMediaBackgroundMocks({
|
||||
taskExecutorMocks,
|
||||
taskDeliveryRuntimeMocks,
|
||||
@@ -34,6 +37,7 @@ describe("video generate background helpers", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("creates a running task with queued progress text", () => {
|
||||
@@ -82,6 +86,40 @@ describe("video generate background helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the detached video tool run context registered until terminal status", () => {
|
||||
taskExecutorMocks.createRunningTaskRun.mockReturnValue({
|
||||
taskId: "task-123",
|
||||
});
|
||||
|
||||
const handle = createVideoGenerationTaskRun({
|
||||
sessionKey: "agent:main:discord:channel:123",
|
||||
prompt: "friendly lobster surfing",
|
||||
providerId: "fal",
|
||||
});
|
||||
if (!handle) {
|
||||
throw new Error("expected video generation task handle");
|
||||
}
|
||||
|
||||
expect(handle.runId).toMatch(/^tool:video_generate:/);
|
||||
expect(getAgentRunContext(handle.runId)).toMatchObject({
|
||||
sessionKey: "agent:main:discord:channel:123",
|
||||
});
|
||||
|
||||
recordVideoGenerationTaskProgress({
|
||||
handle,
|
||||
progressSummary: "Generating video",
|
||||
});
|
||||
|
||||
expect(getAgentRunContext(handle.runId)?.lastActiveAt).toEqual(expect.any(Number));
|
||||
|
||||
failVideoGenerationTaskRun({
|
||||
handle,
|
||||
error: new Error("provider failed"),
|
||||
});
|
||||
|
||||
expect(getAgentRunContext(handle.runId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps long-running media tasks fresh while provider work is pending", async () => {
|
||||
vi.useFakeTimers();
|
||||
let resolveRun!: (value: string) => void;
|
||||
|
||||
@@ -106,6 +106,7 @@ describe("agent-events sequencing", () => {
|
||||
registerAgentRunContext("run-ctx", {
|
||||
verboseLevel: "full",
|
||||
isHeartbeat: true,
|
||||
lastActiveAt: 12_345,
|
||||
});
|
||||
|
||||
expect(getAgentRunContext("run-ctx")).toMatchObject({
|
||||
@@ -113,6 +114,7 @@ describe("agent-events sequencing", () => {
|
||||
verboseLevel: "full",
|
||||
isHeartbeat: true,
|
||||
isControlUiVisible: true,
|
||||
lastActiveAt: 12_345,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -159,6 +159,12 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
|
||||
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
||||
existing.isHeartbeat = context.isHeartbeat;
|
||||
}
|
||||
if (context.registeredAt !== undefined) {
|
||||
existing.registeredAt = context.registeredAt;
|
||||
}
|
||||
if (context.lastActiveAt !== undefined) {
|
||||
existing.lastActiveAt = context.lastActiveAt;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentRunContext(runId: string) {
|
||||
|
||||
@@ -341,6 +341,53 @@ describe("task-registry maintenance issue #60299", () => {
|
||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
||||
});
|
||||
|
||||
it("keeps detached media cli tasks live while their tool run context is active", async () => {
|
||||
const channelKey = "agent:main:discord:channel:1456744319972282449";
|
||||
const runId = "tool:video_generate:ac88dfc5-c2a9-4630-ab48-384e6450a12b";
|
||||
const task = makeStaleTask({
|
||||
runtime: "cli",
|
||||
taskKind: "video_generation",
|
||||
sourceId: "video_generate:fal",
|
||||
runId,
|
||||
ownerKey: channelKey,
|
||||
requesterSessionKey: channelKey,
|
||||
childSessionKey: channelKey,
|
||||
progressSummary: "Generating video",
|
||||
});
|
||||
|
||||
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||
tasks: [task],
|
||||
sessionStore: { [channelKey]: { sessionId: channelKey, updatedAt: Date.now() } },
|
||||
activeRunIds: [runId],
|
||||
});
|
||||
|
||||
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
||||
});
|
||||
|
||||
it("keeps recently refreshed media cli tasks live without a chat run context", async () => {
|
||||
const channelKey = "agent:main:discord:channel:1456744319972282449";
|
||||
const task = makeStaleTask({
|
||||
runtime: "cli",
|
||||
taskKind: "video_generation",
|
||||
sourceId: "video_generate:fal",
|
||||
runId: "tool:video_generate:3a948fb2-79e8-470c-a6bc-46f37732cd3d",
|
||||
ownerKey: channelKey,
|
||||
requesterSessionKey: channelKey,
|
||||
childSessionKey: channelKey,
|
||||
lastEventAt: Date.now() - 60_000,
|
||||
progressSummary: "Generating video",
|
||||
});
|
||||
|
||||
const { currentTasks } = createTaskRegistryMaintenanceHarness({
|
||||
tasks: [task],
|
||||
sessionStore: { [channelKey]: { sessionId: channelKey, updatedAt: Date.now() } },
|
||||
});
|
||||
|
||||
expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: 0 });
|
||||
expect(currentTasks.get(task.taskId)).toMatchObject({ status: "running" });
|
||||
});
|
||||
|
||||
it("skips markTaskLost and counts recovered when recovery hook recovers a stale task", async () => {
|
||||
const task = makeStaleTask({
|
||||
runtime: "cron",
|
||||
|
||||
Reference in New Issue
Block a user