mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 04:04:55 +00:00
fix(agents): keep cron media completions run-scoped
This commit is contained in:
committed by
Peter Steinberger
parent
9445960d9d
commit
f5d2db2a60
@@ -6,6 +6,7 @@ import { callGateway } from "../gateway/call.js";
|
||||
import { isEmbeddedMode } from "../infra/embedded-mode.js";
|
||||
import { getActiveSecretsRuntimeSnapshot } from "../secrets/runtime-state.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
|
||||
import { isCronRunSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
import type { GatewayMessageChannel } from "../utils/message-channel.js";
|
||||
import { resolveAgentWorkspaceDir, resolveSessionAgentIds } from "./agent-scope.js";
|
||||
@@ -201,6 +202,11 @@ export function createOpenClawTools(
|
||||
toolAllowlist: options?.pluginToolAllowlist,
|
||||
toolDenylist: options?.pluginToolDenylist,
|
||||
});
|
||||
const trimmedRunSessionKey = options?.runSessionKey?.trim();
|
||||
const mediaGenerationAgentSessionKey =
|
||||
trimmedRunSessionKey && isCronRunSessionKey(trimmedRunSessionKey)
|
||||
? trimmedRunSessionKey
|
||||
: options?.agentSessionKey;
|
||||
const imageToolAgentDir = options?.agentDir;
|
||||
const imageTool = resolveImageToolFactoryAvailable({
|
||||
config: availabilityConfig ?? resolvedConfig,
|
||||
@@ -226,7 +232,7 @@ export function createOpenClawTools(
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
authProfileStore: options?.authProfileStore,
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentSessionKey: mediaGenerationAgentSessionKey,
|
||||
requesterOrigin: deliveryContext ?? undefined,
|
||||
workspaceDir,
|
||||
sandbox,
|
||||
@@ -240,7 +246,7 @@ export function createOpenClawTools(
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
authProfileStore: options?.authProfileStore,
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentSessionKey: mediaGenerationAgentSessionKey,
|
||||
requesterOrigin: deliveryContext ?? undefined,
|
||||
workspaceDir,
|
||||
sandbox,
|
||||
@@ -254,7 +260,7 @@ export function createOpenClawTools(
|
||||
config: options?.config,
|
||||
agentDir: options?.agentDir,
|
||||
authProfileStore: options?.authProfileStore,
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
agentSessionKey: mediaGenerationAgentSessionKey,
|
||||
requesterOrigin: deliveryContext ?? undefined,
|
||||
workspaceDir,
|
||||
sandbox,
|
||||
|
||||
@@ -17,6 +17,9 @@ const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
stubTool,
|
||||
createCronToolOptions: vi.fn(),
|
||||
createImageGenerateToolOptions: vi.fn(),
|
||||
createMusicGenerateToolOptions: vi.fn(),
|
||||
createVideoGenerateToolOptions: vi.fn(),
|
||||
textToSpeech: vi.fn(async () => ({
|
||||
success: true,
|
||||
audioPath: "/tmp/openclaw/tts-config-test.opus",
|
||||
@@ -50,7 +53,10 @@ vi.mock("./tools/gateway-tool.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./tools/image-generate-tool.js", () => ({
|
||||
createImageGenerateTool: () => mocks.stubTool("image_generate"),
|
||||
createImageGenerateTool: (options: unknown) => {
|
||||
mocks.createImageGenerateToolOptions(options);
|
||||
return mocks.stubTool("image_generate");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./tools/image-tool.js", () => ({
|
||||
@@ -62,7 +68,10 @@ vi.mock("./tools/message-tool.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./tools/music-generate-tool.js", () => ({
|
||||
createMusicGenerateTool: () => mocks.stubTool("music_generate"),
|
||||
createMusicGenerateTool: (options: unknown) => {
|
||||
mocks.createMusicGenerateToolOptions(options);
|
||||
return mocks.stubTool("music_generate");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./tools/nodes-tool.js", () => ({
|
||||
@@ -106,7 +115,10 @@ vi.mock("./tools/update-plan-tool.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./tools/video-generate-tool.js", () => ({
|
||||
createVideoGenerateTool: () => mocks.stubTool("video_generate"),
|
||||
createVideoGenerateTool: (options: unknown) => {
|
||||
mocks.createVideoGenerateToolOptions(options);
|
||||
return mocks.stubTool("video_generate");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./tools/web-tools.js", () => ({
|
||||
@@ -134,6 +146,9 @@ function getTextToSpeechParams() {
|
||||
describe("createOpenClawTools TTS config wiring", () => {
|
||||
beforeEach(() => {
|
||||
mocks.createCronToolOptions.mockClear();
|
||||
mocks.createImageGenerateToolOptions.mockClear();
|
||||
mocks.createMusicGenerateToolOptions.mockClear();
|
||||
mocks.createVideoGenerateToolOptions.mockClear();
|
||||
mocks.textToSpeech.mockClear();
|
||||
});
|
||||
|
||||
@@ -266,6 +281,74 @@ describe("createOpenClawTools TTS config wiring", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createOpenClawTools media generation session wiring", () => {
|
||||
beforeEach(() => {
|
||||
mocks.createImageGenerateToolOptions.mockClear();
|
||||
mocks.createMusicGenerateToolOptions.mockClear();
|
||||
mocks.createVideoGenerateToolOptions.mockClear();
|
||||
});
|
||||
|
||||
it("uses the isolated cron run key for background media completions", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: { primary: "image-owner/model" },
|
||||
videoGenerationModel: { primary: "video-owner/model" },
|
||||
musicGenerationModel: { primary: "music-owner/model" },
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
createOpenClawTools({
|
||||
config,
|
||||
agentSessionKey: "agent:main:cron:daily-media",
|
||||
runSessionKey: "agent:main:cron:daily-media:run:run-123",
|
||||
disableMessageTool: true,
|
||||
disablePluginTools: true,
|
||||
});
|
||||
|
||||
expect(mocks.createImageGenerateToolOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "agent:main:cron:daily-media:run:run-123",
|
||||
}),
|
||||
);
|
||||
expect(mocks.createVideoGenerateToolOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "agent:main:cron:daily-media:run:run-123",
|
||||
}),
|
||||
);
|
||||
expect(mocks.createMusicGenerateToolOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "agent:main:cron:daily-media:run:run-123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the requester session key for non-cron media completions", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: { primary: "image-owner/model" },
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
createOpenClawTools({
|
||||
config,
|
||||
agentSessionKey: "agent:main:slack:channel:C123",
|
||||
runSessionKey: "agent:main:slack:channel:C123:run:run-123",
|
||||
disableMessageTool: true,
|
||||
disablePluginTools: true,
|
||||
});
|
||||
|
||||
expect(mocks.createImageGenerateToolOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentSessionKey: "agent:main:slack:channel:C123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createOpenClawTools cron context wiring", () => {
|
||||
beforeEach(() => {
|
||||
mocks.createCronToolOptions.mockClear();
|
||||
|
||||
@@ -2385,6 +2385,47 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("no-ops stale isolated cron run media completions", async () => {
|
||||
const callGateway = createGatewayMock();
|
||||
const sendMessage = createSendMessageMock();
|
||||
const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true);
|
||||
const result = await deliverSlackChannelAnnouncement({
|
||||
callGateway,
|
||||
sendMessage,
|
||||
queueEmbeddedPiMessageWithOutcome,
|
||||
sessionId: "stale-cron-run-session",
|
||||
isActive: false,
|
||||
requesterSessionKey: "agent:main:cron:daily-media:run:run-123",
|
||||
expectsCompletionMessage: true,
|
||||
directIdempotencyKey: "announce-stale-cron-media",
|
||||
sourceTool: "image_generate",
|
||||
internalEvents: [
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "image_generation",
|
||||
childSessionKey: "image_generate:task-123",
|
||||
childSessionId: "task-123",
|
||||
announceType: "image generation task",
|
||||
taskLabel: "daily media",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "Generated 1 image.\nMEDIA:/tmp/generated-daily.png",
|
||||
mediaUrls: ["/tmp/generated-daily.png"],
|
||||
replyInstruction: "Deliver the generated image through the requester run.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectRecordFields(result, {
|
||||
delivered: true,
|
||||
path: "none",
|
||||
phases: [{ phase: "direct-primary", delivered: true, path: "none", error: undefined }],
|
||||
});
|
||||
expect(queueEmbeddedPiMessageWithOutcome).not.toHaveBeenCalled();
|
||||
expect(callGateway).not.toHaveBeenCalled();
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "legacy Discord channel",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
isAgentMediatedCompletionSourceTool,
|
||||
shouldPreserveUserFacingSessionStateForInputProvenance,
|
||||
} from "../sessions/input-provenance.js";
|
||||
import { isCronSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { isCronRunSessionKey, isCronSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { isNonTerminalAgentRunStatus } from "../shared/agent-run-status.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { mergeDeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
@@ -908,6 +908,16 @@ async function sendSubagentAnnounceDirectly(params: {
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
params.expectsCompletionMessage &&
|
||||
isCronRunSessionKey(canonicalRequesterSessionKey) &&
|
||||
!resolveRequesterSessionActivity(canonicalRequesterSessionKey).isActive
|
||||
) {
|
||||
return {
|
||||
delivered: true,
|
||||
path: "none",
|
||||
};
|
||||
}
|
||||
if (params.signal?.aborted) {
|
||||
return {
|
||||
delivered: false,
|
||||
|
||||
Reference in New Issue
Block a user