@shadowforge0/aquifer-memory 1.3.0 → 1.5.8
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/default/index.js +17 -4
- package/consumers/mcp.js +21 -0
- package/consumers/miranda/index.js +15 -4
- package/consumers/miranda/recall-format.js +5 -3
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +2 -1
- package/consumers/shared/llm.js +1 -1
- package/consumers/shared/recall-format.js +21 -1
- package/core/aquifer.js +669 -92
- package/core/entity-state.js +483 -0
- package/core/insights.js +499 -0
- package/core/mcp-manifest.js +1 -1
- package/core/storage.js +82 -5
- package/package.json +1 -1
- package/pipeline/extract-state-changes.js +205 -0
- package/schema/001-base.sql +186 -16
- package/schema/002-entities.sql +35 -1
- package/schema/004-completion.sql +23 -7
- package/schema/005-entity-state-history.sql +87 -0
- package/schema/006-insights.sql +138 -0
- package/scripts/diagnose-fts-zh.js +37 -4
- package/scripts/drop-entity-state-history.sql +17 -0
- package/scripts/drop-insights.sql +12 -0
- package/scripts/extract-insights-from-recent-sessions.js +315 -0
- package/scripts/find-dburl-hints.js +29 -0
- package/scripts/queries.json +45 -0
- package/scripts/retro-recall-bench.js +409 -0
- package/scripts/sample-bench-queries.sql +75 -0
|
@@ -222,26 +222,39 @@ function createPersona(personaOpts = {}) {
|
|
|
222
222
|
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
223
223
|
return {
|
|
224
224
|
name: 'session_recall',
|
|
225
|
-
description: 'Search stored sessions by keyword.',
|
|
225
|
+
description: 'Search stored sessions by keyword or natural language. Use entities when the user names specific people, projects, files, tools, or concepts; use entity_mode="all" when every named entity must co-occur (default "any" boosts). Use mode to force fts/vector/hybrid (default hybrid).',
|
|
226
226
|
parameters: {
|
|
227
227
|
type: 'object',
|
|
228
228
|
properties: {
|
|
229
|
-
query: { type: 'string' },
|
|
229
|
+
query: { type: 'string', minLength: 1, description: 'Non-empty keyword or natural-language query' },
|
|
230
230
|
limit: { type: 'number' },
|
|
231
231
|
agent_id: { type: 'string' },
|
|
232
|
+
source: { type: 'string' },
|
|
232
233
|
date_from: { type: 'string' },
|
|
233
234
|
date_to: { type: 'string' },
|
|
235
|
+
entities: { type: 'array', items: { type: 'string' }, description: 'Named entities (person/project/tool/file)' },
|
|
236
|
+
entity_mode: { type: 'string', enum: ['any', 'all'], description: '"any" boosts; "all" hard-filters to sessions containing every entity' },
|
|
237
|
+
mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall strategy, default hybrid' },
|
|
234
238
|
},
|
|
235
239
|
},
|
|
236
240
|
async execute(_toolCallId, params) {
|
|
237
241
|
try {
|
|
238
242
|
const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
|
|
239
|
-
const
|
|
243
|
+
const recallOpts = {
|
|
240
244
|
agentId: params?.agent_id || ctx?.agentId || undefined,
|
|
245
|
+
source: params?.source || undefined,
|
|
241
246
|
dateFrom: params?.date_from || undefined,
|
|
242
247
|
dateTo: params?.date_to || undefined,
|
|
243
248
|
limit,
|
|
244
|
-
}
|
|
249
|
+
};
|
|
250
|
+
if (Array.isArray(params?.entities) && params.entities.length > 0) {
|
|
251
|
+
recallOpts.entities = params.entities;
|
|
252
|
+
recallOpts.entityMode = params?.entity_mode || 'any';
|
|
253
|
+
}
|
|
254
|
+
if (params?.mode === 'fts' || params?.mode === 'hybrid' || params?.mode === 'vector') {
|
|
255
|
+
recallOpts.mode = params.mode;
|
|
256
|
+
}
|
|
257
|
+
const results = await aquifer.recall(String(params?.query || ''), recallOpts);
|
|
245
258
|
const lines = results.map((r, i) =>
|
|
246
259
|
`${i+1}. ${r.structuredSummary?.title || r.summaryText?.slice(0, 80) || '(untitled)'}`
|
|
247
260
|
);
|
package/consumers/mcp.js
CHANGED
|
@@ -225,6 +225,27 @@ async function main() {
|
|
|
225
225
|
process.on('SIGINT', cleanup);
|
|
226
226
|
process.on('SIGTERM', cleanup);
|
|
227
227
|
|
|
228
|
+
// Startup handshake: instantiate aquifer + drive init() before MCP transport
|
|
229
|
+
// so schema state is resolved before the first tool call. apply-mode failure
|
|
230
|
+
// is fatal (exit non-zero) — an MCP instance with pending DDL would serve
|
|
231
|
+
// tool traffic against a stale schema and surface confusing errors later.
|
|
232
|
+
const aquifer = getAquifer();
|
|
233
|
+
const envelope = await aquifer.init();
|
|
234
|
+
if (!envelope.ready) {
|
|
235
|
+
const err = envelope.error || { code: 'AQ_MIGRATION_NOT_READY', message: 'aquifer.init() did not reach ready state' };
|
|
236
|
+
process.stderr.write(
|
|
237
|
+
`[aquifer-mcp] startup aborted: migrationMode=${envelope.migrationMode} ` +
|
|
238
|
+
`memoryMode=${envelope.memoryMode} pending=${envelope.pendingMigrations.length} ` +
|
|
239
|
+
`error=${err.code || 'unknown'}: ${err.message}\n`
|
|
240
|
+
);
|
|
241
|
+
await aquifer.close().catch(() => {});
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
process.stderr.write(
|
|
245
|
+
`[aquifer-mcp] init ok: mode=${envelope.migrationMode} applied=${envelope.appliedMigrations.length} ` +
|
|
246
|
+
`pending=${envelope.pendingMigrations.length} durationMs=${envelope.durationMs}\n`
|
|
247
|
+
);
|
|
248
|
+
|
|
228
249
|
const transport = new StdioServerTransport();
|
|
229
250
|
await server.connect(transport);
|
|
230
251
|
|
|
@@ -275,13 +275,16 @@ function registerRecallTool(api, opts = {}) {
|
|
|
275
275
|
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
276
276
|
return {
|
|
277
277
|
name: 'session_recall',
|
|
278
|
-
description: '搜尋歷史 session
|
|
278
|
+
description: '搜尋歷史 session 的摘要和對話記錄。當問題明確提到具體人名、專案、工具、檔名時,傳 entities;只想保留全部命中的 session 用 entity_mode="all",否則 "any" 是 boost。mode 可選 "fts"/"vector"/"hybrid",default hybrid。',
|
|
279
279
|
parameters: {
|
|
280
280
|
type: 'object',
|
|
281
281
|
properties: {
|
|
282
|
-
query: { type: 'string', description: '
|
|
282
|
+
query: { type: 'string', description: '搜尋關鍵字或自然語言描述(必填非空)', minLength: 1 },
|
|
283
283
|
date_from: { type: 'string' }, date_to: { type: 'string' },
|
|
284
284
|
agent_id: { type: 'string' }, source: { type: 'string' },
|
|
285
|
+
entities: { type: 'array', items: { type: 'string' }, description: '具名 entity 清單(人/專案/工具/檔名)' },
|
|
286
|
+
entity_mode: { type: 'string', enum: ['any', 'all'], description: '"any" boost / "all" 硬過濾必含全部 entity' },
|
|
287
|
+
mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'recall 模式,default hybrid' },
|
|
285
288
|
detail: { type: 'string' },
|
|
286
289
|
limit: { type: 'number' },
|
|
287
290
|
},
|
|
@@ -289,13 +292,21 @@ function registerRecallTool(api, opts = {}) {
|
|
|
289
292
|
async execute(_toolCallId, params) {
|
|
290
293
|
try {
|
|
291
294
|
const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
|
|
292
|
-
const
|
|
295
|
+
const recallOpts = {
|
|
293
296
|
agentId: params?.agent_id || ctx?.agentId || undefined,
|
|
294
297
|
source: params?.source || undefined,
|
|
295
298
|
dateFrom: params?.date_from || undefined,
|
|
296
299
|
dateTo: params?.date_to || undefined,
|
|
297
300
|
limit,
|
|
298
|
-
}
|
|
301
|
+
};
|
|
302
|
+
if (Array.isArray(params?.entities) && params.entities.length > 0) {
|
|
303
|
+
recallOpts.entities = params.entities;
|
|
304
|
+
recallOpts.entityMode = params?.entity_mode || 'any';
|
|
305
|
+
}
|
|
306
|
+
if (params?.mode === 'fts' || params?.mode === 'hybrid' || params?.mode === 'vector') {
|
|
307
|
+
recallOpts.mode = params.mode;
|
|
308
|
+
}
|
|
309
|
+
const results = await aquifer.recall(String(params?.query || ''), recallOpts);
|
|
299
310
|
const text = mirandaRecallFormat.formatRecallResults(results.map(r => ({
|
|
300
311
|
sessionId: r.sessionId, agentId: r.agentId, source: r.source,
|
|
301
312
|
startedAt: r.startedAt, summaryText: r.summaryText,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Miranda zh-TW recall formatter — overrides the shared default renderers
|
|
4
4
|
// to produce narrative-style output instead of score-flavored markdown.
|
|
5
5
|
|
|
6
|
-
const { createRecallFormatter, truncate, formatDateIso } = require('../shared/recall-format');
|
|
6
|
+
const { createRecallFormatter, truncate, formatDateIso, formatRelativeZhTw } = require('../shared/recall-format');
|
|
7
7
|
|
|
8
8
|
function formatTopicLines(topics) {
|
|
9
9
|
if (!Array.isArray(topics) || topics.length === 0) return '- 無';
|
|
@@ -32,10 +32,12 @@ function coalesceTitle(structuredSummary, summaryText) {
|
|
|
32
32
|
const mirandaRenderers = {
|
|
33
33
|
empty: () => '找不到符合條件的 session。',
|
|
34
34
|
header: () => null,
|
|
35
|
-
title: (r, i) => {
|
|
35
|
+
title: (r, i, ctx) => {
|
|
36
36
|
const ss = r.structuredSummary || {};
|
|
37
37
|
const title = coalesceTitle(ss, r.summaryText);
|
|
38
|
-
const
|
|
38
|
+
const iso = formatDateIso(r.startedAt);
|
|
39
|
+
const rel = formatRelativeZhTw(r.startedAt, ctx?.now);
|
|
40
|
+
const date = rel ? `${rel}(${iso})` : iso;
|
|
39
41
|
const agent = r.agentId || r.agent_id || 'main';
|
|
40
42
|
return `### ${i + 1}. ${title}\n**Agent**: ${agent} | **Date**: ${date}`;
|
|
41
43
|
},
|
|
@@ -42,6 +42,12 @@ const DEFAULTS = {
|
|
|
42
42
|
timeoutMs: 2000,
|
|
43
43
|
maxRetries: 1,
|
|
44
44
|
},
|
|
45
|
+
migrations: {
|
|
46
|
+
mode: 'apply', // 'apply' | 'check' | 'off'
|
|
47
|
+
lockTimeoutMs: 30000,
|
|
48
|
+
startupTimeoutMs: 60000,
|
|
49
|
+
onEvent: null, // (event) => void, optional observability hook
|
|
50
|
+
},
|
|
45
51
|
};
|
|
46
52
|
|
|
47
53
|
// ---------------------------------------------------------------------------
|
|
@@ -77,6 +83,8 @@ const ENV_MAP = [
|
|
|
77
83
|
['AQUIFER_RERANK_TOP_K', 'rerank.topK', Number],
|
|
78
84
|
['AQUIFER_RERANK_MAX_CHARS', 'rerank.maxChars', Number],
|
|
79
85
|
['AQUIFER_RERANK_TIMEOUT_MS','rerank.timeoutMs', Number],
|
|
86
|
+
['AQUIFER_MIGRATIONS_MODE', 'migrations.mode'],
|
|
87
|
+
['AQUIFER_MIGRATION_LOCK_TIMEOUT_MS', 'migrations.lockTimeoutMs', Number],
|
|
80
88
|
];
|
|
81
89
|
|
|
82
90
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Pool } = require('pg');
|
|
4
|
-
const { createAquifer, createEmbedder
|
|
4
|
+
const { createAquifer, createEmbedder } = require('../../index');
|
|
5
5
|
const { loadConfig } = require('./config');
|
|
6
6
|
const { createLlmFn } = require('./llm');
|
|
7
7
|
|
|
@@ -90,6 +90,7 @@ function createAquiferFromConfig(overrides) {
|
|
|
90
90
|
entities: config.entities,
|
|
91
91
|
rank: config.rank,
|
|
92
92
|
rerank: rerankOpts,
|
|
93
|
+
migrations: config.migrations,
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
return aquifer;
|
package/consumers/shared/llm.js
CHANGED
|
@@ -18,6 +18,25 @@ function formatDateIso(value) {
|
|
|
18
18
|
return Number.isNaN(d.getTime()) ? 'unknown' : d.toISOString().slice(0, 10);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Humanize a past timestamp into zh-TW relative form (e.g. "3 天前", "昨天").
|
|
22
|
+
// Bucketed on raw ms-diff — good enough for model intuition, not calendar-precise.
|
|
23
|
+
// Returns null for invalid / future timestamps so callers can fall back.
|
|
24
|
+
function formatRelativeZhTw(value, now) {
|
|
25
|
+
if (!value) return null;
|
|
26
|
+
const t = new Date(value).getTime();
|
|
27
|
+
if (Number.isNaN(t)) return null;
|
|
28
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
29
|
+
const diffMs = nowMs - t;
|
|
30
|
+
if (diffMs < 0) return null;
|
|
31
|
+
const day = 86400000;
|
|
32
|
+
if (diffMs < day) return '今天';
|
|
33
|
+
if (diffMs < 2 * day) return '昨天';
|
|
34
|
+
if (diffMs < 7 * day) return `${Math.floor(diffMs / day)} 天前`;
|
|
35
|
+
if (diffMs < 30 * day) return `${Math.floor(diffMs / (7 * day))} 週前`;
|
|
36
|
+
if (diffMs < 365 * day) return `${Math.floor(diffMs / (30 * day))} 個月前`;
|
|
37
|
+
return `${Math.floor(diffMs / (365 * day))} 年前`;
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
// Default English renderers --------------------------------------------------
|
|
22
41
|
|
|
23
42
|
const defaultRenderers = {
|
|
@@ -63,7 +82,7 @@ function createRecallFormatter(overrides = {}) {
|
|
|
63
82
|
|
|
64
83
|
return function format(results, opts = {}) {
|
|
65
84
|
const safeResults = Array.isArray(results) ? results : [];
|
|
66
|
-
const ctx = { query: opts.query || null, results: safeResults };
|
|
85
|
+
const ctx = { query: opts.query || null, results: safeResults, now: opts.now };
|
|
67
86
|
|
|
68
87
|
if (safeResults.length === 0) {
|
|
69
88
|
return r.empty(ctx);
|
|
@@ -106,5 +125,6 @@ module.exports = {
|
|
|
106
125
|
formatRecallResults,
|
|
107
126
|
truncate,
|
|
108
127
|
formatDateIso,
|
|
128
|
+
formatRelativeZhTw,
|
|
109
129
|
defaultRenderers,
|
|
110
130
|
};
|