diff --git a/CHANGELOG.md b/CHANGELOG.md index 1980bfad5..68d7e86ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ - Status command reports web session health + session recipients; config paths are locked to `~/.clawdis` with session metadata stored under `~/.clawdis/sessions/`. - 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. +- Pi/Tau sessions now write to `~/.clawdis/sessions/` by default (legacy `~/.tau/agent/sessions/clawdis` files are copied over when present). - Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent. +- Directive/system acks carry a `⚙️` prefix and verbose parsing rejects typoed `/ver*` strings so unrelated text doesn’t flip verbosity. - Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements. - RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text. - Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings. diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index b122289a5..3a89a0825 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -1,8 +1,12 @@ -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, extractVerboseDirective, extractThinkDirective } from "./reply.js"; describe("directive parsing", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it("ignores verbose directive inside URL", () => { const body = "https://x.com/verioussmith/status/1997066835133669687"; const res = extractVerboseDirective(body); @@ -10,6 +14,13 @@ describe("directive parsing", () => { expect(res.cleaned).toBe(body); }); + it("ignores typoed /verioussmith", () => { + const body = "/verioussmith"; + const res = extractVerboseDirective(body); + expect(res.hasDirective).toBe(false); + expect(res.cleaned).toBe(body.trim()); + }); + it("ignores think directive inside URL", () => { const body = "see https://example.com/path/thinkstuff"; const res = extractThinkDirective(body); @@ -61,4 +72,33 @@ describe("directive parsing", () => { expect(text).toBe("done"); expect(rpcMock).toHaveBeenCalledOnce(); }); + + it("acks verbose directive immediately with system marker", async () => { + const rpcMock = vi.spyOn(tauRpc, "runPiRpc").mockResolvedValue({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + }); + + const res = await getReplyFromConfig( + { Body: "/verbose on", From: "+1222", To: "+1222" }, + {}, + { + inbound: { + reply: { + mode: "command", + command: ["pi", "{{Body}}"], + agent: { kind: "pi" }, + session: {}, + }, + }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toMatch(/^⚙️ Verbose logging enabled\./); + expect(rpcMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 3e1257159..535e74830 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -35,6 +35,7 @@ export type { GetReplyOptions, ReplyPayload } from "./types.js"; const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]); const ABORT_MEMORY = new Map(); +const SYSTEM_MARK = "⚙️"; export function extractThinkDirective(body?: string): { cleaned: string; @@ -67,8 +68,8 @@ export function extractVerboseDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - // Require start or whitespace before "/verbose" to avoid matching URLs like /verioussmith. - const match = body.match(/(?:^|\s)\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i); + // Require start or whitespace before "/verbose" and reject "/ver*" typos. + const match = body.match(/(?:^|\s)\/v(?:erbose)?\b\s*:?\s*([a-zA-Z-]+)\b/i); const verboseLevel = normalizeVerboseLevel(match?.[1]); const cleaned = match ? body.replace(match[0], "").replace(/\s+/g, " ").trim() @@ -364,7 +365,7 @@ export async function getReplyFromConfig( if (!inlineThink) { cleanupTyping(); return { - text: `Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`, + text: `${SYSTEM_MARK} Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`, }; } if (sessionEntry && sessionStore && sessionKey) { @@ -413,7 +414,7 @@ export async function getReplyFromConfig( ); } } - const ack = parts.join(" "); + const ack = `${SYSTEM_MARK} ${parts.join(" ")}`; cleanupTyping(); return { text: ack }; } @@ -430,7 +431,7 @@ export async function getReplyFromConfig( if (!inlineVerbose) { cleanupTyping(); return { - text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`, + text: `${SYSTEM_MARK} Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`, }; } if (sessionEntry && sessionStore && sessionKey) { @@ -445,8 +446,8 @@ export async function getReplyFromConfig( } const ack = inlineVerbose === "off" - ? "Verbose logging disabled." - : "Verbose logging enabled."; + ? `${SYSTEM_MARK} Verbose logging disabled.` + : `${SYSTEM_MARK} Verbose logging enabled.`; cleanupTyping(); return { text: ack }; }