diff --git a/CHANGELOG.md b/CHANGELOG.md index eb534b121..31e357465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Simplified send/agent/relay/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code. - Tau RPC timeout is now inactivity-based (5m without events) and error messages show seconds only. - Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent. +- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements. ## 1.4.1 — 2025-12-04 diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 94168c010..7baeb9dd0 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as tauRpc from "../process/tau-rpc.js"; import { getReplyFromConfig } from "./reply.js"; const baseCfg = { @@ -12,6 +13,10 @@ const baseCfg = { }, }; +afterEach(() => { + vi.restoreAllMocks(); +}); + describe("trigger handling", () => { it("aborts even with timestamp prefix", async () => { const runner = vi.fn(); @@ -46,4 +51,38 @@ describe("trigger handling", () => { expect(text?.startsWith("⚙️ Restarting" ?? "")).toBe(true); expect(runner).not.toHaveBeenCalled(); }); + + it("ignores think directives that only appear in the context wrapper", 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: [ + "[Chat messages since your last reply - for context]", + "Peter: /thinking high [2025-12-05T21:45:00.000Z]", + "", + "[Current message - respond to this]", + "Give me the status", + ].join("\n"), + From: "+1002", + To: "+2000", + }, + {}, + baseCfg, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toBe("ok"); + expect(rpcMock).toHaveBeenCalledOnce(); + const prompt = rpcMock.mock.calls[0]?.[0]?.prompt ?? ""; + expect(prompt).toContain("Give me the status"); + expect(prompt).not.toContain("/thinking high"); + }); }); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index b52671c31..ccd183748 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -298,20 +298,33 @@ export async function getReplyFromConfig( IsNewSession: isNewSession ? "true" : "false", }; + const directiveSource = stripStructuralPrefixes( + sessionCtx.BodyStripped ?? sessionCtx.Body ?? "", + ); const { - cleaned: thinkCleaned, + cleaned: thinkCleanedDirective, thinkLevel: inlineThink, rawLevel: rawThinkLevel, hasDirective: hasThinkDirective, - } = extractThinkDirective(sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""); + } = extractThinkDirective(directiveSource); const { - cleaned: verboseCleaned, + cleaned: verboseCleanedDirective, verboseLevel: inlineVerbose, rawLevel: rawVerboseLevel, hasDirective: hasVerboseDirective, - } = extractVerboseDirective(thinkCleaned); - sessionCtx.Body = verboseCleaned; - sessionCtx.BodyStripped = verboseCleaned; + } = extractVerboseDirective(thinkCleanedDirective); + + // Keep the full body (including context wrapper) for the agent, but strip + // directives from it separately so history remains intact. + const { cleaned: thinkCleanedFull } = extractThinkDirective( + sessionCtx.Body ?? "", + ); + const { cleaned: verboseCleanedFull } = extractVerboseDirective( + thinkCleanedFull, + ); + + sessionCtx.Body = verboseCleanedFull; + sessionCtx.BodyStripped = verboseCleanedFull; const isGroup = typeof ctx.From === "string" && @@ -331,16 +344,16 @@ export async function getReplyFromConfig( hasThinkDirective && hasVerboseDirective && (() => { - const stripped = stripStructuralPrefixes(verboseCleaned ?? ""); + const stripped = stripStructuralPrefixes(verboseCleanedDirective ?? ""); const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped; return noMentions.length === 0; })(); const directiveOnly = (() => { if (!hasThinkDirective) return false; - if (!thinkCleaned) return true; + if (!thinkCleanedDirective) return true; // Check after stripping both think and verbose so combined directives count. - const stripped = stripStructuralPrefixes(verboseCleaned); + const stripped = stripStructuralPrefixes(verboseCleanedDirective); const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped; return noMentions.length === 0; })(); @@ -406,8 +419,8 @@ export async function getReplyFromConfig( const verboseDirectiveOnly = (() => { if (!hasVerboseDirective) return false; - if (!verboseCleaned) return true; - const stripped = stripStructuralPrefixes(verboseCleaned); + if (!verboseCleanedDirective) return true; + const stripped = stripStructuralPrefixes(verboseCleanedDirective); const noMentions = isGroup ? stripMentions(stripped, ctx, cfg) : stripped; return noMentions.length === 0; })();