diff --git a/docs/automation/tasks.md b/docs/automation/tasks.md
index d458dc18f491..58d861e8e6a7 100644
--- a/docs/automation/tasks.md
+++ b/docs/automation/tasks.md
@@ -102,7 +102,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
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.
diff --git a/docs/tools/image-generation.md b/docs/tools/image-generation.md
index 596e27fd4a7e..d2946e8e1b7e 100644
--- a/docs/tools/image-generation.md
+++ b/docs/tools/image-generation.md
@@ -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.
The tool only appears when at least one image-generation provider is
diff --git a/docs/tools/media-overview.md b/docs/tools/media-overview.md
index 18fe277a2d85..0bfd62ea4dca 100644
--- a/docs/tools/media-overview.md
+++ b/docs/tools/media-overview.md
@@ -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
diff --git a/docs/tools/music-generation.md b/docs/tools/music-generation.md
index 09e5c9003734..f71e2cb8d615 100644
--- a/docs/tools/music-generation.md
+++ b/docs/tools/music-generation.md
@@ -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.
The built-in shared tool only appears when at least one music-generation
diff --git a/docs/tools/video-generation.md b/docs/tools/video-generation.md
index 2497adce239e..24934118cd04 100644
--- a/docs/tools/video-generation.md
+++ b/docs/tools/video-generation.md
@@ -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
diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts
index 71e6b7db8a1f..2397d66c7031 100644
--- a/src/agents/subagent-announce-delivery.test.ts
+++ b/src/agents/subagent-announce-delivery.test.ts
@@ -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",
diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts
index 4d0cb7ec9156..fd216e4a6d53 100644
--- a/src/agents/subagent-announce-delivery.ts
+++ b/src/agents/subagent-announce-delivery.ts
@@ -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",