@shadowforge0/aquifer-memory 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/consumers/cli.js +40 -10
- package/consumers/mcp.js +8 -4
- package/consumers/openclaw-plugin.js +7 -3
- package/core/aquifer.js +33 -5
- package/core/hybrid-rank.js +7 -5
- package/package.json +1 -1
package/consumers/cli.js
CHANGED
|
@@ -16,6 +16,26 @@
|
|
|
16
16
|
|
|
17
17
|
const { createAquiferFromConfig } = require('./shared/factory');
|
|
18
18
|
|
|
19
|
+
function formatDate(value, fallback) {
|
|
20
|
+
if (!value) return fallback;
|
|
21
|
+
const parsed = new Date(value);
|
|
22
|
+
return isNaN(parsed.getTime()) ? fallback : parsed.toISOString().slice(0, 10);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function quoteIdentifier(identifier) {
|
|
26
|
+
if (!/^[a-zA-Z_]\w{0,62}$/.test(identifier)) {
|
|
27
|
+
throw new Error(`Invalid schema name: "${identifier}"`);
|
|
28
|
+
}
|
|
29
|
+
return `"${identifier}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parsePositiveInt(value, fallback) {
|
|
33
|
+
if (value === undefined || value === null || value === true) return fallback;
|
|
34
|
+
const parsed = parseInt(value, 10);
|
|
35
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
36
|
+
return Math.max(1, parsed);
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
// ---------------------------------------------------------------------------
|
|
20
40
|
// Argument parser (minimal, no deps)
|
|
21
41
|
// ---------------------------------------------------------------------------
|
|
@@ -57,7 +77,7 @@ async function cmdRecall(aquifer, args) {
|
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
const recallOpts = {
|
|
60
|
-
limit:
|
|
80
|
+
limit: parsePositiveInt(args.flags.limit, 5),
|
|
61
81
|
agentId: args.flags['agent-id'] || undefined,
|
|
62
82
|
source: args.flags.source || undefined,
|
|
63
83
|
dateFrom: args.flags['date-from'] || undefined,
|
|
@@ -83,7 +103,7 @@ async function cmdRecall(aquifer, args) {
|
|
|
83
103
|
const r = results[i];
|
|
84
104
|
const ss = r.structuredSummary || {};
|
|
85
105
|
const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
|
|
86
|
-
const date =
|
|
106
|
+
const date = formatDate(r.startedAt, '?');
|
|
87
107
|
console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
|
|
88
108
|
if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
|
|
89
109
|
if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
|
|
@@ -114,7 +134,7 @@ async function cmdFeedback(aquifer, args) {
|
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
async function cmdBackfill(aquifer, args) {
|
|
117
|
-
const limit =
|
|
137
|
+
const limit = parsePositiveInt(args.flags.limit, 100);
|
|
118
138
|
const dryRun = !!args.flags['dry-run'];
|
|
119
139
|
const skipSummary = !!args.flags['skip-summary'];
|
|
120
140
|
const skipTurnEmbed = !!args.flags['skip-turn-embed'];
|
|
@@ -160,7 +180,7 @@ async function cmdStats(aquifer, args) {
|
|
|
160
180
|
console.log(`Summaries: ${stats.summaries}`);
|
|
161
181
|
console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
|
|
162
182
|
console.log(`Entities: ${stats.entities}`);
|
|
163
|
-
if (stats.earliest) console.log(`Range: ${
|
|
183
|
+
if (stats.earliest) console.log(`Range: ${formatDate(stats.earliest, '?')} — ${formatDate(stats.latest, '?')}`);
|
|
164
184
|
}
|
|
165
185
|
}
|
|
166
186
|
|
|
@@ -211,11 +231,21 @@ async function cmdQuickstart(aquifer) {
|
|
|
211
231
|
const { loadConfig } = require('./shared/config');
|
|
212
232
|
const config = loadConfig();
|
|
213
233
|
const pool = new Pool({ connectionString: config.db.url });
|
|
214
|
-
const schema = config.schema || 'aquifer';
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
234
|
+
const schema = quoteIdentifier(config.schema || 'aquifer');
|
|
235
|
+
const tenantId = config.tenantId || 'default';
|
|
236
|
+
try {
|
|
237
|
+
await pool.query('BEGIN');
|
|
238
|
+
await pool.query(
|
|
239
|
+
`DELETE FROM ${schema}.sessions WHERE tenant_id = $1 AND agent_id = $2 AND session_id = $3`,
|
|
240
|
+
[tenantId, 'quickstart', sessionId]
|
|
241
|
+
);
|
|
242
|
+
await pool.query('COMMIT');
|
|
243
|
+
} catch (err) {
|
|
244
|
+
await pool.query('ROLLBACK').catch(() => {});
|
|
245
|
+
throw err;
|
|
246
|
+
} finally {
|
|
247
|
+
await pool.end();
|
|
248
|
+
}
|
|
219
249
|
console.log(' OK\n');
|
|
220
250
|
|
|
221
251
|
console.log('✓ Aquifer is working. You can now start the MCP server:');
|
|
@@ -224,7 +254,7 @@ async function cmdQuickstart(aquifer) {
|
|
|
224
254
|
|
|
225
255
|
async function cmdExport(aquifer, args) {
|
|
226
256
|
const output = args.flags.output || null;
|
|
227
|
-
const limit =
|
|
257
|
+
const limit = parsePositiveInt(args.flags.limit, 1000);
|
|
228
258
|
|
|
229
259
|
const rows = await aquifer.exportSessions({
|
|
230
260
|
agentId: args.flags['agent-id'],
|
package/consumers/mcp.js
CHANGED
|
@@ -38,9 +38,11 @@ function formatResults(results, query) {
|
|
|
38
38
|
const r = results[i];
|
|
39
39
|
const ss = r.structuredSummary || {};
|
|
40
40
|
const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
let date = 'unknown';
|
|
42
|
+
if (r.startedAt) {
|
|
43
|
+
const parsed = new Date(r.startedAt);
|
|
44
|
+
if (!isNaN(parsed.getTime())) date = parsed.toISOString().slice(0, 10);
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
lines.push(`### ${i + 1}. ${title} (${date}, ${r.agentId || 'default'})`);
|
|
46
48
|
if (ss.overview || r.summaryText) {
|
|
@@ -65,9 +67,11 @@ async function main() {
|
|
|
65
67
|
({ StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'));
|
|
66
68
|
({ z } = require('zod'));
|
|
67
69
|
} catch (e) {
|
|
70
|
+
const missingDep = e && (e.code === 'MODULE_NOT_FOUND' || /Cannot find module|^missing\b/i.test(e.message || ''));
|
|
71
|
+
if (!missingDep) throw e;
|
|
68
72
|
process.stderr.write(
|
|
69
73
|
'aquifer mcp requires @modelcontextprotocol/sdk and zod.\n' +
|
|
70
|
-
'
|
|
74
|
+
'Install: npm install @modelcontextprotocol/sdk zod\n'
|
|
71
75
|
);
|
|
72
76
|
process.exit(1);
|
|
73
77
|
}
|
|
@@ -79,15 +79,19 @@ function normalizeEntries(rawEntries) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function formatDate(value) {
|
|
83
|
+
if (!value) return 'unknown';
|
|
84
|
+
const parsed = new Date(value);
|
|
85
|
+
return isNaN(parsed.getTime()) ? 'unknown' : parsed.toISOString().slice(0, 10);
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
function formatRecallResults(results) {
|
|
83
89
|
if (results.length === 0) return 'No matching sessions found.';
|
|
84
90
|
|
|
85
91
|
return results.map((r, i) => {
|
|
86
92
|
const ss = r.structuredSummary || {};
|
|
87
93
|
const title = ss.title || r.summaryText?.slice(0, 60) || '(untitled)';
|
|
88
|
-
const date = r.startedAt
|
|
89
|
-
? new Date(r.startedAt).toISOString().slice(0, 10)
|
|
90
|
-
: 'unknown';
|
|
94
|
+
const date = formatDate(r.startedAt);
|
|
91
95
|
|
|
92
96
|
const lines = [`### ${i + 1}. ${title} (${date}, ${r.agentId || 'default'})`];
|
|
93
97
|
if (ss.overview || r.summaryText) {
|
package/core/aquifer.js
CHANGED
|
@@ -583,7 +583,22 @@ function createAquifer(config) {
|
|
|
583
583
|
weights: overrideWeights,
|
|
584
584
|
entities: explicitEntities,
|
|
585
585
|
entityMode = 'any',
|
|
586
|
+
strictSearchErrors = false,
|
|
586
587
|
} = opts;
|
|
588
|
+
const searchErrors = [];
|
|
589
|
+
|
|
590
|
+
function recordSearchError(pathName, err) {
|
|
591
|
+
searchErrors.push({
|
|
592
|
+
path: pathName,
|
|
593
|
+
message: err && err.message ? err.message : String(err),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function maybeThrowSearchErrors() {
|
|
598
|
+
if (!strictSearchErrors || searchErrors.length === 0) return;
|
|
599
|
+
const details = searchErrors.map(e => `${e.path}: ${e.message}`).join('; ');
|
|
600
|
+
throw new Error(`Recall search failed: ${details}`);
|
|
601
|
+
}
|
|
587
602
|
|
|
588
603
|
// Normalize agentId/agentIds into a single resolved value
|
|
589
604
|
// agentIds takes precedence; agentId is sugar for agentIds: [agentId]
|
|
@@ -692,17 +707,26 @@ function createAquifer(config) {
|
|
|
692
707
|
runFts
|
|
693
708
|
? storage.searchSessions(pool, query, {
|
|
694
709
|
schema, tenantId, agentIds: resolvedAgentIds, source, dateFrom, dateTo, limit: fetchLimit, ftsConfig,
|
|
695
|
-
}).catch(() =>
|
|
710
|
+
}).catch((err) => {
|
|
711
|
+
recordSearchError('fts', err);
|
|
712
|
+
return [];
|
|
713
|
+
})
|
|
696
714
|
: Promise.resolve([]),
|
|
697
715
|
runVector
|
|
698
716
|
? embeddingSearchSummaries(queryVec, {
|
|
699
717
|
agentIds: resolvedAgentIds, source, dateFrom, dateTo, limit: fetchLimit,
|
|
700
|
-
}).catch(() =>
|
|
718
|
+
}).catch((err) => {
|
|
719
|
+
recordSearchError('summary-vector', err);
|
|
720
|
+
return [];
|
|
721
|
+
})
|
|
701
722
|
: Promise.resolve([]),
|
|
702
723
|
runVector
|
|
703
724
|
? storage.searchTurnEmbeddings(pool, {
|
|
704
725
|
schema, tenantId, queryVec, dateFrom, dateTo, agentIds: resolvedAgentIds, source, limit: fetchLimit,
|
|
705
|
-
}).catch(() =>
|
|
726
|
+
}).catch((err) => {
|
|
727
|
+
recordSearchError('turn-vector', err);
|
|
728
|
+
return { rows: [] };
|
|
729
|
+
})
|
|
706
730
|
: Promise.resolve({ rows: [] }),
|
|
707
731
|
]);
|
|
708
732
|
|
|
@@ -718,6 +742,7 @@ function createAquifer(config) {
|
|
|
718
742
|
const filteredTurn = filterFn(turnRows);
|
|
719
743
|
|
|
720
744
|
if (filteredFts.length === 0 && filteredEmb.length === 0 && filteredTurn.length === 0) {
|
|
745
|
+
maybeThrowSearchErrors();
|
|
721
746
|
return [];
|
|
722
747
|
}
|
|
723
748
|
|
|
@@ -737,7 +762,7 @@ function createAquifer(config) {
|
|
|
737
762
|
const EXTERNAL_TIMEOUT = 10000;
|
|
738
763
|
const externalRows = [];
|
|
739
764
|
const externalPromises = [];
|
|
740
|
-
for (const [, sourceConfig] of sources) {
|
|
765
|
+
for (const [name, sourceConfig] of sources) {
|
|
741
766
|
if (typeof sourceConfig.search === 'function') {
|
|
742
767
|
const w = sourceConfig.weight !== null && sourceConfig.weight !== undefined ? sourceConfig.weight : 1.0;
|
|
743
768
|
externalPromises.push(
|
|
@@ -750,7 +775,9 @@ function createAquifer(config) {
|
|
|
750
775
|
if (r && r.session_id) externalRows.push({ ...r, _externalWeight: w });
|
|
751
776
|
}
|
|
752
777
|
}
|
|
753
|
-
}).catch(() => {
|
|
778
|
+
}).catch((err) => {
|
|
779
|
+
recordSearchError(`external:${name}`, err);
|
|
780
|
+
})
|
|
754
781
|
);
|
|
755
782
|
}
|
|
756
783
|
}
|
|
@@ -835,6 +862,7 @@ function createAquifer(config) {
|
|
|
835
862
|
hybridScore: r._hybridScore ?? r._score,
|
|
836
863
|
rerankScore: r._rerankScore ?? null,
|
|
837
864
|
rerankFallback: r._rerankFallback || false,
|
|
865
|
+
searchErrors: searchErrors.slice(),
|
|
838
866
|
},
|
|
839
867
|
}));
|
|
840
868
|
},
|
package/core/hybrid-rank.js
CHANGED
|
@@ -36,12 +36,12 @@ function rrfFusion(ftsResults = [], embResults = [], turnResults = [], K = 60) {
|
|
|
36
36
|
// timeDecay — sigmoid decay based on age in days
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
-
function timeDecay(startedAt, midpointDays = 45, steepness = 0.05) {
|
|
39
|
+
function timeDecay(startedAt, midpointDays = 45, steepness = 0.05, nowMs = Date.now()) {
|
|
40
40
|
if (!startedAt) return 0.5;
|
|
41
41
|
const dt = typeof startedAt === 'string' ? new Date(startedAt) : startedAt;
|
|
42
42
|
if (isNaN(dt.getTime())) return 0.5;
|
|
43
43
|
|
|
44
|
-
const ageDays = (
|
|
44
|
+
const ageDays = (nowMs - dt.getTime()) / (1000 * 60 * 60 * 24);
|
|
45
45
|
return 1 / (1 + Math.exp(steepness * (ageDays - midpointDays)));
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -49,14 +49,14 @@ function timeDecay(startedAt, midpointDays = 45, steepness = 0.05) {
|
|
|
49
49
|
// accessScore — exponential decay on access recency (30-day half-life)
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
|
-
function accessScore(accessCount, lastAccessedAt) {
|
|
52
|
+
function accessScore(accessCount, lastAccessedAt, nowMs = Date.now()) {
|
|
53
53
|
if (!accessCount || accessCount <= 0) return 0;
|
|
54
54
|
if (!lastAccessedAt) return 0;
|
|
55
55
|
|
|
56
56
|
const dt = typeof lastAccessedAt === 'string' ? new Date(lastAccessedAt) : lastAccessedAt;
|
|
57
57
|
if (isNaN(dt.getTime())) return 0;
|
|
58
58
|
|
|
59
|
-
const daysSince = (
|
|
59
|
+
const daysSince = (nowMs - dt.getTime()) / (1000 * 60 * 60 * 24);
|
|
60
60
|
return accessCount * Math.exp(-0.693 * daysSince / 30);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -89,6 +89,7 @@ function hybridRank(ftsResults, embResults, turnResults, opts = {}) {
|
|
|
89
89
|
} = opts;
|
|
90
90
|
|
|
91
91
|
const w = { ...DEFAULT_WEIGHTS, ...weights };
|
|
92
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
92
93
|
|
|
93
94
|
// Build allResults map: session_id → result object
|
|
94
95
|
const allResults = new Map();
|
|
@@ -140,11 +141,12 @@ function hybridRank(ftsResults, embResults, turnResults, opts = {}) {
|
|
|
140
141
|
const rawRrf = rrfScores.get(sessionId) || 0;
|
|
141
142
|
const normRrf = maxRrf > 0 ? rawRrf / maxRrf : 0;
|
|
142
143
|
|
|
143
|
-
const td = timeDecay(result.started_at);
|
|
144
|
+
const td = timeDecay(result.started_at, 45, 0.05, nowMs);
|
|
144
145
|
|
|
145
146
|
const accessEff = accessScore(
|
|
146
147
|
result.access_count || 0,
|
|
147
148
|
result.last_accessed_at,
|
|
149
|
+
nowMs,
|
|
148
150
|
);
|
|
149
151
|
const as = 1 - Math.exp(-accessEff / 5);
|
|
150
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. MCP server, CLI, and library API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|