From 8013c4717c56a7815a3b3c8f150b1538b03c3df0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 01:57:37 +0000 Subject: [PATCH] feat: show memory summary in status --- CHANGELOG.md | 2 +- src/cli/memory-cli.test.ts | 4 ++++ src/commands/status.command.ts | 40 ++++++++++++++++++++++++++++++++++ src/commands/status.scan.ts | 23 ++++++++++++++++++- src/commands/status.test.ts | 28 ++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8103f49d3..80d148c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Docs: https://docs.clawd.bot ### Changes - Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. - Memory: add SQLite embedding cache to speed up reindexing and frequent updates. -- CLI: surface FTS + embedding cache state in `clawdbot memory status`. +- CLI: surface memory search state in `clawdbot status` and detailed FTS + embedding cache state in `clawdbot memory status`. ## 2026.1.18-1 diff --git a/src/cli/memory-cli.test.ts b/src/cli/memory-cli.test.ts index a942bd9c7..33a7f99d3 100644 --- a/src/cli/memory-cli.test.ts +++ b/src/cli/memory-cli.test.ts @@ -42,6 +42,8 @@ describe("memory cli", () => { provider: "openai", model: "text-embedding-3-small", requestedProvider: "openai", + cache: { enabled: true, entries: 123, maxEntries: 50000 }, + fts: { enabled: true, available: true }, vector: { enabled: true, available: true, @@ -62,6 +64,8 @@ describe("memory cli", () => { expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("FTS: ready")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Embedding cache: enabled (123 entries)")); expect(close).toHaveBeenCalled(); }); diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 1333476f4..8fb0efe0d 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -64,6 +64,7 @@ export async function statusCommand( agentStatus, channels, summary, + memory, } = scan; const securityAudit = await withProgress( @@ -114,6 +115,7 @@ export async function statusCommand( ...summary, os: osSummary, update, + memory, gateway: { mode: gatewayMode, url: gatewayConnection.url, @@ -232,6 +234,43 @@ export async function statusCommand( ? `${summary.sessions.paths.length} stores` : (summary.sessions.paths[0] ?? "unknown"); + const memoryValue = (() => { + if (!memory) return muted("disabled"); + const parts: string[] = []; + const dirtySuffix = memory.dirty ? ` · ${warn("dirty")}` : ""; + parts.push(`${memory.files} files · ${memory.chunks} chunks${dirtySuffix}`); + if (memory.sources?.length) parts.push(`sources ${memory.sources.join(", ")}`); + const vector = memory.vector; + parts.push( + vector?.enabled === false + ? muted("vector off") + : vector?.available + ? ok("vector ready") + : vector?.available === false + ? warn("vector unavailable") + : muted("vector unknown"), + ); + const fts = memory.fts; + if (fts) { + parts.push( + fts.enabled === false + ? muted("fts off") + : fts.available + ? ok("fts ready") + : warn("fts unavailable"), + ); + } + const cache = memory.cache; + if (cache) { + parts.push( + cache.enabled + ? ok(`cache on${typeof cache.entries === "number" ? ` (${cache.entries})` : ""}`) + : muted("cache off"), + ); + } + return parts.join(" · "); + })(); + const updateAvailability = resolveUpdateAvailability(update); const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, ""); @@ -254,6 +293,7 @@ export async function statusCommand( { Item: "Gateway", Value: gatewayValue }, { Item: "Daemon", Value: daemonValue }, { Item: "Agents", Value: agentsValue }, + { Item: "Memory", Value: memoryValue }, { Item: "Probes", Value: probesValue }, { Item: "Events", Value: eventsValue }, { Item: "Heartbeat", Value: heartbeatValue }, diff --git a/src/commands/status.scan.ts b/src/commands/status.scan.ts index 9fbee9420..3df1b6d15 100644 --- a/src/commands/status.scan.ts +++ b/src/commands/status.scan.ts @@ -6,6 +6,7 @@ import { probeGateway } from "../gateway/probe.js"; import { collectChannelStatusIssues } from "../infra/channels-status-issues.js"; import { resolveOsSummary } from "../infra/os-summary.js"; import { getTailnetHostname } from "../infra/tailscale.js"; +import { MemoryIndexManager } from "../memory/manager.js"; import { runExec } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { getAgentLocalStatuses } from "./status.agent-local.js"; @@ -14,6 +15,10 @@ import { getStatusSummary } from "./status.summary.js"; import { getUpdateCheckResult } from "./status.update.js"; import { buildChannelsTable } from "./status-all/channels.js"; +type MemoryStatusSnapshot = ReturnType<(typeof MemoryIndexManager)["prototype"]["status"]> & { + agentId: string; +}; + export type StatusScanResult = { cfg: ReturnType; osSummary: ReturnType; @@ -31,6 +36,7 @@ export type StatusScanResult = { agentStatus: Awaited>; channels: Awaited>; summary: Awaited>; + memory: MemoryStatusSnapshot | null; }; export async function scanStatus( @@ -44,7 +50,7 @@ export async function scanStatus( return await withProgress( { label: "Scanning status…", - total: 9, + total: 10, enabled: opts.json !== true, }, async (progress) => { @@ -122,6 +128,20 @@ export async function scanStatus( }); progress.tick(); + progress.setLabel("Checking memory…"); + const memory = await (async (): Promise => { + const agentId = agentStatus.defaultId ?? "main"; + const manager = await MemoryIndexManager.get({ cfg, agentId }).catch(() => null); + if (!manager) return null; + try { + await manager.probeVectorAvailability(); + } catch {} + const status = manager.status(); + await manager.close().catch(() => {}); + return { agentId, ...status }; + })(); + progress.tick(); + progress.setLabel("Reading sessions…"); const summary = await getStatusSummary(); progress.tick(); @@ -146,6 +166,7 @@ export async function scanStatus( agentStatus, channels, summary, + memory, }; }, ); diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 1cfe69751..d3784e83b 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -71,6 +71,31 @@ const mocks = vi.hoisted(() => ({ }), })); +vi.mock("../memory/manager.js", () => ({ + MemoryIndexManager: { + get: vi.fn(async ({ agentId }: { agentId: string }) => ({ + probeVectorAvailability: vi.fn(async () => true), + status: () => ({ + files: 2, + chunks: 3, + dirty: false, + workspaceDir: "/tmp/clawd", + dbPath: "/tmp/memory.sqlite", + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + sources: ["memory"], + sourceCounts: [{ source: "memory", files: 2, chunks: 3 }], + cache: { enabled: true, entries: 10, maxEntries: 500 }, + fts: { enabled: true, available: true }, + vector: { enabled: true, available: true, extensionPath: "/opt/vec0.dylib", dims: 1024 }, + }), + close: vi.fn(async () => {}), + __agentId: agentId, + })), + }, +})); + vi.mock("../config/sessions.js", () => ({ loadSessionStore: mocks.loadSessionStore, resolveMainSessionKey: mocks.resolveMainSessionKey, @@ -234,6 +259,8 @@ describe("statusCommand", () => { await statusCommand({ json: true }, runtime as never); const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]); expect(payload.linkChannel.linked).toBe(true); + expect(payload.memory.agentId).toBe("main"); + expect(payload.memory.vector.available).toBe(true); expect(payload.sessions.count).toBe(1); expect(payload.sessions.paths).toContain("/tmp/sessions.json"); expect(payload.sessions.defaults.model).toBeTruthy(); @@ -256,6 +283,7 @@ describe("statusCommand", () => { expect(logs.some((l) => l.includes("CRITICAL"))).toBe(true); expect(logs.some((l) => l.includes("Dashboard"))).toBe(true); expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true); + expect(logs.some((l) => l.includes("Memory"))).toBe(true); expect(logs.some((l) => l.includes("Channels"))).toBe(true); expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true); expect(logs.some((l) => l.includes("Sessions"))).toBe(true);