From 5b217b20422b555fb6121dad0c4b4d19a31d644f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 5 Dec 2025 22:54:17 +0000 Subject: [PATCH] fix: suppress heartbeat directive acks and add coverage --- src/auto-reply/reply.triggers.test.ts | 35 +++++++++++++++++++++++++++ src/auto-reply/reply.ts | 9 +++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 7baeb9dd0..fd0003b76 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -85,4 +85,39 @@ describe("trigger handling", () => { expect(prompt).toContain("Give me the status"); expect(prompt).not.toContain("/thinking high"); }); + + it("does not emit directive acks for heartbeats with /think", async () => { + const rpcMock = vi.spyOn(tauRpc, "runPiRpc").mockResolvedValue({ + stdout: + '{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}}', + stderr: "", + code: 0, + signal: null, + killed: false, + }); + + const res = await getReplyFromConfig( + { + Body: "HEARTBEAT /think:high", + From: "+1003", + To: "+1003", + }, + { isHeartbeat: true }, + { + inbound: { + reply: { + mode: "command", + command: ["pi", "{{Body}}"], + agent: { kind: "pi" }, + session: {}, + }, + }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toBe("ok"); + expect(text).not.toMatch(/Thinking level set/i); + expect(rpcMock).toHaveBeenCalledOnce(); + }); }); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index ccd183748..ed069fbba 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -329,6 +329,7 @@ export async function getReplyFromConfig( const isGroup = typeof ctx.From === "string" && (ctx.From.includes("@g.us") || ctx.From.startsWith("group:")); + const isHeartbeat = opts?.isHeartbeat === true; let resolvedThinkLevel = inlineThink ?? @@ -359,7 +360,7 @@ export async function getReplyFromConfig( })(); // Directive-only message => persist session thinking level and return ack - if (directiveOnly || combinedDirectiveOnly) { + if (!isHeartbeat && (directiveOnly || combinedDirectiveOnly)) { if (!inlineThink) { cleanupTyping(); return { @@ -425,7 +426,7 @@ export async function getReplyFromConfig( return noMentions.length === 0; })(); - if (verboseDirectiveOnly) { + if (!isHeartbeat && verboseDirectiveOnly) { if (!inlineVerbose) { cleanupTyping(); return { @@ -451,7 +452,7 @@ export async function getReplyFromConfig( } // If any directive (think/verbose) is present anywhere, acknowledge immediately and skip agent execution. - if (hasThinkDirective || hasVerboseDirective) { + if (!isHeartbeat && (hasThinkDirective || hasVerboseDirective)) { if (sessionEntry && sessionStore && sessionKey) { if (hasThinkDirective && inlineThink) { if (inlineThink === "off") { @@ -694,8 +695,6 @@ export async function getReplyFromConfig( return result; } - const isHeartbeat = opts?.isHeartbeat === true; - if (reply && reply.mode === "command") { const heartbeatCommand = isHeartbeat ? (reply as { heartbeatCommand?: string[] }).heartbeatCommand