feat: show memory summary in status
This commit is contained in:
@@ -7,7 +7,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Changes
|
### Changes
|
||||||
- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback.
|
- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback.
|
||||||
- Memory: add SQLite embedding cache to speed up reindexing and frequent updates.
|
- 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
|
## 2026.1.18-1
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ describe("memory cli", () => {
|
|||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "text-embedding-3-small",
|
model: "text-embedding-3-small",
|
||||||
requestedProvider: "openai",
|
requestedProvider: "openai",
|
||||||
|
cache: { enabled: true, entries: 123, maxEntries: 50000 },
|
||||||
|
fts: { enabled: true, available: true },
|
||||||
vector: {
|
vector: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
available: true,
|
available: true,
|
||||||
@@ -62,6 +64,8 @@ describe("memory cli", () => {
|
|||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024"));
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib"));
|
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();
|
expect(close).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export async function statusCommand(
|
|||||||
agentStatus,
|
agentStatus,
|
||||||
channels,
|
channels,
|
||||||
summary,
|
summary,
|
||||||
|
memory,
|
||||||
} = scan;
|
} = scan;
|
||||||
|
|
||||||
const securityAudit = await withProgress(
|
const securityAudit = await withProgress(
|
||||||
@@ -114,6 +115,7 @@ export async function statusCommand(
|
|||||||
...summary,
|
...summary,
|
||||||
os: osSummary,
|
os: osSummary,
|
||||||
update,
|
update,
|
||||||
|
memory,
|
||||||
gateway: {
|
gateway: {
|
||||||
mode: gatewayMode,
|
mode: gatewayMode,
|
||||||
url: gatewayConnection.url,
|
url: gatewayConnection.url,
|
||||||
@@ -232,6 +234,43 @@ export async function statusCommand(
|
|||||||
? `${summary.sessions.paths.length} stores`
|
? `${summary.sessions.paths.length} stores`
|
||||||
: (summary.sessions.paths[0] ?? "unknown");
|
: (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 updateAvailability = resolveUpdateAvailability(update);
|
||||||
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
||||||
|
|
||||||
@@ -254,6 +293,7 @@ export async function statusCommand(
|
|||||||
{ Item: "Gateway", Value: gatewayValue },
|
{ Item: "Gateway", Value: gatewayValue },
|
||||||
{ Item: "Daemon", Value: daemonValue },
|
{ Item: "Daemon", Value: daemonValue },
|
||||||
{ Item: "Agents", Value: agentsValue },
|
{ Item: "Agents", Value: agentsValue },
|
||||||
|
{ Item: "Memory", Value: memoryValue },
|
||||||
{ Item: "Probes", Value: probesValue },
|
{ Item: "Probes", Value: probesValue },
|
||||||
{ Item: "Events", Value: eventsValue },
|
{ Item: "Events", Value: eventsValue },
|
||||||
{ Item: "Heartbeat", Value: heartbeatValue },
|
{ Item: "Heartbeat", Value: heartbeatValue },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { probeGateway } from "../gateway/probe.js";
|
|||||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||||
import { getTailnetHostname } from "../infra/tailscale.js";
|
import { getTailnetHostname } from "../infra/tailscale.js";
|
||||||
|
import { MemoryIndexManager } from "../memory/manager.js";
|
||||||
import { runExec } from "../process/exec.js";
|
import { runExec } from "../process/exec.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { getAgentLocalStatuses } from "./status.agent-local.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 { getUpdateCheckResult } from "./status.update.js";
|
||||||
import { buildChannelsTable } from "./status-all/channels.js";
|
import { buildChannelsTable } from "./status-all/channels.js";
|
||||||
|
|
||||||
|
type MemoryStatusSnapshot = ReturnType<(typeof MemoryIndexManager)["prototype"]["status"]> & {
|
||||||
|
agentId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type StatusScanResult = {
|
export type StatusScanResult = {
|
||||||
cfg: ReturnType<typeof loadConfig>;
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
osSummary: ReturnType<typeof resolveOsSummary>;
|
osSummary: ReturnType<typeof resolveOsSummary>;
|
||||||
@@ -31,6 +36,7 @@ export type StatusScanResult = {
|
|||||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
||||||
channels: Awaited<ReturnType<typeof buildChannelsTable>>;
|
channels: Awaited<ReturnType<typeof buildChannelsTable>>;
|
||||||
summary: Awaited<ReturnType<typeof getStatusSummary>>;
|
summary: Awaited<ReturnType<typeof getStatusSummary>>;
|
||||||
|
memory: MemoryStatusSnapshot | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function scanStatus(
|
export async function scanStatus(
|
||||||
@@ -44,7 +50,7 @@ export async function scanStatus(
|
|||||||
return await withProgress(
|
return await withProgress(
|
||||||
{
|
{
|
||||||
label: "Scanning status…",
|
label: "Scanning status…",
|
||||||
total: 9,
|
total: 10,
|
||||||
enabled: opts.json !== true,
|
enabled: opts.json !== true,
|
||||||
},
|
},
|
||||||
async (progress) => {
|
async (progress) => {
|
||||||
@@ -122,6 +128,20 @@ export async function scanStatus(
|
|||||||
});
|
});
|
||||||
progress.tick();
|
progress.tick();
|
||||||
|
|
||||||
|
progress.setLabel("Checking memory…");
|
||||||
|
const memory = await (async (): Promise<MemoryStatusSnapshot | null> => {
|
||||||
|
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…");
|
progress.setLabel("Reading sessions…");
|
||||||
const summary = await getStatusSummary();
|
const summary = await getStatusSummary();
|
||||||
progress.tick();
|
progress.tick();
|
||||||
@@ -146,6 +166,7 @@ export async function scanStatus(
|
|||||||
agentStatus,
|
agentStatus,
|
||||||
channels,
|
channels,
|
||||||
summary,
|
summary,
|
||||||
|
memory,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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", () => ({
|
vi.mock("../config/sessions.js", () => ({
|
||||||
loadSessionStore: mocks.loadSessionStore,
|
loadSessionStore: mocks.loadSessionStore,
|
||||||
resolveMainSessionKey: mocks.resolveMainSessionKey,
|
resolveMainSessionKey: mocks.resolveMainSessionKey,
|
||||||
@@ -234,6 +259,8 @@ describe("statusCommand", () => {
|
|||||||
await statusCommand({ json: true }, runtime as never);
|
await statusCommand({ json: true }, runtime as never);
|
||||||
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
|
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
|
||||||
expect(payload.linkChannel.linked).toBe(true);
|
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.count).toBe(1);
|
||||||
expect(payload.sessions.paths).toContain("/tmp/sessions.json");
|
expect(payload.sessions.paths).toContain("/tmp/sessions.json");
|
||||||
expect(payload.sessions.defaults.model).toBeTruthy();
|
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("CRITICAL"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Dashboard"))).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("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("Channels"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true);
|
expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Sessions"))).toBe(true);
|
expect(logs.some((l) => l.includes("Sessions"))).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user