diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d47ff5e976..b3230fd5510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,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. - 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/auto-reply/reply/commands-tasks.test.ts b/src/auto-reply/reply/commands-tasks.test.ts index c1bd5d27c36..a12ea1bc291 100644 --- a/src/auto-reply/reply/commands-tasks.test.ts +++ b/src/auto-reply/reply/commands-tasks.test.ts @@ -84,6 +84,29 @@ describe("buildTasksReply", () => { expect(reply.text).toContain("approval denied"); }); + it("lists session-backed video generation tasks for the current session", async () => { + createRunningTaskRun({ + runtime: "cli", + taskKind: "video_generation", + sourceId: "video_generate:openai", + requesterSessionKey: "agent:main:main", + childSessionKey: "agent:main:main", + runId: "tool:video_generate:tasks-visible", + label: "Video generation", + task: "friendly lobster surfing", + progressSummary: "Queued video generation", + deliveryStatus: "not_applicable", + notifyPolicy: "silent", + }); + + const reply = await buildTasksReplyForTest(); + + expect(reply.text).toContain("Current session: 1 active · 1 total"); + expect(reply.text).toContain("🟢 Video generation"); + expect(reply.text).toContain("CLI · running"); + expect(reply.text).toContain("Queued video generation"); + }); + it("sanitizes leaked internal runtime context from visible task details", async () => { createRunningTaskRun({ runtime: "acp", @@ -184,6 +207,31 @@ describe("buildTasksReply", () => { expect(reply.text).not.toContain("hidden progress detail"); }); + it("counts session-backed video generation tasks in agent-local fallback", async () => { + createRunningTaskRun({ + runtime: "cli", + taskKind: "video_generation", + sourceId: "video_generate:openai", + requesterSessionKey: "agent:main:other-session", + childSessionKey: "agent:main:other-session", + runId: "tool:video_generate:tasks-agent-fallback", + label: "Video generation", + task: "hidden video background task", + progressSummary: "Queued video generation", + deliveryStatus: "not_applicable", + notifyPolicy: "silent", + }); + + const reply = await buildTasksReplyForTest({ + sessionKey: "agent:main:empty-session", + }); + + expect(reply.text).toContain("All clear - nothing linked to this session right now."); + expect(reply.text).toContain("Agent-local: 1 active · 1 total"); + expect(reply.text).not.toContain("hidden video background task"); + expect(reply.text).not.toContain("Queued video generation"); + }); + it("uses the canonical target session agent for agent-local fallback counts", async () => { createRunningTaskRun({ runtime: "subagent", diff --git a/src/plugins/captured-registration.test.ts b/src/plugins/captured-registration.test.ts index 8deab71f2e5..7d7aaed08e8 100644 --- a/src/plugins/captured-registration.test.ts +++ b/src/plugins/captured-registration.test.ts @@ -18,6 +18,30 @@ describe("captured plugin registration", () => { label: "Captured Provider", auth: [], }); + api.registerVideoGenerationProvider({ + id: "captured-video", + label: "Captured Video", + defaultModel: "captured-video-model", + capabilities: { + generate: { maxVideos: 1 }, + }, + generateVideo: async () => ({ + provider: "captured-video", + model: "captured-video-model", + videos: [], + }), + }); + api.registerMusicGenerationProvider({ + id: "captured-music", + label: "Captured Music", + defaultModel: "captured-music-model", + capabilities: { + generate: { maxTracks: 1 }, + }, + generateMusic: async () => ({ + tracks: [], + }), + }); api.registerTextTransforms({ input: [{ from: /red basket/g, to: "blue basket" }], output: [{ from: /blue basket/g, to: "red basket" }], @@ -54,6 +78,12 @@ describe("captured plugin registration", () => { expect(captured.tools.map((tool) => tool.name)).toEqual(["captured-tool"]); expect(captured.providers.map((provider) => provider.id)).toEqual(["captured-provider"]); + expect(captured.videoGenerationProviders.map((provider) => provider.id)).toEqual([ + "captured-video", + ]); + expect(captured.musicGenerationProviders.map((provider) => provider.id)).toEqual([ + "captured-music", + ]); expect(captured.textTransforms).toHaveLength(1); expect(captured.textTransforms[0]?.input).toHaveLength(1); expect(captured.agentToolResultMiddlewares).toHaveLength(1); diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index f01503662c2..8cdacb36460 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -25,6 +25,7 @@ import { getTaskById, getTaskRegistrySummary, isParentFlowLinkError, + listTasksForAgentId, listTasksForOwnerKey, listTaskRecords, linkTaskToFlowById, @@ -1407,6 +1408,29 @@ describe("task-registry", () => { }); }); + it("infers agent ids for session-scoped tasks", async () => { + await withTaskRegistryTempDir(async (root) => { + process.env.OPENCLAW_STATE_DIR = root; + resetTaskRegistryForTests({ persist: false }); + + const created = createTaskRecord({ + runtime: "cli", + taskKind: "video_generation", + sourceId: "video_generate:openai", + requesterSessionKey: "agent:main:discord:direct:123", + childSessionKey: "agent:main:discord:direct:123", + runId: "tool:video_generate:agent-index", + task: "Generate a lobster video", + status: "running", + deliveryStatus: "not_applicable", + notifyPolicy: "silent", + }); + + expect(created.agentId).toBe("main"); + expect(listTasksForAgentId("main").map((task) => task.taskId)).toEqual([created.taskId]); + }); + }); + it("projects inspection-time orphaned tasks as lost without mutating the registry", async () => { await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; diff --git a/src/tasks/task-registry.ts b/src/tasks/task-registry.ts index c036480a1e7..45de0684c81 100644 --- a/src/tasks/task-registry.ts +++ b/src/tasks/task-registry.ts @@ -813,6 +813,18 @@ function mergeExistingTaskForCreate( return updateTask(existing.taskId, patch) ?? cloneTaskRecord(existing); } +function resolveTaskAgentId(params: { + explicitAgentId?: string; + ownerKey: string; + requesterSessionKey: string; +}): string | undefined { + return ( + normalizeOptionalString(params.explicitAgentId) ?? + parseAgentSessionKey(params.ownerKey)?.agentId ?? + parseAgentSessionKey(params.requesterSessionKey)?.agentId + ); +} + function taskTerminalDeliveryIdempotencyKey(task: TaskRecord): string { const outcome = task.status === "succeeded" ? (task.terminalOutcome ?? "default") : "default"; return `task-terminal:${task.taskId}:${task.status}:${outcome}`; @@ -1493,6 +1505,11 @@ export function createTaskRecord(params: { requesterSessionKey, ownerKey: params.ownerKey, }); + const agentId = resolveTaskAgentId({ + explicitAgentId: params.agentId, + ownerKey, + requesterSessionKey, + }); assertTaskOwner({ ownerKey, scopeKind, @@ -1513,7 +1530,7 @@ export function createTaskRecord(params: { task: params.task, }); if (existing) { - return mergeExistingTaskForCreate(existing, params); + return mergeExistingTaskForCreate(existing, { ...params, agentId }); } const now = Date.now(); const taskId = crypto.randomUUID(); @@ -1542,7 +1559,7 @@ export function createTaskRecord(params: { childSessionKey: params.childSessionKey, parentFlowId: normalizeOptionalString(params.parentFlowId), parentTaskId: normalizeOptionalString(params.parentTaskId), - agentId: normalizeOptionalString(params.agentId), + agentId, runId: normalizeOptionalString(params.runId), label: normalizeOptionalString(params.label), task: params.task,