fix: harden directive handling
This commit is contained in:
@@ -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/`.
|
- 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.
|
- 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.
|
- 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 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.
|
- 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.
|
- 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.
|
- Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings.
|
||||||
|
|||||||
@@ -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 * as tauRpc from "../process/tau-rpc.js";
|
||||||
import { getReplyFromConfig, extractVerboseDirective, extractThinkDirective } from "./reply.js";
|
import { getReplyFromConfig, extractVerboseDirective, extractThinkDirective } from "./reply.js";
|
||||||
|
|
||||||
describe("directive parsing", () => {
|
describe("directive parsing", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores verbose directive inside URL", () => {
|
it("ignores verbose directive inside URL", () => {
|
||||||
const body = "https://x.com/verioussmith/status/1997066835133669687";
|
const body = "https://x.com/verioussmith/status/1997066835133669687";
|
||||||
const res = extractVerboseDirective(body);
|
const res = extractVerboseDirective(body);
|
||||||
@@ -10,6 +14,13 @@ describe("directive parsing", () => {
|
|||||||
expect(res.cleaned).toBe(body);
|
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", () => {
|
it("ignores think directive inside URL", () => {
|
||||||
const body = "see https://example.com/path/thinkstuff";
|
const body = "see https://example.com/path/thinkstuff";
|
||||||
const res = extractThinkDirective(body);
|
const res = extractThinkDirective(body);
|
||||||
@@ -61,4 +72,33 @@ describe("directive parsing", () => {
|
|||||||
expect(text).toBe("done");
|
expect(text).toBe("done");
|
||||||
expect(rpcMock).toHaveBeenCalledOnce();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
|||||||
|
|
||||||
const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
|
const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
|
||||||
const ABORT_MEMORY = new Map<string, boolean>();
|
const ABORT_MEMORY = new Map<string, boolean>();
|
||||||
|
const SYSTEM_MARK = "⚙️";
|
||||||
|
|
||||||
export function extractThinkDirective(body?: string): {
|
export function extractThinkDirective(body?: string): {
|
||||||
cleaned: string;
|
cleaned: string;
|
||||||
@@ -67,8 +68,8 @@ export function extractVerboseDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
// Require start or whitespace before "/verbose" to avoid matching URLs like /verioussmith.
|
// Require start or whitespace before "/verbose" and reject "/ver*" typos.
|
||||||
const match = body.match(/(?:^|\s)\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i);
|
const match = body.match(/(?:^|\s)\/v(?:erbose)?\b\s*:?\s*([a-zA-Z-]+)\b/i);
|
||||||
const verboseLevel = normalizeVerboseLevel(match?.[1]);
|
const verboseLevel = normalizeVerboseLevel(match?.[1]);
|
||||||
const cleaned = match
|
const cleaned = match
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
||||||
@@ -364,7 +365,7 @@ export async function getReplyFromConfig(
|
|||||||
if (!inlineThink) {
|
if (!inlineThink) {
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return {
|
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) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
@@ -413,7 +414,7 @@ export async function getReplyFromConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ack = parts.join(" ");
|
const ack = `${SYSTEM_MARK} ${parts.join(" ")}`;
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return { text: ack };
|
return { text: ack };
|
||||||
}
|
}
|
||||||
@@ -430,7 +431,7 @@ export async function getReplyFromConfig(
|
|||||||
if (!inlineVerbose) {
|
if (!inlineVerbose) {
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return {
|
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) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
@@ -445,8 +446,8 @@ export async function getReplyFromConfig(
|
|||||||
}
|
}
|
||||||
const ack =
|
const ack =
|
||||||
inlineVerbose === "off"
|
inlineVerbose === "off"
|
||||||
? "Verbose logging disabled."
|
? `${SYSTEM_MARK} Verbose logging disabled.`
|
||||||
: "Verbose logging enabled.";
|
: `${SYSTEM_MARK} Verbose logging enabled.`;
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return { text: ack };
|
return { text: ack };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user