fix: refresh status output
This commit is contained in:
@@ -175,7 +175,7 @@ describe("trigger handling", () => {
|
||||
makeCfg(home),
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Status");
|
||||
expect(text).toContain("ClawdBot");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,8 +25,6 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import { resolveHeartbeatSeconds } from "../../web/reconnect.js";
|
||||
import { getWebAuthAgeMs, webAuthExists } from "../../web/session.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { shouldHandleTextCommands } from "../commands-registry.js";
|
||||
import {
|
||||
@@ -51,6 +49,7 @@ import type { ReplyPayload } from "../types.js";
|
||||
import { isAbortTrigger, setAbortMemory } from "./abort.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
|
||||
import { incrementCompactionCount } from "./session-updates.js";
|
||||
|
||||
function resolveSessionEntryForKey(
|
||||
@@ -384,9 +383,18 @@ export async function handleCommands(params: {
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
const webLinked = await webAuthExists();
|
||||
const webAuthAgeMs = getWebAuthAgeMs();
|
||||
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined);
|
||||
const queueSettings = resolveQueueSettings({
|
||||
cfg,
|
||||
provider: command.provider,
|
||||
sessionEntry,
|
||||
});
|
||||
const queueKey = sessionKey ?? sessionEntry?.sessionId;
|
||||
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
|
||||
const queueOverrides = Boolean(
|
||||
sessionEntry?.queueDebounceMs ??
|
||||
sessionEntry?.queueCap ??
|
||||
sessionEntry?.queueDrop,
|
||||
);
|
||||
const groupActivation = isGroup
|
||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
||||
defaultGroupActivation())
|
||||
@@ -403,11 +411,9 @@ export async function handleCommands(params: {
|
||||
verboseDefault: cfg.agent?.verboseDefault,
|
||||
elevatedDefault: cfg.agent?.elevatedDefault,
|
||||
},
|
||||
workspaceDir,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
sessionScope,
|
||||
storePath,
|
||||
groupActivation,
|
||||
resolvedThink:
|
||||
resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
|
||||
@@ -415,9 +421,15 @@ export async function handleCommands(params: {
|
||||
resolvedReasoning: resolvedReasoningLevel,
|
||||
resolvedElevated: resolvedElevatedLevel,
|
||||
modelAuth: resolveModelAuthLabel(provider, cfg),
|
||||
webLinked,
|
||||
webAuthAgeMs,
|
||||
heartbeatSeconds,
|
||||
queue: {
|
||||
mode: queueSettings.mode,
|
||||
depth: queueDepth,
|
||||
debounceMs: queueSettings.debounceMs,
|
||||
cap: queueSettings.cap,
|
||||
dropPolicy: queueSettings.dropPolicy,
|
||||
showDetails: queueOverrides,
|
||||
},
|
||||
includeTranscriptUsage: false,
|
||||
});
|
||||
return { shouldContinue: false, reply: { text: statusText } };
|
||||
}
|
||||
|
||||
@@ -586,3 +586,11 @@ export function resolveQueueSettings(params: {
|
||||
dropPolicy: dropRaw,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFollowupQueueDepth(key: string): number {
|
||||
const cleaned = key.trim();
|
||||
if (!cleaned) return 0;
|
||||
const queue = FOLLOWUP_QUEUES.get(cleaned);
|
||||
if (!queue) return 0;
|
||||
return queue.items.length;
|
||||
}
|
||||
|
||||
@@ -26,27 +26,23 @@ describe("buildStatusMessage", () => {
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
storePath: "/tmp/sessions.json",
|
||||
resolvedThink: "medium",
|
||||
resolvedVerbose: "off",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
now: 10 * 60_000, // 10 minutes later
|
||||
webLinked: true,
|
||||
webAuthAgeMs: 5 * 60_000,
|
||||
heartbeatSeconds: 45,
|
||||
});
|
||||
|
||||
expect(text).toContain("⚙️ Status");
|
||||
expect(text).toContain("Agent: embedded pi");
|
||||
expect(text).toContain("🦞 ClawdBot");
|
||||
expect(text).toContain("🧠 Model:");
|
||||
expect(text).toContain("Runtime: direct");
|
||||
expect(text).toContain("Context: 16k/32k (50%)");
|
||||
expect(text).toContain("🧹 Compactions: 2");
|
||||
expect(text).toContain("Session: agent:main:main");
|
||||
expect(text).toContain("compactions 2");
|
||||
expect(text).toContain("Web: linked");
|
||||
expect(text).toContain("heartbeat 45s");
|
||||
expect(text).toContain("thinking=medium");
|
||||
expect(text).toContain("verbose=off");
|
||||
expect(text).not.toContain("Shortcuts:");
|
||||
expect(text).not.toContain("set with");
|
||||
expect(text).toContain("updated 10m ago");
|
||||
expect(text).toContain("Think: medium");
|
||||
expect(text).toContain("Verbose: off");
|
||||
expect(text).toContain("Elevated: on");
|
||||
expect(text).toContain("Queue: collect");
|
||||
});
|
||||
|
||||
it("handles missing agent config gracefully", () => {
|
||||
@@ -56,9 +52,9 @@ describe("buildStatusMessage", () => {
|
||||
webLinked: false,
|
||||
});
|
||||
|
||||
expect(text).toContain("Agent: embedded pi");
|
||||
expect(text).toContain("🧠 Model:");
|
||||
expect(text).toContain("Context:");
|
||||
expect(text).toContain("Web: not linked");
|
||||
expect(text).toContain("Queue:");
|
||||
});
|
||||
|
||||
it("includes group activation for group sessions", () => {
|
||||
@@ -72,10 +68,31 @@ describe("buildStatusMessage", () => {
|
||||
},
|
||||
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
||||
sessionScope: "per-sender",
|
||||
webLinked: true,
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
});
|
||||
|
||||
expect(text).toContain("Group activation: always");
|
||||
expect(text).toContain("Activation: always");
|
||||
});
|
||||
|
||||
it("shows queue details when overridden", () => {
|
||||
const text = buildStatusMessage({
|
||||
agent: {},
|
||||
sessionEntry: { sessionId: "q1", updatedAt: 0 },
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: {
|
||||
mode: "collect",
|
||||
depth: 3,
|
||||
debounceMs: 2000,
|
||||
cap: 5,
|
||||
dropPolicy: "old",
|
||||
showDetails: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(text).toContain(
|
||||
"Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers cached prompt tokens from the session log", async () => {
|
||||
@@ -88,14 +105,6 @@ describe("buildStatusMessage", () => {
|
||||
"./status.js"
|
||||
);
|
||||
|
||||
const storePath = path.join(
|
||||
dir,
|
||||
".clawdbot",
|
||||
"agents",
|
||||
"main",
|
||||
"sessions",
|
||||
"sessions.json",
|
||||
);
|
||||
const sessionId = "sess-1";
|
||||
const logPath = path.join(
|
||||
dir,
|
||||
@@ -141,8 +150,8 @@ describe("buildStatusMessage", () => {
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
storePath,
|
||||
webLinked: true,
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
includeTranscriptUsage: true,
|
||||
});
|
||||
|
||||
expect(text).toContain("Context: 1.0k/32k");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import {
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
type SessionEntry,
|
||||
type SessionScope,
|
||||
} from "../config/sessions.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
@@ -29,23 +30,29 @@ import type {
|
||||
|
||||
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
||||
|
||||
type QueueStatus = {
|
||||
mode?: string;
|
||||
depth?: number;
|
||||
debounceMs?: number;
|
||||
cap?: number;
|
||||
dropPolicy?: string;
|
||||
showDetails?: boolean;
|
||||
};
|
||||
|
||||
type StatusArgs = {
|
||||
agent: AgentConfig;
|
||||
workspaceDir?: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionKey?: string;
|
||||
sessionScope?: SessionScope;
|
||||
storePath?: string;
|
||||
groupActivation?: "mention" | "always";
|
||||
resolvedThink?: ThinkLevel;
|
||||
resolvedVerbose?: VerboseLevel;
|
||||
resolvedReasoning?: ReasoningLevel;
|
||||
resolvedElevated?: ElevatedLevel;
|
||||
modelAuth?: string;
|
||||
queue?: QueueStatus;
|
||||
includeTranscriptUsage?: boolean;
|
||||
now?: number;
|
||||
webLinked?: boolean;
|
||||
webAuthAgeMs?: number | null;
|
||||
heartbeatSeconds?: number;
|
||||
};
|
||||
|
||||
const formatAge = (ms?: number | null) => {
|
||||
@@ -84,6 +91,97 @@ export const formatContextUsageShort = (
|
||||
contextTokens: number | null | undefined,
|
||||
) => `Context ${formatTokens(total, contextTokens ?? null)}`;
|
||||
|
||||
const formatCommit = (value?: string | null) => {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
return trimmed.length > 7 ? trimmed.slice(0, 7) : trimmed;
|
||||
};
|
||||
|
||||
const resolveGitHead = (startDir: string) => {
|
||||
let current = startDir;
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const gitPath = path.join(current, ".git");
|
||||
try {
|
||||
const stat = fs.statSync(gitPath);
|
||||
if (stat.isDirectory()) {
|
||||
return path.join(gitPath, "HEAD");
|
||||
}
|
||||
if (stat.isFile()) {
|
||||
const raw = fs.readFileSync(gitPath, "utf-8");
|
||||
const match = raw.match(/gitdir:\s*(.+)/i);
|
||||
if (match?.[1]) {
|
||||
const resolved = path.resolve(current, match[1].trim());
|
||||
return path.join(resolved, "HEAD");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore missing .git at this level
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let cachedCommit: string | null | undefined;
|
||||
const resolveCommitHash = () => {
|
||||
if (cachedCommit !== undefined) return cachedCommit;
|
||||
const envCommit =
|
||||
process.env.GIT_COMMIT?.trim() || process.env.GIT_SHA?.trim();
|
||||
const normalized = formatCommit(envCommit);
|
||||
if (normalized) {
|
||||
cachedCommit = normalized;
|
||||
return cachedCommit;
|
||||
}
|
||||
try {
|
||||
const headPath = resolveGitHead(process.cwd());
|
||||
if (!headPath) {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
}
|
||||
const head = fs.readFileSync(headPath, "utf-8").trim();
|
||||
if (!head) {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
}
|
||||
if (head.startsWith("ref:")) {
|
||||
const ref = head.replace(/^ref:\s*/i, "").trim();
|
||||
const refPath = path.resolve(path.dirname(headPath), ref);
|
||||
const refHash = fs.readFileSync(refPath, "utf-8").trim();
|
||||
cachedCommit = formatCommit(refHash);
|
||||
return cachedCommit;
|
||||
}
|
||||
cachedCommit = formatCommit(head);
|
||||
return cachedCommit;
|
||||
} catch {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
}
|
||||
};
|
||||
|
||||
const formatQueueDetails = (queue?: QueueStatus) => {
|
||||
if (!queue) return "";
|
||||
const depth = typeof queue.depth === "number" ? `depth ${queue.depth}` : null;
|
||||
if (!queue.showDetails) {
|
||||
return depth ? ` (${depth})` : "";
|
||||
}
|
||||
const detailParts: string[] = [];
|
||||
if (depth) detailParts.push(depth);
|
||||
if (typeof queue.debounceMs === "number") {
|
||||
const ms = Math.max(0, Math.round(queue.debounceMs));
|
||||
const label =
|
||||
ms >= 1000
|
||||
? `${ms % 1000 === 0 ? ms / 1000 : (ms / 1000).toFixed(1)}s`
|
||||
: `${ms}ms`;
|
||||
detailParts.push(`debounce ${label}`);
|
||||
}
|
||||
if (typeof queue.cap === "number") detailParts.push(`cap ${queue.cap}`);
|
||||
if (queue.dropPolicy) detailParts.push(`drop ${queue.dropPolicy}`);
|
||||
return detailParts.length ? ` (${detailParts.join(" · ")})` : "";
|
||||
};
|
||||
|
||||
const readUsageFromSessionLog = (
|
||||
sessionId?: string,
|
||||
):
|
||||
@@ -164,15 +262,17 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
|
||||
// Prefer prompt-size tokens from the session transcript when it looks larger
|
||||
// (cached prompt tokens are often missing from agent meta/store).
|
||||
const logUsage = readUsageFromSessionLog(entry?.sessionId);
|
||||
if (logUsage) {
|
||||
const candidate = logUsage.promptTokens || logUsage.total;
|
||||
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
|
||||
totalTokens = candidate;
|
||||
}
|
||||
if (!model) model = logUsage.model ?? model;
|
||||
if (!contextTokens && logUsage.model) {
|
||||
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
||||
if (args.includeTranscriptUsage) {
|
||||
const logUsage = readUsageFromSessionLog(entry?.sessionId);
|
||||
if (logUsage) {
|
||||
const candidate = logUsage.promptTokens || logUsage.total;
|
||||
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
|
||||
totalTokens = candidate;
|
||||
}
|
||||
if (!model) model = logUsage.model ?? model;
|
||||
if (!contextTokens && logUsage.model) {
|
||||
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,8 +288,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
|
||||
const runtime = (() => {
|
||||
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
|
||||
if (sandboxMode === "off")
|
||||
return { line: "Runtime: direct", sandboxed: false };
|
||||
if (sandboxMode === "off") return { label: "direct" };
|
||||
const sessionScope = args.sessionScope ?? "per-sender";
|
||||
const mainKey = resolveMainSessionKey({
|
||||
session: { scope: sessionScope },
|
||||
@@ -199,35 +298,17 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
? 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,
|
||||
label: `${runtime}/${sandboxMode}`,
|
||||
};
|
||||
})();
|
||||
|
||||
const webLine = (() => {
|
||||
if (args.webLinked === false) {
|
||||
return "Web: not linked — run `clawdbot login` to scan the QR.";
|
||||
}
|
||||
const authAge = formatAge(args.webAuthAgeMs);
|
||||
const heartbeat =
|
||||
typeof args.heartbeatSeconds === "number"
|
||||
? ` • heartbeat ${args.heartbeatSeconds}s`
|
||||
: "";
|
||||
return `Web: linked • auth refreshed ${authAge}${heartbeat}`;
|
||||
})();
|
||||
|
||||
const updatedAt = entry?.updatedAt;
|
||||
const sessionLine = [
|
||||
`Session: ${args.sessionKey ?? "unknown"}`,
|
||||
`scope ${args.sessionScope ?? "per-sender"}`,
|
||||
entry?.updatedAt
|
||||
? `updated ${formatAge(now - entry.updatedAt)}`
|
||||
typeof updatedAt === "number"
|
||||
? `updated ${formatAge(now - updatedAt)}`
|
||||
: "no activity",
|
||||
typeof entry?.compactionCount === "number"
|
||||
? `compactions ${entry.compactionCount}`
|
||||
: undefined,
|
||||
args.storePath ? `store ${shortenHomePath(args.storePath)}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
@@ -238,39 +319,42 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
Boolean(args.sessionKey?.includes(":group:")) ||
|
||||
Boolean(args.sessionKey?.includes(":channel:")) ||
|
||||
Boolean(args.sessionKey?.startsWith("group:"));
|
||||
const groupActivationLine = isGroupSession
|
||||
? `Group activation: ${args.groupActivation ?? entry?.groupActivation ?? "mention"}`
|
||||
const groupActivationValue = isGroupSession
|
||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||
: undefined;
|
||||
|
||||
const contextLine = `Context: ${formatTokens(
|
||||
totalTokens,
|
||||
contextTokens ?? null,
|
||||
)}${entry?.abortedLastRun ? " • last run aborted" : ""}`;
|
||||
const contextLine = [
|
||||
`Context: ${formatTokens(totalTokens, contextTokens ?? null)}`,
|
||||
`🧹 Compactions: ${entry?.compactionCount ?? 0}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
const optionsLine = runtime.sandboxed
|
||||
? `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | reasoning=${reasoningLevel} | elevated=${elevatedLevel}`
|
||||
: `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | reasoning=${reasoningLevel}`;
|
||||
const queueMode = args.queue?.mode ?? "unknown";
|
||||
const queueDetails = formatQueueDetails(args.queue);
|
||||
const optionParts = [
|
||||
`Runtime: ${runtime.label}`,
|
||||
`Think: ${thinkLevel}`,
|
||||
`Verbose: ${verboseLevel}`,
|
||||
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
||||
`Elevated: ${elevatedLevel}`,
|
||||
groupActivationValue ? `👥 Activation: ${groupActivationValue}` : null,
|
||||
`🪢 Queue: ${queueMode}${queueDetails}`,
|
||||
];
|
||||
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
||||
|
||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||
|
||||
const agentLine = `Agent: embedded pi • ${modelLabel}`;
|
||||
const authLine = args.modelAuth ? `Model auth: ${args.modelAuth}` : undefined;
|
||||
|
||||
const workspaceLine = args.workspaceDir
|
||||
? `Workspace: ${shortenHomePath(args.workspaceDir)}`
|
||||
: undefined;
|
||||
const authLabel = args.modelAuth ? ` · 🔑 ${args.modelAuth}` : "";
|
||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
||||
const commit = resolveCommitHash();
|
||||
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
||||
|
||||
return [
|
||||
"⚙️ Status",
|
||||
webLine,
|
||||
agentLine,
|
||||
authLine,
|
||||
runtime.line,
|
||||
workspaceLine,
|
||||
contextLine,
|
||||
sessionLine,
|
||||
groupActivationLine,
|
||||
optionsLine,
|
||||
versionLine,
|
||||
modelLine,
|
||||
`📚 ${contextLine}`,
|
||||
`🧵 ${sessionLine}`,
|
||||
`⚙️ ${optionsLine}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
Reference in New Issue
Block a user