mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 20:54:33 +00:00
fix(memory): readonly sync recovery (openclaw#25799) thanks @rodrigouroz
Verified: - pnpm build - pnpm check - pnpm test:macmini (fails in this environment at src/daemon/launchd.integration.test.ts beforeAll hook timeout; merged with Tak override) Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -39,6 +39,7 @@ const BATCH_FAILURE_LIMIT = 2;
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
|
||||
const INDEX_CACHE_PENDING = new Map<string, Promise<MemoryIndexManager>>();
|
||||
|
||||
export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager {
|
||||
private readonly cacheKey: string;
|
||||
@@ -99,6 +100,10 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
>();
|
||||
private sessionWarm = new Set<string>();
|
||||
private syncing: Promise<void> | null = null;
|
||||
private readonlyRecoveryAttempts = 0;
|
||||
private readonlyRecoverySuccesses = 0;
|
||||
private readonlyRecoveryFailures = 0;
|
||||
private readonlyRecoveryLastError?: string;
|
||||
|
||||
static async get(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -116,26 +121,44 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const providerResult = await createEmbeddingProvider({
|
||||
config: cfg,
|
||||
agentDir: resolveAgentDir(cfg, agentId),
|
||||
provider: settings.provider,
|
||||
remote: settings.remote,
|
||||
model: settings.model,
|
||||
fallback: settings.fallback,
|
||||
local: settings.local,
|
||||
});
|
||||
const manager = new MemoryIndexManager({
|
||||
cacheKey: key,
|
||||
cfg,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
settings,
|
||||
providerResult,
|
||||
purpose: params.purpose,
|
||||
});
|
||||
INDEX_CACHE.set(key, manager);
|
||||
return manager;
|
||||
const pending = INDEX_CACHE_PENDING.get(key);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
const createPromise = (async () => {
|
||||
const providerResult = await createEmbeddingProvider({
|
||||
config: cfg,
|
||||
agentDir: resolveAgentDir(cfg, agentId),
|
||||
provider: settings.provider,
|
||||
remote: settings.remote,
|
||||
model: settings.model,
|
||||
fallback: settings.fallback,
|
||||
local: settings.local,
|
||||
});
|
||||
const refreshed = INDEX_CACHE.get(key);
|
||||
if (refreshed) {
|
||||
return refreshed;
|
||||
}
|
||||
const manager = new MemoryIndexManager({
|
||||
cacheKey: key,
|
||||
cfg,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
settings,
|
||||
providerResult,
|
||||
purpose: params.purpose,
|
||||
});
|
||||
INDEX_CACHE.set(key, manager);
|
||||
return manager;
|
||||
})();
|
||||
INDEX_CACHE_PENDING.set(key, createPromise);
|
||||
try {
|
||||
return await createPromise;
|
||||
} finally {
|
||||
if (INDEX_CACHE_PENDING.get(key) === createPromise) {
|
||||
INDEX_CACHE_PENDING.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(params: {
|
||||
@@ -388,12 +411,97 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
if (this.syncing) {
|
||||
return this.syncing;
|
||||
}
|
||||
this.syncing = this.runSync(params).finally(() => {
|
||||
this.syncing = this.runSyncWithReadonlyRecovery(params).finally(() => {
|
||||
this.syncing = null;
|
||||
});
|
||||
return this.syncing ?? Promise.resolve();
|
||||
}
|
||||
|
||||
private isReadonlyDbError(err: unknown): boolean {
|
||||
const readonlyPattern =
|
||||
/attempt to write a readonly database|database is read-only|SQLITE_READONLY/i;
|
||||
const messages = new Set<string>();
|
||||
|
||||
const pushValue = (value: unknown): void => {
|
||||
if (typeof value !== "string") {
|
||||
return;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
messages.add(normalized);
|
||||
};
|
||||
|
||||
pushValue(err instanceof Error ? err.message : String(err));
|
||||
if (err && typeof err === "object") {
|
||||
const record = err as Record<string, unknown>;
|
||||
pushValue(record.message);
|
||||
pushValue(record.code);
|
||||
pushValue(record.name);
|
||||
if (record.cause && typeof record.cause === "object") {
|
||||
const cause = record.cause as Record<string, unknown>;
|
||||
pushValue(cause.message);
|
||||
pushValue(cause.code);
|
||||
pushValue(cause.name);
|
||||
}
|
||||
}
|
||||
|
||||
return [...messages].some((value) => readonlyPattern.test(value));
|
||||
}
|
||||
|
||||
private extractErrorReason(err: unknown): string {
|
||||
if (err instanceof Error && err.message.trim()) {
|
||||
return err.message;
|
||||
}
|
||||
if (err && typeof err === "object") {
|
||||
const record = err as Record<string, unknown>;
|
||||
if (typeof record.message === "string" && record.message.trim()) {
|
||||
return record.message;
|
||||
}
|
||||
if (typeof record.code === "string" && record.code.trim()) {
|
||||
return record.code;
|
||||
}
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
private async runSyncWithReadonlyRecovery(params?: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
progress?: (update: MemorySyncProgressUpdate) => void;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await this.runSync(params);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (!this.isReadonlyDbError(err) || this.closed) {
|
||||
throw err;
|
||||
}
|
||||
const reason = this.extractErrorReason(err);
|
||||
this.readonlyRecoveryAttempts += 1;
|
||||
this.readonlyRecoveryLastError = reason;
|
||||
log.warn(`memory sync readonly handle detected; reopening sqlite connection`, { reason });
|
||||
try {
|
||||
this.db.close();
|
||||
} catch {}
|
||||
this.db = this.openDatabase();
|
||||
this.vectorReady = null;
|
||||
this.vector.available = null;
|
||||
this.vector.loadError = undefined;
|
||||
this.ensureSchema();
|
||||
const meta = this.readMeta();
|
||||
this.vector.dims = meta?.vectorDims;
|
||||
try {
|
||||
await this.runSync(params);
|
||||
this.readonlyRecoverySuccesses += 1;
|
||||
} catch (retryErr) {
|
||||
this.readonlyRecoveryFailures += 1;
|
||||
throw retryErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(params: {
|
||||
relPath: string;
|
||||
from?: number;
|
||||
@@ -571,6 +679,12 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
custom: {
|
||||
searchMode,
|
||||
providerUnavailableReason: this.providerUnavailableReason,
|
||||
readonlyRecovery: {
|
||||
attempts: this.readonlyRecoveryAttempts,
|
||||
successes: this.readonlyRecoverySuccesses,
|
||||
failures: this.readonlyRecoveryFailures,
|
||||
lastError: this.readonlyRecoveryLastError,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user