mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix: dedupe degraded sqlite-vec warnings (#67898) (thanks @rubencu)
* Agents: dedupe bootstrap truncation warnings * Memory: dedupe sqlite-vec degradation warnings * Memory: align degraded vector warning * test(memory-core): remove stale vector warning arg * fix(memory-core): reset degraded warning on vector reset * fix(memory-core): preserve warning latch across reindex rollback * fix: dedupe degraded sqlite-vec warnings (#67898) (thanks @rubencu) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/webhooks: enforce synchronous plugin registration with full rollback of failed plugin side effects, and cache SecretRef-backed webhook auth per route so plugin startup and inbound webhook auth stay deterministic. (#67941) Thanks @obviyus.
|
||||
- Telegram/ACP bindings: drop persisted DM bindings that still point at missing or failed ACP sessions on restart, while preserving plugin-owned bindings and uncertain store reads. (#67822) Thanks @chinar-amrutkar.
|
||||
- Telegram/streaming: keep a transient preview on the same Telegram message when auto-compaction retries an in-flight answer, so streamed replies no longer appear duplicated after compaction. (#66939) Thanks @rubencu.
|
||||
- Memory/sqlite-vec: emit the degraded sqlite-vec warning once per degraded episode instead of repeating it for every file write, while preserving the latch across safe-reindex rollback and resetting it when vector state is genuinely rebuilt. (#67898) Thanks @rubencu.
|
||||
|
||||
## 2026.4.15
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "./manager-embedding-policy.js";
|
||||
import { deleteMemoryFtsRows } from "./manager-fts-state.js";
|
||||
import { MemoryManagerSyncOps } from "./manager-sync-ops.js";
|
||||
import { logMemoryVectorDegradedWrite } from "./manager-vector-warning.js";
|
||||
import { replaceMemoryVectorRow } from "./manager-vector-write.js";
|
||||
|
||||
const VECTOR_TABLE = "chunks_vec";
|
||||
@@ -568,12 +569,14 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps {
|
||||
.run(chunk.text, id, entry.path, source, model, chunk.startLine, chunk.endLine);
|
||||
}
|
||||
}
|
||||
if (this.vector.enabled && !vectorReady && chunks.length > 0) {
|
||||
const errDetail = this.vector.loadError ? `: ${this.vector.loadError}` : "";
|
||||
log.warn(
|
||||
`chunks written for ${entry.path} without vector embeddings — chunks_vec not updated (sqlite-vec unavailable${errDetail}). Vector recall degraded for this file.`,
|
||||
);
|
||||
}
|
||||
this.vectorDegradedWriteWarningShown = logMemoryVectorDegradedWrite({
|
||||
vectorEnabled: this.vector.enabled,
|
||||
vectorReady,
|
||||
chunkCount: chunks.length,
|
||||
warningShown: this.vectorDegradedWriteWarningShown,
|
||||
loadError: this.vector.loadError,
|
||||
warn: (message) => log.warn(message),
|
||||
});
|
||||
this.upsertFileRecord(entry, source);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ const log = createSubsystemLogger("memory");
|
||||
export type MemoryReadonlyRecoveryState = {
|
||||
closed: boolean;
|
||||
db: DatabaseSync;
|
||||
vectorReady: Promise<boolean> | null;
|
||||
vector: {
|
||||
enabled: boolean;
|
||||
available: boolean | null;
|
||||
extensionPath?: string;
|
||||
loadError?: string;
|
||||
dims?: number;
|
||||
};
|
||||
readonlyRecoveryAttempts: number;
|
||||
@@ -30,6 +25,7 @@ export type MemoryReadonlyRecoveryState = {
|
||||
progress?: (update: MemorySyncProgressUpdate) => void;
|
||||
}) => Promise<void>;
|
||||
openDatabase: () => DatabaseSync;
|
||||
resetVectorState: () => void;
|
||||
ensureSchema: () => void;
|
||||
readMeta: () => { vectorDims?: number } | undefined;
|
||||
};
|
||||
@@ -107,9 +103,7 @@ export async function runMemorySyncWithReadonlyRecovery(
|
||||
state.db.close();
|
||||
} catch {}
|
||||
state.db = state.openDatabase();
|
||||
state.vectorReady = null;
|
||||
state.vector.available = null;
|
||||
state.vector.loadError = undefined;
|
||||
state.resetVectorState();
|
||||
state.ensureSchema();
|
||||
const meta = state.readMeta();
|
||||
state.vector.dims = meta?.vectorDims;
|
||||
|
||||
@@ -166,6 +166,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
string,
|
||||
{ lastSize: number; pendingBytes: number; pendingMessages: number }
|
||||
>();
|
||||
protected vectorDegradedWriteWarningShown = false;
|
||||
private lastMetaSerialized: string | null = null;
|
||||
|
||||
protected abstract readonly cache: { enabled: boolean; maxEntries?: number };
|
||||
@@ -190,6 +191,14 @@ export abstract class MemoryManagerSyncOps {
|
||||
options: { source: MemorySource; content?: string },
|
||||
): Promise<void>;
|
||||
|
||||
protected resetVectorState(): void {
|
||||
this.vectorReady = null;
|
||||
this.vector.available = null;
|
||||
this.vector.loadError = undefined;
|
||||
this.vector.dims = undefined;
|
||||
this.vectorDegradedWriteWarningShown = false;
|
||||
}
|
||||
|
||||
protected async ensureVectorReady(dimensions?: number): Promise<boolean> {
|
||||
if (!this.vector.enabled) {
|
||||
return false;
|
||||
@@ -1122,6 +1131,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
vectorAvailable: this.vector.available,
|
||||
vectorLoadError: this.vector.loadError,
|
||||
vectorDims: this.vector.dims,
|
||||
vectorDegradedWriteWarningShown: this.vectorDegradedWriteWarningShown,
|
||||
vectorReady: this.vectorReady,
|
||||
};
|
||||
|
||||
@@ -1136,14 +1146,12 @@ export abstract class MemoryManagerSyncOps {
|
||||
this.vector.available = originalDbClosed ? null : originalState.vectorAvailable;
|
||||
this.vector.loadError = originalState.vectorLoadError;
|
||||
this.vector.dims = originalState.vectorDims;
|
||||
this.vectorDegradedWriteWarningShown = originalState.vectorDegradedWriteWarningShown;
|
||||
this.vectorReady = originalDbClosed ? null : originalState.vectorReady;
|
||||
};
|
||||
|
||||
this.db = tempDb;
|
||||
this.vectorReady = null;
|
||||
this.vector.available = null;
|
||||
this.vector.loadError = undefined;
|
||||
this.vector.dims = undefined;
|
||||
this.resetVectorState();
|
||||
this.fts.available = false;
|
||||
this.fts.loadError = undefined;
|
||||
this.ensureSchema();
|
||||
@@ -1211,9 +1219,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
});
|
||||
|
||||
this.db = openMemoryDatabaseAtPath(dbPath, this.settings.store.vector.enabled);
|
||||
this.vectorReady = null;
|
||||
this.vector.available = null;
|
||||
this.vector.loadError = undefined;
|
||||
this.resetVectorState();
|
||||
this.ensureSchema();
|
||||
this.vector.dims = nextMeta?.vectorDims;
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { logMemoryVectorDegradedWrite } from "./manager-vector-warning.js";
|
||||
|
||||
describe("memory vector degradation warnings", () => {
|
||||
it("emits the degraded warning only once for a manager", () => {
|
||||
const warn = vi.fn();
|
||||
|
||||
const first = logMemoryVectorDegradedWrite({
|
||||
vectorEnabled: true,
|
||||
vectorReady: false,
|
||||
chunkCount: 3,
|
||||
warningShown: false,
|
||||
loadError: "load failed",
|
||||
warn,
|
||||
});
|
||||
const second = logMemoryVectorDegradedWrite({
|
||||
vectorEnabled: true,
|
||||
vectorReady: false,
|
||||
chunkCount: 2,
|
||||
warningShown: first,
|
||||
loadError: "load failed",
|
||||
warn,
|
||||
});
|
||||
|
||||
expect(first).toBe(true);
|
||||
expect(second).toBe(true);
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"chunks_vec not updated — sqlite-vec unavailable: load failed. Vector recall degraded. Further duplicate warnings suppressed.",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips the warning when vector writes are available", () => {
|
||||
const warn = vi.fn();
|
||||
|
||||
const shown = logMemoryVectorDegradedWrite({
|
||||
vectorEnabled: true,
|
||||
vectorReady: true,
|
||||
chunkCount: 1,
|
||||
warningShown: false,
|
||||
warn,
|
||||
});
|
||||
|
||||
expect(shown).toBe(false);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
22
extensions/memory-core/src/memory/manager-vector-warning.ts
Normal file
22
extensions/memory-core/src/memory/manager-vector-warning.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export function logMemoryVectorDegradedWrite(params: {
|
||||
vectorEnabled: boolean;
|
||||
vectorReady: boolean;
|
||||
chunkCount: number;
|
||||
warningShown: boolean;
|
||||
loadError?: string;
|
||||
warn: (message: string) => void;
|
||||
}): boolean {
|
||||
if (
|
||||
!params.vectorEnabled ||
|
||||
params.vectorReady ||
|
||||
params.chunkCount <= 0 ||
|
||||
params.warningShown
|
||||
) {
|
||||
return params.warningShown;
|
||||
}
|
||||
const errDetail = params.loadError ? `: ${params.loadError}` : "";
|
||||
params.warn(
|
||||
`chunks_vec not updated — sqlite-vec unavailable${errDetail}. Vector recall degraded. Further duplicate warnings suppressed.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -16,10 +16,12 @@ type ReadonlyRecoveryHarness = MemoryReadonlyRecoveryState & {
|
||||
syncing: Promise<void> | null;
|
||||
queuedSessionFiles: Set<string>;
|
||||
queuedSessionSync: Promise<void> | null;
|
||||
vectorDegradedWriteWarningShown: boolean;
|
||||
ensureProviderInitialized: ReturnType<typeof vi.fn>;
|
||||
enqueueTargetedSessionSync: ReturnType<typeof vi.fn>;
|
||||
runSync: ReturnType<typeof vi.fn>;
|
||||
openDatabase: ReturnType<typeof vi.fn>;
|
||||
resetVectorState: ReturnType<typeof vi.fn>;
|
||||
ensureSchema: ReturnType<typeof vi.fn>;
|
||||
readMeta: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
@@ -66,13 +68,10 @@ describe("memory manager readonly recovery", () => {
|
||||
queuedSessionFiles: new Set<string>(),
|
||||
queuedSessionSync: null,
|
||||
db: initialDb,
|
||||
vectorReady: null,
|
||||
vector: {
|
||||
enabled: false,
|
||||
available: null,
|
||||
loadError: "stale",
|
||||
dims: 123,
|
||||
},
|
||||
vectorDegradedWriteWarningShown: true,
|
||||
readonlyRecoveryAttempts: 0,
|
||||
readonlyRecoverySuccesses: 0,
|
||||
readonlyRecoveryFailures: 0,
|
||||
@@ -81,6 +80,10 @@ describe("memory manager readonly recovery", () => {
|
||||
enqueueTargetedSessionSync: vi.fn(async () => {}),
|
||||
runSync: vi.fn(async (_params) => undefined) as ReadonlyRecoveryHarness["runSync"],
|
||||
openDatabase: vi.fn(() => reopenedDb),
|
||||
resetVectorState: vi.fn(function (this: ReadonlyRecoveryHarness) {
|
||||
this.vector.dims = undefined;
|
||||
this.vectorDegradedWriteWarningShown = false;
|
||||
}) as ReadonlyRecoveryHarness["resetVectorState"],
|
||||
ensureSchema: vi.fn(() => undefined) as ReadonlyRecoveryHarness["ensureSchema"],
|
||||
readMeta: vi.fn(() => undefined),
|
||||
};
|
||||
@@ -132,6 +135,7 @@ describe("memory manager readonly recovery", () => {
|
||||
|
||||
expect(harness.runSync).toHaveBeenCalledTimes(2);
|
||||
expect(harness.openDatabase).toHaveBeenCalledTimes(1);
|
||||
expect(harness.resetVectorState).toHaveBeenCalledTimes(1);
|
||||
expect(initialClose).toHaveBeenCalledTimes(1);
|
||||
expectReadonlyRecoveryStatus(harness, params.expectedLastError);
|
||||
}
|
||||
@@ -173,9 +177,23 @@ describe("memory manager readonly recovery", () => {
|
||||
).rejects.toThrow("embedding timeout");
|
||||
expect(harness.runSync).toHaveBeenCalledTimes(1);
|
||||
expect(harness.openDatabase).not.toHaveBeenCalled();
|
||||
expect(harness.resetVectorState).not.toHaveBeenCalled();
|
||||
expect(initialClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears the degraded warning latch before retrying", async () => {
|
||||
const { harness } = createReadonlyRecoveryHarness();
|
||||
harness.runSync.mockRejectedValueOnce(new Error("attempt to write a readonly database"));
|
||||
|
||||
await expect(
|
||||
runSyncWithReadonlyRecovery(harness, {
|
||||
reason: "test",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(harness.vectorDegradedWriteWarningShown).toBe(false);
|
||||
});
|
||||
|
||||
it("sets busy_timeout on memory sqlite connections", async () => {
|
||||
const db = openMemoryDatabaseAtPath(indexPath, false);
|
||||
const row = db.prepare("PRAGMA busy_timeout").get() as
|
||||
|
||||
@@ -615,10 +615,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
const setDb = (value: DatabaseSync) => {
|
||||
this.db = value;
|
||||
};
|
||||
const getVectorReady = () => this.vectorReady;
|
||||
const setVectorReady = (value: Promise<boolean> | null) => {
|
||||
this.vectorReady = value;
|
||||
};
|
||||
const getReadonlyRecoveryAttempts = () => this.readonlyRecoveryAttempts;
|
||||
const setReadonlyRecoveryAttempts = (value: number) => {
|
||||
this.readonlyRecoveryAttempts = value;
|
||||
@@ -645,12 +641,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
set db(value) {
|
||||
setDb(value);
|
||||
},
|
||||
get vectorReady() {
|
||||
return getVectorReady();
|
||||
},
|
||||
set vectorReady(value) {
|
||||
setVectorReady(value);
|
||||
},
|
||||
vector: this.vector,
|
||||
get readonlyRecoveryAttempts() {
|
||||
return getReadonlyRecoveryAttempts();
|
||||
@@ -678,6 +668,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
},
|
||||
runSync: (nextParams) => this.runSync(nextParams),
|
||||
openDatabase: () => this.openDatabase(),
|
||||
resetVectorState: () => this.resetVectorState(),
|
||||
ensureSchema: () => this.ensureSchema(),
|
||||
readMeta: () => this.readMeta() ?? undefined,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user