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:
Jason (Json)
2026-05-25 10:23:20 -06:00
committed by GitHub
parent 159e4406ab
commit 1b64ccbfff
7 changed files with 87 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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