Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import type { DatabaseSync } from "node:sqlite";
import { type FSWatcher } from "chokidar";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
createSubsystemLogger,
resolveAgentDir,
resolveAgentWorkspaceDir,
resolveMemorySearchConfig,
type OpenClawConfig,
type ResolvedMemorySearchConfig,
} from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
import { extractKeywords } from "openclaw/plugin-sdk/memory-core-host-engine-qmd";
import {
readMemoryFile,
type MemoryEmbeddingProbeResult,
type MemoryProviderStatus,
type MemorySearchManager,
type MemorySearchRuntimeDebug,
type MemorySearchResult,
type MemorySource,
type MemorySyncProgressUpdate,
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
import {
createEmbeddingProvider,
type EmbeddingProvider,
type EmbeddingProviderId,
type EmbeddingProviderRequest,
type EmbeddingProviderResult,
type EmbeddingProviderRuntime,
} from "./embeddings.js";
import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js";
import { awaitPendingManagerWork, startAsyncSearchSync } from "./manager-async-state.js";
import { MEMORY_BATCH_FAILURE_LIMIT } from "./manager-batch-state.js";
import {
closeManagedCacheEntries,
getOrCreateManagedCacheEntry,
resolveSingletonManagedCache,
} from "./manager-cache.js";
import { MemoryManagerEmbeddingOps } from "./manager-embedding-ops.js";
import {
resolveMemoryPrimaryProviderRequest,
resolveMemoryProviderState,
} from "./manager-provider-state.js";
import { resolveMemorySearchPreflight } from "./manager-search-preflight.js";
import { searchKeyword, searchVector } from "./manager-search.js";
import {
collectMemoryStatusAggregate,
resolveInitialMemoryDirty,
resolveStatusProviderInfo,
} from "./manager-status-state.js";
import {
enqueueMemoryTargetedSessionSync,
extractMemoryErrorReason,
isMemoryReadonlyDbError,
runMemorySyncWithReadonlyRecovery,
type MemoryReadonlyRecoveryState,
} from "./manager-sync-control.js";
import { applyTemporalDecayToHybridResults } from "./temporal-decay.js";
const SNIPPET_MAX_CHARS = 700;
const VECTOR_TABLE = "chunks_vec";
const FTS_TABLE = "chunks_fts";
const EMBEDDING_CACHE_TABLE = "embedding_cache";
const MEMORY_INDEX_MANAGER_CACHE_KEY = Symbol.for("openclaw.memoryIndexManagerCache");
const log = createSubsystemLogger("memory");
const { cache: INDEX_CACHE, pending: INDEX_CACHE_PENDING } =
resolveSingletonManagedCache<MemoryIndexManager>(MEMORY_INDEX_MANAGER_CACHE_KEY);
export async function closeAllMemoryIndexManagers(): Promise<void> {
await closeManagedCacheEntries({
cache: INDEX_CACHE,
pending: INDEX_CACHE_PENDING,
onCloseError: (err) => {
log.warn(`failed to close memory index manager: ${String(err)}`);
},
});
}
export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager {
private readonly cacheKey: string;
protected readonly cfg: OpenClawConfig;
protected readonly agentId: string;
protected readonly workspaceDir: string;
protected readonly settings: ResolvedMemorySearchConfig;
protected provider: EmbeddingProvider | null;
private readonly requestedProvider: EmbeddingProviderRequest;
private providerInitPromise: Promise<void> | null = null;
private providerInitialized = false;
protected fallbackFrom?: EmbeddingProviderId;
protected fallbackReason?: string;
private providerUnavailableReason?: string;
protected providerRuntime?: EmbeddingProviderRuntime;
protected batch: {
enabled: boolean;
wait: boolean;
concurrency: number;
pollIntervalMs: number;
timeoutMs: number;
};
protected batchFailureCount = 0;
protected batchFailureLastError?: string;
protected batchFailureLastProvider?: string;
protected batchFailureLock: Promise<void> = Promise.resolve();
protected db: DatabaseSync;
protected readonly sources: Set<MemorySource>;
protected providerKey: string;
protected readonly cache: { enabled: boolean; maxEntries?: number };
protected readonly vector: {
enabled: boolean;
available: boolean | null;
extensionPath?: string;
loadError?: string;
dims?: number;
};
protected readonly fts: {
enabled: boolean;
available: boolean;
loadError?: string;
};
protected vectorReady: Promise<boolean> | null = null;
protected watcher: FSWatcher | null = null;
protected watchTimer: NodeJS.Timeout | null = null;
protected sessionWatchTimer: NodeJS.Timeout | null = null;
protected sessionUnsubscribe: (() => void) | null = null;
protected intervalTimer: NodeJS.Timeout | null = null;
protected closed = false;
protected dirty = false;
protected sessionsDirty = false;
protected sessionsDirtyFiles = new Set<string>();
protected sessionPendingFiles = new Set<string>();
protected sessionDeltas = new Map<
string,
{ lastSize: number; pendingBytes: number; pendingMessages: number }
>();
private sessionWarm = new Set<string>();
private syncing: Promise<void> | null = null;
private queuedSessionFiles = new Set<string>();
private queuedSessionSync: Promise<void> | null = null;
private readonlyRecoveryAttempts = 0;
private readonlyRecoverySuccesses = 0;
private readonlyRecoveryFailures = 0;
private readonlyRecoveryLastError?: string;
private static async loadProviderResult(params: {
cfg: OpenClawConfig;
agentId: string;
settings: ResolvedMemorySearchConfig;
}): Promise<EmbeddingProviderResult> {
return await createEmbeddingProvider({
config: params.cfg,
agentDir: resolveAgentDir(params.cfg, params.agentId),
...resolveMemoryPrimaryProviderRequest({ settings: params.settings }),
});
}
static async get(params: {
cfg: OpenClawConfig;
agentId: string;
purpose?: "default" | "status";
}): Promise<MemoryIndexManager | null> {
const { cfg, agentId } = params;
const settings = resolveMemorySearchConfig(cfg, agentId);
if (!settings) {
return null;
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const purpose = params.purpose === "status" ? "status" : "default";
const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}:${purpose}`;
const statusOnly = params.purpose === "status";
return await getOrCreateManagedCacheEntry({
cache: INDEX_CACHE,
pending: INDEX_CACHE_PENDING,
key,
bypassCache: statusOnly,
create: async () =>
new MemoryIndexManager({
cacheKey: key,
cfg,
agentId,
workspaceDir,
settings,
purpose: params.purpose,
}),
});
}
private constructor(params: {
cacheKey: string;
cfg: OpenClawConfig;
agentId: string;
workspaceDir: string;
settings: ResolvedMemorySearchConfig;
providerResult?: EmbeddingProviderResult;
purpose?: "default" | "status";
}) {
super();
this.cacheKey = params.cacheKey;
this.cfg = params.cfg;
this.agentId = params.agentId;
this.workspaceDir = params.workspaceDir;
this.settings = params.settings;
this.provider = null;
this.requestedProvider = params.settings.provider;
if (params.providerResult) {
this.applyProviderResult(params.providerResult);
}
this.sources = new Set(params.settings.sources);
this.db = this.openDatabase();
this.providerKey = this.computeProviderKey();
this.cache = {
enabled: params.settings.cache.enabled,
maxEntries: params.settings.cache.maxEntries,
};
this.fts = { enabled: params.settings.query.hybrid.enabled, available: false };
this.ensureSchema();
this.vector = {
enabled: params.settings.store.vector.enabled,
available: null,
extensionPath: params.settings.store.vector.extensionPath,
};
const meta = this.readMeta();
if (meta?.vectorDims) {
this.vector.dims = meta.vectorDims;
}
const statusOnly = params.purpose === "status";
if (!statusOnly) {
this.ensureWatcher();
this.ensureSessionListener();
this.ensureIntervalSync();
}
this.dirty = resolveInitialMemoryDirty({
hasMemorySource: this.sources.has("memory"),
statusOnly,
hasIndexedMeta: Boolean(meta),
});
this.batch = this.resolveBatchConfig();
}
private applyProviderResult(providerResult: EmbeddingProviderResult): void {
const providerState = resolveMemoryProviderState(providerResult);
this.provider = providerState.provider;
this.fallbackFrom = providerState.fallbackFrom;
this.fallbackReason = providerState.fallbackReason;
this.providerUnavailableReason = providerState.providerUnavailableReason;
this.providerRuntime = providerState.providerRuntime;
this.providerInitialized = true;
}
private async ensureProviderInitialized(): Promise<void> {
if (this.providerInitialized) {
return;
}
if (!this.providerInitPromise) {
this.providerInitPromise = (async () => {
const providerResult = await MemoryIndexManager.loadProviderResult({
cfg: this.cfg,
agentId: this.agentId,
settings: this.settings,
});
this.applyProviderResult(providerResult);
this.providerKey = this.computeProviderKey();
this.batch = this.resolveBatchConfig();
})();
}
try {
await this.providerInitPromise;
} finally {
if (this.providerInitialized) {
this.providerInitPromise = null;
}
}
}
async warmSession(sessionKey?: string): Promise<void> {
if (!this.settings.sync.onSessionStart) {
return;
}
const key = sessionKey?.trim() || "";
if (key && this.sessionWarm.has(key)) {
return;
}
void this.sync({ reason: "session-start" }).catch((err) => {
log.warn(`memory sync failed (session-start): ${String(err)}`);
});
if (key) {
this.sessionWarm.add(key);
}
}
async search(
query: string,
opts?: {
maxResults?: number;
minScore?: number;
sessionKey?: string;
qmdSearchModeOverride?: "query" | "search" | "vsearch";
onDebug?: (debug: MemorySearchRuntimeDebug) => void;
/** When set, only these chunk sources are considered (must be enabled for this manager). */
sources?: MemorySource[];
},
): Promise<MemorySearchResult[]> {
opts?.onDebug?.({ backend: "builtin" });
let hasIndexedContent = this.hasIndexedContent();
if (!hasIndexedContent) {
try {
// A fresh process can receive its first search before background watch/session
// syncs have built the index. Force one synchronous bootstrap so the first
// lookup after restart does not fail closed with empty results.
await this.sync({ reason: "search", force: true });
} catch (err) {
log.warn(`memory sync failed (search-bootstrap): ${String(err)}`);
}
hasIndexedContent = this.hasIndexedContent();
}
const preflight = resolveMemorySearchPreflight({
query,
hasIndexedContent,
});
if (!preflight.shouldSearch) {
return [];
}
const cleaned = preflight.normalizedQuery;
void this.warmSession(opts?.sessionKey);
startAsyncSearchSync({
enabled: this.settings.sync.onSearch,
dirty: this.dirty,
sessionsDirty: this.sessionsDirty,
sync: async (params) => await this.sync(params),
onError: (err) => {
log.warn(`memory sync failed (search): ${String(err)}`);
},
});
if (preflight.shouldInitializeProvider) {
await this.ensureProviderInitialized();
}
const minScore = opts?.minScore ?? this.settings.query.minScore;
const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
const searchSources =
opts?.sources && opts.sources.length > 0
? [...new Set(opts.sources)].filter((s) => this.sources.has(s))
: undefined;
if (opts?.sources && opts.sources.length > 0 && (!searchSources || searchSources.length === 0)) {
return [];
}
const sourceFilterList = searchSources ?? [...this.sources];
const hybrid = this.settings.query.hybrid;
const candidates = Math.min(
200,
Math.max(1, Math.floor(maxResults * hybrid.candidateMultiplier)),
);
// FTS-only mode: no embedding provider available
if (!this.provider) {
if (!this.fts.enabled || !this.fts.available) {
log.warn("memory search: no provider and FTS unavailable");
return [];
}
const fullQueryResults = await this.searchKeyword(
cleaned,
candidates,
{
boostFallbackRanking: true,
},
sourceFilterList,
).catch(() => []);
const resultSets =
fullQueryResults.length > 0
? [fullQueryResults]
: await Promise.all(
// Fallback: broaden recall for conversational queries when the
// exact AND query is too strict to return any results.
(() => {
const keywords = extractKeywords(cleaned, {
ftsTokenizer: this.settings.store.fts.tokenizer,
});
const searchTerms = keywords.length > 0 ? keywords : [cleaned];
return searchTerms.map((term) =>
this.searchKeyword(term, candidates, { boostFallbackRanking: true }, sourceFilterList).catch(
() => [],
),
);
})(),
);
// Merge and deduplicate results, keeping highest score for each chunk
const seenIds = new Map<string, (typeof resultSets)[0][0]>();
for (const results of resultSets) {
for (const result of results) {
const existing = seenIds.get(result.id);
if (!existing || result.score > existing.score) {
seenIds.set(result.id, result);
}
}
}
const merged = [...seenIds.values()];
const decayed = await applyTemporalDecayToHybridResults({
results: merged,
temporalDecay: hybrid.temporalDecay,
workspaceDir: this.workspaceDir,
});
const sorted = decayed.toSorted((a, b) => b.score - a.score);
return this.selectScoredResults(sorted, maxResults, minScore, 0);
}
// If FTS isn't available, hybrid mode cannot use keyword search; degrade to vector-only.
const keywordResults =
hybrid.enabled && this.fts.enabled && this.fts.available
? await this.searchKeyword(cleaned, candidates, undefined, sourceFilterList).catch(() => [])
: [];
const queryVec = await this.embedQueryWithTimeout(cleaned);
const hasVector = queryVec.some((v) => v !== 0);
const vectorResults = hasVector
? await this.searchVector(queryVec, candidates, sourceFilterList).catch(() => [])
: [];
if (!hybrid.enabled || !this.fts.enabled || !this.fts.available) {
return vectorResults.filter((entry) => entry.score >= minScore).slice(0, maxResults);
}
const merged = await this.mergeHybridResults({
vector: vectorResults,
keyword: keywordResults,
vectorWeight: hybrid.vectorWeight,
textWeight: hybrid.textWeight,
mmr: hybrid.mmr,
temporalDecay: hybrid.temporalDecay,
});
const strict = merged.filter((entry) => entry.score >= minScore);
if (strict.length > 0 || keywordResults.length === 0) {
return strict.slice(0, maxResults);
}
// Hybrid defaults can produce keyword-only matches with max score equal to
// textWeight (for example 0.3). If minScore is higher (for example 0.35),
// these exact lexical hits get filtered out even when they are the only
// relevant results.
const relaxedMinScore = Math.min(minScore, hybrid.textWeight);
const keywordKeys = new Set(
keywordResults.map(
(entry) => `${entry.source}:${entry.path}:${entry.startLine}:${entry.endLine}`,
),
);
return this.selectScoredResults(
merged.filter((entry) =>
keywordKeys.has(`${entry.source}:${entry.path}:${entry.startLine}:${entry.endLine}`),
),
maxResults,
minScore,
relaxedMinScore,
);
}
private selectScoredResults<T extends MemorySearchResult & { score: number }>(
results: T[],
maxResults: number,
minScore: number,
relaxedMinScore = minScore,
): T[] {
const strict = results.filter((entry) => entry.score >= minScore);
if (strict.length > 0) {
return strict.slice(0, maxResults);
}
return results.filter((entry) => entry.score >= relaxedMinScore).slice(0, maxResults);
}
private hasIndexedContent(): boolean {
const chunkRow = this.db.prepare(`SELECT 1 as found FROM chunks LIMIT 1`).get() as
| {
found?: number;
}
| undefined;
if (chunkRow?.found === 1) {
return true;
}
if (!this.fts.enabled || !this.fts.available) {
return false;
}
const ftsRow = this.db.prepare(`SELECT 1 as found FROM ${FTS_TABLE} LIMIT 1`).get() as
| {
found?: number;
}
| undefined;
return ftsRow?.found === 1;
}
private async searchVector(
queryVec: number[],
limit: number,
sourceFilterList: MemorySource[],
): Promise<Array<MemorySearchResult & { id: string }>> {
// This method should never be called without a provider
if (!this.provider) {
return [];
}
const results = await searchVector({
db: this.db,
vectorTable: VECTOR_TABLE,
providerModel: this.provider.model,
queryVec,
limit,
snippetMaxChars: SNIPPET_MAX_CHARS,
ensureVectorReady: async (dimensions) => await this.ensureVectorReady(dimensions),
sourceFilterVec: this.buildSourceFilter("c", sourceFilterList),
sourceFilterChunks: this.buildSourceFilter(undefined, sourceFilterList),
});
return results.map((entry) => entry as MemorySearchResult & { id: string });
}
private buildFtsQuery(raw: string): string | null {
return buildFtsQuery(raw);
}
private async searchKeyword(
query: string,
limit: number,
options?: { boostFallbackRanking?: boolean },
sourceFilterList?: MemorySource[],
): Promise<Array<MemorySearchResult & { id: string; textScore: number }>> {
if (!this.fts.enabled || !this.fts.available) {
return [];
}
const sourceFilter = this.buildSourceFilter(undefined, sourceFilterList);
// In FTS-only mode (no provider), search all models; otherwise filter by current provider's model
const providerModel = this.provider?.model;
const results = await searchKeyword({
db: this.db,
ftsTable: FTS_TABLE,
providerModel,
query,
ftsTokenizer: this.settings.store.fts.tokenizer,
limit,
snippetMaxChars: SNIPPET_MAX_CHARS,
sourceFilter,
buildFtsQuery: (raw) => this.buildFtsQuery(raw),
bm25RankToScore,
boostFallbackRanking: options?.boostFallbackRanking,
});
return results.map((entry) => entry as MemorySearchResult & { id: string; textScore: number });
}
private mergeHybridResults(params: {
vector: Array<MemorySearchResult & { id: string }>;
keyword: Array<MemorySearchResult & { id: string; textScore: number }>;
vectorWeight: number;
textWeight: number;
mmr?: { enabled: boolean; lambda: number };
temporalDecay?: { enabled: boolean; halfLifeDays: number };
}): Promise<MemorySearchResult[]> {
return mergeHybridResults({
vector: params.vector.map((r) => ({
id: r.id,
path: r.path,
startLine: r.startLine,
endLine: r.endLine,
source: r.source,
snippet: r.snippet,
vectorScore: r.score,
})),
keyword: params.keyword.map((r) => ({
id: r.id,
path: r.path,
startLine: r.startLine,
endLine: r.endLine,
source: r.source,
snippet: r.snippet,
textScore: r.textScore,
})),
vectorWeight: params.vectorWeight,
textWeight: params.textWeight,
mmr: params.mmr,
temporalDecay: params.temporalDecay,
workspaceDir: this.workspaceDir,
}).then((entries) => entries.map((entry) => entry as MemorySearchResult));
}
async sync(params?: {
reason?: string;
force?: boolean;
sessionFiles?: string[];
progress?: (update: MemorySyncProgressUpdate) => void;
}): Promise<void> {
if (this.closed) {
return;
}
await this.ensureProviderInitialized();
if (this.syncing) {
if (params?.sessionFiles?.some((sessionFile) => sessionFile.trim().length > 0)) {
return this.enqueueTargetedSessionSync(params.sessionFiles);
}
return this.syncing;
}
this.syncing = this.runSyncWithReadonlyRecovery(params).finally(() => {
this.syncing = null;
});
return this.syncing ?? Promise.resolve();
}
private enqueueTargetedSessionSync(sessionFiles?: string[]): Promise<void> {
return enqueueMemoryTargetedSessionSync(
{
isClosed: () => this.closed,
getSyncing: () => this.syncing,
getQueuedSessionFiles: () => this.queuedSessionFiles,
getQueuedSessionSync: () => this.queuedSessionSync,
setQueuedSessionSync: (value) => {
this.queuedSessionSync = value;
},
sync: async (params) => await this.sync(params),
},
sessionFiles,
);
}
private isReadonlyDbError(err: unknown): boolean {
return isMemoryReadonlyDbError(err);
}
private extractErrorReason(err: unknown): string {
return extractMemoryErrorReason(err);
}
private async runSyncWithReadonlyRecovery(params?: {
reason?: string;
force?: boolean;
sessionFiles?: string[];
progress?: (update: MemorySyncProgressUpdate) => void;
}): Promise<void> {
const getClosed = () => this.closed;
const getDb = () => this.db;
const setDb = (value: DatabaseSync) => {
this.db = value;
};
const getReadonlyRecoveryAttempts = () => this.readonlyRecoveryAttempts;
const setReadonlyRecoveryAttempts = (value: number) => {
this.readonlyRecoveryAttempts = value;
};
const getReadonlyRecoverySuccesses = () => this.readonlyRecoverySuccesses;
const setReadonlyRecoverySuccesses = (value: number) => {
this.readonlyRecoverySuccesses = value;
};
const getReadonlyRecoveryFailures = () => this.readonlyRecoveryFailures;
const setReadonlyRecoveryFailures = (value: number) => {
this.readonlyRecoveryFailures = value;
};
const getReadonlyRecoveryLastError = () => this.readonlyRecoveryLastError;
const setReadonlyRecoveryLastError = (value: string | undefined) => {
this.readonlyRecoveryLastError = value;
};
const state: MemoryReadonlyRecoveryState = {
get closed() {
return getClosed();
},
get db() {
return getDb();
},
set db(value) {
setDb(value);
},
vector: this.vector,
get readonlyRecoveryAttempts() {
return getReadonlyRecoveryAttempts();
},
set readonlyRecoveryAttempts(value) {
setReadonlyRecoveryAttempts(value);
},
get readonlyRecoverySuccesses() {
return getReadonlyRecoverySuccesses();
},
set readonlyRecoverySuccesses(value) {
setReadonlyRecoverySuccesses(value);
},
get readonlyRecoveryFailures() {
return getReadonlyRecoveryFailures();
},
set readonlyRecoveryFailures(value) {
setReadonlyRecoveryFailures(value);
},
get readonlyRecoveryLastError() {
return getReadonlyRecoveryLastError();
},
set readonlyRecoveryLastError(value) {
setReadonlyRecoveryLastError(value);
},
runSync: (nextParams) => this.runSync(nextParams),
openDatabase: () => this.openDatabase(),
resetVectorState: () => this.resetVectorState(),
ensureSchema: () => this.ensureSchema(),
readMeta: () => this.readMeta() ?? undefined,
};
await runMemorySyncWithReadonlyRecovery(state, params);
}
async readFile(params: {
relPath: string;
from?: number;
lines?: number;
}): Promise<{ text: string; path: string }> {
return await readMemoryFile({
workspaceDir: this.workspaceDir,
extraPaths: this.settings.extraPaths,
relPath: params.relPath,
from: params.from,
lines: params.lines,
});
}
status(): MemoryProviderStatus {
const sourceFilter = this.buildSourceFilter();
const aggregateState = collectMemoryStatusAggregate({
db: {
prepare: (sql) => ({
all: (...args) =>
this.db.prepare(sql).all(...args) as Array<{
kind: "files" | "chunks";
source: MemorySource;
c: number;
}>,
}),
},
sources: this.sources,
sourceFilterSql: sourceFilter.sql,
sourceFilterParams: sourceFilter.params,
});
const providerInfo = resolveStatusProviderInfo({
provider: this.provider,
providerInitialized: this.providerInitialized,
requestedProvider: this.requestedProvider,
configuredModel: this.settings.model || undefined,
});
return {
backend: "builtin",
files: aggregateState.files,
chunks: aggregateState.chunks,
dirty: this.dirty || this.sessionsDirty,
workspaceDir: this.workspaceDir,
dbPath: this.settings.store.path,
provider: providerInfo.provider,
model: providerInfo.model,
requestedProvider: this.requestedProvider,
sources: Array.from(this.sources),
extraPaths: this.settings.extraPaths,
sourceCounts: aggregateState.sourceCounts,
cache: this.cache.enabled
? {
enabled: true,
entries:
(
this.db.prepare(`SELECT COUNT(*) as c FROM ${EMBEDDING_CACHE_TABLE}`).get() as
| { c: number }
| undefined
)?.c ?? 0,
maxEntries: this.cache.maxEntries,
}
: { enabled: false, maxEntries: this.cache.maxEntries },
fts: {
enabled: this.fts.enabled,
available: this.fts.available,
error: this.fts.loadError,
},
fallback: this.fallbackReason
? { from: this.fallbackFrom ?? "local", reason: this.fallbackReason }
: undefined,
vector: {
enabled: this.vector.enabled,
available: this.vector.available ?? undefined,
extensionPath: this.vector.extensionPath,
loadError: this.vector.loadError,
dims: this.vector.dims,
},
batch: {
enabled: this.batch.enabled,
failures: this.batchFailureCount,
limit: MEMORY_BATCH_FAILURE_LIMIT,
wait: this.batch.wait,
concurrency: this.batch.concurrency,
pollIntervalMs: this.batch.pollIntervalMs,
timeoutMs: this.batch.timeoutMs,
lastError: this.batchFailureLastError,
lastProvider: this.batchFailureLastProvider,
},
custom: {
searchMode: providerInfo.searchMode,
providerUnavailableReason: this.providerUnavailableReason,
readonlyRecovery: {
attempts: this.readonlyRecoveryAttempts,
successes: this.readonlyRecoverySuccesses,
failures: this.readonlyRecoveryFailures,
lastError: this.readonlyRecoveryLastError,
},
},
};
}
async probeVectorAvailability(): Promise<boolean> {
if (!this.vector.enabled) {
return false;
}
await this.ensureProviderInitialized();
// FTS-only mode: vector search not available
if (!this.provider) {
return false;
}
return this.ensureVectorReady();
}
async probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult> {
await this.ensureProviderInitialized();
// FTS-only mode: embeddings not available but search still works
if (!this.provider) {
return {
ok: false,
error: this.providerUnavailableReason ?? "No embedding provider available (FTS-only mode)",
};
}
try {
await this.embedBatchWithRetry(["ping"]);
return { ok: true };
} catch (err) {
const message = formatErrorMessage(err);
return { ok: false, error: message };
}
}
async close(): Promise<void> {
if (this.closed) {
return;
}
this.closed = true;
const pendingSync = this.syncing;
const pendingProviderInit = this.providerInitPromise;
if (this.watchTimer) {
clearTimeout(this.watchTimer);
this.watchTimer = null;
}
if (this.sessionWatchTimer) {
clearTimeout(this.sessionWatchTimer);
this.sessionWatchTimer = null;
}
if (this.intervalTimer) {
clearInterval(this.intervalTimer);
this.intervalTimer = null;
}
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
if (this.sessionUnsubscribe) {
this.sessionUnsubscribe();
this.sessionUnsubscribe = null;
}
await awaitPendingManagerWork({ pendingSync, pendingProviderInit });
this.db.close();
INDEX_CACHE.delete(this.cacheKey);
}
}
¤ Dauer der Verarbeitung: 0.23 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|