mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: fallback after active media wake failure (#85489)
* fix: fallback after active media wake failure * docs: clarify generated media fallback docs
This commit is contained in:
@@ -102,7 +102,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
|
||||
<Accordion title="Notify defaults for cron and media">
|
||||
Main-session cron tasks use `silent` notify policy by default - they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
|
||||
|
||||
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
|
||||
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Concurrent media-generation guardrail">
|
||||
|
||||
@@ -12,9 +12,10 @@ The `image_generate` tool lets the agent create and edit images using your
|
||||
configured providers. In chat sessions, image generation runs asynchronously:
|
||||
OpenClaw records a background task, returns the task id immediately, and wakes
|
||||
the agent when the provider finishes. The completion agent must send generated
|
||||
images through the `message` tool. If the requester session is inactive and
|
||||
some generated images are still missing from message-tool delivery, OpenClaw
|
||||
sends an idempotent direct fallback with only the missing images.
|
||||
images through the `message` tool. If the requester session is inactive or
|
||||
its active wake fails, and some generated images are still missing from
|
||||
message-tool delivery, OpenClaw sends an idempotent direct fallback with only
|
||||
the missing images.
|
||||
|
||||
<Note>
|
||||
The tool only appears when at least one image-generation provider is
|
||||
|
||||
@@ -99,9 +99,10 @@ id immediately, and tracks the job in the task ledger. The agent continues
|
||||
responding to other messages while the job runs. When the provider finishes,
|
||||
OpenClaw wakes the agent with the generated media paths so it can tell the
|
||||
user and relay the result through the message tool. If the requester session
|
||||
is inactive and some generated media is still missing from message-tool
|
||||
delivery, OpenClaw sends an idempotent direct fallback with only the missing
|
||||
media. Media already delivered through the message tool is not posted again.
|
||||
is inactive or its active wake fails, and some generated media is still
|
||||
missing from message-tool delivery, OpenClaw sends an idempotent direct
|
||||
fallback with only the missing media. Media already delivered through the
|
||||
message tool is not posted again.
|
||||
|
||||
## Speech-to-text and Voice Call
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ For session-backed agent runs, OpenClaw starts music generation as a
|
||||
background task, tracks it in the task ledger, then wakes the agent again
|
||||
when the track is ready so the agent can tell the user and attach the
|
||||
finished audio. Generated-media completions are delivered by the agent through
|
||||
the message tool. If the requester session is inactive and some generated
|
||||
audio is still missing from message-tool delivery, OpenClaw sends an
|
||||
idempotent direct fallback with only the missing audio. The completion wake
|
||||
explicitly warns the agent that normal final replies are private for this
|
||||
route.
|
||||
the message tool. If the requester session is inactive or its active wake
|
||||
fails, and some generated audio is still missing from message-tool delivery,
|
||||
OpenClaw sends an idempotent direct fallback with only the missing audio. The
|
||||
completion wake explicitly warns the agent that normal final replies are
|
||||
private for this route.
|
||||
|
||||
<Note>
|
||||
The built-in shared tool only appears when at least one music-generation
|
||||
|
||||
@@ -63,9 +63,9 @@ session:
|
||||
2. The provider processes the job in the background (typically 30 seconds to several minutes depending on the provider and resolution; slow queue-backed providers can run up to the configured timeout).
|
||||
3. When the video is ready, OpenClaw wakes the same session with an internal completion event.
|
||||
4. The agent tells the user and attaches the finished video through the
|
||||
message tool. If the requester session is inactive and some generated
|
||||
video is still missing from message-tool delivery, OpenClaw sends an
|
||||
idempotent direct fallback with only the missing video.
|
||||
message tool. If the requester session is inactive or its active wake
|
||||
fails, and some generated video is still missing from message-tool delivery,
|
||||
OpenClaw sends an idempotent direct fallback with only the missing video.
|
||||
|
||||
While a job is in flight, duplicate `video_generate` calls in the same
|
||||
session return the current task status instead of starting another
|
||||
|
||||
@@ -2318,6 +2318,73 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("directly delivers missing generated media after active requester wake failure", async () => {
|
||||
const callGateway = createGatewayMock({
|
||||
result: {
|
||||
payloads: [],
|
||||
messagingToolSentTargets: [
|
||||
{
|
||||
tool: "message",
|
||||
provider: "slack",
|
||||
accountId: "acct-1",
|
||||
to: "channel:C123",
|
||||
text: "The first image is ready.",
|
||||
mediaUrls: ["/tmp/generated-robot-1.png"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeSequenceMock([
|
||||
"transcript_commit_wait_unsupported",
|
||||
"no_active_run",
|
||||
]);
|
||||
const sendMessage = createSendMessageMock();
|
||||
const result = await deliverSlackChannelAnnouncement({
|
||||
callGateway,
|
||||
sendMessage,
|
||||
queueEmbeddedPiMessageWithOutcome,
|
||||
sessionId: "requester-session-channel",
|
||||
isActive: true,
|
||||
expectsCompletionMessage: true,
|
||||
directIdempotencyKey: "announce-channel-media-active-wake-failed",
|
||||
sourceTool: "image_generate",
|
||||
internalEvents: [
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "image_generation",
|
||||
childSessionKey: "image_generate:task-123",
|
||||
childSessionId: "task-123",
|
||||
announceType: "image generation task",
|
||||
taskLabel: "two proof images",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result:
|
||||
"Generated 2 images.\nMEDIA:/tmp/generated-robot-1.png\nMEDIA:/tmp/generated-robot-2.png",
|
||||
mediaUrls: ["/tmp/generated-robot-1.png", "/tmp/generated-robot-2.png"],
|
||||
replyInstruction:
|
||||
"Tell the user the images are ready and send them through the message tool.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectRecordFields(result, {
|
||||
delivered: true,
|
||||
path: "direct",
|
||||
});
|
||||
expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(2);
|
||||
expect(callGateway).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "slack",
|
||||
accountId: "acct-1",
|
||||
to: "channel:C123",
|
||||
content: "The generated image is ready.",
|
||||
mediaUrls: ["/tmp/generated-robot-2.png"],
|
||||
idempotencyKey: "announce-channel-media-active-wake-failed:generated-media-direct",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "legacy Discord channel",
|
||||
|
||||
@@ -829,8 +829,9 @@ async function sendSubagentAnnounceDirectly(params: {
|
||||
completionRouteRequiresMessageToolDelivery ||
|
||||
(agentMediatedCompletion && expectedMediaUrls.length > 0);
|
||||
const requesterActivity = resolveRequesterSessionActivity(canonicalRequesterSessionKey);
|
||||
let activeRequesterWakeFailed = false;
|
||||
const tryGeneratedMediaDirectDelivery = async (announceResponse?: unknown) => {
|
||||
if (requesterActivity.isActive) {
|
||||
if (requesterActivity.isActive && !activeRequesterWakeFailed) {
|
||||
return undefined;
|
||||
}
|
||||
const missingMediaUrls = resolveGeneratedMediaDirectFallbackUrls({
|
||||
@@ -899,6 +900,7 @@ async function sendSubagentAnnounceDirectly(params: {
|
||||
path: "steered",
|
||||
};
|
||||
}
|
||||
activeRequesterWakeFailed = true;
|
||||
defaultRuntime.log(
|
||||
`[warn] Active requester session could not be woken for subagent completion; falling back to requester-agent handoff: ${formatQueueWakeFailureError(
|
||||
"active requester session could not be woken",
|
||||
|
||||
Reference in New Issue
Block a user