feat(queue): add reset/default directive
This commit is contained in:
@@ -44,6 +44,7 @@ Configure globally or per surface via `routing.queue`:
|
|||||||
## Per-session overrides
|
## Per-session overrides
|
||||||
- `/queue <mode>` as a standalone command stores the mode for the current session.
|
- `/queue <mode>` as a standalone command stores the mode for the current session.
|
||||||
- `/queue <mode>` embedded in a message applies **once** (no persistence).
|
- `/queue <mode>` embedded in a message applies **once** (no persistence).
|
||||||
|
- `/queue default` or `/queue reset` clears the session override.
|
||||||
|
|
||||||
## Scope and guarantees
|
## Scope and guarantees
|
||||||
- Applies only to config-driven command replies; plain text replies are unaffected.
|
- Applies only to config-driven command replies; plain text replies are unaffected.
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ describe("directive parsing", () => {
|
|||||||
const res = extractQueueDirective("please /queue interrupt now");
|
const res = extractQueueDirective("please /queue interrupt now");
|
||||||
expect(res.hasDirective).toBe(true);
|
expect(res.hasDirective).toBe(true);
|
||||||
expect(res.queueMode).toBe("interrupt");
|
expect(res.queueMode).toBe("interrupt");
|
||||||
|
expect(res.queueReset).toBe(false);
|
||||||
expect(res.cleaned).toBe("please now");
|
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 () => {
|
it("updates tool verbose during an in-flight run (toggle on)", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const storePath = path.join(home, "sessions.json");
|
const storePath = path.join(home, "sessions.json");
|
||||||
|
|||||||
@@ -130,19 +130,24 @@ function normalizeQueueMode(raw?: string): QueueMode | undefined {
|
|||||||
export function extractQueueDirective(body?: string): {
|
export function extractQueueDirective(body?: string): {
|
||||||
cleaned: string;
|
cleaned: string;
|
||||||
queueMode?: QueueMode;
|
queueMode?: QueueMode;
|
||||||
|
queueReset: boolean;
|
||||||
rawMode?: string;
|
rawMode?: string;
|
||||||
hasDirective: boolean;
|
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 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
|
const cleaned = match
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
||||||
: body.trim();
|
: body.trim();
|
||||||
return {
|
return {
|
||||||
cleaned,
|
cleaned,
|
||||||
queueMode,
|
queueMode,
|
||||||
rawMode: match?.[1],
|
queueReset,
|
||||||
|
rawMode,
|
||||||
hasDirective: !!match,
|
hasDirective: !!match,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -442,6 +447,7 @@ export async function getReplyFromConfig(
|
|||||||
const {
|
const {
|
||||||
cleaned: queueCleaned,
|
cleaned: queueCleaned,
|
||||||
queueMode: inlineQueueMode,
|
queueMode: inlineQueueMode,
|
||||||
|
queueReset: inlineQueueReset,
|
||||||
rawMode: rawQueueMode,
|
rawMode: rawQueueMode,
|
||||||
hasDirective: hasQueueDirective,
|
hasDirective: hasQueueDirective,
|
||||||
} = extractQueueDirective(modelCleaned);
|
} = extractQueueDirective(modelCleaned);
|
||||||
@@ -580,7 +586,7 @@ export async function getReplyFromConfig(
|
|||||||
text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
|
text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (hasQueueDirective && !inlineQueueMode) {
|
if (hasQueueDirective && !inlineQueueMode && !inlineQueueReset) {
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return {
|
return {
|
||||||
text: `Unrecognized queue mode "${rawQueueMode ?? ""}". Valid modes: queue, interrupt, drop.`,
|
text: `Unrecognized queue mode "${rawQueueMode ?? ""}". Valid modes: queue, interrupt, drop.`,
|
||||||
@@ -628,7 +634,9 @@ export async function getReplyFromConfig(
|
|||||||
sessionEntry.modelOverride = modelSelection.model;
|
sessionEntry.modelOverride = modelSelection.model;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasQueueDirective && inlineQueueMode) {
|
if (hasQueueDirective && inlineQueueReset) {
|
||||||
|
delete sessionEntry.queueMode;
|
||||||
|
} else if (hasQueueDirective && inlineQueueMode) {
|
||||||
sessionEntry.queueMode = inlineQueueMode;
|
sessionEntry.queueMode = inlineQueueMode;
|
||||||
}
|
}
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
@@ -661,6 +669,8 @@ export async function getReplyFromConfig(
|
|||||||
}
|
}
|
||||||
if (hasQueueDirective && inlineQueueMode) {
|
if (hasQueueDirective && inlineQueueMode) {
|
||||||
parts.push(`${SYSTEM_MARK} Queue mode set to ${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();
|
const ack = parts.join(" ").trim();
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
@@ -711,13 +721,18 @@ export async function getReplyFromConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasQueueDirective && inlineQueueReset) {
|
||||||
|
delete sessionEntry.queueMode;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
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)
|
// Optional allowlist by origin number (E.164 without whatsapp: prefix)
|
||||||
const configuredAllowFrom = cfg.routing?.allowFrom;
|
const configuredAllowFrom = cfg.routing?.allowFrom;
|
||||||
|
|||||||
Reference in New Issue
Block a user