fix(auto-reply): never allow silent replies for mentioned group turns

The default group silentReply policy sanctions silence for ambient chatter,
which also swallowed empty completions on turns that explicitly mentioned the
bot - the reported production failure. A mention is a directed turn: treat it
like a direct chat and let the no-visible-reply fallback fire. Proven live:
group mention + empty completion delivers exactly one fallback message; ambient
group turns stay silent; normal replies produce no fallback.
This commit is contained in:
Ayaan Zaidi
2026-07-27 21:07:12 +09:00
parent 21ba3bfcca
commit 885e690f3e
2 changed files with 54 additions and 0 deletions
@@ -425,6 +425,56 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
expect(result.noVisibleReplyFallbackEligible).toBeUndefined();
});
it("delivers fallback for mentioned group turns even when group silence is allowed", async () => {
setNoAbort();
const dispatcher = createDispatcher();
const replyResolver = vi.fn(async () => undefined);
const ctx = buildTestCtx({
ChatType: "group",
Surface: "telegram",
Provider: "telegram",
SessionKey: "agent:main:telegram:group:oc_group",
WasMentioned: true,
});
// No silentReply config: group default is "allow", but an explicit mention
// is a directed turn and must never end silently.
const result = await dispatchReplyFromConfig({
ctx,
cfg: emptyConfig,
dispatcher,
replyResolver,
});
expect(dispatcher.sendFinalReply).toHaveBeenCalledWith({
text: NO_VISIBLE_REPLY_FALLBACK_TEXT,
});
expect(result.noVisibleReplyFallbackDelivered).toBe(true);
});
it("keeps ambient group turns silent under the default group policy", async () => {
setNoAbort();
const dispatcher = createDispatcher();
const replyResolver = vi.fn(async () => undefined);
const ctx = buildTestCtx({
ChatType: "group",
Surface: "telegram",
Provider: "telegram",
SessionKey: "agent:main:telegram:group:oc_group",
});
const result = await dispatchReplyFromConfig({
ctx,
cfg: emptyConfig,
dispatcher,
replyResolver,
});
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
expect(result.noVisibleReplyFallbackDelivered).toBeUndefined();
expect(result.noVisibleReplyFallbackEligible).toBeUndefined();
});
it("does not deliver no-visible fallback when silentReply allows empty finals", async () => {
setNoAbort();
const dispatcher = createDispatcher();
@@ -224,7 +224,11 @@ export async function prepareDispatchOperationContext(state: PrepareDispatchDeli
const chatType = normalizeChatType(ctx.ChatType);
const silentReplyConversationType = resolveRoutedPolicyConversationType(ctx);
const silentReplySurface = normalizeLowercaseStringOrEmpty(ctx.Surface ?? ctx.Provider);
// Group silent-reply policy sanctions silence for ambient chatter only. A turn
// that explicitly addressed the bot (mention) must never end silently, matching
// the hard-coded direct-chat rule in resolveSilentReplyPolicyFromPolicies.
const emptyFinalAllowedAsSilent =
ctx.WasMentioned !== true &&
silentReplyConversationType !== undefined &&
resolveSilentReplyPolicyFromPolicies({
conversationType: silentReplyConversationType,