feat(cli): expand memory status across agents

Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-18 15:29:43 +00:00
parent 9464774133
commit a4aad1c76a
2 changed files with 255 additions and 282 deletions

View File

@@ -7,8 +7,8 @@ read_when:
# `clawdbot memory` # `clawdbot memory`
Memory search tools (semantic memory status/index/search). Manage semantic memory indexing and search.
Provided by the active memory plugin (default: `memory-core`; use `plugins.slots.memory = "none"` to disable). Provided by the active memory plugin (default: `memory-core`; set `plugins.slots.memory = "none"` to disable).
Related: Related:
- Memory concept: [Memory](/concepts/memory) - Memory concept: [Memory](/concepts/memory)
@@ -22,11 +22,20 @@ clawdbot memory status --deep
clawdbot memory status --deep --index clawdbot memory status --deep --index
clawdbot memory status --deep --index --verbose clawdbot memory status --deep --index --verbose
clawdbot memory index clawdbot memory index
clawdbot memory index --verbose
clawdbot memory search "release checklist" clawdbot memory search "release checklist"
clawdbot memory status --agent main
clawdbot memory index --agent main --verbose
``` ```
## Options ## Options
- `--verbose`: emit debug logs during memory probes and indexing. Common:
- `--index-mode auto|batch|direct`: override batch usage when indexing (`direct` favors speed; `batch` favors OpenAI Batch pricing).
- `--progress auto|line|log|none`: progress output mode (`log` prints updates even without a TTY). - `--agent <id>`: scope to a single agent (default: all configured agents).
- `--verbose`: emit detailed logs during probes and indexing.
Notes:
- `memory status --deep` probes vector + embedding availability.
- `memory status --deep --index` runs a reindex if the store is dirty.
- `memory index --verbose` prints per-phase details (provider, model, sources, batch activity).

View File

@@ -1,36 +1,39 @@
import os from "node:os";
import path from "node:path";
import type { Command } from "commander"; import type { Command } from "commander";
import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { ClawdbotConfig } from "../config/config.js";
import type { MemorySearchConfig } from "../config/types.tools.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { setVerbose } from "../globals.js"; import { setVerbose } from "../globals.js";
import { withProgress, withProgressTotals } from "./progress.js"; import { withProgress, withProgressTotals } from "./progress.js";
import { formatErrorMessage, withManager } from "./cli-utils.js"; import { formatErrorMessage, withManager } from "./cli-utils.js";
import { getMemorySearchManager, type MemorySearchManagerResult } from "../memory/index.js"; import { getMemorySearchManager, type MemorySearchManagerResult } from "../memory/index.js";
import {
resolveMemoryCacheState,
resolveMemoryFtsState,
resolveMemoryVectorState,
type Tone,
} from "../memory/status-format.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js"; import { formatDocsLink } from "../terminal/links.js";
import { colorize, isRich, theme } from "../terminal/theme.js"; import { colorize, isRich, theme } from "../terminal/theme.js";
import { resolveStateDir } from "../config/paths.js";
type MemoryCommandOptions = { type MemoryCommandOptions = {
agent?: string; agent?: string;
json?: boolean; json?: boolean;
deep?: boolean; deep?: boolean;
index?: boolean; index?: boolean;
indexMode?: IndexMode;
progress?: ProgressMode;
verbose?: boolean; verbose?: boolean;
}; };
type MemoryManager = NonNullable<MemorySearchManagerResult["manager"]>; type MemoryManager = NonNullable<MemorySearchManagerResult["manager"]>;
type IndexMode = "auto" | "batch" | "direct";
type ProgressMode = "auto" | "line" | "log" | "none"; function formatSourceLabel(source: string, workspaceDir: string, agentId: string): string {
if (source === "memory") {
return `memory (MEMORY.md + ${path.join(workspaceDir, "memory")}${path.sep}*.md)`;
}
if (source === "sessions") {
const stateDir = resolveStateDir(process.env, os.homedir);
return `sessions (${path.join(stateDir, "agents", agentId, "sessions")}${path.sep}*.jsonl)`;
}
return source;
}
function resolveAgent(cfg: ReturnType<typeof loadConfig>, agent?: string) { function resolveAgent(cfg: ReturnType<typeof loadConfig>, agent?: string) {
const trimmed = agent?.trim(); const trimmed = agent?.trim();
@@ -38,66 +41,14 @@ function resolveAgent(cfg: ReturnType<typeof loadConfig>, agent?: string) {
return resolveDefaultAgentId(cfg); return resolveDefaultAgentId(cfg);
} }
function resolveIndexMode(raw?: string): IndexMode { function resolveAgentIds(cfg: ReturnType<typeof loadConfig>, agent?: string): string[] {
if (!raw) return "auto"; const trimmed = agent?.trim();
const trimmed = raw.trim().toLowerCase(); if (trimmed) return [trimmed];
if (trimmed === "batch") return "batch"; const list = cfg.agents?.list ?? [];
if (trimmed === "direct") return "direct"; if (list.length > 0) {
return "auto"; return list.map((entry) => entry.id).filter(Boolean);
}
function resolveProgressMode(raw?: string): ProgressMode {
if (!raw) return "auto";
const trimmed = raw.trim().toLowerCase();
if (trimmed === "line") return "line";
if (trimmed === "log") return "log";
if (trimmed === "none") return "none";
return "auto";
}
function applyIndexMode(cfg: ClawdbotConfig, agentId: string, mode: IndexMode): ClawdbotConfig {
if (mode === "auto") return cfg;
const enabled = mode === "batch";
const patchMemorySearch = (memorySearch?: MemorySearchConfig) => {
const remote = memorySearch?.remote;
const batch = remote?.batch;
return {
...memorySearch,
remote: {
...remote,
batch: {
...batch,
enabled,
},
},
};
};
const nextAgents = { ...cfg.agents };
nextAgents.defaults = {
...cfg.agents?.defaults,
memorySearch: patchMemorySearch(cfg.agents?.defaults?.memorySearch),
};
if (cfg.agents?.list?.length) {
nextAgents.list = cfg.agents.list.map((agent) =>
agent.id === agentId
? {
...agent,
memorySearch: patchMemorySearch(agent.memorySearch),
} }
: agent, return [resolveDefaultAgentId(cfg)];
);
}
return { ...cfg, agents: nextAgents };
}
function resolveProgressOptions(
mode: ProgressMode,
verbose: boolean,
): { enabled?: boolean; fallback?: "spinner" | "line" | "log" | "none" } {
if (mode === "none") return { enabled: false, fallback: "none" };
if (mode === "line") return { fallback: "line" };
if (mode === "log") return { fallback: "log" };
return { fallback: verbose ? "line" : undefined };
} }
export function registerMemoryCli(program: Command) { export function registerMemoryCli(program: Command) {
@@ -117,17 +68,19 @@ export function registerMemoryCli(program: Command) {
.option("--json", "Print JSON") .option("--json", "Print JSON")
.option("--deep", "Probe embedding provider availability") .option("--deep", "Probe embedding provider availability")
.option("--index", "Reindex if dirty (implies --deep)") .option("--index", "Reindex if dirty (implies --deep)")
.option("--index-mode <mode>", "Index mode (auto|batch|direct) when indexing", "auto")
.option("--progress <mode>", "Progress output (auto|line|log|none)", "auto")
.option("--verbose", "Verbose logging", false) .option("--verbose", "Verbose logging", false)
.action(async (opts: MemoryCommandOptions) => { .action(async (opts: MemoryCommandOptions) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));
const rawCfg = loadConfig(); const cfg = loadConfig();
const agentId = resolveAgent(rawCfg, opts.agent); const agentIds = resolveAgentIds(cfg, opts.agent);
const indexMode = resolveIndexMode(opts.indexMode); const allResults: Array<{
const progressMode = resolveProgressMode(opts.progress); agentId: string;
const progressOptions = resolveProgressOptions(progressMode, Boolean(opts.verbose)); status: ReturnType<MemoryManager["status"]>;
const cfg = applyIndexMode(rawCfg, agentId, indexMode); embeddingProbe?: Awaited<ReturnType<MemoryManager["probeEmbeddingAvailability"]>>;
indexError?: string;
}> = [];
for (const agentId of agentIds) {
await withManager<MemoryManager>({ await withManager<MemoryManager>({
getManager: () => getMemorySearchManager({ cfg, agentId }), getManager: () => getMemorySearchManager({ cfg, agentId }),
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
@@ -141,23 +94,20 @@ export function registerMemoryCli(program: Command) {
| undefined; | undefined;
let indexError: string | undefined; let indexError: string | undefined;
if (deep) { if (deep) {
await withProgress( await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => {
{ label: "Checking memory…", total: 2, ...progressOptions },
async (progress) => {
progress.setLabel("Probing vector…"); progress.setLabel("Probing vector…");
await manager.probeVectorAvailability(); await manager.probeVectorAvailability();
progress.tick(); progress.tick();
progress.setLabel("Probing embeddings…"); progress.setLabel("Probing embeddings…");
embeddingProbe = await manager.probeEmbeddingAvailability(); embeddingProbe = await manager.probeEmbeddingAvailability();
progress.tick(); progress.tick();
}, });
);
if (opts.index) { if (opts.index) {
await withProgressTotals( await withProgressTotals(
{ {
label: "Indexing memory…", label: "Indexing memory…",
total: 0, total: 0,
...progressOptions, fallback: opts.verbose ? "line" : undefined,
}, },
async (update, progress) => { async (update, progress) => {
try { try {
@@ -184,28 +134,16 @@ export function registerMemoryCli(program: Command) {
await manager.probeVectorAvailability(); await manager.probeVectorAvailability();
} }
const status = manager.status(); const status = manager.status();
if (opts.json) { allResults.push({ agentId, status, embeddingProbe, indexError });
defaultRuntime.log(
JSON.stringify(
{
...status,
embeddings: embeddingProbe
? { ok: embeddingProbe.ok, error: embeddingProbe.error }
: undefined,
indexError,
}, },
null, });
2, }
),
); if (opts.json) {
defaultRuntime.log(JSON.stringify(allResults, null, 2));
return; return;
} }
if (opts.index) {
const line = indexError
? `Memory index failed: ${indexError}`
: "Memory index complete.";
defaultRuntime.log(line);
}
const rich = isRich(); const rich = isRich();
const heading = (text: string) => colorize(rich, theme.heading, text); const heading = (text: string) => colorize(rich, theme.heading, text);
const muted = (text: string) => colorize(rich, theme.muted, text); const muted = (text: string) => colorize(rich, theme.muted, text);
@@ -214,8 +152,15 @@ export function registerMemoryCli(program: Command) {
const warn = (text: string) => colorize(rich, theme.warn, text); const warn = (text: string) => colorize(rich, theme.warn, text);
const accent = (text: string) => colorize(rich, theme.accent, text); const accent = (text: string) => colorize(rich, theme.accent, text);
const label = (text: string) => muted(`${text}:`); const label = (text: string) => muted(`${text}:`);
const colorForTone = (tone: Tone) =>
tone === "ok" ? theme.success : tone === "warn" ? theme.warn : theme.muted; for (const result of allResults) {
const { agentId, status, embeddingProbe, indexError } = result;
if (opts.index) {
const line = indexError
? `Memory index failed: ${indexError}`
: "Memory index complete.";
defaultRuntime.log(line);
}
const lines = [ const lines = [
`${heading("Memory Search")} ${muted(`(${agentId})`)}`, `${heading("Memory Search")} ${muted(`(${agentId})`)}`,
`${label("Provider")} ${info(status.provider)} ${muted( `${label("Provider")} ${info(status.provider)} ${muted(
@@ -249,9 +194,18 @@ export function registerMemoryCli(program: Command) {
lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`);
} }
if (status.vector) { if (status.vector) {
const vectorState = resolveMemoryVectorState(status.vector); const vectorState = status.vector.enabled
const vectorColor = colorForTone(vectorState.tone); ? status.vector.available
lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState.state)}`); ? "ready"
: "unavailable"
: "disabled";
const vectorColor =
vectorState === "ready"
? theme.success
: vectorState === "unavailable"
? theme.warn
: theme.muted;
lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState)}`);
if (status.vector.dims) { if (status.vector.dims) {
lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`); lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`);
} }
@@ -263,22 +217,31 @@ export function registerMemoryCli(program: Command) {
} }
} }
if (status.fts) { if (status.fts) {
const ftsState = resolveMemoryFtsState(status.fts); const ftsState = status.fts.enabled
const ftsColor = colorForTone(ftsState.tone); ? status.fts.available
lines.push(`${label("FTS")} ${colorize(rich, ftsColor, ftsState.state)}`); ? "ready"
: "unavailable"
: "disabled";
const ftsColor =
ftsState === "ready"
? theme.success
: ftsState === "unavailable"
? theme.warn
: theme.muted;
lines.push(`${label("FTS")} ${colorize(rich, ftsColor, ftsState)}`);
if (status.fts.error) { if (status.fts.error) {
lines.push(`${label("FTS error")} ${warn(status.fts.error)}`); lines.push(`${label("FTS error")} ${warn(status.fts.error)}`);
} }
} }
if (status.cache) { if (status.cache) {
const cacheState = resolveMemoryCacheState(status.cache); const cacheState = status.cache.enabled ? "enabled" : "disabled";
const cacheColor = colorForTone(cacheState.tone); const cacheColor = status.cache.enabled ? theme.success : theme.muted;
const suffix = const suffix =
status.cache.enabled && typeof status.cache.entries === "number" status.cache.enabled && typeof status.cache.entries === "number"
? ` (${status.cache.entries} entries)` ? ` (${status.cache.entries} entries)`
: ""; : "";
lines.push( lines.push(
`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState.state)}${suffix}`, `${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`,
); );
if (status.cache.enabled && typeof status.cache.maxEntries === "number") { if (status.cache.enabled && typeof status.cache.maxEntries === "number") {
lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`); lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`);
@@ -291,8 +254,8 @@ export function registerMemoryCli(program: Command) {
lines.push(`${label("Index error")} ${warn(indexError)}`); lines.push(`${label("Index error")} ${warn(indexError)}`);
} }
defaultRuntime.log(lines.join("\n")); defaultRuntime.log(lines.join("\n"));
}, if (agentIds.length > 1) defaultRuntime.log("");
}); }
}); });
memory memory
@@ -300,15 +263,12 @@ export function registerMemoryCli(program: Command) {
.description("Reindex memory files") .description("Reindex memory files")
.option("--agent <id>", "Agent id (default: default agent)") .option("--agent <id>", "Agent id (default: default agent)")
.option("--force", "Force full reindex", false) .option("--force", "Force full reindex", false)
.option("--index-mode <mode>", "Index mode (auto|batch|direct) when indexing", "auto") .option("--verbose", "Verbose logging", false)
.option("--progress <mode>", "Progress output (auto|line|log|none)", "auto")
.action(async (opts: MemoryCommandOptions & { force?: boolean }) => { .action(async (opts: MemoryCommandOptions & { force?: boolean }) => {
const rawCfg = loadConfig(); setVerbose(Boolean(opts.verbose));
const agentId = resolveAgent(rawCfg, opts.agent); const cfg = loadConfig();
const indexMode = resolveIndexMode(opts.indexMode); const agentIds = resolveAgentIds(cfg, opts.agent);
const progressMode = resolveProgressMode(opts.progress); for (const agentId of agentIds) {
const progressOptions = resolveProgressOptions(progressMode, Boolean(opts.verbose));
const cfg = applyIndexMode(rawCfg, agentId, indexMode);
await withManager<MemoryManager>({ await withManager<MemoryManager>({
getManager: () => getMemorySearchManager({ cfg, agentId }), getManager: () => getMemorySearchManager({ cfg, agentId }),
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
@@ -317,39 +277,43 @@ export function registerMemoryCli(program: Command) {
close: (manager) => manager.close(), close: (manager) => manager.close(),
run: async (manager) => { run: async (manager) => {
try { try {
if (progressMode === "none") { if (opts.verbose) {
await manager.sync({ reason: "cli", force: opts.force }); const status = manager.status();
} else { const rich = isRich();
await withProgressTotals( const heading = (text: string) => colorize(rich, theme.heading, text);
{ const muted = (text: string) => colorize(rich, theme.muted, text);
label: "Indexing memory…", const info = (text: string) => colorize(rich, theme.info, text);
total: 0, const warn = (text: string) => colorize(rich, theme.warn, text);
...progressOptions, const label = (text: string) => muted(`${text}:`);
}, const sourceLabels = status.sources.map((source) =>
async (update, progress) => { formatSourceLabel(source, status.workspaceDir, agentId),
await manager.sync({
reason: "cli",
force: opts.force,
progress: (syncUpdate) => {
update({
completed: syncUpdate.completed,
total: syncUpdate.total,
label: syncUpdate.label,
});
if (syncUpdate.label) progress.setLabel(syncUpdate.label);
},
});
},
); );
const lines = [
`${heading("Memory Index")} ${muted(`(${agentId})`)}`,
`${label("Provider")} ${info(status.provider)} ${muted(
`(requested: ${status.requestedProvider})`,
)}`,
`${label("Model")} ${info(status.model)}`,
sourceLabels.length
? `${label("Sources")} ${info(sourceLabels.join(", "))}`
: null,
].filter(Boolean) as string[];
if (status.fallback) {
lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`);
} }
defaultRuntime.log("Memory index updated."); defaultRuntime.log(lines.join("\n"));
defaultRuntime.log("");
}
await manager.sync({ reason: "cli", force: opts.force });
defaultRuntime.log(`Memory index updated (${agentId}).`);
} catch (err) { } catch (err) {
const message = formatErrorMessage(err); const message = formatErrorMessage(err);
defaultRuntime.error(`Memory index failed: ${message}`); defaultRuntime.error(`Memory index failed (${agentId}): ${message}`);
process.exitCode = 1; process.exitCode = 1;
} }
}, },
}); });
}
}); });
memory memory