fix: status runtime + help

This commit is contained in:
Peter Steinberger
2026-01-05 07:07:17 +01:00
parent 0d0da2e297
commit 17ef7b3b0e
7 changed files with 81 additions and 9 deletions

View File

@@ -13,6 +13,7 @@
- Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json. - Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json.
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding. - Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments. - Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
### Maintenance ### Maintenance
- Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome. - Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome.

View File

@@ -1,7 +1,9 @@
const CONTROL_COMMAND_RE = const CONTROL_COMMAND_RE =
/(?:^|\s)\/(?:status|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new)(?=$|\s|:)\b/i; /(?:^|\s)\/(?:status|help|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new)(?=$|\s|:)\b/i;
const CONTROL_COMMAND_EXACT = new Set([ const CONTROL_COMMAND_EXACT = new Set([
"help",
"/help",
"status", "status",
"/status", "/status",
"restart", "restart",

View File

@@ -126,6 +126,24 @@ describe("trigger handling", () => {
}); });
}); });
it("returns help without invoking the agent", async () => {
await withTempHome(async (home) => {
const res = await getReplyFromConfig(
{
Body: "/help",
From: "+1002",
To: "+2000",
},
{},
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Help");
expect(text).toContain("Shortcuts");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("allows owner to set send policy", async () => { it("allows owner to set send policy", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
const cfg = { const cfg = {

View File

@@ -472,6 +472,7 @@ export async function getReplyFromConfig(
defaultGroupActivation: () => defaultActivation, defaultGroupActivation: () => defaultActivation,
resolvedThinkLevel, resolvedThinkLevel,
resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
resolvedElevatedLevel,
resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel, resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
provider, provider,
model, model,

View File

@@ -15,9 +15,9 @@ import {
parseActivationCommand, parseActivationCommand,
} from "../group-activation.js"; } from "../group-activation.js";
import { parseSendPolicyCommand } from "../send-policy.js"; import { parseSendPolicyCommand } from "../send-policy.js";
import { buildStatusMessage } from "../status.js"; import { buildHelpMessage, buildStatusMessage } from "../status.js";
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import type { ThinkLevel, VerboseLevel } from "../thinking.js"; import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
import type { ReplyPayload } from "../types.js"; import type { ReplyPayload } from "../types.js";
import { isAbortTrigger, setAbortMemory } from "./abort.js"; import { isAbortTrigger, setAbortMemory } from "./abort.js";
import type { InlineDirectives } from "./directive-handling.js"; import type { InlineDirectives } from "./directive-handling.js";
@@ -121,6 +121,7 @@ export async function handleCommands(params: {
defaultGroupActivation: () => "always" | "mention"; defaultGroupActivation: () => "always" | "mention";
resolvedThinkLevel?: ThinkLevel; resolvedThinkLevel?: ThinkLevel;
resolvedVerboseLevel: VerboseLevel; resolvedVerboseLevel: VerboseLevel;
resolvedElevatedLevel?: ElevatedLevel;
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>; resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
provider: string; provider: string;
model: string; model: string;
@@ -143,6 +144,7 @@ export async function handleCommands(params: {
defaultGroupActivation, defaultGroupActivation,
resolvedThinkLevel, resolvedThinkLevel,
resolvedVerboseLevel, resolvedVerboseLevel,
resolvedElevatedLevel,
resolveDefaultThinkingLevel, resolveDefaultThinkingLevel,
model, model,
contextTokens, contextTokens,
@@ -260,6 +262,20 @@ export async function handleCommands(params: {
}; };
} }
const helpRequested =
command.commandBodyNormalized === "/help" ||
command.commandBodyNormalized === "help" ||
/(?:^|\s)\/help(?=$|\s|:)\b/i.test(command.commandBodyNormalized);
if (helpRequested) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /help from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
return { shouldContinue: false, reply: { text: buildHelpMessage() } };
}
const statusRequested = const statusRequested =
directives.hasStatusDirective || directives.hasStatusDirective ||
command.commandBodyNormalized === "/status" || command.commandBodyNormalized === "/status" ||
@@ -281,10 +297,12 @@ export async function handleCommands(params: {
: undefined; : undefined;
const statusText = buildStatusMessage({ const statusText = buildStatusMessage({
agent: { agent: {
...(cfg.agent ?? {}),
model, model,
contextTokens, contextTokens,
thinkingDefault: cfg.agent?.thinkingDefault, thinkingDefault: cfg.agent?.thinkingDefault,
verboseDefault: cfg.agent?.verboseDefault, verboseDefault: cfg.agent?.verboseDefault,
elevatedDefault: cfg.agent?.elevatedDefault,
}, },
workspaceDir, workspaceDir,
sessionEntry, sessionEntry,
@@ -295,6 +313,7 @@ export async function handleCommands(params: {
resolvedThink: resolvedThink:
resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()), resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
resolvedVerbose: resolvedVerboseLevel, resolvedVerbose: resolvedVerboseLevel,
resolvedElevated: resolvedElevatedLevel,
webLinked, webLinked,
webAuthAgeMs, webAuthAgeMs,
heartbeatSeconds, heartbeatSeconds,

View File

@@ -36,12 +36,14 @@ describe("buildStatusMessage", () => {
expect(text).toContain("⚙️ Status"); expect(text).toContain("⚙️ Status");
expect(text).toContain("Agent: embedded pi"); expect(text).toContain("Agent: embedded pi");
expect(text).toContain("Runtime: direct");
expect(text).toContain("Context: 16k/32k (50%)"); expect(text).toContain("Context: 16k/32k (50%)");
expect(text).toContain("Session: main"); expect(text).toContain("Session: main");
expect(text).toContain("Web: linked"); expect(text).toContain("Web: linked");
expect(text).toContain("heartbeat 45s"); expect(text).toContain("heartbeat 45s");
expect(text).toContain("thinking=medium"); expect(text).toContain("thinking=medium");
expect(text).toContain("verbose=off"); expect(text).toContain("verbose=off");
expect(text).not.toContain("Shortcuts:");
}); });
it("handles missing agent config gracefully", () => { it("handles missing agent config gracefully", () => {

View File

@@ -15,11 +15,12 @@ import {
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { import {
resolveSessionTranscriptPath, resolveSessionTranscriptPath,
resolveMainSessionKey,
type SessionEntry, type SessionEntry,
type SessionScope, type SessionScope,
} from "../config/sessions.js"; } from "../config/sessions.js";
import { shortenHomePath } from "../utils.js"; import { shortenHomePath } from "../utils.js";
import type { ThinkLevel, VerboseLevel } from "./thinking.js"; import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>; type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
@@ -33,6 +34,7 @@ type StatusArgs = {
groupActivation?: "mention" | "always"; groupActivation?: "mention" | "always";
resolvedThink?: ThinkLevel; resolvedThink?: ThinkLevel;
resolvedVerbose?: VerboseLevel; resolvedVerbose?: VerboseLevel;
resolvedElevated?: ElevatedLevel;
now?: number; now?: number;
webLinked?: boolean; webLinked?: boolean;
webAuthAgeMs?: number | null; webAuthAgeMs?: number | null;
@@ -164,8 +166,29 @@ export function buildStatusMessage(args: StatusArgs): string {
const verboseLevel = const verboseLevel =
args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off";
const elevatedLevel = const elevatedLevel =
args.resolvedElevated ??
args.sessionEntry?.elevatedLevel ?? args.agent?.elevatedDefault ?? "on"; args.sessionEntry?.elevatedLevel ?? args.agent?.elevatedDefault ?? "on";
const runtime = (() => {
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
if (sandboxMode === "off")
return { line: "Runtime: direct", sandboxed: false };
const sessionScope = args.sessionScope ?? "per-sender";
const mainKey = resolveMainSessionKey({
session: { scope: sessionScope },
});
const sessionKey = args.sessionKey?.trim();
const sandboxed = sessionKey
? sandboxMode === "all" || sessionKey !== mainKey.trim()
: false;
const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
const suffix = sandboxed ? ` • elevated ${elevatedLevel}` : "";
return {
line: `Runtime: ${runtime} (sandbox ${sandboxMode})${suffix}`,
sandboxed,
};
})();
const webLine = (() => { const webLine = (() => {
if (args.webLinked === false) { if (args.webLinked === false) {
return "Web: not linked — run `clawdbot login` to scan the QR."; return "Web: not linked — run `clawdbot login` to scan the QR.";
@@ -204,7 +227,9 @@ export function buildStatusMessage(args: StatusArgs): string {
contextTokens ?? null, contextTokens ?? null,
)}${entry?.abortedLastRun ? " • last run aborted" : ""}`; )}${entry?.abortedLastRun ? " • last run aborted" : ""}`;
const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel} (set with /think <level>, /verbose on|off, /elevated on|off, /model <id>)`; const optionsLine = runtime.sandboxed
? `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel} (set with /think <level>, /verbose on|off, /elevated on|off, /model <id>)`
: `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think <level>, /verbose on|off, /model <id>)`;
const modelLabel = model ? `${provider}/${model}` : "unknown"; const modelLabel = model ? `${provider}/${model}` : "unknown";
@@ -214,17 +239,21 @@ export function buildStatusMessage(args: StatusArgs): string {
? `Workspace: ${shortenHomePath(args.workspaceDir)}` ? `Workspace: ${shortenHomePath(args.workspaceDir)}`
: undefined; : undefined;
const helpersLine = "Shortcuts: /new reset | /restart relink";
return [ return [
"⚙️ Status", "⚙️ Status",
webLine, webLine,
agentLine, agentLine,
runtime.line,
workspaceLine, workspaceLine,
contextLine, contextLine,
sessionLine, sessionLine,
groupActivationLine, groupActivationLine,
optionsLine, optionsLine,
helpersLine, ]
].join("\n"); .filter(Boolean)
.join("\n");
}
export function buildHelpMessage(): string {
return [" Help", "Shortcuts: /new reset | /restart relink"].join("\n");
} }