fix(gateway): preflight strict agent delivery

This commit is contained in:
Vincent Koc
2026-05-01 03:11:39 -07:00
parent 8be40059fe
commit 6fb9e9e558
3 changed files with 57 additions and 0 deletions

View File

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

View File

@@ -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();

View File

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