From 3b1f8e3461b203100c04e604a57a3d6e2cbfe8db Mon Sep 17 00:00:00 2001 From: HansY Date: Wed, 1 Apr 2026 04:03:15 +0000 Subject: [PATCH] fix: strip inbound metadata before slash command detection (#58674) Slash commands like /model and /new were silently ignored when the inbound message body included metadata prefix blocks (Conversation info, Sender info, timestamps) injected by buildInboundUserContextPrefix. The command detection functions (hasControlCommand, isControlCommandMessage, parseSendPolicyCommand) now call stripInboundMetadata before normalizeCommandBody so embedded slash commands are correctly recognized. --- src/auto-reply/command-control.test.ts | 36 ++++++++++++++++++++++++++ src/auto-reply/command-detection.ts | 10 +++++-- src/auto-reply/send-policy.ts | 4 ++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/command-control.test.ts b/src/auto-reply/command-control.test.ts index 18a511f145c..6212a295086 100644 --- a/src/auto-reply/command-control.test.ts +++ b/src/auto-reply/command-control.test.ts @@ -877,4 +877,40 @@ describe("control command parsing", () => { }), ).toBe(true); }); + + it("detects commands wrapped in inbound metadata blocks", () => { + const metaWrapped = [ + "Conversation info (untrusted metadata):", + "```json", + '{"message_id":"msg-abc","chat_id":"chat-123"}', + "```", + "", + "/model spark", + ].join("\n"); + expect(hasControlCommand(metaWrapped)).toBe(true); + }); + + it("detects /new command after metadata prefix", () => { + const metaWrapped = [ + "Sender (untrusted metadata):", + "```json", + '{"name":"Alice","id":"user-1"}', + "```", + "", + "/new spark", + ].join("\n"); + expect(hasControlCommand(metaWrapped)).toBe(true); + }); + + it("detects /status command after timestamp + metadata prefix", () => { + const metaWrapped = [ + "[Wed 2026-03-11 23:51 PDT] Conversation info (untrusted metadata):", + "```json", + '{"chat_id":"chat-123"}', + "```", + "", + "/status", + ].join("\n"); + expect(hasControlCommand(metaWrapped)).toBe(true); + }); }); diff --git a/src/auto-reply/command-detection.ts b/src/auto-reply/command-detection.ts index f186ad771ee..bd0dd11d6f4 100644 --- a/src/auto-reply/command-detection.ts +++ b/src/auto-reply/command-detection.ts @@ -6,6 +6,7 @@ import { normalizeCommandBody, } from "./commands-registry.js"; import { isAbortTrigger } from "./reply/abort-primitives.js"; +import { stripInboundMetadata } from "./reply/strip-inbound-meta.js"; export function hasControlCommand( text?: string, @@ -19,7 +20,11 @@ export function hasControlCommand( if (!trimmed) { return false; } - const normalizedBody = normalizeCommandBody(trimmed, options); + const stripped = stripInboundMetadata(trimmed); + if (!stripped) { + return false; + } + const normalizedBody = normalizeCommandBody(stripped, options); if (!normalizedBody) { return false; } @@ -60,7 +65,8 @@ export function isControlCommandMessage( if (hasControlCommand(trimmed, cfg, options)) { return true; } - const normalized = normalizeCommandBody(trimmed, options).trim().toLowerCase(); + const stripped = stripInboundMetadata(trimmed); + const normalized = normalizeCommandBody(stripped, options).trim().toLowerCase(); return isAbortTrigger(normalized); } diff --git a/src/auto-reply/send-policy.ts b/src/auto-reply/send-policy.ts index 84dd8f83343..f86791710a8 100644 --- a/src/auto-reply/send-policy.ts +++ b/src/auto-reply/send-policy.ts @@ -1,4 +1,5 @@ import { normalizeCommandBody } from "./commands-registry.js"; +import { stripInboundMetadata } from "./reply/strip-inbound-meta.js"; export type SendPolicyOverride = "allow" | "deny"; @@ -27,7 +28,8 @@ export function parseSendPolicyCommand(raw?: string): { if (!trimmed) { return { hasCommand: false }; } - const normalized = normalizeCommandBody(trimmed); + const stripped = stripInboundMetadata(trimmed); + const normalized = normalizeCommandBody(stripped); const match = normalized.match(/^\/send(?:\s+([a-zA-Z]+))?\s*$/i); if (!match) { return { hasCommand: false };