From 4b2099498c836b82881ad2aa064d00030f76f11d Mon Sep 17 00:00:00 2001 From: Storme-bit Date: Sun, 23 Aug 2026 22:41:21 -0700 Subject: [PATCH] consolidation and aging --- packages/memory-service/src/episodic/index.js | 19 +++++++++++++++- packages/memory-service/src/index.js | 22 ++++++++++++++++++- packages/shared/src/config/constants.js | 7 ++++++ packages/shared/src/index.js | 4 +++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/memory-service/src/episodic/index.js b/packages/memory-service/src/episodic/index.js index 187413c..3e7156b 100644 --- a/packages/memory-service/src/episodic/index.js +++ b/packages/memory-service/src/episodic/index.js @@ -1,5 +1,5 @@ const {getDB} = require('../db'); -const { EPISODIC, getEnv, SERVICES, parseRow, formatEpisodeText, SUMMARIES, logger, isTrivialTurn } = require('@nexusai/shared'); +const { EPISODIC, getEnv, SERVICES, parseRow, formatEpisodeText, SUMMARIES, logger, isTrivialTurn, CONSOLIDATION } = require('@nexusai/shared'); const semantic = require('../semantic'); const { extractAndStoreEntities } = require('../entities/extraction') @@ -311,6 +311,22 @@ function getEpisodesByProject(projectId, limit = SUMMARIES.MAX_PROJECT_EPISODE_L `).all(projectId, limit).map(parseRow); } +function getConsolidationCandidates(sessionId, limit = CONSOLIDATION.CANDIDATE_LIMIT) { + const db = getDB(); + const now = Date.now(); + const cutoff = Math.floor(now / 1000) - CONSOLIDATION.MIN_AGE_DAYS *86400 // seconds, so it matches created_at + + return db.prepare(` + SELECT id, access_count, created_at, last_accessed_at, + SUBSTR(user_message, 1, 80) AS preview, + CAST( access_count AS REAL) / (1+(?-COALESCE(last_accessed_at, created_at * 1000)) / 86400000.0) AS aging_score + FROM episodes + WHERE session_id = ? AND created_at < ? + ORDER BY aging_score ASC + LIMIT ? + `).all(now, sessionId, cutoff, limit); +} + module.exports = { createSession, getSession, @@ -331,4 +347,5 @@ module.exports = { touchEpisodes, getEpisodesByProject, buildFtsQuery, + getConsolidationCandidates, }; \ No newline at end of file diff --git a/packages/memory-service/src/index.js b/packages/memory-service/src/index.js index af2e644..545b98d 100644 --- a/packages/memory-service/src/index.js +++ b/packages/memory-service/src/index.js @@ -1,6 +1,6 @@ require ('dotenv').config(); const express = require('express'); -const {getEnv, PORTS, EPISODIC, logger} = require('@nexusai/shared'); +const {getEnv, PORTS, EPISODIC, logger, CONSOLIDATION} = require('@nexusai/shared'); const { getDB } = require('./db'); const { createProject, getProjects, getProject, updateProject, deleteProject } = require('./db/projects'); const { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary } = require('./db/summaries'); @@ -182,6 +182,26 @@ app.get('/sessions/:id/episodes/since/:afterId', (req, res) => { res.json(episodes); }); +app.get('/sessions/:id/consolidation-candidates', (req, res) => { + const sessionId = Number(req.params.id); + const stats = episodic.getSessionEpisodeStats(sessionId); + + if(stats.count < CONSOLIDATION.MIN_SESSION_EPISODES) { + return res.json({ + eligible: false, + reason: `session has ${stats.count} episodes, floor is ${CONSOLIDATION.MIN_SESSION_EPISODES}`, + candidates: [], + }); + } + + const candidates = episodic.getConsolidationCandidates(sessionId); + res.json({ + eligible:true, + sessionEpisodeCount: stats.count, + candidates + }) +}) + app.delete('/episodes/:id', (req, res) => { const id = Number(req.params.id); episodic.deleteEpisode(id); diff --git a/packages/shared/src/config/constants.js b/packages/shared/src/config/constants.js index 4436dcb..0a57f91 100644 --- a/packages/shared/src/config/constants.js +++ b/packages/shared/src/config/constants.js @@ -120,6 +120,12 @@ const UTILITY = { TIMEOUT_MS: 120_000, // extraction previously used 60s; summaries had none — standardized }; +const CONSOLIDATION = { + MIN_AGE_DAYS: 7, // episodes younger than this are never consolidation candidates + MIN_SESSION_EPISODES: 20, // sessions smaller than this are left entirely alone + CANDIDATE_LIMIT: 50, +} + module.exports = { QDRANT, COLLECTIONS, @@ -135,4 +141,5 @@ module.exports = { ENTITIES, RETRIEVAL, UTILITY, + CONSOLIDATION, }; \ No newline at end of file diff --git a/packages/shared/src/index.js b/packages/shared/src/index.js index 1e11098..0422efe 100644 --- a/packages/shared/src/index.js +++ b/packages/shared/src/index.js @@ -13,7 +13,8 @@ const { SUMMARIES, ENTITIES, RETRIEVAL, - UTILITY + UTILITY, + CONSOLIDATION } = require('./config/constants'); const {parseRow, formatEpisodeText, isTrivialTurn} = require('./utils') @@ -40,5 +41,6 @@ module.exports = { logger, RETRIEVAL, UTILITY, + CONSOLIDATION, utilityInference, }; \ No newline at end of file