fix(tasks): index async media tasks by agent

This commit is contained in:
Vincent Koc
2026-04-28 02:39:18 -07:00
parent a6bb0265f0
commit 210cccb0fe
5 changed files with 122 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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