refactor: tidy directive parsing + queue status

This commit is contained in:
Peter Steinberger
2026-01-08 03:40:39 +01:00
parent e657e59b46
commit 8aa3efb9e8
7 changed files with 342 additions and 211 deletions

View File

@@ -43,7 +43,7 @@ Text + native (when enabled):
- `/reasoning on|off|stream` (alias: `/reason`; `stream` = Telegram draft only)
- `/elevated on|off` (alias: `/elev`)
- `/model <name>` (or `/<alias>` from `agent.models.*.alias`)
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`)
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
Text-only:
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))

View File

@@ -1,3 +1,5 @@
import { normalizeCommandBody } from "./commands-registry.js";
export type GroupActivationMode = "mention" | "always";
export function normalizeGroupActivation(
@@ -16,11 +18,9 @@ export function parseActivationCommand(raw?: string): {
if (!raw) return { hasCommand: false };
const trimmed = raw.trim();
if (!trimmed) return { hasCommand: false };
const match = trimmed.match(
/^\/activation(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i,
);
const normalized = normalizeCommandBody(trimmed);
const match = normalized.match(/^\/activation(?:\s+([a-zA-Z]+))?\s*$/i);
if (!match) return { hasCommand: false };
const token = match[1] ?? match[2];
const mode = normalizeGroupActivation(token);
const mode = normalizeGroupActivation(match[1]);
return { hasCommand: true, mode };
}

View File

@@ -0,0 +1,154 @@
import { describe, expect, it } from "vitest";
import {
extractElevatedDirective,
extractQueueDirective,
extractReasoningDirective,
extractReplyToTag,
extractThinkDirective,
extractVerboseDirective,
} from "./reply.js";
describe("directive parsing", () => {
it("ignores verbose directive inside URL", () => {
const body = "https://x.com/verioussmith/status/1997066835133669687";
const res = extractVerboseDirective(body);
expect(res.hasDirective).toBe(false);
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);
expect(res.hasDirective).toBe(false);
});
it("matches verbose with leading space", () => {
const res = extractVerboseDirective(" please /verbose on now");
expect(res.hasDirective).toBe(true);
expect(res.verboseLevel).toBe("on");
});
it("matches reasoning directive", () => {
const res = extractReasoningDirective("/reasoning on please");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBe("on");
});
it("matches reasoning stream directive", () => {
const res = extractReasoningDirective("/reasoning stream please");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBe("stream");
});
it("matches elevated with leading space", () => {
const res = extractElevatedDirective(" please /elevated on now");
expect(res.hasDirective).toBe(true);
expect(res.elevatedLevel).toBe("on");
});
it("matches think at start of line", () => {
const res = extractThinkDirective("/think:high run slow");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBe("high");
});
it("does not match /think followed by extra letters", () => {
// e.g. someone typing "/think" + extra letter "hink"
const res = extractThinkDirective("/thinkstuff");
expect(res.hasDirective).toBe(false);
});
it("matches /think with no argument", () => {
const res = extractThinkDirective("/think");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
});
it("matches /t with no argument", () => {
const res = extractThinkDirective("/t");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
});
it("matches think with no argument and consumes colon", () => {
const res = extractThinkDirective("/think:");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches verbose with no argument", () => {
const res = extractVerboseDirective("/verbose:");
expect(res.hasDirective).toBe(true);
expect(res.verboseLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches reasoning with no argument", () => {
const res = extractReasoningDirective("/reasoning:");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches elevated with no argument", () => {
const res = extractElevatedDirective("/elevated:");
expect(res.hasDirective).toBe(true);
expect(res.elevatedLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches queue directive", () => {
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");
});
it("parses queue options and modes", () => {
const res = extractQueueDirective(
"please /queue steer+backlog debounce:2s cap:5 drop:summarize now",
);
expect(res.hasDirective).toBe(true);
expect(res.queueMode).toBe("steer-backlog");
expect(res.debounceMs).toBe(2000);
expect(res.cap).toBe(5);
expect(res.dropPolicy).toBe("summarize");
expect(res.cleaned).toBe("please now");
});
it("extracts reply_to_current tag", () => {
const res = extractReplyToTag("ok [[reply_to_current]]", "msg-1");
expect(res.replyToId).toBe("msg-1");
expect(res.cleaned).toBe("ok");
});
it("extracts reply_to id tag", () => {
const res = extractReplyToTag("see [[reply_to:12345]] now", "msg-1");
expect(res.replyToId).toBe("12345");
expect(res.cleaned).toBe("see now");
});
it("preserves newlines when stripping reply tags", () => {
const res = extractReplyToTag(
"line 1\nline 2 [[reply_to_current]]\n\nline 3",
"msg-2",
);
expect(res.replyToId).toBe("msg-2");
expect(res.cleaned).toBe("line 1\nline 2\n\nline 3");
});
});

View File

@@ -12,15 +12,7 @@ import {
saveSessionStore,
} from "../config/sessions.js";
import { drainSystemEvents } from "../infra/system-events.js";
import {
extractElevatedDirective,
extractQueueDirective,
extractReasoningDirective,
extractReplyToTag,
extractThinkDirective,
extractVerboseDirective,
getReplyFromConfig,
} from "./reply.js";
import { getReplyFromConfig } from "./reply.js";
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
@@ -60,7 +52,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
}
}
describe("directive parsing", () => {
describe("directive behavior", () => {
beforeEach(() => {
vi.mocked(runEmbeddedPiAgent).mockReset();
vi.mocked(loadModelCatalog).mockResolvedValue([
@@ -74,127 +66,6 @@ describe("directive parsing", () => {
vi.restoreAllMocks();
});
it("ignores verbose directive inside URL", () => {
const body = "https://x.com/verioussmith/status/1997066835133669687";
const res = extractVerboseDirective(body);
expect(res.hasDirective).toBe(false);
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);
expect(res.hasDirective).toBe(false);
});
it("matches verbose with leading space", () => {
const res = extractVerboseDirective(" please /verbose on now");
expect(res.hasDirective).toBe(true);
expect(res.verboseLevel).toBe("on");
});
it("matches reasoning directive", () => {
const res = extractReasoningDirective("/reasoning on please");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBe("on");
});
it("matches reasoning stream directive", () => {
const res = extractReasoningDirective("/reasoning stream please");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBe("stream");
});
it("matches elevated with leading space", () => {
const res = extractElevatedDirective(" please /elevated on now");
expect(res.hasDirective).toBe(true);
expect(res.elevatedLevel).toBe("on");
});
it("matches think at start of line", () => {
const res = extractThinkDirective("/think:high run slow");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBe("high");
});
it("does not match /think followed by extra letters", () => {
// e.g. someone typing "/think" + extra letter "hink"
const res = extractThinkDirective("/thinkstuff");
expect(res.hasDirective).toBe(false);
});
it("matches /think with no argument", () => {
const res = extractThinkDirective("/think");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
});
it("matches /t with no argument", () => {
const res = extractThinkDirective("/t");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
});
it("matches think with no argument and consumes colon", () => {
const res = extractThinkDirective("/think:");
expect(res.hasDirective).toBe(true);
expect(res.thinkLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches verbose with no argument", () => {
const res = extractVerboseDirective("/verbose:");
expect(res.hasDirective).toBe(true);
expect(res.verboseLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches reasoning with no argument", () => {
const res = extractReasoningDirective("/reasoning:");
expect(res.hasDirective).toBe(true);
expect(res.reasoningLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches elevated with no argument", () => {
const res = extractElevatedDirective("/elevated:");
expect(res.hasDirective).toBe(true);
expect(res.elevatedLevel).toBeUndefined();
expect(res.rawLevel).toBeUndefined();
expect(res.cleaned).toBe("");
});
it("matches queue directive", () => {
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");
});
it("parses queue options and modes", () => {
const res = extractQueueDirective(
"please /queue steer+backlog debounce:2s cap:5 drop:summarize now",
);
expect(res.hasDirective).toBe(true);
expect(res.queueMode).toBe("steer-backlog");
expect(res.debounceMs).toBe(2000);
expect(res.cap).toBe(5);
expect(res.dropPolicy).toBe("summarize");
expect(res.cleaned).toBe("please now");
});
it("keeps reserved command aliases from matching after trimming", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
@@ -254,6 +125,44 @@ describe("directive parsing", () => {
});
});
it("shows current queue settings when /queue has no arguments", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{
Body: "/queue",
From: "+1222",
To: "+1222",
Provider: "whatsapp",
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
routing: {
queue: {
mode: "collect",
debounceMs: 1500,
cap: 9,
drop: "summarize",
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
},
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain(
"Current queue settings: mode=collect, debounce=1500ms, cap=9, drop=summarize.",
);
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("shows current think level when /think has no argument", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
@@ -299,27 +208,6 @@ describe("directive parsing", () => {
});
});
it("extracts reply_to_current tag", () => {
const res = extractReplyToTag("ok [[reply_to_current]]", "msg-1");
expect(res.replyToId).toBe("msg-1");
expect(res.cleaned).toBe("ok");
});
it("extracts reply_to id tag", () => {
const res = extractReplyToTag("see [[reply_to:12345]] now", "msg-1");
expect(res.replyToId).toBe("12345");
expect(res.cleaned).toBe("see now");
});
it("preserves newlines when stripping reply tags", () => {
const res = extractReplyToTag(
"line 1\nline 2 [[reply_to_current]]\n\nline 3",
"msg-2",
);
expect(res.replyToId).toBe("msg-2");
expect(res.cleaned).toBe("line 1\nline 2\n\nline 3");
});
it("strips reply tags and maps reply_to_current to MessageSid", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({

View File

@@ -50,6 +50,7 @@ import {
extractQueueDirective,
type QueueDropPolicy,
type QueueMode,
resolveQueueSettings,
} from "./queue.js";
const SYSTEM_MARK = "⚙️";
@@ -328,6 +329,7 @@ export async function handleDirectiveOnly(params: {
allowedModelKeys,
allowedModelCatalog,
resetModelOverride,
provider,
initialModelLabel,
formatModelSwitchEvent,
currentThinkLevel,
@@ -433,6 +435,33 @@ export async function handleDirectiveOnly(params: {
return { text: "elevated is not available right now." };
}
if (
directives.hasQueueDirective &&
!directives.queueMode &&
!directives.queueReset &&
!directives.hasQueueOptions &&
directives.rawQueueMode === undefined &&
directives.rawDebounce === undefined &&
directives.rawCap === undefined &&
directives.rawDrop === undefined
) {
const settings = resolveQueueSettings({
cfg: params.cfg,
provider,
sessionEntry,
});
const debounceLabel =
typeof settings.debounceMs === "number"
? `${settings.debounceMs}ms`
: "default";
const capLabel =
typeof settings.cap === "number" ? String(settings.cap) : "default";
const dropLabel = settings.dropPolicy ?? "default";
return {
text: `Current queue settings: mode=${settings.mode}, debounce=${debounceLabel}, cap=${capLabel}, drop=${dropLabel}.`,
};
}
const queueModeInvalid =
directives.hasQueueDirective &&
!directives.queueMode &&

View File

@@ -9,6 +9,81 @@ import {
type VerboseLevel,
} from "../thinking.js";
type ExtractedLevel<T> = {
cleaned: string;
level?: T;
rawLevel?: string;
hasDirective: boolean;
};
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const matchLevelDirective = (
body: string,
names: string[],
): { start: number; end: number; rawLevel?: string } | null => {
const namePattern = names.map(escapeRegExp).join("|");
const match = body.match(
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"),
);
if (!match || match.index === undefined) return null;
const start = match.index;
let end = match.index + match[0].length;
let i = end;
while (i < body.length && /\s/.test(body[i])) i += 1;
if (body[i] === ":") {
i += 1;
while (i < body.length && /\s/.test(body[i])) i += 1;
}
const argStart = i;
while (i < body.length && /[A-Za-z-]/.test(body[i])) i += 1;
const rawLevel = i > argStart ? body.slice(argStart, i) : undefined;
end = i;
return { start, end, rawLevel };
};
const extractLevelDirective = <T>(
body: string,
names: string[],
normalize: (raw?: string) => T | undefined,
): ExtractedLevel<T> => {
const match = matchLevelDirective(body, names);
if (!match) {
return { cleaned: body.trim(), hasDirective: false };
}
const rawLevel = match.rawLevel;
const level = normalize(rawLevel);
const cleaned = body
.slice(0, match.start)
.concat(body.slice(match.end))
.replace(/\s+/g, " ")
.trim();
return {
cleaned,
level,
rawLevel,
hasDirective: true,
};
};
const extractSimpleDirective = (
body: string,
names: string[],
): { cleaned: string; hasDirective: boolean } => {
const namePattern = names.map(escapeRegExp).join("|");
const match = body.match(
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"),
);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
hasDirective: Boolean(match),
};
};
export function extractThinkDirective(body?: string): {
cleaned: string;
thinkLevel?: ThinkLevel;
@@ -16,19 +91,16 @@ export function extractThinkDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
// Match with optional argument - require word boundary via lookahead after keyword
const match = body.match(
/(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
const extracted = extractLevelDirective(
body,
["thinking", "think", "t"],
normalizeThinkLevel,
);
const thinkLevel = normalizeThinkLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
thinkLevel,
rawLevel: match?.[1],
hasDirective: !!match,
cleaned: extracted.cleaned,
thinkLevel: extracted.level,
rawLevel: extracted.rawLevel,
hasDirective: extracted.hasDirective,
};
}
@@ -39,18 +111,16 @@ export function extractVerboseDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(
/(?:^|\s)\/(?:verbose|v)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
const extracted = extractLevelDirective(
body,
["verbose", "v"],
normalizeVerboseLevel,
);
const verboseLevel = normalizeVerboseLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
verboseLevel,
rawLevel: match?.[1],
hasDirective: !!match,
cleaned: extracted.cleaned,
verboseLevel: extracted.level,
rawLevel: extracted.rawLevel,
hasDirective: extracted.hasDirective,
};
}
@@ -61,18 +131,16 @@ export function extractElevatedDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(
/(?:^|\s)\/(?:elevated|elev)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
const extracted = extractLevelDirective(
body,
["elevated", "elev"],
normalizeElevatedLevel,
);
const elevatedLevel = normalizeElevatedLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
elevatedLevel,
rawLevel: match?.[1],
hasDirective: !!match,
cleaned: extracted.cleaned,
elevatedLevel: extracted.level,
rawLevel: extracted.rawLevel,
hasDirective: extracted.hasDirective,
};
}
@@ -83,18 +151,16 @@ export function extractReasoningDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(
/(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
const extracted = extractLevelDirective(
body,
["reasoning", "reason"],
normalizeReasoningLevel,
);
const reasoningLevel = normalizeReasoningLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
reasoningLevel,
rawLevel: match?.[1],
hasDirective: !!match,
cleaned: extracted.cleaned,
reasoningLevel: extracted.level,
rawLevel: extracted.rawLevel,
hasDirective: extracted.hasDirective,
};
}
@@ -103,14 +169,7 @@ export function extractStatusDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(/(?:^|\s)\/status(?=$|\s|:)(?:\s*:\s*)?/i);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
hasDirective: !!match,
};
return extractSimpleDirective(body, ["status"]);
}
export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };

View File

@@ -1,3 +1,5 @@
import { normalizeCommandBody } from "./commands-registry.js";
export type SendPolicyOverride = "allow" | "deny";
export function normalizeSendPolicyOverride(
@@ -17,11 +19,10 @@ export function parseSendPolicyCommand(raw?: string): {
if (!raw) return { hasCommand: false };
const trimmed = raw.trim();
if (!trimmed) return { hasCommand: false };
const match = trimmed.match(
/^\/send(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i,
);
const normalized = normalizeCommandBody(trimmed);
const match = normalized.match(/^\/send(?:\s+([a-zA-Z]+))?\s*$/i);
if (!match) return { hasCommand: false };
const token = (match[1] ?? match[2])?.trim().toLowerCase();
const token = match[1]?.trim().toLowerCase();
if (!token) return { hasCommand: true };
if (token === "inherit" || token === "default" || token === "reset") {
return { hasCommand: true, mode: "inherit" };