From 9aa9c3ff6262e781d68d46f36f86e84d2fa1f955 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Apr 2026 00:17:46 +0100 Subject: [PATCH] fix(auto-reply): stop mention-only inline status turns --- ...ine-actions.skip-when-config-empty.test.ts | 41 +++++++++++++++++++ .../reply/get-reply-inline-actions.ts | 17 +++++++- 2 files changed, 57 insertions(+), 1 deletion(-) 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 8d96c800499..51f8f4d375c 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 @@ -391,6 +391,47 @@ describe("handleInlineActions", () => { expect(typing.cleanup).toHaveBeenCalled(); }); + it("continues into the agent when mention-wrapped inline status leaves real text", async () => { + const typing = createTypingController(); + const ctx = buildTestCtx({ + Body: "<@123> /status what's next?", + CommandBody: "<@123> /status what's next?", + Provider: "discord", + Surface: "discord", + ChatType: "channel", + WasMentioned: true, + }); + + const result = await handleInlineActions( + createHandleInlineActionsInput({ + ctx, + typing, + cleanedBody: "<@123> what's next?", + command: { + surface: "discord", + channel: "discord", + channelId: "discord", + isAuthorizedSender: true, + rawBodyNormalized: "<@123> /status what's next?", + commandBodyNormalized: "<@123> /status what's next?", + }, + overrides: { + allowTextCommands: true, + inlineStatusRequested: true, + isGroup: true, + }, + }), + ); + + expect(result).toEqual({ + kind: "continue", + directives: clearInlineDirectives("<@123> what's next?"), + abortedLastRun: false, + }); + expect(buildStatusReplyMock).toHaveBeenCalledTimes(1); + expect(handleCommandsMock).toHaveBeenCalledTimes(1); + }); + 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 3d9412e302a..e11da667b9a 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -76,6 +76,17 @@ function expandBundleCommandPromptTemplate(template: string, args?: string): str return `${rendered.trim()}\n\nUser input:\n${normalizedArgs}`; } +function isMentionOnlyResidualText(text: string, wasMentioned: boolean | undefined): boolean { + if (wasMentioned !== true) { + return false; + } + const trimmed = text.trim(); + if (!trimmed) { + return false; + } + return /^(?:<@[!&]?[A-Za-z0-9._:-]+>||[:,.!?-]|\s)+$/u.test(trimmed); +} + export type InlineActionResult = | { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined } | { @@ -474,7 +485,11 @@ export async function handleInlineActions(params: { } return stripMentions(stripped, ctx, cfg, agentId).trim(); })(); - if (didSendInlineStatus && remainingBodyAfterInlineStatus.length === 0) { + if ( + didSendInlineStatus && + (remainingBodyAfterInlineStatus.length === 0 || + isMentionOnlyResidualText(remainingBodyAfterInlineStatus, ctx.WasMentioned)) + ) { typing.cleanup(); return { kind: "reply", reply: undefined }; }