From ee8baf6766b2f9071b709261ec846f238f29fc78 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 1 Apr 2026 06:31:16 +0900 Subject: [PATCH] fix(reply): stop mention-wrapped status double replies --- ...ine-actions.skip-when-config-empty.test.ts | 38 +++++++++++++++++++ .../reply/get-reply-inline-actions.ts | 15 +++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts b/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts index cd599bab77d..f1843f9a604 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.skip-when-config-empty.test.ts @@ -214,6 +214,44 @@ describe("handleInlineActions", () => { expect(typing.cleanup).toHaveBeenCalled(); }); + it("does not continue into the agent after a mention-wrapped inline status-only turn", async () => { + const typing = createTypingController(); + const ctx = buildTestCtx({ + Body: "<@123> /status", + CommandBody: "<@123> /status", + Provider: "discord", + Surface: "discord", + ChatType: "channel", + WasMentioned: true, + }); + + const result = await handleInlineActions( + createHandleInlineActionsInput({ + ctx, + typing, + cleanedBody: "<@123>", + command: { + surface: "discord", + channel: "discord", + channelId: "discord", + isAuthorizedSender: true, + rawBodyNormalized: "<@123> /status", + commandBodyNormalized: "<@123> /status", + }, + overrides: { + allowTextCommands: true, + inlineStatusRequested: true, + isGroup: true, + }, + }), + ); + + expect(result).toEqual({ kind: "reply", reply: undefined }); + expect(buildStatusReplyMock).toHaveBeenCalledTimes(1); + expect(handleCommandsMock).not.toHaveBeenCalled(); + expect(typing.cleanup).toHaveBeenCalled(); + }); + it("skips stale queued messages that are at or before the /stop cutoff", async () => { const typing = createTypingController(); const sessionEntry: SessionEntry = { diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index 8907acd314c..a025c7cdd7e 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -25,8 +25,9 @@ import type { buildStatusReply, handleCommands } from "./commands.runtime.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; import { isDirectiveOnly } from "./directive-handling.parse.js"; import { extractExplicitGroupId } from "./group-id.js"; +import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; import type { createModelSelectionState } from "./model-selection.js"; -import { extractInlineSimpleCommand, stripInlineStatus } from "./reply-inline.js"; +import { extractInlineSimpleCommand } from "./reply-inline.js"; import type { TypingController } from "./typing.js"; let builtinSlashCommands: Set | null = null; @@ -458,10 +459,14 @@ export async function handleInlineActions(params: { abortedLastRun, }; } - const statusOnlyCommand = - command.commandBodyNormalized.trim().length > 0 && - stripInlineStatus(command.commandBodyNormalized).cleaned.length === 0; - if (didSendInlineStatus && statusOnlyCommand) { + const remainingBodyAfterInlineStatus = (() => { + const stripped = stripStructuralPrefixes(cleanedBody); + if (!isGroup) { + return stripped.trim(); + } + return stripMentions(stripped, ctx, cfg, agentId).trim(); + })(); + if (didSendInlineStatus && remainingBodyAfterInlineStatus.length === 0) { typing.cleanup(); return { kind: "reply", reply: undefined }; }