@shadowforge0/aquifer-memory 1.5.9 → 1.6.0
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/.env.example +23 -0
- package/README.md +96 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +374 -39
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +131 -7
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +44 -4
- package/consumers/shared/config.js +28 -0
- package/consumers/shared/factory.js +2 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +53 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +384 -18
- package/core/finalization-review.js +319 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +69 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +456 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +31 -9
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -119
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// CREATE TABLE jenny.daily_entries (LIKE miranda.daily_entries INCLUDING ALL);
|
|
10
10
|
|
|
11
11
|
const crypto = require('crypto');
|
|
12
|
-
const { parseHandoffSection } = require('../
|
|
12
|
+
const { parseHandoffSection } = require('../shared/summary-parser');
|
|
13
13
|
|
|
14
14
|
const UPSERT_TAGS = new Set(['[FOCUS]', '[TODO]', '[STATS]', '[HIGHLIGHT]', '[SYSTEM]', '[HANDOFF]']);
|
|
15
15
|
|
|
@@ -27,10 +27,27 @@ function textHash6(text) {
|
|
|
27
27
|
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 6);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function quoteIdentifier(identifier) {
|
|
31
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(identifier)) {
|
|
32
|
+
throw new Error(`Invalid dailyTable identifier: ${identifier}`);
|
|
33
|
+
}
|
|
34
|
+
return `"${identifier}"`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function quoteTableName(tableName) {
|
|
38
|
+
if (typeof tableName !== 'string') throw new Error('dailyTable must be a string');
|
|
39
|
+
const parts = tableName.split('.');
|
|
40
|
+
if (parts.length < 1 || parts.length > 2 || parts.some(part => !part)) {
|
|
41
|
+
throw new Error(`Invalid dailyTable name: ${tableName}`);
|
|
42
|
+
}
|
|
43
|
+
return parts.map(quoteIdentifier).join('.');
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
async function insertDailyEntry(pool, tableName, { eventAt, source, tag, text, agentId, sessionId, metadata, dedupeKey }) {
|
|
47
|
+
const tableSql = quoteTableName(tableName);
|
|
31
48
|
const shouldUpsert = dedupeKey && UPSERT_TAGS.has(tag);
|
|
32
49
|
const sql = shouldUpsert
|
|
33
|
-
? `INSERT INTO ${
|
|
50
|
+
? `INSERT INTO ${tableSql}
|
|
34
51
|
(event_at, source, tag, text, agent_id, session_id, metadata, dedupe_key)
|
|
35
52
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
36
53
|
ON CONFLICT (dedupe_key) DO UPDATE SET
|
|
@@ -38,7 +55,7 @@ async function insertDailyEntry(pool, tableName, { eventAt, source, tag, text, a
|
|
|
38
55
|
event_at = EXCLUDED.event_at,
|
|
39
56
|
metadata = EXCLUDED.metadata
|
|
40
57
|
RETURNING id, event_at, source, tag, text`
|
|
41
|
-
: `INSERT INTO ${
|
|
58
|
+
: `INSERT INTO ${tableSql}
|
|
42
59
|
(event_at, source, tag, text, agent_id, session_id, metadata, dedupe_key)
|
|
43
60
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
44
61
|
ON CONFLICT (dedupe_key) DO NOTHING
|
|
@@ -53,8 +70,9 @@ async function insertDailyEntry(pool, tableName, { eventAt, source, tag, text, a
|
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
async function getDailyEntries(pool, tableName, date, agentId) {
|
|
73
|
+
const tableSql = quoteTableName(tableName);
|
|
56
74
|
const result = await pool.query(
|
|
57
|
-
`SELECT * FROM ${
|
|
75
|
+
`SELECT * FROM ${tableSql}
|
|
58
76
|
WHERE (event_at AT TIME ZONE 'Asia/Taipei')::date = $1
|
|
59
77
|
AND ($2::text IS NULL OR agent_id = $2)
|
|
60
78
|
ORDER BY event_at ASC`,
|
|
@@ -191,6 +209,7 @@ async function writeDailyEntries({
|
|
|
191
209
|
|
|
192
210
|
module.exports = {
|
|
193
211
|
taipeiDateString, textHash6,
|
|
212
|
+
quoteIdentifier, quoteTableName,
|
|
194
213
|
insertDailyEntry, getDailyEntries, fetchDailyContext, writeDailyEntries,
|
|
195
214
|
UPSERT_TAGS,
|
|
196
215
|
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// briefingIntro: '你是 Dobby。以下是現況...', // optional context-inject preamble
|
|
16
16
|
// });
|
|
17
17
|
//
|
|
18
|
-
// Returns a persona module with the
|
|
18
|
+
// Returns a persona module with the standard persona adapter shape — host
|
|
19
19
|
// can do `AQUIFER_PERSONA=<host-path>` where <host-path>/index.js does:
|
|
20
20
|
// module.exports = require('@shadowforge0/aquifer-memory/consumers/default')
|
|
21
21
|
// .createPersona({ ... });
|
|
@@ -222,7 +222,7 @@ function createPersona(personaOpts = {}) {
|
|
|
222
222
|
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
223
223
|
return {
|
|
224
224
|
name: 'session_recall',
|
|
225
|
-
description: 'Search
|
|
225
|
+
description: 'Search Aquifer memory by keyword or natural language. In curated serving mode this searches active curated memory; legacy/evidence lookup is exposed by the MCP stdio evidence_recall tool. 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: {
|
|
@@ -139,15 +139,15 @@ TODO_DONE: 已完成待辦(需匹配既有)
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// parseSummaryOutput / parseRecapLines —
|
|
143
|
-
// The output format is intentionally
|
|
142
|
+
// parseSummaryOutput / parseRecapLines — shared parser used by all personas.
|
|
143
|
+
// The output format is intentionally stable so downstream daily entries work.
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
145
145
|
|
|
146
|
-
const
|
|
146
|
+
const summaryParser = require('../../shared/summary-parser');
|
|
147
147
|
|
|
148
148
|
module.exports = {
|
|
149
149
|
buildSummaryPrompt,
|
|
150
|
-
parseSummaryOutput:
|
|
151
|
-
parseRecapLines:
|
|
152
|
-
parseWorkingFacts:
|
|
150
|
+
parseSummaryOutput: summaryParser.parseSummaryOutput,
|
|
151
|
+
parseRecapLines: summaryParser.parseRecapLines,
|
|
152
|
+
parseWorkingFacts: summaryParser.parseWorkingFacts,
|
|
153
153
|
};
|
package/consumers/mcp.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This is the primary integration surface for Aquifer. Agent hosts (Claude Code,
|
|
8
8
|
* Codex, OpenCode, etc.) should integrate through this MCP server.
|
|
9
9
|
*
|
|
10
|
-
* Tools: session_recall, session_feedback,
|
|
10
|
+
* Tools: session_recall, evidence_recall, session_feedback, memory_feedback,
|
|
11
|
+
* feedback_stats, session_bootstrap, memory_stats, memory_pending
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
13
14
|
* npx aquifer mcp
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
const { createAquiferFromConfig } = require('./shared/factory');
|
|
22
|
+
const { version: packageVersion } = require('../package.json');
|
|
21
23
|
|
|
22
24
|
let _aquifer = null;
|
|
23
25
|
|
|
@@ -32,8 +34,8 @@ function getAquifer() {
|
|
|
32
34
|
|
|
33
35
|
const { formatRecallResults } = require('./shared/recall-format');
|
|
34
36
|
|
|
35
|
-
function formatResults(results, query) {
|
|
36
|
-
return formatRecallResults(results, { query, showScore: true });
|
|
37
|
+
function formatResults(results, query, explain) {
|
|
38
|
+
return formatRecallResults(results, { query, showScore: true, showExplain: !!explain });
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// ---------------------------------------------------------------------------
|
|
@@ -58,12 +60,12 @@ async function main() {
|
|
|
58
60
|
|
|
59
61
|
const server = new McpServer({
|
|
60
62
|
name: 'aquifer-memory',
|
|
61
|
-
version:
|
|
63
|
+
version: packageVersion,
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
server.tool(
|
|
65
67
|
'session_recall',
|
|
66
|
-
'Search
|
|
68
|
+
'Search Aquifer memory. In curated serving mode this searches active curated memory only; use evidence_recall for legacy session/evidence lookup.',
|
|
67
69
|
{
|
|
68
70
|
query: z.string().min(1).describe('Search query (keyword or natural language)'),
|
|
69
71
|
limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
|
|
@@ -74,6 +76,9 @@ async function main() {
|
|
|
74
76
|
entities: z.array(z.string()).optional().describe('Entity names to match'),
|
|
75
77
|
entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
|
|
76
78
|
mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
|
|
79
|
+
explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
|
|
80
|
+
activeScopeKey: z.string().optional().describe('Active curated memory scope key'),
|
|
81
|
+
activeScopePath: z.array(z.string()).optional().describe('Ordered curated scope path'),
|
|
77
82
|
},
|
|
78
83
|
async (params) => {
|
|
79
84
|
try {
|
|
@@ -85,6 +90,8 @@ async function main() {
|
|
|
85
90
|
source: params.source || undefined,
|
|
86
91
|
dateFrom: params.dateFrom || undefined,
|
|
87
92
|
dateTo: params.dateTo || undefined,
|
|
93
|
+
activeScopeKey: params.activeScopeKey || undefined,
|
|
94
|
+
activeScopePath: params.activeScopePath || undefined,
|
|
88
95
|
};
|
|
89
96
|
if (params.entities && params.entities.length > 0) {
|
|
90
97
|
recallOpts.entities = params.entities;
|
|
@@ -93,7 +100,7 @@ async function main() {
|
|
|
93
100
|
if (params.mode) recallOpts.mode = params.mode;
|
|
94
101
|
|
|
95
102
|
const results = await aquifer.recall(params.query, recallOpts);
|
|
96
|
-
const text = formatResults(results, params.query);
|
|
103
|
+
const text = formatResults(results, params.query, params.explain);
|
|
97
104
|
return { content: [{ type: 'text', text }] };
|
|
98
105
|
} catch (err) {
|
|
99
106
|
return {
|
|
@@ -104,9 +111,55 @@ async function main() {
|
|
|
104
111
|
}
|
|
105
112
|
);
|
|
106
113
|
|
|
114
|
+
server.tool(
|
|
115
|
+
'evidence_recall',
|
|
116
|
+
'Explicit legacy/evidence search over stored sessions and summaries. Use for audit/debug/distillation; it does not feed bootstrap implicitly.',
|
|
117
|
+
{
|
|
118
|
+
query: z.string().min(1).describe('Evidence search query (keyword or natural language)'),
|
|
119
|
+
limit: z.number().int().min(1).max(20).optional().describe('Max results (default 5)'),
|
|
120
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
121
|
+
source: z.string().optional().describe('Filter by source (e.g., gateway, cc)'),
|
|
122
|
+
dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
123
|
+
dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
124
|
+
entities: z.array(z.string()).optional().describe('Entity names to match'),
|
|
125
|
+
entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
|
|
126
|
+
mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Legacy evidence recall mode'),
|
|
127
|
+
explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
|
|
128
|
+
allowUnsafeDebug: z.boolean().optional().describe('Allow broad evidence/debug search without an audit boundary'),
|
|
129
|
+
},
|
|
130
|
+
async (params) => {
|
|
131
|
+
try {
|
|
132
|
+
const aquifer = getAquifer();
|
|
133
|
+
const limit = params.limit || 5;
|
|
134
|
+
const recallOpts = {
|
|
135
|
+
limit,
|
|
136
|
+
agentId: params.agentId || undefined,
|
|
137
|
+
source: params.source || undefined,
|
|
138
|
+
dateFrom: params.dateFrom || undefined,
|
|
139
|
+
dateTo: params.dateTo || undefined,
|
|
140
|
+
};
|
|
141
|
+
if (params.entities && params.entities.length > 0) {
|
|
142
|
+
recallOpts.entities = params.entities;
|
|
143
|
+
recallOpts.entityMode = params.entityMode || 'any';
|
|
144
|
+
}
|
|
145
|
+
if (params.mode) recallOpts.mode = params.mode;
|
|
146
|
+
if (params.allowUnsafeDebug) recallOpts.allowUnsafeDebug = true;
|
|
147
|
+
|
|
148
|
+
const results = await aquifer.evidenceRecall(params.query, recallOpts);
|
|
149
|
+
const text = formatResults(results, params.query, params.explain);
|
|
150
|
+
return { content: [{ type: 'text', text }] };
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: `evidence_recall error: ${err.message}` }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
107
160
|
server.tool(
|
|
108
161
|
'session_feedback',
|
|
109
|
-
'
|
|
162
|
+
'After using legacy session_recall or bootstrap, mark the recalled session helpful or unhelpful. This only targets legacy session trust, not curated memory rows.',
|
|
110
163
|
{
|
|
111
164
|
sessionId: z.string().min(1).describe('Session ID to give feedback on'),
|
|
112
165
|
verdict: z.enum(['helpful', 'unhelpful']).describe('Was the recalled session useful?'),
|
|
@@ -133,6 +186,73 @@ async function main() {
|
|
|
133
186
|
}
|
|
134
187
|
);
|
|
135
188
|
|
|
189
|
+
server.tool(
|
|
190
|
+
'memory_feedback',
|
|
191
|
+
'Record append-only feedback on a curated memory row. This affects curated ranking/review priority only and does not mutate memory truth.',
|
|
192
|
+
{
|
|
193
|
+
memoryId: z.string().min(1).optional().describe('Curated memory record ID to give feedback on'),
|
|
194
|
+
canonicalKey: z.string().min(1).optional().describe('Canonical key of the active curated memory record'),
|
|
195
|
+
feedbackType: z.enum(['helpful', 'confirm', 'irrelevant', 'scope_mismatch', 'stale', 'incorrect']).describe('Curated memory feedback event type'),
|
|
196
|
+
note: z.string().optional().describe('Optional reason'),
|
|
197
|
+
agentId: z.string().optional().describe('Optional actor/agent label for audit metadata'),
|
|
198
|
+
},
|
|
199
|
+
async (params) => {
|
|
200
|
+
try {
|
|
201
|
+
if (!params.memoryId && !params.canonicalKey) {
|
|
202
|
+
throw new Error('memory_feedback requires memoryId or canonicalKey');
|
|
203
|
+
}
|
|
204
|
+
const aquifer = getAquifer();
|
|
205
|
+
const result = await aquifer.memoryFeedback({
|
|
206
|
+
memoryId: params.memoryId || undefined,
|
|
207
|
+
canonicalKey: params.canonicalKey || undefined,
|
|
208
|
+
}, {
|
|
209
|
+
feedbackType: params.feedbackType,
|
|
210
|
+
note: params.note || undefined,
|
|
211
|
+
agentId: params.agentId || undefined,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: 'text', text: `Memory feedback: ${result.feedbackType}` }],
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: `memory_feedback error: ${err.message}` }],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
server.tool(
|
|
226
|
+
'feedback_stats',
|
|
227
|
+
'Return trust feedback statistics: total feedback count, helpful/unhelpful breakdown, trust score distribution, and coverage.',
|
|
228
|
+
{
|
|
229
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
230
|
+
dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
231
|
+
dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
232
|
+
},
|
|
233
|
+
async (params) => {
|
|
234
|
+
try {
|
|
235
|
+
const aquifer = getAquifer();
|
|
236
|
+
const stats = await aquifer.feedbackStats({
|
|
237
|
+
agentId: params.agentId || undefined,
|
|
238
|
+
dateFrom: params.dateFrom || undefined,
|
|
239
|
+
dateTo: params.dateTo || undefined,
|
|
240
|
+
});
|
|
241
|
+
const lines = [
|
|
242
|
+
`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`,
|
|
243
|
+
`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`,
|
|
244
|
+
`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`,
|
|
245
|
+
];
|
|
246
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: 'text', text: `feedback_stats error: ${err.message}` }],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
136
256
|
server.tool(
|
|
137
257
|
'memory_stats',
|
|
138
258
|
'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
|
|
@@ -196,6 +316,8 @@ async function main() {
|
|
|
196
316
|
limit: z.number().int().min(1).max(20).optional().describe('Max sessions (default 5)'),
|
|
197
317
|
lookbackDays: z.number().int().min(1).max(90).optional().describe('How far back in days (default 14)'),
|
|
198
318
|
maxChars: z.number().int().min(500).max(12000).optional().describe('Max output characters (default 4000)'),
|
|
319
|
+
activeScopeKey: z.string().optional().describe('Active curated memory scope key'),
|
|
320
|
+
activeScopePath: z.array(z.string()).optional().describe('Ordered curated scope path'),
|
|
199
321
|
},
|
|
200
322
|
async (params) => {
|
|
201
323
|
try {
|
|
@@ -205,6 +327,8 @@ async function main() {
|
|
|
205
327
|
limit: params.limit,
|
|
206
328
|
lookbackDays: params.lookbackDays,
|
|
207
329
|
maxChars: params.maxChars,
|
|
330
|
+
activeScopeKey: params.activeScopeKey || undefined,
|
|
331
|
+
activeScopePath: params.activeScopePath || undefined,
|
|
208
332
|
format: 'text',
|
|
209
333
|
});
|
|
210
334
|
return { content: [{ type: 'text', text: result.text }] };
|
|
@@ -201,7 +201,7 @@ function register(api) {
|
|
|
201
201
|
|
|
202
202
|
return {
|
|
203
203
|
name: 'session_recall',
|
|
204
|
-
description: 'Search
|
|
204
|
+
description: 'Search Aquifer memory by keyword. In curated serving mode this searches active curated memory; legacy/evidence lookup is exposed by the MCP stdio evidence_recall tool. Supports entity intersection for precise multi-entity queries.',
|
|
205
205
|
parameters: {
|
|
206
206
|
type: 'object',
|
|
207
207
|
properties: {
|
|
@@ -214,6 +214,7 @@ function register(api) {
|
|
|
214
214
|
entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
|
|
215
215
|
entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" (only sessions with every entity)' },
|
|
216
216
|
mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Recall mode: "fts" (keyword only), "hybrid" (default), "vector" (vector only)' },
|
|
217
|
+
explain: { type: 'boolean', description: 'Include per-result score breakdown (diagnostic)' },
|
|
217
218
|
},
|
|
218
219
|
required: ['query'],
|
|
219
220
|
},
|
|
@@ -234,7 +235,7 @@ function register(api) {
|
|
|
234
235
|
if (params.mode) recallOpts.mode = params.mode;
|
|
235
236
|
|
|
236
237
|
const results = await aquifer.recall(params.query, recallOpts);
|
|
237
|
-
const text = formatRecallResults(results);
|
|
238
|
+
const text = formatRecallResults(results, { showScore: true, showExplain: !!params.explain });
|
|
238
239
|
return { content: [{ type: 'text', text }] };
|
|
239
240
|
} catch (err) {
|
|
240
241
|
return {
|
|
@@ -253,7 +254,7 @@ function register(api) {
|
|
|
253
254
|
|
|
254
255
|
return {
|
|
255
256
|
name: 'session_feedback',
|
|
256
|
-
description: '
|
|
257
|
+
description: 'After using session_recall, mark the result helpful if it directly informed your answer, or unhelpful if it was irrelevant/outdated. Include a short note. Sessions with more helpful feedback rank higher in future recalls.',
|
|
257
258
|
parameters: {
|
|
258
259
|
type: 'object',
|
|
259
260
|
properties: {
|
|
@@ -285,5 +286,44 @@ function register(api) {
|
|
|
285
286
|
};
|
|
286
287
|
}, { name: 'session_feedback' });
|
|
287
288
|
|
|
288
|
-
|
|
289
|
+
// --- feedback_stats tool ---
|
|
290
|
+
|
|
291
|
+
api.registerTool((ctx) => {
|
|
292
|
+
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
name: 'feedback_stats',
|
|
296
|
+
description: 'Return trust feedback statistics: total feedback count, helpful/unhelpful breakdown, trust score distribution, and coverage.',
|
|
297
|
+
parameters: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
agentId: { type: 'string', description: 'Filter by agent ID' },
|
|
301
|
+
dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
|
|
302
|
+
dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
async execute(_toolCallId, params) {
|
|
306
|
+
try {
|
|
307
|
+
const stats = await aquifer.feedbackStats({
|
|
308
|
+
agentId: params.agentId || undefined,
|
|
309
|
+
dateFrom: params.dateFrom || undefined,
|
|
310
|
+
dateTo: params.dateTo || undefined,
|
|
311
|
+
});
|
|
312
|
+
const lines = [
|
|
313
|
+
`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`,
|
|
314
|
+
`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`,
|
|
315
|
+
`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`,
|
|
316
|
+
];
|
|
317
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: 'text', text: `feedback_stats error: ${err.message}` }],
|
|
321
|
+
isError: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}, { name: 'feedback_stats' });
|
|
327
|
+
|
|
328
|
+
api.logger.info('[aquifer-memory] registered (before_reset + session_recall + session_feedback + feedback_stats)');
|
|
289
329
|
}
|
|
@@ -30,7 +30,21 @@ const DEFAULTS = {
|
|
|
30
30
|
temperature: 0,
|
|
31
31
|
},
|
|
32
32
|
entities: { enabled: false, mergeCall: true, scope: 'default' },
|
|
33
|
+
insights: {
|
|
34
|
+
recallWeights: null,
|
|
35
|
+
recencyWindowDays: null,
|
|
36
|
+
dedup: {
|
|
37
|
+
mode: 'off',
|
|
38
|
+
cosineThreshold: 0.88,
|
|
39
|
+
closeBandFrom: 0.85,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
33
42
|
rank: { rrf: 0.65, timeDecay: 0.25, access: 0.10, entityBoost: 0.18 },
|
|
43
|
+
memory: {
|
|
44
|
+
servingMode: 'legacy', // 'legacy' | 'curated'
|
|
45
|
+
activeScopeKey: null,
|
|
46
|
+
activeScopePath: null,
|
|
47
|
+
},
|
|
34
48
|
rerank: {
|
|
35
49
|
enabled: false,
|
|
36
50
|
provider: null, // 'tei' | 'jina' | 'openrouter' | 'custom'
|
|
@@ -75,6 +89,12 @@ const ENV_MAP = [
|
|
|
75
89
|
['AQUIFER_LLM_TEMPERATURE', 'llm.temperature', Number],
|
|
76
90
|
['AQUIFER_ENTITIES_ENABLED', 'entities.enabled', Boolean],
|
|
77
91
|
['AQUIFER_ENTITY_SCOPE', 'entities.scope'],
|
|
92
|
+
['AQUIFER_INSIGHTS_DEDUP_MODE', 'insights.dedup.mode'],
|
|
93
|
+
['AQUIFER_INSIGHTS_DEDUP_COSINE', 'insights.dedup.cosineThreshold', Number],
|
|
94
|
+
['AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM', 'insights.dedup.closeBandFrom', Number],
|
|
95
|
+
['AQUIFER_MEMORY_SERVING_MODE', 'memory.servingMode'],
|
|
96
|
+
['AQUIFER_MEMORY_ACTIVE_SCOPE_KEY', 'memory.activeScopeKey'],
|
|
97
|
+
['AQUIFER_MEMORY_ACTIVE_SCOPE_PATH', 'memory.activeScopePath'],
|
|
78
98
|
['AQUIFER_RERANK_ENABLED', 'rerank.enabled', Boolean],
|
|
79
99
|
['AQUIFER_RERANK_PROVIDER', 'rerank.provider'],
|
|
80
100
|
['AQUIFER_RERANK_BASE_URL', 'rerank.baseUrl'],
|
|
@@ -165,6 +185,14 @@ function loadConfig(opts = {}) {
|
|
|
165
185
|
config = deepMerge(config, opts.overrides);
|
|
166
186
|
}
|
|
167
187
|
|
|
188
|
+
// insights.dedup shorthand: true → enforce, false → off
|
|
189
|
+
if (config.insights && typeof config.insights.dedup === 'boolean') {
|
|
190
|
+
config.insights.dedup = {
|
|
191
|
+
...DEFAULTS.insights.dedup,
|
|
192
|
+
mode: config.insights.dedup ? 'enforce' : 'off',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
return config;
|
|
169
197
|
}
|
|
170
198
|
|
|
@@ -37,7 +37,7 @@ function evictStale(dedupMap, now = Date.now()) {
|
|
|
37
37
|
* @param {string} [opts.source] — caller-provided source tag (e.g. 'openclaw', 'cc', 'opencode')
|
|
38
38
|
* @param {string} [opts.sessionKey] — passed through to commit()
|
|
39
39
|
* @param {any[]} opts.rawEntries — host-native session entries
|
|
40
|
-
* @param {'gateway'|'cc'|'claude-code'|'preNormalized'} [opts.adapter]
|
|
40
|
+
* @param {'gateway'|'cc'|'claude-code'|'codex'|'preNormalized'} [opts.adapter]
|
|
41
41
|
* 'preNormalized' means rawEntries already matches normalizeMessages output
|
|
42
42
|
* (used by OpenCode which reads SQLite directly).
|
|
43
43
|
* @param {object} [opts.preNormalized] — { messages, userCount, ... } ready to commit,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// session-level metadata. Wraps pipeline/normalize so consumers don't each
|
|
6
6
|
// reinvent their own role/content extraction.
|
|
7
7
|
//
|
|
8
|
-
// Supported adapters: 'gateway' | 'cc' (alias of 'claude-code'). The OpenCode
|
|
8
|
+
// Supported adapters: 'gateway' | 'cc' (alias of 'claude-code') | 'codex'. The OpenCode
|
|
9
9
|
// consumer reads from SQLite and constructs the output shape directly; it is
|
|
10
10
|
// not expected to route through here.
|
|
11
11
|
//
|
|
@@ -21,13 +21,14 @@ const ADAPTER_ALIASES = {
|
|
|
21
21
|
'cc': 'claude-code',
|
|
22
22
|
'claude-code': 'claude-code',
|
|
23
23
|
'gateway': 'gateway',
|
|
24
|
+
'codex': 'codex',
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
function resolveAdapter(adapter) {
|
|
27
28
|
if (!adapter) return null; // auto-detect
|
|
28
29
|
const name = ADAPTER_ALIASES[adapter];
|
|
29
30
|
if (!name) {
|
|
30
|
-
throw new Error(`Unknown adapter: "${adapter}". Supported: gateway, cc (alias claude-code).`);
|
|
31
|
+
throw new Error(`Unknown adapter: "${adapter}". Supported: gateway, cc (alias claude-code), codex.`);
|
|
31
32
|
}
|
|
32
33
|
return name;
|
|
33
34
|
}
|
|
@@ -39,6 +40,16 @@ function extractRawMeta(rawEntries) {
|
|
|
39
40
|
|
|
40
41
|
for (const entry of rawEntries || []) {
|
|
41
42
|
if (!entry || typeof entry !== 'object') continue;
|
|
43
|
+
|
|
44
|
+
if (entry.type === 'turn_context' && entry.payload?.model && !model) {
|
|
45
|
+
model = entry.payload.model;
|
|
46
|
+
}
|
|
47
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
|
|
48
|
+
const usage = entry.payload?.info?.last_token_usage || {};
|
|
49
|
+
tokensIn += usage.input_tokens || usage.input || 0;
|
|
50
|
+
tokensOut += usage.output_tokens || usage.output || 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
const msg = entry.message || entry;
|
|
43
54
|
if (msg && typeof msg === 'object') {
|
|
44
55
|
if (msg.model && !model) model = msg.model;
|
|
@@ -57,7 +68,7 @@ function extractRawMeta(rawEntries) {
|
|
|
57
68
|
*
|
|
58
69
|
* @param {any[]} rawEntries
|
|
59
70
|
* @param {object} [opts]
|
|
60
|
-
* @param {'gateway'|'cc'|'claude-code'} [opts.adapter] — host adapter; auto-detected if omitted
|
|
71
|
+
* @param {'gateway'|'cc'|'claude-code'|'codex'} [opts.adapter] — host adapter; auto-detected if omitted
|
|
61
72
|
* @returns {{
|
|
62
73
|
* messages: {role:string,content:string,timestamp:string|null}[],
|
|
63
74
|
* userCount: number, assistantCount: number,
|
|
@@ -18,6 +18,24 @@ function formatDateIso(value) {
|
|
|
18
18
|
return Number.isNaN(d.getTime()) ? 'unknown' : d.toISOString().slice(0, 10);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function isCuratedMemoryResult(result) {
|
|
22
|
+
return Boolean(result && (
|
|
23
|
+
result.memoryType || result.memory_type ||
|
|
24
|
+
result.canonicalKey || result.canonical_key ||
|
|
25
|
+
result.scopeKey || result.scope_key
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function curatedTitle(result) {
|
|
30
|
+
const type = result.memoryType || result.memory_type || 'memory';
|
|
31
|
+
const title = result.title || result.summary || result.canonicalKey || result.canonical_key || type;
|
|
32
|
+
return truncate(title, 80);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resultDate(result) {
|
|
36
|
+
return result.startedAt || result.acceptedAt || result.accepted_at || result.observedAt || result.observed_at || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
// Humanize a past timestamp into zh-TW relative form (e.g. "3 天前", "昨天").
|
|
22
40
|
// Bucketed on raw ms-diff — good enough for model intuition, not calendar-precise.
|
|
23
41
|
// Returns null for invalid / future timestamps so callers can fall back.
|
|
@@ -48,6 +66,11 @@ const defaultRenderers = {
|
|
|
48
66
|
return query ? `No results found for "${query}".` : 'No matching sessions found.';
|
|
49
67
|
},
|
|
50
68
|
title(result, index) {
|
|
69
|
+
if (isCuratedMemoryResult(result)) {
|
|
70
|
+
const date = formatDateIso(resultDate(result));
|
|
71
|
+
const scope = result.scopeKey || result.scope_key || 'scope:unknown';
|
|
72
|
+
return `### ${index + 1}. ${curatedTitle(result)} (${date}, ${scope})`;
|
|
73
|
+
}
|
|
51
74
|
const ss = result.structuredSummary || {};
|
|
52
75
|
const title = ss.title || truncate(result.summaryText, 60) || '(untitled)';
|
|
53
76
|
const date = formatDateIso(result.startedAt);
|
|
@@ -55,6 +78,10 @@ const defaultRenderers = {
|
|
|
55
78
|
return `### ${index + 1}. ${title} (${date}, ${agent})`;
|
|
56
79
|
},
|
|
57
80
|
body(result) {
|
|
81
|
+
if (isCuratedMemoryResult(result)) {
|
|
82
|
+
const text = result.summary || result.title || result.canonicalKey || result.canonical_key || '';
|
|
83
|
+
return text ? truncate(text, 300) : null;
|
|
84
|
+
}
|
|
58
85
|
const ss = result.structuredSummary || {};
|
|
59
86
|
const text = ss.overview || result.summaryText || '';
|
|
60
87
|
return text ? truncate(text, 300) : null;
|
|
@@ -66,6 +93,30 @@ const defaultRenderers = {
|
|
|
66
93
|
if (!showScore) return null;
|
|
67
94
|
return `Score: ${typeof result.score === 'number' ? result.score.toFixed(3) : '?'}`;
|
|
68
95
|
},
|
|
96
|
+
explain(result, { showExplain }) {
|
|
97
|
+
if (!showExplain) return null;
|
|
98
|
+
const d = result._debug;
|
|
99
|
+
if (!d) return null;
|
|
100
|
+
const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
|
|
101
|
+
const parts = [
|
|
102
|
+
`rrf=${f(d.rrf)}`,
|
|
103
|
+
`td=${f(d.timeDecay)}`,
|
|
104
|
+
`access=${f(d.access)}`,
|
|
105
|
+
`entity=${f(d.entityScore)}`,
|
|
106
|
+
`trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
|
|
107
|
+
`ol=${f(d.openLoopBoost)}`,
|
|
108
|
+
`\u2192 hybrid=${f(d.hybridScore)}`,
|
|
109
|
+
];
|
|
110
|
+
if (d.rerankApplied) {
|
|
111
|
+
parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
|
|
112
|
+
} else {
|
|
113
|
+
parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(d.searchErrors) && d.searchErrors.length > 0) {
|
|
116
|
+
parts.push(`errors: ${d.searchErrors.map(e => (e && e.path) || '?').join(',')}`);
|
|
117
|
+
}
|
|
118
|
+
return ` ${parts.join(' ')}`;
|
|
119
|
+
},
|
|
69
120
|
separator() {
|
|
70
121
|
return '';
|
|
71
122
|
},
|
|
@@ -102,6 +153,8 @@ function createRecallFormatter(overrides = {}) {
|
|
|
102
153
|
if (matched) lines.push(matched);
|
|
103
154
|
const score = r.score(res, { showScore: !!opts.showScore, ...ctx });
|
|
104
155
|
if (score) lines.push(score);
|
|
156
|
+
const explain = r.explain(res, { showExplain: !!opts.showExplain, ...ctx });
|
|
157
|
+
if (explain) lines.push(explain);
|
|
105
158
|
const sep = r.separator(i, ctx);
|
|
106
159
|
if (sep !== null && sep !== undefined) lines.push(sep);
|
|
107
160
|
}
|