From 3e4fc7ff7fc707a4addde3a7247cfcd2f847cb74 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 26 Dec 2025 14:24:53 +0100 Subject: [PATCH] feat(queue): add reset/default directive --- docs/queue.md | 1 + src/auto-reply/reply.directive.test.ts | 41 ++++++++++++++++++++++++++ src/auto-reply/reply.ts | 27 +++++++++++++---- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/docs/queue.md b/docs/queue.md index 122039daa..05ce67465 100644 --- a/docs/queue.md +++ b/docs/queue.md @@ -44,6 +44,7 @@ Configure globally or per surface via `routing.queue`: ## Per-session overrides - `/queue ` as a standalone command stores the mode for the current session. - `/queue ` embedded in a message applies **once** (no persistence). +- `/queue default` or `/queue reset` clears the session override. ## Scope and guarantees - Applies only to config-driven command replies; plain text replies are unaffected. diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 70738c35f..ae9e5ca9c 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -91,6 +91,7 @@ describe("directive parsing", () => { const res = extractQueueDirective("please /queue interrupt now"); expect(res.hasDirective).toBe(true); expect(res.queueMode).toBe("interrupt"); + expect(res.queueReset).toBe(false); expect(res.cleaned).toBe("please now"); }); @@ -180,6 +181,46 @@ describe("directive parsing", () => { }); }); + it("resets queue mode to default", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + const storePath = path.join(home, "sessions.json"); + + await getReplyFromConfig( + { Body: "/queue interrupt", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + }, + routing: { allowFrom: ["*"] }, + session: { store: storePath }, + }, + ); + + const res = await getReplyFromConfig( + { Body: "/queue reset", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + }, + routing: { allowFrom: ["*"] }, + session: { store: storePath }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toMatch(/^⚙️ Queue mode reset to default\./); + const store = loadSessionStore(storePath); + const entry = Object.values(store)[0]; + expect(entry?.queueMode).toBeUndefined(); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("updates tool verbose during an in-flight run (toggle on)", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index b749632a3..26fb0c864 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -130,19 +130,24 @@ function normalizeQueueMode(raw?: string): QueueMode | undefined { export function extractQueueDirective(body?: string): { cleaned: string; queueMode?: QueueMode; + queueReset: boolean; rawMode?: string; hasDirective: boolean; } { - if (!body) return { cleaned: "", hasDirective: false }; + if (!body) return { cleaned: "", hasDirective: false, queueReset: false }; const match = body.match(/(?:^|\s)\/queue(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i); - const queueMode = normalizeQueueMode(match?.[1]); + const rawMode = match?.[1]; + const lowered = rawMode?.trim().toLowerCase(); + const queueReset = lowered === "default" || lowered === "reset" || lowered === "clear"; + const queueMode = queueReset ? undefined : normalizeQueueMode(rawMode); const cleaned = match ? body.replace(match[0], "").replace(/\s+/g, " ").trim() : body.trim(); return { cleaned, queueMode, - rawMode: match?.[1], + queueReset, + rawMode, hasDirective: !!match, }; } @@ -442,6 +447,7 @@ export async function getReplyFromConfig( const { cleaned: queueCleaned, queueMode: inlineQueueMode, + queueReset: inlineQueueReset, rawMode: rawQueueMode, hasDirective: hasQueueDirective, } = extractQueueDirective(modelCleaned); @@ -580,7 +586,7 @@ export async function getReplyFromConfig( text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`, }; } - if (hasQueueDirective && !inlineQueueMode) { + if (hasQueueDirective && !inlineQueueMode && !inlineQueueReset) { cleanupTyping(); return { text: `Unrecognized queue mode "${rawQueueMode ?? ""}". Valid modes: queue, interrupt, drop.`, @@ -628,7 +634,9 @@ export async function getReplyFromConfig( sessionEntry.modelOverride = modelSelection.model; } } - if (hasQueueDirective && inlineQueueMode) { + if (hasQueueDirective && inlineQueueReset) { + delete sessionEntry.queueMode; + } else if (hasQueueDirective && inlineQueueMode) { sessionEntry.queueMode = inlineQueueMode; } sessionEntry.updatedAt = Date.now(); @@ -661,6 +669,8 @@ export async function getReplyFromConfig( } if (hasQueueDirective && inlineQueueMode) { parts.push(`${SYSTEM_MARK} Queue mode set to ${inlineQueueMode}.`); + } else if (hasQueueDirective && inlineQueueReset) { + parts.push(`${SYSTEM_MARK} Queue mode reset to default.`); } const ack = parts.join(" ").trim(); cleanupTyping(); @@ -711,13 +721,18 @@ export async function getReplyFromConfig( } } } + if (hasQueueDirective && inlineQueueReset) { + delete sessionEntry.queueMode; + updated = true; + } if (updated) { sessionEntry.updatedAt = Date.now(); sessionStore[sessionKey] = sessionEntry; await saveSessionStore(storePath, sessionStore); } } - const perMessageQueueMode = hasQueueDirective ? inlineQueueMode : undefined; + const perMessageQueueMode = + hasQueueDirective && !inlineQueueReset ? inlineQueueMode : undefined; // Optional allowlist by origin number (E.164 without whatsapp: prefix) const configuredAllowFrom = cfg.routing?.allowFrom;