Skip to content

Commit 8217c3a

Browse files
authored
Merge b5f0441 into 89a21db
2 parents 89a21db + b5f0441 commit 8217c3a

7 files changed

Lines changed: 73 additions & 0 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
2828
- Agents/heartbeat: stop heartbeat turns after the first valid `heartbeat_respond` so repeated response loops do not burn tokens. (#86357) Thanks @udaymanish6.
2929
- Tasks: keep retained lost tasks out of default status health counts, explain their cleanup window during maintenance, and prune lost task records after 24 hours instead of the general 7-day terminal retention.
3030
- Memory-core: keep REM dreaming focused on live light-staged memories and mark staged entries as considered so old recall history no longer dominates fresh candidates. (#86302) Thanks @SebTardif.
31+
- Memory: abort sync instead of downgrading an existing semantic vector index to FTS-only when the configured embedding provider is temporarily unavailable. (#85704) Thanks @yaaboo-gif.
3132
- Telegram: propagate forum topic names through the account-scoped topic cache for native command context and topic create/edit actions. (#86299) Thanks @SebTardif.
3233
- Slack: keep downloaded read-only files out of reply media so Slack file reads do not echo files back to the conversation. (#86318) Thanks @neeravmakwana.
3334
- Cron: accept leading-plus relative durations such as `+5m` for one-shot `--at` schedules. (#86341) Thanks @mushuiyu886.

extensions/memory-core/src/memory/manager-sync-ops.archive-delta-bypass.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ class SessionDeltaHarness extends MemoryManagerSyncOps {
100100

101101
protected pruneEmbeddingCacheIfNeeded(): void {}
102102

103+
protected resetProviderInitializationForRetry(): void {}
104+
103105
protected async indexFile(
104106
_entry: MemoryIndexEntry,
105107
_options: { source: MemorySource; content?: string },

extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ class SessionStartupCatchupHarness extends MemoryManagerSyncOps {
110110

111111
protected pruneEmbeddingCacheIfNeeded(): void {}
112112

113+
protected resetProviderInitializationForRetry(): void {}
114+
113115
protected async indexFile(
114116
_entry: MemoryIndexEntry,
115117
_options: { source: MemorySource; content?: string },

extensions/memory-core/src/memory/manager-sync-ops.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export abstract class MemoryManagerSyncOps {
235235
): Promise<T>;
236236
protected abstract getIndexConcurrency(): number;
237237
protected abstract pruneEmbeddingCacheIfNeeded(): void;
238+
protected abstract resetProviderInitializationForRetry(): void;
238239
protected abstract indexFile(
239240
entry: MemoryIndexEntry,
240241
options: { source: MemorySource; content?: string },
@@ -1093,12 +1094,39 @@ export abstract class MemoryManagerSyncOps {
10931094
return state;
10941095
}
10951096

1097+
private assertFtsOnlySyncAllowed(): void {
1098+
if (this.provider) {
1099+
return;
1100+
}
1101+
const existingMeta = this.readMeta();
1102+
if (
1103+
!existingMeta ||
1104+
existingMeta.model === "fts-only" ||
1105+
!this.settings.provider ||
1106+
this.settings.provider === "none"
1107+
) {
1108+
return;
1109+
}
1110+
this.resetProviderInitializationForRetry();
1111+
throw new Error(
1112+
`Memory sync aborted: embedding provider "${this.settings.provider}" is configured but unavailable. ` +
1113+
`Refusing to run sync in fts-only fallback mode to protect existing vector index (current model: ${existingMeta.model}).`,
1114+
);
1115+
}
1116+
10961117
protected async runSync(params?: {
10971118
reason?: string;
10981119
force?: boolean;
10991120
sessionFiles?: string[];
11001121
progress?: (update: MemorySyncProgressUpdate) => void;
11011122
}) {
1123+
// Guard: if an embedding provider is configured but currently unavailable,
1124+
// abort sync to prevent silently degrading an existing semantic vector index
1125+
// to fts-only and wiping existing semantic vectors.
1126+
// This only protects existing semantic indexes; fresh or already-fts-only
1127+
// indexes can safely sync without an embedding provider.
1128+
this.assertFtsOnlySyncAllowed();
1129+
11021130
const progress = params?.progress ? this.createSyncProgress(params.progress) : undefined;
11031131
if (progress) {
11041132
progress.report({
@@ -1309,6 +1337,8 @@ export abstract class MemoryManagerSyncOps {
13091337
force?: boolean;
13101338
progress?: MemorySyncProgressState;
13111339
}): Promise<void> {
1340+
this.assertFtsOnlySyncAllowed();
1341+
13121342
const dbPath = resolveUserPath(this.settings.store.path);
13131343
const tempDbPath = `${dbPath}.tmp-${randomUUID()}`;
13141344
const tempDb = openMemoryDatabaseAtPath(tempDbPath, this.settings.store.vector.enabled);

extensions/memory-core/src/memory/manager-sync-yield.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class SessionSyncYieldHarness extends MemoryManagerSyncOps {
124124

125125
protected pruneEmbeddingCacheIfNeeded(): void {}
126126

127+
protected resetProviderInitializationForRetry(): void {}
128+
127129
protected async indexFile(
128130
entry: MemoryIndexEntry,
129131
_options: { source: MemorySource; content?: string },

extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DatabaseSync } from "node:sqlite";
55
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
66
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
77
import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js";
8+
import type { MemoryIndexMeta } from "./manager-reindex-state.js";
89
import type { MemoryIndexManager } from "./manager.js";
910
import "./test-runtime-mocks.js";
1011

@@ -89,6 +90,19 @@ describe("memory manager FTS-only reindex", () => {
8990
}
9091
}
9192

93+
function writeExistingMeta(memoryManager: MemoryIndexManager, model: string): void {
94+
const metaWriter = memoryManager as unknown as {
95+
writeMeta(meta: MemoryIndexMeta): void;
96+
};
97+
metaWriter.writeMeta({
98+
model,
99+
provider: "openai",
100+
chunkTokens: 600,
101+
chunkOverlap: 120,
102+
sources: ["memory"],
103+
});
104+
}
105+
92106
it("preserves indexed chunks across forced reindex in FTS-only mode", async () => {
93107
const memoryManager = await createManager();
94108

@@ -116,4 +130,14 @@ describe("memory manager FTS-only reindex", () => {
116130
expect(countChunksContaining("refresh marker")).toBeGreaterThan(0);
117131
expect(countChunksContaining("Alpha topic")).toBe(0);
118132
});
133+
134+
it("aborts instead of downgrading an existing semantic index to FTS-only", async () => {
135+
const memoryManager = await createManager();
136+
writeExistingMeta(memoryManager, "mock-embed");
137+
138+
await expect(memoryManager.sync({ force: true })).rejects.toThrow(
139+
"Refusing to run sync in fts-only fallback mode to protect existing vector index (current model: mock-embed).",
140+
);
141+
expect(memoryManager.status().provider).toBe("auto");
142+
});
119143
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,13 +312,25 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
312312
}
313313
try {
314314
await this.providerInitPromise;
315+
} catch (err) {
316+
// Clear the cached rejected promise so subsequent calls can retry
317+
// initialization instead of being permanently stuck with a stale failure.
318+
this.providerInitPromise = null;
319+
throw err;
315320
} finally {
316321
if (this.providerInitialized) {
317322
this.providerInitPromise = null;
318323
}
319324
}
320325
}
321326

327+
protected resetProviderInitializationForRetry(): void {
328+
this.providerInitialized = false;
329+
this.providerInitPromise = null;
330+
this.providerUnavailableReason = undefined;
331+
this.providerLifecycle = createPendingMemoryProviderLifecycle(this.requestedProvider);
332+
}
333+
322334
protected markLocalEmbeddingProviderDegraded(err: unknown): void {
323335
if (this.provider?.id !== "local") {
324336
return;

0 commit comments

Comments
 (0)