mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 00:23:25 -06:00
fix(memory): report truthful index outcomes (#123863)
This commit is contained in:
committed by
GitHub
parent
ad508a51c2
commit
156af00a78
+4
-1
@@ -50,7 +50,10 @@ openclaw memory index [--agent <id>] [--force] [--verbose]
|
||||
|
||||
Same per-agent scoping as `status`. `--force` runs a full reindex instead of
|
||||
an incremental one. `--verbose` prints per-agent provider, model, sources, and
|
||||
extra-path details before showing indexing progress.
|
||||
extra-path details before showing indexing progress. The completion message
|
||||
reports the indexed file count. An empty corpus is a successful no-op: the
|
||||
command reports the resolved workspace path and that nothing was indexed, and
|
||||
leaves the missing `memory/` directory for the first memory write to create.
|
||||
|
||||
## `memory search`
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
buildCliMemorySearchSessionKey,
|
||||
formatAuditCounts,
|
||||
formatExtraPaths,
|
||||
formatMemoryIndexOutcome,
|
||||
resolveMemoryPluginConfig,
|
||||
scanMemoryManagerSources,
|
||||
withMemoryCommand,
|
||||
} from "./cli-runtime-common.js";
|
||||
import {
|
||||
@@ -149,6 +151,8 @@ export async function runMemoryIndex(
|
||||
},
|
||||
);
|
||||
let postIndexStatus = manager.status();
|
||||
const scan = await scanMemoryManagerSources(postIndexStatus, agentId);
|
||||
const outcome = formatMemoryIndexOutcome(postIndexStatus, scan, agentId);
|
||||
let semanticVectorAvailable = postIndexStatus.vector?.semanticAvailable;
|
||||
const vectorStoreAvailable =
|
||||
postIndexStatus.vector?.storeAvailable ?? postIndexStatus.vector?.available;
|
||||
@@ -171,14 +175,13 @@ export async function runMemoryIndex(
|
||||
postIndexStatus.vector?.available ??
|
||||
postIndexStatus.vector?.storeAvailable;
|
||||
const vectorLoadErr = postIndexStatus.vector?.loadError;
|
||||
defaultRuntime.log(outcome);
|
||||
if (vectorEnabled && vectorAvailable === false) {
|
||||
// Indexing still persisted chunks/FTS state; keep the command successful but
|
||||
// emit a stderr warning so operators and scripts can detect degraded recall.
|
||||
defaultRuntime.error(
|
||||
`Memory index WARNING (${agentId}): chunks_vec not updated — ${formatMemoryVectorDegradedWriteReason(vectorLoadErr)}. Vector recall degraded.`,
|
||||
);
|
||||
} else {
|
||||
defaultRuntime.log(`Memory index updated (${agentId}).`);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = formatErrorMessage(err);
|
||||
|
||||
@@ -216,7 +216,7 @@ export async function withMemoryCommand(params: {
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
export type MemorySourceName = "memory" | "sessions";
|
||||
type MemorySourceName = "memory" | "sessions";
|
||||
type SourceScan = {
|
||||
source: MemorySourceName;
|
||||
totalFiles: number | null;
|
||||
@@ -343,7 +343,7 @@ async function scanMemoryFiles(
|
||||
}
|
||||
return { source: "memory", totalFiles, issues };
|
||||
}
|
||||
export async function scanMemorySources(params: {
|
||||
async function scanMemorySources(params: {
|
||||
workspaceDir: string;
|
||||
agentId: string;
|
||||
sources: MemorySourceName[];
|
||||
@@ -367,3 +367,28 @@ export async function scanMemorySources(params: {
|
||||
: numericTotals.reduce((sum, total) => sum + total, 0);
|
||||
return { sources: scans, totalFiles, issues };
|
||||
}
|
||||
|
||||
export async function scanMemoryManagerSources(
|
||||
status: ReturnType<MemoryManager["status"]>,
|
||||
agentId: string,
|
||||
): Promise<MemorySourceScan | undefined> {
|
||||
const workspaceDir = status.workspaceDir;
|
||||
if (!workspaceDir) {
|
||||
return undefined;
|
||||
}
|
||||
const sources = (status.sources?.length ? status.sources : ["memory"]) as MemorySourceName[];
|
||||
return await scanMemorySources({ workspaceDir, agentId, sources, extraPaths: status.extraPaths });
|
||||
}
|
||||
|
||||
export function formatMemoryIndexOutcome(
|
||||
status: ReturnType<MemoryManager["status"]>,
|
||||
scan: MemorySourceScan | undefined,
|
||||
agentId: string,
|
||||
): string {
|
||||
if (status.workspaceDir && scan?.totalFiles === 0) {
|
||||
return `No memory files found in ${shortenHomePath(status.workspaceDir)}; nothing indexed (${agentId}).`;
|
||||
}
|
||||
const indexedFiles = status.files ?? 0;
|
||||
const fileLabel = indexedFiles === 1 ? "file" : "files";
|
||||
return `Memory index updated (${agentId}): ${indexedFiles} ${fileLabel} indexed.`;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
formatAuditCounts,
|
||||
formatExtraPaths,
|
||||
formatMemoryIndexOutcome,
|
||||
resolveMemoryPluginConfig,
|
||||
scanMemorySources,
|
||||
scanMemoryManagerSources,
|
||||
withMemoryCommand,
|
||||
type MemoryManager,
|
||||
type MemorySourceName,
|
||||
type MemorySourceScan,
|
||||
} from "./cli-runtime-common.js";
|
||||
import {
|
||||
@@ -231,16 +231,8 @@ export async function runMemoryStatus(
|
||||
}
|
||||
}
|
||||
const status = manager.status();
|
||||
const sources = (status.sources?.length ? status.sources : ["memory"]) as MemorySourceName[];
|
||||
const scan = await scanMemoryManagerSources(status, agentId);
|
||||
const workspaceDir = status.workspaceDir;
|
||||
const scan = workspaceDir
|
||||
? await scanMemorySources({
|
||||
workspaceDir,
|
||||
agentId,
|
||||
sources,
|
||||
extraPaths: status.extraPaths,
|
||||
})
|
||||
: undefined;
|
||||
let audit: ShortTermAuditSummary | undefined;
|
||||
let repair: RepairShortTermPromotionArtifactsResult | undefined;
|
||||
let dreamingAudit: DreamingArtifactsAuditSummary | undefined;
|
||||
@@ -294,7 +286,9 @@ export async function runMemoryStatus(
|
||||
? `${filesIndexed}/? files · ${chunksIndexed} chunks`
|
||||
: `${filesIndexed}/${totalFiles} files · ${chunksIndexed} chunks`;
|
||||
if (opts.index) {
|
||||
const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete.";
|
||||
const line = indexError
|
||||
? `Memory index failed: ${indexError}`
|
||||
: formatMemoryIndexOutcome(status, scan, agentId);
|
||||
defaultRuntime.log(line);
|
||||
}
|
||||
const requestedProvider = status.requestedProvider ?? status.provider;
|
||||
|
||||
@@ -1312,51 +1312,108 @@ describe("memory cli", () => {
|
||||
});
|
||||
|
||||
it("reindexes on status --index", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-08-14", ["# Indexed memory"]);
|
||||
const close = vi.fn(async () => {});
|
||||
const sync = vi.fn(async () => {});
|
||||
const probeVectorStoreAvailability = vi.fn(async () => true);
|
||||
const probeVectorAvailability = vi.fn(async () => true);
|
||||
const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true }));
|
||||
mockManager({
|
||||
probeVectorStoreAvailability,
|
||||
probeVectorAvailability,
|
||||
probeEmbeddingAvailability,
|
||||
sync,
|
||||
status: () => makeMemoryStatus({ workspaceDir, sources: ["memory"], files: 1, chunks: 1 }),
|
||||
close,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs(defaultRuntime);
|
||||
await runMemoryCli(["status", "--index"]);
|
||||
|
||||
expectCliSync(sync);
|
||||
expect(probeVectorStoreAvailability).toHaveBeenCalled();
|
||||
expect(probeVectorAvailability).toHaveBeenCalled();
|
||||
expect(probeEmbeddingAvailability).toHaveBeenCalled();
|
||||
expect(getMemorySearchManager).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
purpose: "cli",
|
||||
});
|
||||
expectLogged(log, "Memory index updated (main): 1 file indexed.");
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the same truthful no-op from status --index", async () => {
|
||||
const workspaceDir = path.join(workspaceFixtureRoot, `case-${workspaceCaseId++}`);
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const close = vi.fn(async () => {});
|
||||
const sync = vi.fn(async () => {});
|
||||
const probeVectorStoreAvailability = vi.fn(async () => true);
|
||||
const probeVectorAvailability = vi.fn(async () => true);
|
||||
const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true }));
|
||||
mockManager({
|
||||
probeVectorStoreAvailability,
|
||||
probeVectorAvailability,
|
||||
probeEmbeddingAvailability,
|
||||
probeVectorAvailability: vi.fn(async () => true),
|
||||
probeEmbeddingAvailability: vi.fn(async () => ({ ok: true })),
|
||||
sync,
|
||||
status: () => makeMemoryStatus({ files: 1, chunks: 1 }),
|
||||
status: () => makeMemoryStatus({ workspaceDir, sources: ["memory"] }),
|
||||
close,
|
||||
});
|
||||
|
||||
spyRuntimeLogs(defaultRuntime);
|
||||
const log = spyRuntimeLogs(defaultRuntime);
|
||||
await runMemoryCli(["status", "--index"]);
|
||||
|
||||
expectCliSync(sync);
|
||||
expect(probeVectorStoreAvailability).toHaveBeenCalled();
|
||||
expect(probeVectorAvailability).toHaveBeenCalled();
|
||||
expect(probeEmbeddingAvailability).toHaveBeenCalled();
|
||||
expect(getMemorySearchManager).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
purpose: "cli",
|
||||
});
|
||||
expectLogged(log, `No memory files found in ${workspaceDir}; nothing indexed (main).`);
|
||||
expectNotLogged(log, "Memory index complete");
|
||||
await expectPathMissing(path.join(workspaceDir, "memory"));
|
||||
expect(close).toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("closes manager after index", async () => {
|
||||
it("reports a truthful no-op when the memory directory is missing", async () => {
|
||||
const workspaceDir = path.join(workspaceFixtureRoot, `case-${workspaceCaseId++}`);
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const close = vi.fn(async () => {});
|
||||
const sync = vi.fn(async () => {});
|
||||
mockManager({ sync, status: () => makeMemoryStatus(), close });
|
||||
mockManager({
|
||||
sync,
|
||||
status: () => makeMemoryStatus({ workspaceDir, sources: ["memory"] }),
|
||||
close,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs(defaultRuntime);
|
||||
await runMemoryCli(["index"]);
|
||||
|
||||
expectCliSync(sync);
|
||||
expect(getMemorySearchManager).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
purpose: "cli",
|
||||
});
|
||||
expectLogged(log, `No memory files found in ${workspaceDir}; nothing indexed (main).`);
|
||||
expectNotLogged(log, "Memory index updated");
|
||||
await expectPathMissing(path.join(workspaceDir, "memory"));
|
||||
expect(close).toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith("Memory index updated (main).");
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reports the indexed file count and closes the manager after index", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-08-14", ["# Indexed memory"]);
|
||||
const close = vi.fn(async () => {});
|
||||
const sync = vi.fn(async () => {});
|
||||
mockManager({
|
||||
sync,
|
||||
status: () => makeMemoryStatus({ workspaceDir, sources: ["memory"], files: 1 }),
|
||||
close,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs(defaultRuntime);
|
||||
await runMemoryCli(["index"]);
|
||||
|
||||
expectCliSync(sync);
|
||||
expect(getMemorySearchManager).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
purpose: "cli",
|
||||
});
|
||||
expect(close).toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith("Memory index updated (main): 1 file indexed.");
|
||||
});
|
||||
});
|
||||
|
||||
it("warns on stderr when index completes without sqlite-vec embeddings", async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
readMemoryFile,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { scanMemorySources } from "./cli-runtime-common.js";
|
||||
import { scanMemoryManagerSources } from "./cli-runtime-common.js";
|
||||
import { resolveMemoryPathClassification } from "./memory/memory-path-provenance.js";
|
||||
|
||||
describe.skipIf(process.platform !== "win32")("Windows explicit memory extra-file casing", () => {
|
||||
@@ -52,12 +52,16 @@ describe.skipIf(process.platform !== "win32")("Windows explicit memory extra-fil
|
||||
}),
|
||||
).resolves.toMatchObject({ text: "shared Windows memory" });
|
||||
await expect(
|
||||
scanMemorySources({
|
||||
workspaceDir,
|
||||
agentId: "main",
|
||||
sources: ["memory"],
|
||||
extraPaths: [configuredPath],
|
||||
}),
|
||||
scanMemoryManagerSources(
|
||||
{
|
||||
backend: "builtin",
|
||||
provider: "none",
|
||||
workspaceDir,
|
||||
sources: ["memory"],
|
||||
extraPaths: [configuredPath],
|
||||
},
|
||||
"main",
|
||||
),
|
||||
).resolves.toMatchObject({ totalFiles: 1, issues: [] });
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user