Skip to content

Commit 6fb9e9e

Browse files
committed
fix(gateway): preflight strict agent delivery
1 parent 8be4005 commit 6fb9e9e

3 files changed

Lines changed: 57 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131
- 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.
3232
- WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001.
3333
- 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.
34+
- 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.
3435
- 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.
3536
- 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.
3637
- 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.

src/gateway/server-methods/agent.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,38 @@ describe("gateway agent handler", () => {
11101110
expect(callArgs.bestEffortDeliver).toBe(false);
11111111
});
11121112

1113+
it("rejects strict delivery with a missing target before dispatching the agent", async () => {
1114+
mocks.agentCommand.mockClear();
1115+
primeMainAgentRun();
1116+
const respond = vi.fn();
1117+
1118+
await invokeAgent(
1119+
{
1120+
message: "strict missing delivery target",
1121+
agentId: "main",
1122+
sessionKey: "agent:main:main",
1123+
deliver: true,
1124+
replyChannel: "telegram",
1125+
bestEffortDeliver: false,
1126+
idempotencyKey: "test-strict-delivery-missing-target",
1127+
},
1128+
{
1129+
reqId: "strict-delivery-missing-target",
1130+
respond,
1131+
flushDispatch: false,
1132+
},
1133+
);
1134+
1135+
expect(mocks.agentCommand).not.toHaveBeenCalled();
1136+
expect(respond).toHaveBeenCalledWith(
1137+
false,
1138+
undefined,
1139+
expect.objectContaining({
1140+
message: expect.stringContaining("requires target"),
1141+
}),
1142+
);
1143+
});
1144+
11131145
it("downgrades to session-only when bestEffortDeliver=true and no external channel is configured", async () => {
11141146
mocks.agentCommand.mockClear();
11151147
primeMainAgentRun();

src/gateway/server-methods/agent.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,7 @@ export const agentHandlers: GatewayRequestHandlers = {
11281128
let resolvedTo = deliveryPlan.resolvedTo;
11291129
let effectivePlan = deliveryPlan;
11301130
let deliveryDowngradeReason: string | null = null;
1131+
let deliveryTargetResolutionError: Error | undefined;
11311132

11321133
if (wantsDelivery && resolvedChannel === INTERNAL_MESSAGE_CHANNEL) {
11331134
const cfgResolved = cfgForAgent ?? cfg;
@@ -1165,9 +1166,32 @@ export const agentHandlers: GatewayRequestHandlers = {
11651166
});
11661167
if (fallback.resolvedTarget?.ok) {
11671168
resolvedTo = fallback.resolvedTo;
1169+
} else if (fallback.resolvedTarget && !fallback.resolvedTarget.ok) {
1170+
deliveryTargetResolutionError = fallback.resolvedTarget.error;
11681171
}
11691172
}
11701173

1174+
if (wantsDelivery && isDeliverableMessageChannel(resolvedChannel) && !resolvedTo) {
1175+
if (!bestEffortDeliver) {
1176+
respond(
1177+
false,
1178+
undefined,
1179+
errorShape(
1180+
ErrorCodes.INVALID_REQUEST,
1181+
deliveryTargetResolutionError
1182+
? String(deliveryTargetResolutionError)
1183+
: `delivery target is required for ${resolvedChannel}: pass --to/--reply-to or configure a default target`,
1184+
),
1185+
);
1186+
return;
1187+
}
1188+
context.logGateway.info(
1189+
deliveryTargetResolutionError
1190+
? `agent delivery target missing (bestEffortDeliver): ${String(deliveryTargetResolutionError)}`
1191+
: "agent delivery target missing (bestEffortDeliver): no deliverable target",
1192+
);
1193+
}
1194+
11711195
if (wantsDelivery && resolvedChannel === INTERNAL_MESSAGE_CHANNEL) {
11721196
const shouldDowngrade = shouldDowngradeDeliveryToSessionOnly({
11731197
wantsDelivery,

0 commit comments

Comments
 (0)