mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
fix message-tool-only telegram fallback (#76272)
This commit is contained in:
@@ -3006,6 +3006,40 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(deliveredReplies?.[0]?.text?.trim()).not.toBe("NO_REPLY");
|
||||
});
|
||||
|
||||
it("does not add silent-reply fallback for message-tool-only turns", async () => {
|
||||
const draftStream = createDraftStream(999);
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
});
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext({
|
||||
ctxPayload: {
|
||||
SessionKey: "agent:main:telegram:direct:123",
|
||||
} as unknown as TelegramMessageContext["ctxPayload"],
|
||||
}),
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
silentReply: {
|
||||
direct: "disallow",
|
||||
group: "allow",
|
||||
internal: "allow",
|
||||
},
|
||||
silentReplyRewrite: {
|
||||
direct: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(deliverReplies).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add silent-reply fallback after visible block delivery", async () => {
|
||||
const draftStream = createDraftStream(999);
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
|
||||
@@ -665,6 +665,7 @@ export const dispatchTelegramMessage = async ({
|
||||
const silentErrorReplies = telegramCfg.silentErrorReplies === true;
|
||||
const isDmTopic = !isGroup && threadSpec.scope === "dm" && threadSpec.id != null;
|
||||
let queuedFinal = false;
|
||||
let suppressSilentReplyFallback = false;
|
||||
let hadErrorReplyFailureOrSkip = false;
|
||||
let isFirstTurnInSession = false;
|
||||
let dispatchError: unknown;
|
||||
@@ -1171,6 +1172,8 @@ export const dispatchTelegramMessage = async ({
|
||||
return;
|
||||
}
|
||||
({ queuedFinal } = turnResult.dispatchResult);
|
||||
suppressSilentReplyFallback =
|
||||
turnResult.dispatchResult.sourceReplyDeliveryMode === "message_tool_only";
|
||||
} catch (err) {
|
||||
dispatchError = err;
|
||||
runtime.error?.(danger(`telegram dispatch failed: ${String(err)}`));
|
||||
@@ -1300,7 +1303,13 @@ export const dispatchTelegramMessage = async ({
|
||||
sentFallback = result.delivered;
|
||||
}
|
||||
|
||||
if (!queuedFinal && !sentFallback && !dispatchError && !deliverySummary.delivered) {
|
||||
if (
|
||||
!queuedFinal &&
|
||||
!sentFallback &&
|
||||
!dispatchError &&
|
||||
!deliverySummary.delivered &&
|
||||
!suppressSilentReplyFallback
|
||||
) {
|
||||
const policySessionKey =
|
||||
ctxPayload.CommandSource === "native"
|
||||
? (ctxPayload.CommandTargetSessionKey ?? ctxPayload.SessionKey)
|
||||
@@ -1332,7 +1341,7 @@ export const dispatchTelegramMessage = async ({
|
||||
const hasFinalResponse = hasFinalInboundReplyDispatch(
|
||||
{ queuedFinal },
|
||||
{
|
||||
fallbackDelivered: sentFallback,
|
||||
fallbackDelivered: sentFallback || suppressSilentReplyFallback,
|
||||
deliverySummaryDelivered: deliverySummary.delivered,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -113,6 +113,7 @@ function finalizeDispatchResult(
|
||||
final: Math.max(0, result.counts.final - cancelledCounts.final),
|
||||
};
|
||||
return {
|
||||
...result,
|
||||
queuedFinal: result.queuedFinal && counts.final > 0,
|
||||
counts,
|
||||
};
|
||||
|
||||
@@ -4158,6 +4158,8 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
|
||||
);
|
||||
hookMocks.runner.runReplyDispatch.mockResolvedValue(undefined);
|
||||
hookMocks.runner.runBeforeDispatch.mockResolvedValue(undefined);
|
||||
threadInfoMocks.parseSessionThreadInfo.mockReset();
|
||||
threadInfoMocks.parseSessionThreadInfo.mockImplementation(parseGenericThreadSessionInfo);
|
||||
});
|
||||
|
||||
it("still calls the replyResolver when sendPolicy is deny", async () => {
|
||||
@@ -4539,6 +4541,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(result.queuedFinal).toBe(false);
|
||||
expect(result.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
expect(dispatcher.sendBlockReply).not.toHaveBeenCalled();
|
||||
expect(dispatcher.sendToolResult).not.toHaveBeenCalled();
|
||||
|
||||
@@ -754,6 +754,12 @@ export async function dispatchReplyFromConfig(
|
||||
suppressHookUserDelivery,
|
||||
suppressHookReplyLifecycle,
|
||||
} = sourceReplyPolicy;
|
||||
const attachSourceReplyDeliveryMode = (
|
||||
result: DispatchFromConfigResult,
|
||||
): DispatchFromConfigResult =>
|
||||
sourceReplyDeliveryMode === "message_tool_only"
|
||||
? { ...result, sourceReplyDeliveryMode }
|
||||
: result;
|
||||
|
||||
let pluginFallbackReason:
|
||||
| "plugin-bound-fallback-missing-plugin"
|
||||
@@ -797,7 +803,10 @@ export async function dispatchReplyFromConfig(
|
||||
markIdle("plugin_binding_dispatch");
|
||||
recordProcessed("completed", { reason: "plugin-bound-handled" });
|
||||
commitInboundDedupeIfClaimed();
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: false,
|
||||
counts: dispatcher.getQueuedCounts(),
|
||||
});
|
||||
}
|
||||
case "missing_plugin":
|
||||
case "no_handler": {
|
||||
@@ -824,7 +833,10 @@ export async function dispatchReplyFromConfig(
|
||||
markIdle("plugin_binding_declined");
|
||||
recordProcessed("completed", { reason: "plugin-bound-declined" });
|
||||
commitInboundDedupeIfClaimed();
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: false,
|
||||
counts: dispatcher.getQueuedCounts(),
|
||||
});
|
||||
}
|
||||
case "error": {
|
||||
logVerbose(
|
||||
@@ -837,7 +849,10 @@ export async function dispatchReplyFromConfig(
|
||||
markIdle("plugin_binding_error");
|
||||
recordProcessed("completed", { reason: "plugin-bound-error" });
|
||||
commitInboundDedupeIfClaimed();
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: false,
|
||||
counts: dispatcher.getQueuedCounts(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -910,7 +925,7 @@ export async function dispatchReplyFromConfig(
|
||||
recordProcessed("completed", { reason: "fast_abort" });
|
||||
markIdle("message_completed");
|
||||
commitInboundDedupeIfClaimed();
|
||||
return { queuedFinal, counts };
|
||||
return attachSourceReplyDeliveryMode({ queuedFinal, counts });
|
||||
}
|
||||
|
||||
const isSlackNonDirectSurface =
|
||||
@@ -989,7 +1004,7 @@ export async function dispatchReplyFromConfig(
|
||||
recordProcessed("completed", { reason: "before_dispatch_handled" });
|
||||
markIdle("message_completed");
|
||||
commitInboundDedupeIfClaimed();
|
||||
return { queuedFinal, counts };
|
||||
return attachSourceReplyDeliveryMode({ queuedFinal, counts });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1023,10 +1038,10 @@ export async function dispatchReplyFromConfig(
|
||||
);
|
||||
if (replyDispatchResult?.handled) {
|
||||
commitInboundDedupeIfClaimed();
|
||||
return {
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: replyDispatchResult.queuedFinal,
|
||||
counts: replyDispatchResult.counts,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1443,10 +1458,10 @@ export async function dispatchReplyFromConfig(
|
||||
},
|
||||
);
|
||||
if (tailDispatchResult?.handled) {
|
||||
return {
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: tailDispatchResult.queuedFinal,
|
||||
counts: tailDispatchResult.counts,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1535,7 +1550,7 @@ export async function dispatchReplyFromConfig(
|
||||
pluginFallbackReason ? { reason: pluginFallbackReason } : undefined,
|
||||
);
|
||||
markIdle("message_completed");
|
||||
return { queuedFinal, counts };
|
||||
return attachSourceReplyDeliveryMode({ queuedFinal, counts });
|
||||
} catch (err) {
|
||||
if (inboundDedupeClaim.status === "claimed") {
|
||||
if (inboundDedupeReplayUnsafe) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { GetReplyOptions } from "../get-reply-options.types.js";
|
||||
import type { GetReplyOptions, SourceReplyDeliveryMode } from "../get-reply-options.types.js";
|
||||
import type { FinalizedMsgContext } from "../templating.js";
|
||||
import type { FormatAbortReplyText, TryFastAbortFromMessage } from "./abort.runtime-types.js";
|
||||
import type { GetReplyFromConfig } from "./get-reply.types.js";
|
||||
@@ -8,6 +8,7 @@ import type { ReplyDispatchKind, ReplyDispatcher } from "./reply-dispatcher.type
|
||||
export type DispatchFromConfigResult = {
|
||||
queuedFinal: boolean;
|
||||
counts: Record<ReplyDispatchKind, number>;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
};
|
||||
|
||||
export type DispatchFromConfigParams = {
|
||||
|
||||
@@ -130,6 +130,7 @@ let runReplyAgent: typeof import("./agent-runner.runtime.js").runReplyAgent;
|
||||
let routeReply: typeof import("./route-reply.runtime.js").routeReply;
|
||||
let drainFormattedSystemEvents: typeof import("./session-system-events.js").drainFormattedSystemEvents;
|
||||
let resolveTypingMode: typeof import("./typing-mode.js").resolveTypingMode;
|
||||
let buildDirectChatContext: typeof import("./groups.js").buildDirectChatContext;
|
||||
let buildGroupChatContext: typeof import("./groups.js").buildGroupChatContext;
|
||||
let buildInboundUserContextPrefix: typeof import("./inbound-meta.js").buildInboundUserContextPrefix;
|
||||
let getActiveReplyRunCount: typeof import("./reply-run-registry.js").getActiveReplyRunCount;
|
||||
@@ -242,7 +243,7 @@ describe("runPreparedReply media-only handling", () => {
|
||||
({ routeReply } = await import("./route-reply.runtime.js"));
|
||||
({ drainFormattedSystemEvents } = await import("./session-system-events.js"));
|
||||
({ resolveTypingMode } = await import("./typing-mode.js"));
|
||||
({ buildGroupChatContext } = await import("./groups.js"));
|
||||
({ buildDirectChatContext, buildGroupChatContext } = await import("./groups.js"));
|
||||
({ buildInboundUserContextPrefix } = await import("./inbound-meta.js"));
|
||||
({ __testing: replyRunTesting, getActiveReplyRunCount } =
|
||||
await import("./reply-run-registry.js"));
|
||||
@@ -331,6 +332,45 @@ describe("runPreparedReply media-only handling", () => {
|
||||
expect(call?.followupRun.run.allowEmptyAssistantReplyAsSilent).toBe(false);
|
||||
});
|
||||
|
||||
it("passes message-tool-only delivery into direct chat prompt context", async () => {
|
||||
await runPreparedReply(
|
||||
baseParams({
|
||||
ctx: {
|
||||
Body: "yo",
|
||||
RawBody: "yo",
|
||||
CommandBody: "yo",
|
||||
ThreadHistoryBody: "Earlier direct message",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram-direct-test-id",
|
||||
ChatType: "direct",
|
||||
},
|
||||
sessionCtx: {
|
||||
Body: "yo",
|
||||
BodyStripped: "yo",
|
||||
ThreadHistoryBody: "Earlier direct message",
|
||||
MediaPath: "/tmp/input.png",
|
||||
Provider: "telegram",
|
||||
ChatType: "direct",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram-direct-test-id",
|
||||
},
|
||||
opts: {
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(buildDirectChatContext).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionCtx: expect.objectContaining({
|
||||
Provider: "telegram",
|
||||
ChatType: "direct",
|
||||
}),
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["direct", "dm"] as const)(
|
||||
"propagates empty-assistant silence for %s runs with explicit direct silent replies",
|
||||
async (chatType) => {
|
||||
|
||||
Reference in New Issue
Block a user