chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -179,11 +179,15 @@ export class MemoryIndexManager {
}): Promise<MemoryIndexManager | null> {
const { cfg, agentId } = params;
const settings = resolveMemorySearchConfig(cfg, agentId);
if (!settings) return null;
if (!settings) {
return null;
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`;
const existing = INDEX_CACHE.get(key);
if (existing) return existing;
if (existing) {
return existing;
}
const providerResult = await createEmbeddingProvider({
config: cfg,
agentDir: resolveAgentDir(cfg, agentId),
@@ -250,13 +254,19 @@ export class MemoryIndexManager {
}
async warmSession(sessionKey?: string): Promise<void> {
if (!this.settings.sync.onSessionStart) return;
if (!this.settings.sync.onSessionStart) {
return;
}
const key = sessionKey?.trim() || "";
if (key && this.sessionWarm.has(key)) return;
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);
if (key) {
this.sessionWarm.add(key);
}
}
async search(
@@ -274,7 +284,9 @@ export class MemoryIndexManager {
});
}
const cleaned = query.trim();
if (!cleaned) return [];
if (!cleaned) {
return [];
}
const minScore = opts?.minScore ?? this.settings.query.minScore;
const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
const hybrid = this.settings.query.hybrid;
@@ -333,7 +345,9 @@ export class MemoryIndexManager {
query: string,
limit: number,
): Promise<Array<MemorySearchResult & { id: string; textScore: number }>> {
if (!this.fts.enabled || !this.fts.available) return [];
if (!this.fts.enabled || !this.fts.available) {
return [];
}
const sourceFilter = this.buildSourceFilter();
const results = await searchKeyword({
db: this.db,
@@ -385,7 +399,9 @@ export class MemoryIndexManager {
force?: boolean;
progress?: (update: MemorySyncProgressUpdate) => void;
}): Promise<void> {
if (this.syncing) return this.syncing;
if (this.syncing) {
return this.syncing;
}
this.syncing = this.runSync(params).finally(() => {
this.syncing = null;
});
@@ -417,7 +433,9 @@ export class MemoryIndexManager {
for (const additionalPath of additionalPaths) {
try {
const stat = await fs.lstat(additionalPath);
if (stat.isSymbolicLink()) continue;
if (stat.isSymbolicLink()) {
continue;
}
if (stat.isDirectory()) {
if (absPath === additionalPath || absPath.startsWith(`${additionalPath}${path.sep}`)) {
allowedAdditional = true;
@@ -502,7 +520,9 @@ export class MemoryIndexManager {
};
const sourceCounts = (() => {
const sources = Array.from(this.sources);
if (sources.length === 0) return [];
if (sources.length === 0) {
return [];
}
const bySource = new Map<MemorySource, { files: number; chunks: number }>();
for (const source of sources) {
bySource.set(source, { files: 0, chunks: 0 });
@@ -583,7 +603,9 @@ export class MemoryIndexManager {
}
async probeVectorAvailability(): Promise<boolean> {
if (!this.vector.enabled) return false;
if (!this.vector.enabled) {
return false;
}
return this.ensureVectorReady();
}
@@ -598,7 +620,9 @@ export class MemoryIndexManager {
}
async close(): Promise<void> {
if (this.closed) return;
if (this.closed) {
return;
}
this.closed = true;
if (this.watchTimer) {
clearTimeout(this.watchTimer);
@@ -625,7 +649,9 @@ export class MemoryIndexManager {
}
private async ensureVectorReady(dimensions?: number): Promise<boolean> {
if (!this.vector.enabled) return false;
if (!this.vector.enabled) {
return false;
}
if (!this.vectorReady) {
this.vectorReady = this.withTimeout(
this.loadVectorExtension(),
@@ -651,7 +677,9 @@ export class MemoryIndexManager {
}
private async loadVectorExtension(): Promise<boolean> {
if (this.vector.available !== null) return this.vector.available;
if (this.vector.available !== null) {
return this.vector.available;
}
if (!this.vector.enabled) {
this.vector.available = false;
return false;
@@ -661,7 +689,9 @@ export class MemoryIndexManager {
? resolveUserPath(this.vector.extensionPath)
: undefined;
const loaded = await loadSqliteVecExtension({ db: this.db, extensionPath: resolvedPath });
if (!loaded.ok) throw new Error(loaded.error ?? "unknown sqlite-vec load error");
if (!loaded.ok) {
throw new Error(loaded.error ?? "unknown sqlite-vec load error");
}
this.vector.extensionPath = loaded.extensionPath;
this.vector.available = true;
return true;
@@ -675,7 +705,9 @@ export class MemoryIndexManager {
}
private ensureVectorTable(dimensions: number): void {
if (this.vector.dims === dimensions) return;
if (this.vector.dims === dimensions) {
return;
}
if (this.vector.dims && this.vector.dims !== dimensions) {
this.dropVectorTable();
}
@@ -699,7 +731,9 @@ export class MemoryIndexManager {
private buildSourceFilter(alias?: string): { sql: string; params: MemorySource[] } {
const sources = Array.from(this.sources);
if (sources.length === 0) return { sql: "", params: [] };
if (sources.length === 0) {
return { sql: "", params: [] };
}
const column = alias ? `${alias}.source` : "source";
const placeholders = sources.map(() => "?").join(", ");
return { sql: ` AND ${column} IN (${placeholders})`, params: sources };
@@ -718,7 +752,9 @@ export class MemoryIndexManager {
}
private seedEmbeddingCache(sourceDb: DatabaseSync): void {
if (!this.cache.enabled) return;
if (!this.cache.enabled) {
return;
}
try {
const rows = sourceDb
.prepare(
@@ -733,7 +769,9 @@ export class MemoryIndexManager {
dims: number | null;
updated_at: number;
}>;
if (!rows.length) return;
if (!rows.length) {
return;
}
const insert = this.db.prepare(
`INSERT INTO ${EMBEDDING_CACHE_TABLE} (provider, model, provider_key, hash, embedding, dims, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -810,7 +848,9 @@ export class MemoryIndexManager {
}
private ensureWatcher() {
if (!this.sources.has("memory") || !this.settings.sync.watch || this.watcher) return;
if (!this.sources.has("memory") || !this.settings.sync.watch || this.watcher) {
return;
}
const additionalPaths = normalizeExtraMemoryPaths(this.workspaceDir, this.settings.extraPaths)
.map((entry) => {
try {
@@ -844,18 +884,26 @@ export class MemoryIndexManager {
}
private ensureSessionListener() {
if (!this.sources.has("sessions") || this.sessionUnsubscribe) return;
if (!this.sources.has("sessions") || this.sessionUnsubscribe) {
return;
}
this.sessionUnsubscribe = onSessionTranscriptUpdate((update) => {
if (this.closed) return;
if (this.closed) {
return;
}
const sessionFile = update.sessionFile;
if (!this.isSessionFileForAgent(sessionFile)) return;
if (!this.isSessionFileForAgent(sessionFile)) {
return;
}
this.scheduleSessionDirty(sessionFile);
});
}
private scheduleSessionDirty(sessionFile: string) {
this.sessionPendingFiles.add(sessionFile);
if (this.sessionWatchTimer) return;
if (this.sessionWatchTimer) {
return;
}
this.sessionWatchTimer = setTimeout(() => {
this.sessionWatchTimer = null;
void this.processSessionDeltaBatch().catch((err) => {
@@ -865,13 +913,17 @@ export class MemoryIndexManager {
}
private async processSessionDeltaBatch(): Promise<void> {
if (this.sessionPendingFiles.size === 0) return;
if (this.sessionPendingFiles.size === 0) {
return;
}
const pending = Array.from(this.sessionPendingFiles);
this.sessionPendingFiles.clear();
let shouldSync = false;
for (const sessionFile of pending) {
const delta = await this.updateSessionDelta(sessionFile);
if (!delta) continue;
if (!delta) {
continue;
}
const bytesThreshold = delta.deltaBytes;
const messagesThreshold = delta.deltaMessages;
const bytesHit =
@@ -880,7 +932,9 @@ export class MemoryIndexManager {
messagesThreshold <= 0
? delta.pendingMessages > 0
: delta.pendingMessages >= messagesThreshold;
if (!bytesHit && !messagesHit) continue;
if (!bytesHit && !messagesHit) {
continue;
}
this.sessionsDirtyFiles.add(sessionFile);
this.sessionsDirty = true;
delta.pendingBytes =
@@ -903,7 +957,9 @@ export class MemoryIndexManager {
pendingMessages: number;
} | null> {
const thresholds = this.settings.sync.sessions;
if (!thresholds) return null;
if (!thresholds) {
return null;
}
let stat: { size: number };
try {
stat = await fs.stat(sessionFile);
@@ -954,7 +1010,9 @@ export class MemoryIndexManager {
}
private async countNewlines(absPath: string, start: number, end: number): Promise<number> {
if (end <= start) return 0;
if (end <= start) {
return 0;
}
const handle = await fs.open(absPath, "r");
try {
let offset = start;
@@ -963,9 +1021,13 @@ export class MemoryIndexManager {
while (offset < end) {
const toRead = Math.min(buffer.length, end - offset);
const { bytesRead } = await handle.read(buffer, 0, toRead, offset);
if (bytesRead <= 0) break;
if (bytesRead <= 0) {
break;
}
for (let i = 0; i < bytesRead; i += 1) {
if (buffer[i] === 10) count += 1;
if (buffer[i] === 10) {
count += 1;
}
}
offset += bytesRead;
}
@@ -977,14 +1039,18 @@ export class MemoryIndexManager {
private resetSessionDelta(absPath: string, size: number): void {
const state = this.sessionDeltas.get(absPath);
if (!state) return;
if (!state) {
return;
}
state.lastSize = size;
state.pendingBytes = 0;
state.pendingMessages = 0;
}
private isSessionFileForAgent(sessionFile: string): boolean {
if (!sessionFile) return false;
if (!sessionFile) {
return false;
}
const sessionsDir = resolveSessionTranscriptsDirForAgent(this.agentId);
const resolvedFile = path.resolve(sessionFile);
const resolvedDir = path.resolve(sessionsDir);
@@ -993,7 +1059,9 @@ export class MemoryIndexManager {
private ensureIntervalSync() {
const minutes = this.settings.sync.intervalMinutes;
if (!minutes || minutes <= 0 || this.intervalTimer) return;
if (!minutes || minutes <= 0 || this.intervalTimer) {
return;
}
const ms = minutes * 60 * 1000;
this.intervalTimer = setInterval(() => {
void this.sync({ reason: "interval" }).catch((err) => {
@@ -1003,8 +1071,12 @@ export class MemoryIndexManager {
}
private scheduleWatchSync() {
if (!this.sources.has("memory") || !this.settings.sync.watch) return;
if (this.watchTimer) clearTimeout(this.watchTimer);
if (!this.sources.has("memory") || !this.settings.sync.watch) {
return;
}
if (this.watchTimer) {
clearTimeout(this.watchTimer);
}
this.watchTimer = setTimeout(() => {
this.watchTimer = null;
void this.sync({ reason: "watch" }).catch((err) => {
@@ -1017,11 +1089,19 @@ export class MemoryIndexManager {
params?: { reason?: string; force?: boolean },
needsFullReindex = false,
) {
if (!this.sources.has("sessions")) return false;
if (params?.force) return true;
if (!this.sources.has("sessions")) {
return false;
}
if (params?.force) {
return true;
}
const reason = params?.reason;
if (reason === "session-start" || reason === "watch") return false;
if (needsFullReindex) return true;
if (reason === "session-start" || reason === "watch") {
return false;
}
if (needsFullReindex) {
return true;
}
return this.sessionsDirty && this.sessionsDirtyFiles.size > 0;
}
@@ -1078,7 +1158,9 @@ export class MemoryIndexManager {
.prepare(`SELECT path FROM files WHERE source = ?`)
.all("memory") as Array<{ path: string }>;
for (const stale of staleRows) {
if (activePaths.has(stale.path)) continue;
if (activePaths.has(stale.path)) {
continue;
}
this.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory");
try {
this.db
@@ -1173,7 +1255,9 @@ export class MemoryIndexManager {
.prepare(`SELECT path FROM files WHERE source = ?`)
.all("sessions") as Array<{ path: string }>;
for (const stale of staleRows) {
if (activePaths.has(stale.path)) continue;
if (activePaths.has(stale.path)) {
continue;
}
this.db
.prepare(`DELETE FROM files WHERE path = ? AND source = ?`)
.run(stale.path, "sessions");
@@ -1205,7 +1289,9 @@ export class MemoryIndexManager {
total: 0,
label: undefined,
report: (update) => {
if (update.label) state.label = update.label;
if (update.label) {
state.label = update.label;
}
const label =
update.total > 0 && state.label
? `${state.label} ${update.completed}/${update.total}`
@@ -1316,8 +1402,12 @@ export class MemoryIndexManager {
private async activateFallbackProvider(reason: string): Promise<boolean> {
const fallback = this.settings.fallback;
if (!fallback || fallback === "none" || fallback === this.provider.id) return false;
if (this.fallbackFrom) return false;
if (!fallback || fallback === "none" || fallback === this.provider.id) {
return false;
}
if (this.fallbackFrom) {
return false;
}
const fallbackFrom = this.provider.id as "openai" | "gemini" | "local";
const fallbackModel =
@@ -1469,7 +1559,9 @@ export class MemoryIndexManager {
const row = this.db.prepare(`SELECT value FROM meta WHERE key = ?`).get(META_KEY) as
| { value: string }
| undefined;
if (!row?.value) return null;
if (!row?.value) {
return null;
}
try {
return JSON.parse(row.value) as MemoryIndexMeta;
} catch {
@@ -1516,16 +1608,26 @@ export class MemoryIndexManager {
const normalized = this.normalizeSessionText(content);
return normalized ? normalized : null;
}
if (!Array.isArray(content)) return null;
if (!Array.isArray(content)) {
return null;
}
const parts: string[] = [];
for (const block of content) {
if (!block || typeof block !== "object") continue;
if (!block || typeof block !== "object") {
continue;
}
const record = block as { type?: unknown; text?: unknown };
if (record.type !== "text" || typeof record.text !== "string") continue;
if (record.type !== "text" || typeof record.text !== "string") {
continue;
}
const normalized = this.normalizeSessionText(record.text);
if (normalized) parts.push(normalized);
if (normalized) {
parts.push(normalized);
}
}
if (parts.length === 0) {
return null;
}
if (parts.length === 0) return null;
return parts.join(" ");
}
@@ -1536,7 +1638,9 @@ export class MemoryIndexManager {
const lines = raw.split("\n");
const collected: string[] = [];
for (const line of lines) {
if (!line.trim()) continue;
if (!line.trim()) {
continue;
}
let record: unknown;
try {
record = JSON.parse(line);
@@ -1553,10 +1657,16 @@ export class MemoryIndexManager {
const message = (record as { message?: unknown }).message as
| { role?: unknown; content?: unknown }
| undefined;
if (!message || typeof message.role !== "string") continue;
if (message.role !== "user" && message.role !== "assistant") continue;
if (!message || typeof message.role !== "string") {
continue;
}
if (message.role !== "user" && message.role !== "assistant") {
continue;
}
const text = this.extractSessionText(message.content);
if (!text) continue;
if (!text) {
continue;
}
const label = message.role === "user" ? "User" : "Assistant";
collected.push(`${label}: ${text}`);
}
@@ -1576,7 +1686,9 @@ export class MemoryIndexManager {
}
private estimateEmbeddingTokens(text: string): number {
if (!text) return 0;
if (!text) {
return 0;
}
return Math.ceil(text.length / EMBEDDING_APPROX_CHARS_PER_TOKEN);
}
@@ -1609,17 +1721,27 @@ export class MemoryIndexManager {
}
private loadEmbeddingCache(hashes: string[]): Map<string, number[]> {
if (!this.cache.enabled) return new Map();
if (hashes.length === 0) return new Map();
if (!this.cache.enabled) {
return new Map();
}
if (hashes.length === 0) {
return new Map();
}
const unique: string[] = [];
const seen = new Set<string>();
for (const hash of hashes) {
if (!hash) continue;
if (seen.has(hash)) continue;
if (!hash) {
continue;
}
if (seen.has(hash)) {
continue;
}
seen.add(hash);
unique.push(hash);
}
if (unique.length === 0) return new Map();
if (unique.length === 0) {
return new Map();
}
const out = new Map<string, number[]>();
const baseParams = [this.provider.id, this.provider.model, this.providerKey];
@@ -1641,8 +1763,12 @@ export class MemoryIndexManager {
}
private upsertEmbeddingCache(entries: Array<{ hash: string; embedding: number[] }>): void {
if (!this.cache.enabled) return;
if (entries.length === 0) return;
if (!this.cache.enabled) {
return;
}
if (entries.length === 0) {
return;
}
const now = Date.now();
const stmt = this.db.prepare(
`INSERT INTO ${EMBEDDING_CACHE_TABLE} (provider, model, provider_key, hash, embedding, dims, updated_at)\n` +
@@ -1667,14 +1793,20 @@ export class MemoryIndexManager {
}
private pruneEmbeddingCacheIfNeeded(): void {
if (!this.cache.enabled) return;
if (!this.cache.enabled) {
return;
}
const max = this.cache.maxEntries;
if (!max || max <= 0) return;
if (!max || max <= 0) {
return;
}
const row = this.db.prepare(`SELECT COUNT(*) as c FROM ${EMBEDDING_CACHE_TABLE}`).get() as
| { c: number }
| undefined;
const count = row?.c ?? 0;
if (count <= max) return;
if (count <= max) {
return;
}
const excess = count - max;
this.db
.prepare(
@@ -1689,7 +1821,9 @@ export class MemoryIndexManager {
}
private async embedChunksInBatches(chunks: MemoryChunk[]): Promise<number[][]> {
if (chunks.length === 0) return [];
if (chunks.length === 0) {
return [];
}
const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash));
const embeddings: number[][] = Array.from({ length: chunks.length }, () => []);
const missing: Array<{ index: number; chunk: MemoryChunk }> = [];
@@ -1704,7 +1838,9 @@ export class MemoryIndexManager {
}
}
if (missing.length === 0) return embeddings;
if (missing.length === 0) {
return embeddings;
}
const missingChunks = missing.map((m) => m.chunk);
const batches = this.buildEmbeddingBatches(missingChunks);
@@ -1784,7 +1920,9 @@ export class MemoryIndexManager {
if (!openAi) {
return this.embedChunksInBatches(chunks);
}
if (chunks.length === 0) return [];
if (chunks.length === 0) {
return [];
}
const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash));
const embeddings: number[][] = Array.from({ length: chunks.length }, () => []);
const missing: Array<{ index: number; chunk: MemoryChunk }> = [];
@@ -1799,7 +1937,9 @@ export class MemoryIndexManager {
}
}
if (missing.length === 0) return embeddings;
if (missing.length === 0) {
return embeddings;
}
const requests: OpenAiBatchRequest[] = [];
const mapping = new Map<string, { index: number; hash: string }>();
@@ -1834,13 +1974,17 @@ export class MemoryIndexManager {
}),
fallback: async () => await this.embedChunksInBatches(chunks),
});
if (Array.isArray(batchResult)) return batchResult;
if (Array.isArray(batchResult)) {
return batchResult;
}
const byCustomId = batchResult;
const toCache: Array<{ hash: string; embedding: number[] }> = [];
for (const [customId, embedding] of byCustomId.entries()) {
const mapped = mapping.get(customId);
if (!mapped) continue;
if (!mapped) {
continue;
}
embeddings[mapped.index] = embedding;
toCache.push({ hash: mapped.hash, embedding });
}
@@ -1857,7 +2001,9 @@ export class MemoryIndexManager {
if (!gemini) {
return this.embedChunksInBatches(chunks);
}
if (chunks.length === 0) return [];
if (chunks.length === 0) {
return [];
}
const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash));
const embeddings: number[][] = Array.from({ length: chunks.length }, () => []);
const missing: Array<{ index: number; chunk: MemoryChunk }> = [];
@@ -1872,7 +2018,9 @@ export class MemoryIndexManager {
}
}
if (missing.length === 0) return embeddings;
if (missing.length === 0) {
return embeddings;
}
const requests: GeminiBatchRequest[] = [];
const mapping = new Map<string, { index: number; hash: string }>();
@@ -1904,13 +2052,17 @@ export class MemoryIndexManager {
}),
fallback: async () => await this.embedChunksInBatches(chunks),
});
if (Array.isArray(batchResult)) return batchResult;
if (Array.isArray(batchResult)) {
return batchResult;
}
const byCustomId = batchResult;
const toCache: Array<{ hash: string; embedding: number[] }> = [];
for (const [customId, embedding] of byCustomId.entries()) {
const mapped = mapping.get(customId);
if (!mapped) continue;
if (!mapped) {
continue;
}
embeddings[mapped.index] = embedding;
toCache.push({ hash: mapped.hash, embedding });
}
@@ -1919,7 +2071,9 @@ export class MemoryIndexManager {
}
private async embedBatchWithRetry(texts: string[]): Promise<number[][]> {
if (texts.length === 0) return [];
if (texts.length === 0) {
return [];
}
let attempt = 0;
let delayMs = EMBEDDING_RETRY_BASE_DELAY_MS;
while (true) {
@@ -1981,7 +2135,9 @@ export class MemoryIndexManager {
timeoutMs: number,
message: string,
): Promise<T> {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return await promise;
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
return await promise;
}
let timer: NodeJS.Timeout | null = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
@@ -1989,12 +2145,16 @@ export class MemoryIndexManager {
try {
return (await Promise.race([promise, timeoutPromise])) as T;
} finally {
if (timer) clearTimeout(timer);
if (timer) {
clearTimeout(timer);
}
}
}
private async runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]> {
if (tasks.length === 0) return [];
if (tasks.length === 0) {
return [];
}
const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
const results: T[] = Array.from({ length: tasks.length });
let next = 0;
@@ -2002,10 +2162,14 @@ export class MemoryIndexManager {
const workers = Array.from({ length: resolvedLimit }, async () => {
while (true) {
if (firstError) return;
if (firstError) {
return;
}
const index = next;
next += 1;
if (index >= tasks.length) return;
if (index >= tasks.length) {
return;
}
try {
results[index] = await tasks[index]();
} catch (err) {
@@ -2016,7 +2180,9 @@ export class MemoryIndexManager {
});
await Promise.allSettled(workers);
if (firstError) throw firstError;
if (firstError) {
throw firstError;
}
return results;
}