mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -38,7 +38,9 @@ const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBED
|
||||
const log = createSubsystemLogger("memory/embeddings");
|
||||
|
||||
const debugLog = (message: string, meta?: Record<string, unknown>) => {
|
||||
if (!debugEmbeddings) return;
|
||||
if (!debugEmbeddings) {
|
||||
return;
|
||||
}
|
||||
const suffix = meta ? ` ${JSON.stringify(meta)}` : "";
|
||||
log.raw(`${message}${suffix}`);
|
||||
};
|
||||
@@ -71,7 +73,9 @@ function getGeminiUploadUrl(baseUrl: string): string {
|
||||
}
|
||||
|
||||
function splitGeminiBatchRequests(requests: GeminiBatchRequest[]): GeminiBatchRequest[][] {
|
||||
if (requests.length <= GEMINI_BATCH_MAX_REQUESTS) return [requests];
|
||||
if (requests.length <= GEMINI_BATCH_MAX_REQUESTS) {
|
||||
return [requests];
|
||||
}
|
||||
const groups: GeminiBatchRequest[][] = [];
|
||||
for (let i = 0; i < requests.length; i += GEMINI_BATCH_MAX_REQUESTS) {
|
||||
groups.push(requests.slice(i, i + GEMINI_BATCH_MAX_REQUESTS));
|
||||
@@ -218,7 +222,9 @@ async function fetchGeminiFileContent(params: {
|
||||
}
|
||||
|
||||
function parseGeminiBatchOutput(text: string): GeminiBatchOutputLine[] {
|
||||
if (!text.trim()) return [];
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
@@ -272,7 +278,9 @@ async function waitForGeminiBatch(params: {
|
||||
}
|
||||
|
||||
async function 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;
|
||||
@@ -280,10 +288,14 @@ async function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: numb
|
||||
|
||||
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) {
|
||||
@@ -294,7 +306,9 @@ async function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: numb
|
||||
});
|
||||
|
||||
await Promise.allSettled(workers);
|
||||
if (firstError) throw firstError;
|
||||
if (firstError) {
|
||||
throw firstError;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -308,7 +322,9 @@ export async function runGeminiEmbeddingBatches(params: {
|
||||
concurrency: number;
|
||||
debug?: (message: string, data?: Record<string, unknown>) => void;
|
||||
}): Promise<Map<string, number[]>> {
|
||||
if (params.requests.length === 0) return new Map();
|
||||
if (params.requests.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
const groups = splitGeminiBatchRequests(params.requests);
|
||||
const byCustomId = new Map<string, number[]>();
|
||||
|
||||
@@ -373,7 +389,9 @@ export async function runGeminiEmbeddingBatches(params: {
|
||||
|
||||
for (const line of outputLines) {
|
||||
const customId = line.key ?? line.custom_id ?? line.request_id;
|
||||
if (!customId) continue;
|
||||
if (!customId) {
|
||||
continue;
|
||||
}
|
||||
remaining.delete(customId);
|
||||
if (line.error?.message) {
|
||||
errors.push(`${customId}: ${line.error.message}`);
|
||||
|
||||
@@ -56,7 +56,9 @@ function getOpenAiHeaders(
|
||||
}
|
||||
|
||||
function splitOpenAiBatchRequests(requests: OpenAiBatchRequest[]): OpenAiBatchRequest[][] {
|
||||
if (requests.length <= OPENAI_BATCH_MAX_REQUESTS) return [requests];
|
||||
if (requests.length <= OPENAI_BATCH_MAX_REQUESTS) {
|
||||
return [requests];
|
||||
}
|
||||
const groups: OpenAiBatchRequest[][] = [];
|
||||
for (let i = 0; i < requests.length; i += OPENAI_BATCH_MAX_REQUESTS) {
|
||||
groups.push(requests.slice(i, i + OPENAI_BATCH_MAX_REQUESTS));
|
||||
@@ -163,7 +165,9 @@ async function fetchOpenAiFileContent(params: {
|
||||
}
|
||||
|
||||
function parseOpenAiBatchOutput(text: string): OpenAiBatchOutputLine[] {
|
||||
if (!text.trim()) return [];
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
@@ -242,7 +246,9 @@ async function waitForOpenAiBatch(params: {
|
||||
}
|
||||
|
||||
async function 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;
|
||||
@@ -250,10 +256,14 @@ async function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: numb
|
||||
|
||||
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) {
|
||||
@@ -264,7 +274,9 @@ async function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: numb
|
||||
});
|
||||
|
||||
await Promise.allSettled(workers);
|
||||
if (firstError) throw firstError;
|
||||
if (firstError) {
|
||||
throw firstError;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -278,7 +290,9 @@ export async function runOpenAiEmbeddingBatches(params: {
|
||||
concurrency: number;
|
||||
debug?: (message: string, data?: Record<string, unknown>) => void;
|
||||
}): Promise<Map<string, number[]>> {
|
||||
if (params.requests.length === 0) return new Map();
|
||||
if (params.requests.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
const groups = splitOpenAiBatchRequests(params.requests);
|
||||
const byCustomId = new Map<string, number[]>();
|
||||
|
||||
@@ -335,7 +349,9 @@ export async function runOpenAiEmbeddingBatches(params: {
|
||||
|
||||
for (const line of outputLines) {
|
||||
const customId = line.custom_id;
|
||||
if (!customId) continue;
|
||||
if (!customId) {
|
||||
continue;
|
||||
}
|
||||
remaining.delete(customId);
|
||||
if (line.error?.message) {
|
||||
errors.push(`${customId}: ${line.error.message}`);
|
||||
|
||||
@@ -16,14 +16,18 @@ const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBED
|
||||
const log = createSubsystemLogger("memory/embeddings");
|
||||
|
||||
const debugLog = (message: string, meta?: Record<string, unknown>) => {
|
||||
if (!debugEmbeddings) return;
|
||||
if (!debugEmbeddings) {
|
||||
return;
|
||||
}
|
||||
const suffix = meta ? ` ${JSON.stringify(meta)}` : "";
|
||||
log.raw(`${message}${suffix}`);
|
||||
};
|
||||
|
||||
function resolveRemoteApiKey(remoteApiKey?: string): string | undefined {
|
||||
const trimmed = remoteApiKey?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed === "GOOGLE_API_KEY" || trimmed === "GEMINI_API_KEY") {
|
||||
return process.env[trimmed]?.trim();
|
||||
}
|
||||
@@ -32,17 +36,25 @@ function resolveRemoteApiKey(remoteApiKey?: string): string | undefined {
|
||||
|
||||
function normalizeGeminiModel(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) return DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
if (!trimmed) {
|
||||
return DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
}
|
||||
const withoutPrefix = trimmed.replace(/^models\//, "");
|
||||
if (withoutPrefix.startsWith("gemini/")) return withoutPrefix.slice("gemini/".length);
|
||||
if (withoutPrefix.startsWith("google/")) return withoutPrefix.slice("google/".length);
|
||||
if (withoutPrefix.startsWith("gemini/")) {
|
||||
return withoutPrefix.slice("gemini/".length);
|
||||
}
|
||||
if (withoutPrefix.startsWith("google/")) {
|
||||
return withoutPrefix.slice("google/".length);
|
||||
}
|
||||
return withoutPrefix;
|
||||
}
|
||||
|
||||
function normalizeGeminiBaseUrl(raw: string): string {
|
||||
const trimmed = raw.replace(/\/+$/, "");
|
||||
const openAiIndex = trimmed.indexOf("/openai");
|
||||
if (openAiIndex > -1) return trimmed.slice(0, openAiIndex);
|
||||
if (openAiIndex > -1) {
|
||||
return trimmed.slice(0, openAiIndex);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -59,7 +71,9 @@ export async function createGeminiEmbeddingProvider(
|
||||
const batchUrl = `${baseUrl}/${client.modelPath}:batchEmbedContents`;
|
||||
|
||||
const embedQuery = async (text: string): Promise<number[]> => {
|
||||
if (!text.trim()) return [];
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
const res = await fetch(embedUrl, {
|
||||
method: "POST",
|
||||
headers: client.headers,
|
||||
@@ -77,7 +91,9 @@ export async function createGeminiEmbeddingProvider(
|
||||
};
|
||||
|
||||
const embedBatch = async (texts: string[]): Promise<number[][]> => {
|
||||
if (texts.length === 0) return [];
|
||||
if (texts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const requests = texts.map((text) => ({
|
||||
model: client.modelPath,
|
||||
content: { parts: [{ text }] },
|
||||
|
||||
@@ -12,8 +12,12 @@ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||
|
||||
export function normalizeOpenAiModel(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) return DEFAULT_OPENAI_EMBEDDING_MODEL;
|
||||
if (trimmed.startsWith("openai/")) return trimmed.slice("openai/".length);
|
||||
if (!trimmed) {
|
||||
return DEFAULT_OPENAI_EMBEDDING_MODEL;
|
||||
}
|
||||
if (trimmed.startsWith("openai/")) {
|
||||
return trimmed.slice("openai/".length);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -24,7 +28,9 @@ export async function createOpenAiEmbeddingProvider(
|
||||
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
|
||||
|
||||
const embed = async (input: string[]): Promise<number[][]> => {
|
||||
if (input.length === 0) return [];
|
||||
if (input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: client.headers,
|
||||
|
||||
@@ -5,7 +5,9 @@ import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
||||
vi.mock("../agents/model-auth.js", () => ({
|
||||
resolveApiKeyForProvider: vi.fn(),
|
||||
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
|
||||
if (auth?.apiKey) return auth.apiKey;
|
||||
if (auth?.apiKey) {
|
||||
return auth.apiKey;
|
||||
}
|
||||
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -47,8 +47,12 @@ const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma
|
||||
|
||||
function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean {
|
||||
const modelPath = options.local?.modelPath?.trim();
|
||||
if (!modelPath) return false;
|
||||
if (/^(hf:|https?:)/i.test(modelPath)) return false;
|
||||
if (!modelPath) {
|
||||
return false;
|
||||
}
|
||||
if (/^(hf:|https?:)/i.test(modelPath)) {
|
||||
return false;
|
||||
}
|
||||
const resolved = resolveUserPath(modelPath);
|
||||
try {
|
||||
return fsSync.statSync(resolved).isFile();
|
||||
@@ -193,12 +197,16 @@ export async function createEmbeddingProvider(
|
||||
}
|
||||
|
||||
function formatError(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
function isNodeLlamaCppMissing(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) return false;
|
||||
if (!(err instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const code = (err as Error & { code?: unknown }).code;
|
||||
if (code === "ERR_MODULE_NOT_FOUND") {
|
||||
return err.message.includes("node-llama-cpp");
|
||||
|
||||
@@ -3,11 +3,15 @@ function normalizeHeaderName(name: string): string {
|
||||
}
|
||||
|
||||
export function fingerprintHeaderNames(headers: Record<string, string> | undefined): string[] {
|
||||
if (!headers) return [];
|
||||
if (!headers) {
|
||||
return [];
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const key of Object.keys(headers)) {
|
||||
const normalized = normalizeHeaderName(key);
|
||||
if (!normalized) continue;
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
out.push(normalized);
|
||||
}
|
||||
out.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
@@ -26,7 +26,9 @@ export function buildFtsQuery(raw: string): string | null {
|
||||
.match(/[A-Za-z0-9_]+/g)
|
||||
?.map((t) => t.trim())
|
||||
.filter(Boolean) ?? [];
|
||||
if (tokens.length === 0) return null;
|
||||
if (tokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const quoted = tokens.map((t) => `"${t.replaceAll('"', "")}"`);
|
||||
return quoted.join(" AND ");
|
||||
}
|
||||
@@ -80,7 +82,9 @@ export function mergeHybridResults(params: {
|
||||
const existing = byId.get(r.id);
|
||||
if (existing) {
|
||||
existing.textScore = r.textScore;
|
||||
if (r.snippet && r.snippet.length > 0) existing.snippet = r.snippet;
|
||||
if (r.snippet && r.snippet.length > 0) {
|
||||
existing.snippet = r.snippet;
|
||||
}
|
||||
} else {
|
||||
byId.set(r.id, {
|
||||
id: r.id,
|
||||
|
||||
@@ -79,7 +79,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await result.manager.sync({ force: true });
|
||||
const results = await result.manager.search("alpha");
|
||||
@@ -130,7 +132,9 @@ describe("memory index", () => {
|
||||
agentId: "main",
|
||||
});
|
||||
expect(first.manager).not.toBeNull();
|
||||
if (!first.manager) throw new Error("manager missing");
|
||||
if (!first.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
await first.manager.sync({ force: true });
|
||||
await first.manager.close();
|
||||
|
||||
@@ -151,7 +155,9 @@ describe("memory index", () => {
|
||||
agentId: "main",
|
||||
});
|
||||
expect(second.manager).not.toBeNull();
|
||||
if (!second.manager) throw new Error("manager missing");
|
||||
if (!second.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = second.manager;
|
||||
await second.manager.sync({ reason: "test" });
|
||||
const results = await second.manager.search("alpha");
|
||||
@@ -177,7 +183,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await manager.sync({ force: true });
|
||||
const afterFirst = embedBatchCalls;
|
||||
@@ -206,7 +214,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
await manager.sync({ force: true });
|
||||
@@ -245,11 +255,15 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
const status = manager.status();
|
||||
if (!status.fts?.available) return;
|
||||
if (!status.fts?.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.sync({ force: true });
|
||||
const results = await manager.search("zebra");
|
||||
@@ -294,11 +308,15 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
const status = manager.status();
|
||||
if (!status.fts?.available) return;
|
||||
if (!status.fts?.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.sync({ force: true });
|
||||
const results = await manager.search("alpha beta id123");
|
||||
@@ -348,11 +366,15 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
const status = manager.status();
|
||||
if (!status.fts?.available) return;
|
||||
if (!status.fts?.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.sync({ force: true });
|
||||
const results = await manager.search("alpha beta id123");
|
||||
@@ -382,7 +404,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
const available = await result.manager.probeVectorAvailability();
|
||||
const status = result.manager.status();
|
||||
@@ -408,7 +432,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await expect(result.manager.readFile({ relPath: "NOTES.md" })).rejects.toThrow("path required");
|
||||
});
|
||||
@@ -435,7 +461,9 @@ describe("memory index", () => {
|
||||
};
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await expect(result.manager.readFile({ relPath: "extra/extra.md" })).resolves.toEqual({
|
||||
path: "extra/extra.md",
|
||||
|
||||
@@ -31,7 +31,9 @@ export function normalizeRelPath(value: string): string {
|
||||
}
|
||||
|
||||
export function normalizeExtraMemoryPaths(workspaceDir: string, extraPaths?: string[]): string[] {
|
||||
if (!extraPaths?.length) return [];
|
||||
if (!extraPaths?.length) {
|
||||
return [];
|
||||
}
|
||||
const resolved = extraPaths
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
@@ -43,8 +45,12 @@ export function normalizeExtraMemoryPaths(workspaceDir: string, extraPaths?: str
|
||||
|
||||
export function isMemoryPath(relPath: string): boolean {
|
||||
const normalized = normalizeRelPath(relPath);
|
||||
if (!normalized) return false;
|
||||
if (normalized === "MEMORY.md" || normalized === "memory.md") return true;
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized === "MEMORY.md" || normalized === "memory.md") {
|
||||
return true;
|
||||
}
|
||||
return normalized.startsWith("memory/");
|
||||
}
|
||||
|
||||
@@ -52,13 +58,19 @@ async function walkDir(dir: string, files: string[]) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isSymbolicLink()) continue;
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
await walkDir(full, files);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.endsWith(".md")) continue;
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith(".md")) {
|
||||
continue;
|
||||
}
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
@@ -75,8 +87,12 @@ export async function listMemoryFiles(
|
||||
const addMarkdownFile = async (absPath: string) => {
|
||||
try {
|
||||
const stat = await fs.lstat(absPath);
|
||||
if (stat.isSymbolicLink() || !stat.isFile()) return;
|
||||
if (!absPath.endsWith(".md")) return;
|
||||
if (stat.isSymbolicLink() || !stat.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (!absPath.endsWith(".md")) {
|
||||
return;
|
||||
}
|
||||
result.push(absPath);
|
||||
} catch {}
|
||||
};
|
||||
@@ -95,7 +111,9 @@ export async function listMemoryFiles(
|
||||
for (const inputPath of normalizedExtraPaths) {
|
||||
try {
|
||||
const stat = await fs.lstat(inputPath);
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (stat.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
await walkDir(inputPath, result);
|
||||
continue;
|
||||
@@ -106,7 +124,9 @@ export async function listMemoryFiles(
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (result.length <= 1) return result;
|
||||
if (result.length <= 1) {
|
||||
return result;
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const deduped: string[] = [];
|
||||
for (const entry of result) {
|
||||
@@ -114,7 +134,9 @@ export async function listMemoryFiles(
|
||||
try {
|
||||
key = await fs.realpath(entry);
|
||||
} catch {}
|
||||
if (seen.has(key)) continue;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
deduped.push(entry);
|
||||
}
|
||||
@@ -146,7 +168,9 @@ export function chunkMarkdown(
|
||||
chunking: { tokens: number; overlap: number },
|
||||
): MemoryChunk[] {
|
||||
const lines = content.split("\n");
|
||||
if (lines.length === 0) return [];
|
||||
if (lines.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const maxChars = Math.max(32, chunking.tokens * 4);
|
||||
const overlapChars = Math.max(0, chunking.overlap * 4);
|
||||
const chunks: MemoryChunk[] = [];
|
||||
@@ -155,10 +179,14 @@ export function chunkMarkdown(
|
||||
let currentChars = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (current.length === 0) return;
|
||||
if (current.length === 0) {
|
||||
return;
|
||||
}
|
||||
const firstEntry = current[0];
|
||||
const lastEntry = current[current.length - 1];
|
||||
if (!firstEntry || !lastEntry) return;
|
||||
if (!firstEntry || !lastEntry) {
|
||||
return;
|
||||
}
|
||||
const text = current.map((entry) => entry.line).join("\n");
|
||||
const startLine = firstEntry.lineNo;
|
||||
const endLine = lastEntry.lineNo;
|
||||
@@ -180,10 +208,14 @@ export function chunkMarkdown(
|
||||
const kept: Array<{ line: string; lineNo: number }> = [];
|
||||
for (let i = current.length - 1; i >= 0; i -= 1) {
|
||||
const entry = current[i];
|
||||
if (!entry) continue;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
acc += entry.line.length + 1;
|
||||
kept.unshift(entry);
|
||||
if (acc >= overlapChars) break;
|
||||
if (acc >= overlapChars) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
current = kept;
|
||||
currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
|
||||
@@ -224,7 +256,9 @@ export function parseEmbedding(raw: string): number[] {
|
||||
}
|
||||
|
||||
export function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length === 0 || b.length === 0) return 0;
|
||||
if (a.length === 0 || b.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const len = Math.min(a.length, b.length);
|
||||
let dot = 0;
|
||||
let normA = 0;
|
||||
@@ -236,6 +270,8 @@ export function cosineSimilarity(a: number[], b: number[]): number {
|
||||
normA += av * av;
|
||||
normB += bv * bv;
|
||||
}
|
||||
if (normA === 0 || normB === 0) return 0;
|
||||
if (normA === 0 || normB === 0) {
|
||||
return 0;
|
||||
}
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ export async function searchVector(params: {
|
||||
sourceFilterVec: { sql: string; params: SearchSource[] };
|
||||
sourceFilterChunks: { sql: string; params: SearchSource[] };
|
||||
}): Promise<SearchRowResult[]> {
|
||||
if (params.queryVec.length === 0 || params.limit <= 0) return [];
|
||||
if (params.queryVec.length === 0 || params.limit <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (await params.ensureVectorReady(params.queryVec.length)) {
|
||||
const rows = params.db
|
||||
.prepare(
|
||||
@@ -143,9 +145,13 @@ export async function searchKeyword(params: {
|
||||
buildFtsQuery: (raw: string) => string | null;
|
||||
bm25RankToScore: (rank: number) => number;
|
||||
}): Promise<Array<SearchRowResult & { textScore: number }>> {
|
||||
if (params.limit <= 0) return [];
|
||||
if (params.limit <= 0) {
|
||||
return [];
|
||||
}
|
||||
const ftsQuery = params.buildFtsQuery(params.query);
|
||||
if (!ftsQuery) return [];
|
||||
if (!ftsQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows = params.db
|
||||
.prepare(
|
||||
|
||||
@@ -67,7 +67,9 @@ describe("memory search async sync", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
const pending = new Promise<void>(() => {});
|
||||
|
||||
@@ -76,7 +76,9 @@ describe("memory manager atomic reindex", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
await manager.sync({ force: true });
|
||||
|
||||
@@ -79,7 +79,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
throw new Error("expected FormData upload");
|
||||
}
|
||||
for (const [key, value] of body.entries()) {
|
||||
if (key !== "file") continue;
|
||||
if (key !== "file") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
uploadedRequests = value
|
||||
.split("\n")
|
||||
@@ -149,13 +151,17 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
const labels: string[] = [];
|
||||
await manager.sync({
|
||||
force: true,
|
||||
progress: (update) => {
|
||||
if (update.label) labels.push(update.label);
|
||||
if (update.label) {
|
||||
labels.push(update.label);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -181,7 +187,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
throw new Error("expected FormData upload");
|
||||
}
|
||||
for (const [key, value] of body.entries()) {
|
||||
if (key !== "file") continue;
|
||||
if (key !== "file") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
uploadedRequests = value
|
||||
.split("\n")
|
||||
@@ -255,7 +263,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await manager.sync({ force: true });
|
||||
|
||||
@@ -279,7 +289,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
throw new Error("expected FormData upload");
|
||||
}
|
||||
for (const [key, value] of body.entries()) {
|
||||
if (key !== "file") continue;
|
||||
if (key !== "file") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
uploadedRequests = value
|
||||
.split("\n")
|
||||
@@ -352,7 +364,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
await manager.sync({ force: true });
|
||||
@@ -388,7 +402,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
throw new Error("expected FormData upload");
|
||||
}
|
||||
for (const [key, value] of body.entries()) {
|
||||
if (key !== "file") continue;
|
||||
if (key !== "file") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
uploadedRequests = value
|
||||
.split("\n")
|
||||
@@ -449,7 +465,9 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
await manager.sync({ force: true });
|
||||
|
||||
@@ -66,7 +66,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await manager.sync({ force: true });
|
||||
|
||||
@@ -100,7 +102,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await manager.sync({ force: true });
|
||||
|
||||
@@ -131,7 +135,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
const updates: Array<{ completed: number; total: number; label?: string }> = [];
|
||||
await manager.sync({
|
||||
@@ -194,7 +200,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
try {
|
||||
await manager.sync({ force: true });
|
||||
@@ -251,7 +259,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
try {
|
||||
await manager.sync({ force: true });
|
||||
@@ -283,7 +293,9 @@ describe("memory embedding batches", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
await manager.sync({ force: true });
|
||||
|
||||
|
||||
@@ -77,7 +77,9 @@ describe("memory manager sync failures", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
const syncSpy = vi.spyOn(manager, "sync");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ describe("memory vector dedupe", () => {
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
if (!result.manager) {
|
||||
throw new Error("manager missing");
|
||||
}
|
||||
manager = result.manager;
|
||||
|
||||
const db = (
|
||||
|
||||
@@ -89,6 +89,8 @@ function ensureColumn(
|
||||
definition: string,
|
||||
): void {
|
||||
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
||||
if (rows.some((row) => row.name === column)) return;
|
||||
if (rows.some((row) => row.name === column)) {
|
||||
return;
|
||||
}
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||
}
|
||||
|
||||
@@ -46,16 +46,26 @@ export function extractSessionText(content: unknown): string | null {
|
||||
const normalized = 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 = 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(" ");
|
||||
}
|
||||
|
||||
@@ -66,7 +76,9 @@ export async function buildSessionEntry(absPath: string): Promise<SessionFileEnt
|
||||
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);
|
||||
@@ -83,10 +95,16 @@ export async function buildSessionEntry(absPath: string): Promise<SessionFileEnt
|
||||
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 = extractSessionText(message.content);
|
||||
if (!text) continue;
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
const label = message.role === "user" ? "User" : "Assistant";
|
||||
collected.push(`${label}: ${text}`);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ export function resolveMemoryVectorState(vector: { enabled: boolean; available?:
|
||||
tone: Tone;
|
||||
state: "ready" | "unavailable" | "disabled" | "unknown";
|
||||
} {
|
||||
if (!vector.enabled) return { tone: "muted", state: "disabled" };
|
||||
if (vector.available === true) return { tone: "ok", state: "ready" };
|
||||
if (vector.available === false) return { tone: "warn", state: "unavailable" };
|
||||
if (!vector.enabled) {
|
||||
return { tone: "muted", state: "disabled" };
|
||||
}
|
||||
if (vector.available === true) {
|
||||
return { tone: "ok", state: "ready" };
|
||||
}
|
||||
if (vector.available === false) {
|
||||
return { tone: "warn", state: "unavailable" };
|
||||
}
|
||||
return { tone: "muted", state: "unknown" };
|
||||
}
|
||||
|
||||
@@ -14,7 +20,9 @@ export function resolveMemoryFtsState(fts: { enabled: boolean; available: boolea
|
||||
tone: Tone;
|
||||
state: "ready" | "unavailable" | "disabled";
|
||||
} {
|
||||
if (!fts.enabled) return { tone: "muted", state: "disabled" };
|
||||
if (!fts.enabled) {
|
||||
return { tone: "muted", state: "disabled" };
|
||||
}
|
||||
return fts.available ? { tone: "ok", state: "ready" } : { tone: "warn", state: "unavailable" };
|
||||
}
|
||||
|
||||
@@ -22,7 +30,9 @@ export function resolveMemoryCacheSummary(cache: { enabled: boolean; entries?: n
|
||||
tone: Tone;
|
||||
text: string;
|
||||
} {
|
||||
if (!cache.enabled) return { tone: "muted", text: "cache off" };
|
||||
if (!cache.enabled) {
|
||||
return { tone: "muted", text: "cache off" };
|
||||
}
|
||||
const suffix = typeof cache.entries === "number" ? ` (${cache.entries})` : "";
|
||||
return { tone: "ok", text: `cache on${suffix}` };
|
||||
}
|
||||
|
||||
@@ -80,7 +80,9 @@ export async function syncMemoryFiles(params: {
|
||||
.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;
|
||||
}
|
||||
params.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory");
|
||||
try {
|
||||
params.db
|
||||
|
||||
@@ -105,7 +105,9 @@ export async function syncSessionFiles(params: {
|
||||
.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;
|
||||
}
|
||||
params.db
|
||||
.prepare(`DELETE FROM files WHERE path = ? AND source = ?`)
|
||||
.run(stale.path, "sessions");
|
||||
|
||||
Reference in New Issue
Block a user