Merge branch 'main' into perf/async-memory-save-18240015948353785856

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-18 09:48:42 -05:00
commit 0dfbf3f86f
46 files changed
+1755 -633

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import { ensureDir } from '../../utils.mjs';
const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json');
const debug = util.debuglog('torch-memory');
+21 -15
View File
@@ -67,22 +67,29 @@ function evaluateCandidate(memory, policy) {
* @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options
*/
function groupNearDuplicates(memories, options) {
// Sort by created_at to enable early exit in the inner loop
const sortedMemories = [...memories].sort((a, b) => a.created_at - b.created_at);
const groups = [];
const consumed = new Set();
for (let i = 0; i < memories.length; i += 1) {
const base = memories[i];
for (let i = 0; i < sortedMemories.length; i += 1) {
const base = sortedMemories[i];
if (consumed.has(base.id) || base.merged_into) continue;
const baseEmbedding = options.getEmbedding?.(base);
const group = [base];
for (let j = i + 1; j < memories.length; j += 1) {
const probe = memories[j];
for (let j = i + 1; j < sortedMemories.length; j += 1) {
const probe = sortedMemories[j];
// Optimization: Since sorted by created_at, if the time difference exceeds the window,
// all subsequent items will also be outside the window.
if (probe.created_at - base.created_at > options.duplicateWindowMs) break;
if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue;
const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs;
if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
if (!hasTagOverlap(base.tags, probe.tags)) continue;
const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe));
if (similarity >= options.similarityThreshold) {
@@ -189,12 +196,13 @@ export async function createLifecyclePlan(memories, options) {
});
const templates = loadMemoryPromptTemplates({ templateDir: options.templateDir });
const condensedGroups = [];
const condensedGroups = await Promise.all(
groups.map((group) => condenseGroup(group, options.generateSummary, templates.condense))
);
const mergedIntoMap = new Map();
for (const group of groups) {
const condensed = await condenseGroup(group, options.generateSummary, templates.condense);
condensedGroups.push(condensed);
for (const condensed of condensedGroups) {
for (const mergedId of condensed.mergedIds) {
mergedIntoMap.set(mergedId, condensed.merged.id);
}
@@ -265,9 +273,7 @@ export async function createLifecyclePlan(memories, options) {
* @param {Awaited<ReturnType<typeof createLifecyclePlan>>} plan
*/
export async function applyLifecycleActions(repository, plan) {
const results = [];
for (const decision of plan.actions) {
const results = await Promise.all(plan.actions.map(async (decision) => {
if (decision.merged_into && typeof repository.markMerged === 'function') {
await repository.markMerged(decision.id, decision.merged_into);
}
@@ -280,8 +286,8 @@ export async function applyLifecycleActions(repository, plan) {
await repository.keepMemory?.(decision.id, decision.reason);
}
results.push(decision);
}
return decision;
}));
return {
applied: results,
+15 -9
View File
@@ -31,17 +31,20 @@ function computeRecencyDecay(timestamp, now, halfLifeMs) {
return Math.exp(-(age / halfLifeMs));
}
function textMatchSimilarity(query, memory) {
const normalizedQuery = String(query ?? '').trim().toLowerCase();
if (!normalizedQuery) return 0;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const haystack = `${memory.summary} ${memory.content}`.toLowerCase();
if (haystack.includes(normalizedQuery)) return 1;
function textMatchSimilarity(queryTerms, queryRegexes, memory) {
if (!queryTerms || queryTerms.length === 0) return 0;
const queryTerms = normalizedQuery.split(/\s+/).filter(Boolean);
if (queryTerms.length === 0) return 0;
let matchedTerms = 0;
for (const regex of queryRegexes) {
if (regex.test(memory.summary) || regex.test(memory.content)) {
matchedTerms++;
}
}
const matchedTerms = queryTerms.filter((term) => haystack.includes(term)).length;
return matchedTerms / queryTerms.length;
}
@@ -102,8 +105,11 @@ async function buildSimilarityIndex(memories, params) {
}
}
const queryTerms = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean);
const queryRegexes = queryTerms.map((t) => new RegExp(escapeRegExp(t), 'i'));
for (const memory of memories) {
const lexicalScore = textMatchSimilarity(normalizedQuery, memory);
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
if (lexicalScore > 0) {
similarityById.set(memory.id, lexicalScore);
}