fix: refine status usage and elevated directives
This commit is contained in:
@@ -56,6 +56,11 @@
|
|||||||
- Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj
|
- Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj
|
||||||
- Docs: add community showcase entries from Discord. (#476) — thanks @gupsammy
|
- Docs: add community showcase entries from Discord. (#476) — thanks @gupsammy
|
||||||
- TUI: refresh status bar after think/verbose/reasoning changes. (#519) — thanks @jdrhyne
|
- TUI: refresh status bar after think/verbose/reasoning changes. (#519) — thanks @jdrhyne
|
||||||
|
- Status: show Verbose/Elevated only when enabled.
|
||||||
|
- Status: filter usage summary to the active model provider.
|
||||||
|
- Commands: allow /elevated off in groups without a mention; keep /elevated on mention-gated.
|
||||||
|
- Commands: keep multi-directive messages from clearing directive handling.
|
||||||
|
- Commands: warn when /elevated runs in direct (unsandboxed) runtime.
|
||||||
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
||||||
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
|
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
|
||||||
|
|
||||||
|
|||||||
@@ -472,6 +472,40 @@ describe("directive behavior", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("warns when elevated is used in direct runtime", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1222",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
|
},
|
||||||
|
sandbox: { mode: "off" },
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["+1222"] },
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
expect(text).toContain("Runtime is direct; sandboxing does not apply.");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects invalid elevated level", async () => {
|
it("rejects invalid elevated level", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
|||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const usageMocks = vi.hoisted(() => ({
|
||||||
|
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
||||||
|
updatedAt: 0,
|
||||||
|
providers: [],
|
||||||
|
}),
|
||||||
|
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
abortEmbeddedPiRun,
|
abortEmbeddedPiRun,
|
||||||
compactEmbeddedPiSession,
|
compactEmbeddedPiSession,
|
||||||
@@ -66,6 +76,30 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
|
it("filters usage summary to the current model provider", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
usageMocks.loadProviderUsageSummary.mockClear();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/status",
|
||||||
|
From: "+1000",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
makeCfg(home),
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("📊 Usage: Claude 80% left");
|
||||||
|
expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ providers: ["anthropic"] }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("aborts even with timestamp prefix", async () => {
|
it("aborts even with timestamp prefix", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
@@ -383,6 +417,48 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows elevated off in groups without mention", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
payloads: [{ text: "ok" }],
|
||||||
|
meta: {
|
||||||
|
durationMs: 1,
|
||||||
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cfg = {
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: {
|
||||||
|
allowFrom: ["+1000"],
|
||||||
|
groups: { "*": { requireMention: false } },
|
||||||
|
},
|
||||||
|
session: { store: join(home, "sessions.json") },
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off",
|
||||||
|
From: "group:123@g.us",
|
||||||
|
To: "whatsapp:+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
ChatType: "group",
|
||||||
|
WasMentioned: false,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows elevated directive in groups when mentioned", async () => {
|
it("allows elevated directive in groups when mentioned", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|||||||
@@ -329,11 +329,19 @@ export async function getReplyFromConfig(
|
|||||||
.map((entry) => entry.alias?.trim())
|
.map((entry) => entry.alias?.trim())
|
||||||
.filter((alias): alias is string => Boolean(alias))
|
.filter((alias): alias is string => Boolean(alias))
|
||||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||||
const disableElevatedInGroup = isGroup && ctx.WasMentioned !== true;
|
|
||||||
let parsedDirectives = parseInlineDirectives(rawBody, {
|
let parsedDirectives = parseInlineDirectives(rawBody, {
|
||||||
modelAliases: configuredAliases,
|
modelAliases: configuredAliases,
|
||||||
disableElevated: disableElevatedInGroup,
|
|
||||||
});
|
});
|
||||||
|
if (isGroup && ctx.WasMentioned !== true && parsedDirectives.hasElevatedDirective) {
|
||||||
|
if (parsedDirectives.elevatedLevel !== "off") {
|
||||||
|
parsedDirectives = {
|
||||||
|
...parsedDirectives,
|
||||||
|
hasElevatedDirective: false,
|
||||||
|
elevatedLevel: undefined,
|
||||||
|
rawElevatedLevel: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
const hasDirective =
|
const hasDirective =
|
||||||
parsedDirectives.hasThinkDirective ||
|
parsedDirectives.hasThinkDirective ||
|
||||||
parsedDirectives.hasVerboseDirective ||
|
parsedDirectives.hasVerboseDirective ||
|
||||||
@@ -348,7 +356,13 @@ export async function getReplyFromConfig(
|
|||||||
? stripMentions(stripped, ctx, cfg, agentId)
|
? stripMentions(stripped, ctx, cfg, agentId)
|
||||||
: stripped;
|
: stripped;
|
||||||
if (noMentions.trim().length > 0) {
|
if (noMentions.trim().length > 0) {
|
||||||
parsedDirectives = clearInlineDirectives(parsedDirectives.cleaned);
|
const directiveOnlyCheck = parseInlineDirectives(noMentions, {
|
||||||
|
modelAliases: configuredAliases,
|
||||||
|
disableElevated: disableElevatedInGroup,
|
||||||
|
});
|
||||||
|
if (directiveOnlyCheck.cleaned.trim().length > 0) {
|
||||||
|
parsedDirectives = clearInlineDirectives(parsedDirectives.cleaned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const directives = commandAuthorized
|
const directives = commandAuthorized
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
normalizeCommandBody,
|
normalizeCommandBody,
|
||||||
shouldHandleTextCommands,
|
shouldHandleTextCommands,
|
||||||
} from "../commands-registry.js";
|
} from "../commands-registry.js";
|
||||||
|
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||||
import {
|
import {
|
||||||
normalizeGroupActivation,
|
normalizeGroupActivation,
|
||||||
parseActivationCommand,
|
parseActivationCommand,
|
||||||
@@ -424,6 +425,7 @@ export async function handleCommands(params: {
|
|||||||
try {
|
try {
|
||||||
const usageSummary = await loadProviderUsageSummary({
|
const usageSummary = await loadProviderUsageSummary({
|
||||||
timeoutMs: 3500,
|
timeoutMs: 3500,
|
||||||
|
providers: [normalizeProviderId(provider)],
|
||||||
});
|
});
|
||||||
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
|
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import {
|
|||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import {
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
|
resolveAgentMainSessionKey,
|
||||||
|
type SessionEntry,
|
||||||
|
saveSessionStore,
|
||||||
|
} from "../../config/sessions.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { shortenHomePath } from "../../utils.js";
|
import { shortenHomePath } from "../../utils.js";
|
||||||
import { extractModelDirective } from "../model.js";
|
import { extractModelDirective } from "../model.js";
|
||||||
@@ -57,6 +62,8 @@ const SYSTEM_MARK = "⚙️";
|
|||||||
const formatOptionsLine = (options: string) => `Options: ${options}.`;
|
const formatOptionsLine = (options: string) => `Options: ${options}.`;
|
||||||
const withOptions = (line: string, options: string) =>
|
const withOptions = (line: string, options: string) =>
|
||||||
`${line}\n${formatOptionsLine(options)}`;
|
`${line}\n${formatOptionsLine(options)}`;
|
||||||
|
const formatElevatedRuntimeHint = () =>
|
||||||
|
`${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`;
|
||||||
|
|
||||||
const maskApiKey = (value: string): string => {
|
const maskApiKey = (value: string): string => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -350,6 +357,21 @@ export async function handleDirectiveOnly(params: {
|
|||||||
currentReasoningLevel,
|
currentReasoningLevel,
|
||||||
currentElevatedLevel,
|
currentElevatedLevel,
|
||||||
} = params;
|
} = params;
|
||||||
|
const runtimeIsSandboxed = (() => {
|
||||||
|
const sandboxMode = params.cfg.agent?.sandbox?.mode ?? "off";
|
||||||
|
if (sandboxMode === "off") return false;
|
||||||
|
const sessionKey = params.sessionKey?.trim();
|
||||||
|
if (!sessionKey) return false;
|
||||||
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||||
|
const mainKey = resolveAgentMainSessionKey({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
if (sandboxMode === "all") return true;
|
||||||
|
return sessionKey !== mainKey;
|
||||||
|
})();
|
||||||
|
const shouldHintDirectRuntime =
|
||||||
|
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||||
|
|
||||||
if (directives.hasModelDirective) {
|
if (directives.hasModelDirective) {
|
||||||
const modelDirective = directives.rawModelDirective?.trim().toLowerCase();
|
const modelDirective = directives.rawModelDirective?.trim().toLowerCase();
|
||||||
@@ -463,7 +485,12 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}
|
}
|
||||||
const level = currentElevatedLevel ?? "off";
|
const level = currentElevatedLevel ?? "off";
|
||||||
return {
|
return {
|
||||||
text: withOptions(`Current elevated level: ${level}.`, "on, off"),
|
text: [
|
||||||
|
withOptions(`Current elevated level: ${level}.`, "on, off"),
|
||||||
|
shouldHintDirectRuntime ? formatElevatedRuntimeHint() : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -681,6 +708,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
? `${SYSTEM_MARK} Elevated mode disabled.`
|
? `${SYSTEM_MARK} Elevated mode disabled.`
|
||||||
: `${SYSTEM_MARK} Elevated mode enabled.`,
|
: `${SYSTEM_MARK} Elevated mode enabled.`,
|
||||||
);
|
);
|
||||||
|
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
|
||||||
}
|
}
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ describe("buildStatusMessage", () => {
|
|||||||
expect(text).toContain("Session: agent:main:main");
|
expect(text).toContain("Session: agent:main:main");
|
||||||
expect(text).toContain("updated 10m ago");
|
expect(text).toContain("updated 10m ago");
|
||||||
expect(text).toContain("Think: medium");
|
expect(text).toContain("Think: medium");
|
||||||
expect(text).toContain("Verbose: off");
|
expect(text).not.toContain("Verbose");
|
||||||
expect(text).toContain("Elevated: on");
|
expect(text).toContain("Elevated");
|
||||||
expect(text).toContain("Queue: collect");
|
expect(text).toContain("Queue: collect");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -267,9 +267,9 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
const optionParts = [
|
const optionParts = [
|
||||||
`Runtime: ${runtime.label}`,
|
`Runtime: ${runtime.label}`,
|
||||||
`Think: ${thinkLevel}`,
|
`Think: ${thinkLevel}`,
|
||||||
`Verbose: ${verboseLevel}`,
|
verboseLevel === "on" ? "Verbose" : null,
|
||||||
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
||||||
`Elevated: ${elevatedLevel}`,
|
elevatedLevel === "on" ? "Elevated" : null,
|
||||||
];
|
];
|
||||||
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
||||||
const activationParts = [
|
const activationParts = [
|
||||||
|
|||||||
Reference in New Issue
Block a user