Skip to content

Commit a4b4fed

Browse files
authored
fix(memory): validate memory index identity
* docs: add memory index identity plan * fix(memory): validate memory index identity * fix(memory): align status index identity with vector probe * fix(memory): fail closed on stale fts-only search * fix(memory): clear sessions-only identity reindex dirty state * fix(memory): gate targeted session sync by index identity * fix(memory): clear resolved index identity dirtiness * fix(memory): block search on missing index identity * fix(memory): preserve dirty events during identity reindex * fix(memory): resolve provider aliases for index identity * fix(memory): report missing identity states accurately * fix(memory): mark missing session index identity dirty * test(memory): expose provider alias resolver in mocks * chore(memory): remove scratch implementation plan * fix(memory): avoid automatic full reindex on provider cutover * docs(memory): plan no-schema cutover repair * fix(memory): pause vector search on index identity mismatch * fix(memory): freeze dirty identity sync writes * fix(memory): skip paused-index search retry * test(memory): keep retry tests on same provider identity * fix(memory): surface paused index recall * chore(memory): remove scratch plan from pr * fix(memory): preserve paused session dirtiness * fix(memory): make paused recall warning explicit * docs(memory): document explicit index repair
1 parent 5be282e commit a4b4fed

22 files changed

Lines changed: 1224 additions & 197 deletions

docs/reference/memory-config.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ explicitly to use Gemini, Voyage, Mistral, DeepInfra, Bedrock, GitHub Copilot,
5858
Ollama, a local GGUF model, or an OpenAI-compatible `/v1/embeddings` endpoint.
5959
Legacy configs that still say `provider: "auto"` resolve to `openai`.
6060

61+
<Warning>
62+
Changing the embedding provider, model, provider settings, sources, scope,
63+
chunking, or tokenizer can make the existing SQLite vector index incompatible.
64+
OpenClaw pauses vector search and reports an index identity warning instead of
65+
automatically re-embedding everything. Rebuild when you are ready with
66+
`openclaw memory status --index --agent <id>` or
67+
`openclaw memory index --force --agent <id>`.
68+
</Warning>
69+
6170
If OpenAI embeddings are unreachable from your network, memory recall fails open
6271
instead of blocking the turn. Set the existing `memorySearch.provider` field to a
6372
reachable local, Ollama, regional, or OpenAI-compatible provider to restore
@@ -155,7 +164,8 @@ Use `provider: "openai-compatible"` for a generic OpenAI-compatible
155164
| `outputDimensionality` | `number` | `3072` | For Embedding 2: 768, 1536, or 3072 |
156165

157166
<Warning>
158-
Changing model or `outputDimensionality` triggers an automatic full reindex.
167+
Changing model or `outputDimensionality` changes the index identity. OpenClaw
168+
pauses vector search until you explicitly rebuild the memory index.
159169
</Warning>
160170

161171
</Accordion>

extensions/memory-core/src/cli.runtime.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ type MemoryManagerPurpose = Parameters<typeof getMemorySearchManager>[0]["purpos
7171

7272
type MemorySourceName = "memory" | "sessions";
7373

74+
function formatMemoryIndexIdentityWarning(
75+
status: ReturnType<MemoryManager["status"]>,
76+
agentId: string,
77+
): {
78+
reason: string;
79+
fix: string;
80+
} | null {
81+
const indexIdentity = asRecord(asRecord(status.custom)?.indexIdentity);
82+
const reason =
83+
(indexIdentity?.status === "mismatched" || indexIdentity?.status === "missing") &&
84+
typeof indexIdentity.reason === "string"
85+
? indexIdentity.reason
86+
: undefined;
87+
if (!reason) {
88+
return null;
89+
}
90+
return {
91+
reason,
92+
fix: `Run: openclaw memory status --index --agent ${agentId}`,
93+
};
94+
}
95+
7496
type SourceScan = {
7597
source: MemorySourceName;
7698
totalFiles: number | null;
@@ -868,6 +890,12 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
868890
lines.push(`${label("Embeddings error")} ${warn(embeddingProbe.error)}`);
869891
}
870892
}
893+
const identityWarning = formatMemoryIndexIdentityWarning(status, agentId);
894+
if (identityWarning) {
895+
lines.push(`${label("Index identity")} ${warn(identityWarning.reason)}`);
896+
lines.push(`${label("Vector search")} ${warn("paused until memory is rebuilt")}`);
897+
lines.push(`${label("Fix")} ${muted(identityWarning.fix)}`);
898+
}
871899
if (status.sourceCounts?.length) {
872900
lines.push(label("By source"));
873901
for (const entry of status.sourceCounts) {
@@ -1256,6 +1284,15 @@ export async function runMemorySearch(
12561284
defaultRuntime.writeJson({ results });
12571285
return;
12581286
}
1287+
const identityWarning =
1288+
typeof manager.status === "function"
1289+
? formatMemoryIndexIdentityWarning(manager.status(), agentId)
1290+
: null;
1291+
if (identityWarning) {
1292+
defaultRuntime.error(
1293+
`Memory index warning: ${identityWarning.reason}. Vector memory search is paused until the index is rebuilt. ${identityWarning.fix}`,
1294+
);
1295+
}
12591296
if (results.length === 0) {
12601297
defaultRuntime.log("No matches.");
12611298
return;

extensions/memory-core/src/cli.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,36 @@ describe("memory cli", () => {
415415
expect(close).toHaveBeenCalled();
416416
});
417417

418+
it("prints index identity mismatch reasons", async () => {
419+
const close = vi.fn(async () => {});
420+
mockManager({
421+
status: () =>
422+
makeMemoryStatus({
423+
dirty: true,
424+
provider: "ollama",
425+
model: "nomic-embed-text",
426+
requestedProvider: "ollama",
427+
custom: {
428+
indexIdentity: {
429+
status: "mismatched",
430+
reason: "index was built for provider openai, expected ollama",
431+
},
432+
},
433+
}),
434+
close,
435+
});
436+
437+
const log = spyRuntimeLogs(defaultRuntime);
438+
await runMemoryCli(["status"]);
439+
440+
expectLogged(log, "Provider: ollama (requested: ollama)");
441+
expectLogged(log, "Dirty: yes");
442+
expectLogged(log, "Index identity: index was built for provider openai, expected ollama");
443+
expectLogged(log, "Vector search: paused until memory is rebuilt");
444+
expectLogged(log, "Fix: Run: openclaw memory status --index --agent main");
445+
expect(close).toHaveBeenCalled();
446+
});
447+
418448
it("keeps plain status from probing vector or embeddings", async () => {
419449
const close = vi.fn(async () => {});
420450
const probeVectorAvailability = vi.fn(async () => {

extensions/memory-core/src/memory-tool-manager-mock.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export function setMemoryWorkspaceDir(next: string): void {
8686
workspaceDir = next;
8787
}
8888

89+
export function setMemoryCustomStatus(next: Record<string, unknown> | undefined): void {
90+
customStatus = next;
91+
}
92+
8993
export function setMemorySearchImpl(next: SearchImpl): void {
9094
searchImpl = next;
9195
}
@@ -130,6 +134,10 @@ export function getMemorySearchManagerMockCalls(): number {
130134
return getMemorySearchManagerMock.mock.calls.length;
131135
}
132136

137+
export function getMemorySyncMockCalls(): number {
138+
return stubManager.sync.mock.calls.length;
139+
}
140+
133141
export function getMemorySearchManagerMockConfigs(): unknown[] {
134142
return getMemorySearchManagerMock.mock.calls.map(([params]) => params.cfg);
135143
}

extensions/memory-core/src/memory/embedding.test-mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function resetEmbeddingMocks(): void {
2626
}
2727

2828
vi.mock("./embeddings.js", () => ({
29+
resolveEmbeddingProviderAdapterId: (providerId: string) => providerId,
2930
createEmbeddingProvider: async () => ({
3031
requestedProvider: "openai",
3132
provider: {

extensions/memory-core/src/memory/embeddings.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ export function resolveEmbeddingProviderFallbackModel(
146146
return adapter?.defaultModel ?? fallbackSourceModel;
147147
}
148148

149+
export function resolveEmbeddingProviderAdapterId(
150+
providerId: string,
151+
config?: MemoryEmbeddingProviderCreateOptions["config"],
152+
): string | undefined {
153+
try {
154+
return getAdapter(providerId, config).id;
155+
} catch {
156+
return undefined;
157+
}
158+
}
159+
149160
async function createWithAdapter(
150161
adapter: MemoryEmbeddingProviderAdapter,
151162
options: CreateEmbeddingProviderOptions,

0 commit comments

Comments
 (0)