diff --git a/CHANGELOG.md b/CHANGELOG.md index 89258107566..8cfabf3b6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - Discord/voice: merge configured media-understanding providers such as Deepgram into partial active provider registries, so follow-up voice turns keep transcribing after another media plugin is already active. Fixes #65687. Thanks @OneMintJulep. - WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001. - Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao. +- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc. - Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram. - Plugins/runtime-deps: include packaged OpenClaw identity in bundled plugin loader cache keys, so same-path package upgrades stop reusing stale versioned runtime-deps mirrors. Fixes #75045. Thanks @sahilsatralkar. - Plugin SDK: restore reply-prefix and reply-pipeline helpers on the deprecated root/compat SDK surface so external plugins still using `openclaw/plugin-sdk` do not fail message dispatch after update. Fixes #75171. Thanks @zhangxiliang. diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 045310c18d7..45cb711f6e4 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -1110,6 +1110,38 @@ describe("gateway agent handler", () => { expect(callArgs.bestEffortDeliver).toBe(false); }); + it("rejects strict delivery with a missing target before dispatching the agent", async () => { + mocks.agentCommand.mockClear(); + primeMainAgentRun(); + const respond = vi.fn(); + + await invokeAgent( + { + message: "strict missing delivery target", + agentId: "main", + sessionKey: "agent:main:main", + deliver: true, + replyChannel: "telegram", + bestEffortDeliver: false, + idempotencyKey: "test-strict-delivery-missing-target", + }, + { + reqId: "strict-delivery-missing-target", + respond, + flushDispatch: false, + }, + ); + + expect(mocks.agentCommand).not.toHaveBeenCalled(); + expect(respond).toHaveBeenCalledWith( + false, + undefined, + expect.objectContaining({ + message: expect.stringContaining("requires target"), + }), + ); + }); + it("downgrades to session-only when bestEffortDeliver=true and no external channel is configured", async () => { mocks.agentCommand.mockClear(); primeMainAgentRun(); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index d3ec30084e0..044ef0b6045 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -1128,6 +1128,7 @@ export const agentHandlers: GatewayRequestHandlers = { let resolvedTo = deliveryPlan.resolvedTo; let effectivePlan = deliveryPlan; let deliveryDowngradeReason: string | null = null; + let deliveryTargetResolutionError: Error | undefined; if (wantsDelivery && resolvedChannel === INTERNAL_MESSAGE_CHANNEL) { const cfgResolved = cfgForAgent ?? cfg; @@ -1165,9 +1166,32 @@ export const agentHandlers: GatewayRequestHandlers = { }); if (fallback.resolvedTarget?.ok) { resolvedTo = fallback.resolvedTo; + } else if (fallback.resolvedTarget && !fallback.resolvedTarget.ok) { + deliveryTargetResolutionError = fallback.resolvedTarget.error; } } + if (wantsDelivery && isDeliverableMessageChannel(resolvedChannel) && !resolvedTo) { + if (!bestEffortDeliver) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + deliveryTargetResolutionError + ? String(deliveryTargetResolutionError) + : `delivery target is required for ${resolvedChannel}: pass --to/--reply-to or configure a default target`, + ), + ); + return; + } + context.logGateway.info( + deliveryTargetResolutionError + ? `agent delivery target missing (bestEffortDeliver): ${String(deliveryTargetResolutionError)}` + : "agent delivery target missing (bestEffortDeliver): no deliverable target", + ); + } + if (wantsDelivery && resolvedChannel === INTERNAL_MESSAGE_CHANNEL) { const shouldDowngrade = shouldDowngradeDeliveryToSessionOnly({ wantsDelivery,