diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index df003eaf7..9ec850872 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -222,6 +222,7 @@ export function buildAgentSystemPrompt(params: { "To request a native reply/quote on supported surfaces, include one tag in your reply:", "- [[reply_to_current]] replies to the triggering message.", "- [[reply_to:]] replies to a specific message id when you have it.", + "Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).", "Tags are stripped before sending; support depends on the current provider config.", "", "## Messaging", diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index b494c1057..0b555e9bf 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -249,6 +249,42 @@ describe("directive behavior", () => { }); }); + it("strips reply tags with whitespace and maps reply_to_current to MessageSid", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "hello [[ reply_to_current ]]" }], + meta: { + durationMs: 5, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); + + const res = await getReplyFromConfig( + { + Body: "ping", + From: "+1004", + To: "+2000", + MessageSid: "msg-123", + }, + {}, + { + agents: { + defaults: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + }, + }, + whatsapp: { allowFrom: ["*"] }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const payload = Array.isArray(res) ? res[0] : res; + expect(payload?.text).toBe("hello"); + expect(payload?.replyToId).toBe("msg-123"); + }); + }); + it("prefers explicit reply_to id over reply_to_current", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ diff --git a/src/auto-reply/reply/reply-tags.ts b/src/auto-reply/reply/reply-tags.ts index ef2de524a..9e81affcb 100644 --- a/src/auto-reply/reply/reply-tags.ts +++ b/src/auto-reply/reply/reply-tags.ts @@ -1,3 +1,13 @@ +const REPLY_TAG_RE = + /\[\[\s*(?:reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi; + +function normalizeReplyText(text: string) { + return text + .replace(/[ \t]+/g, " ") + .replace(/[ \t]*\n[ \t]*/g, "\n") + .trim(); +} + export function extractReplyToTag( text?: string, currentMessageId?: string, @@ -7,29 +17,28 @@ export function extractReplyToTag( hasTag: boolean; } { if (!text) return { cleaned: "", hasTag: false }; - let cleaned = text; - let replyToId: string | undefined; + + let sawCurrent = false; + let lastExplicitId: string | undefined; let hasTag = false; - const currentMatch = cleaned.match(/\[\[\s*reply_to_current\s*\]\]/i); - if (currentMatch) { - cleaned = cleaned.replace(/\[\[\s*reply_to_current\s*\]\]/gi, " "); - hasTag = true; - if (currentMessageId?.trim()) { - replyToId = currentMessageId.trim(); - } - } + const cleaned = normalizeReplyText( + text.replace(REPLY_TAG_RE, (_full, idRaw: string | undefined) => { + hasTag = true; + if (idRaw === undefined) { + sawCurrent = true; + return " "; + } - const idMatch = cleaned.match(/\[\[\s*reply_to\s*:\s*([^\]\n]+)\s*\]\]/i); - if (idMatch?.[1]) { - cleaned = cleaned.replace(/\[\[\s*reply_to\s*:\s*[^\]\n]+\s*\]\]/gi, " "); - replyToId = idMatch[1].trim(); - hasTag = true; - } + const id = idRaw.trim(); + if (id) lastExplicitId = id; + return " "; + }), + ); + + const replyToId = + lastExplicitId ?? + (sawCurrent ? currentMessageId?.trim() || undefined : undefined); - cleaned = cleaned - .replace(/[ \t]+/g, " ") - .replace(/[ \t]*\n[ \t]*/g, "\n") - .trim(); return { cleaned, replyToId, hasTag }; }