@shadowforge0/aquifer-memory 1.5.8 → 1.5.12
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/README.md +100 -3
- package/consumers/cli.js +34 -0
- package/consumers/mcp.js +38 -5
- package/consumers/miranda/context-inject.js +1 -0
- package/consumers/openclaw-plugin.js +43 -3
- package/consumers/shared/config.js +20 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/recall-format.js +26 -0
- package/core/aquifer.js +12 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +18 -1
- package/core/storage.js +71 -0
- package/package.json +10 -2
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
package/README.md
CHANGED
|
@@ -132,14 +132,34 @@ Need LLM summarization, the knowledge graph, OpenAI embeddings, or the reranker?
|
|
|
132
132
|
| `AQUIFER_RERANK_PROVIDER` | No | Reranker provider: `tei`, `jina`, `openrouter` | `tei` |
|
|
133
133
|
| `AQUIFER_RERANK_BASE_URL` | No | Reranker endpoint | `http://localhost:8080` |
|
|
134
134
|
| `AQUIFER_AGENT_ID` | No | Default agent ID | `main` |
|
|
135
|
+
| `AQUIFER_MIGRATIONS_MODE` | No | Startup handshake mode: `apply` (default), `check`, `off` | `apply` |
|
|
136
|
+
| `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | No | Advisory-lock wait before `AQ_MIGRATION_LOCK_TIMEOUT` (default 30000) | `30000` |
|
|
137
|
+
| `AQUIFER_INSIGHTS_DEDUP_MODE` | No | Insights semantic dedup mode: `off` (default), `shadow`, `enforce` — env wins over code for this field only, so operators can kill-switch without redeploy | `shadow` |
|
|
138
|
+
| `AQUIFER_INSIGHTS_DEDUP_COSINE` | No | Cosine threshold for semantic merge (default `0.88`; warn outside `[0.75, 0.95]`) | `0.90` |
|
|
139
|
+
| `AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM` | No | Lower bound for close-band logging (`dedupNear`); must be below threshold (default `0.85`) | `0.82` |
|
|
135
140
|
|
|
136
141
|
Full env-to-config mapping is in [consumers/shared/config.js](consumers/shared/config.js).
|
|
137
142
|
|
|
143
|
+
### Insights semantic dedup (1.5.10)
|
|
144
|
+
|
|
145
|
+
When a cron extractor (`scripts/extract-insights-from-recent-sessions.js`) or any other caller writes insights via `commitInsight`, the canonical-key layer (1.5.3+) dedupes rows whose `canonicalClaim + entities` hash to the same value. But LLMs don't always produce the same `canonicalClaim` across runs, so 1.5.10 adds a second tier: `title + body` are embedded, matched against `(tenant, agent, type)`-scoped active rows, and a top cosine above `AQUIFER_INSIGHTS_DEDUP_COSINE` triggers supersede (enforce) or metadata-only would-merge logging (shadow). Close-band hits (`closeBandFrom ≤ cos < threshold`) write `metadata.dedupNear` without supersede so operators can tune thresholds without committing.
|
|
146
|
+
|
|
147
|
+
Recommended rollout: `shadow` for one weekly cycle, inspect `SELECT metadata->>'shadowMatch' FROM insights WHERE metadata ? 'shadowMatch'`, then flip to `enforce`. Kill-switch: `AQUIFER_INSIGHTS_DEDUP_MODE=off` and restart.
|
|
148
|
+
|
|
149
|
+
Pre-1.5.3 rows with `canonical_key_v2 IS NULL` are caught by the semantic tier but skip the canonical path; a startup warn points at the one-shot backfill:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
DATABASE_URL=... \
|
|
153
|
+
node scripts/backfill-canonical-key.js --schema <schema> --agent <id>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The script is idempotent (`WHERE canonical_key_v2 IS NULL` guard) and race-safe with live writers.
|
|
157
|
+
|
|
138
158
|
---
|
|
139
159
|
|
|
140
160
|
## Host Integration
|
|
141
161
|
|
|
142
|
-
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes
|
|
162
|
+
MCP is the primary integration surface. Agent hosts connect to the Aquifer MCP server, which exposes six tools: `session_recall`, `session_feedback`, `feedback_stats`, `session_bootstrap`, `memory_stats`, `memory_pending`.
|
|
143
163
|
|
|
144
164
|
| Integration | Route | Status | When to use |
|
|
145
165
|
|-------------|-------|--------|-------------|
|
|
@@ -194,7 +214,7 @@ Add to `openclaw.json` under `mcp.servers`:
|
|
|
194
214
|
}
|
|
195
215
|
```
|
|
196
216
|
|
|
197
|
-
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__session_bootstrap`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
217
|
+
Tools materialize as `aquifer__session_recall`, `aquifer__session_feedback`, `aquifer__feedback_stats`, `aquifer__session_bootstrap`, `aquifer__memory_stats`, `aquifer__memory_pending` (server name prefix added by the host).
|
|
198
218
|
|
|
199
219
|
The OpenClaw plugin (`consumers/openclaw-plugin.js`) is retained for session capture via `before_reset` but is **not** the recommended tool delivery path. Use MCP.
|
|
200
220
|
|
|
@@ -377,9 +397,36 @@ Returns an Aquifer instance. Config:
|
|
|
377
397
|
}
|
|
378
398
|
```
|
|
379
399
|
|
|
400
|
+
#### `aquifer.init()`
|
|
401
|
+
|
|
402
|
+
Startup handshake — resolves pending migrations and returns a StartupEnvelope. Hosts should `await` this before accepting traffic. In `apply` mode a `ready=false` envelope is the signal to abort startup.
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
const envelope = await aquifer.init();
|
|
406
|
+
// {
|
|
407
|
+
// ready: true,
|
|
408
|
+
// memoryMode: 'rw', // 'rw' | 'ro' | 'off'
|
|
409
|
+
// migrationMode: 'apply', // 'apply' | 'check' | 'off'
|
|
410
|
+
// pendingMigrations: [], // migration ids still outstanding
|
|
411
|
+
// appliedMigrations: ['001-base', '003-trust-feedback', '004-completion', '006-insights'],
|
|
412
|
+
// error: null, // { code, message } on failure
|
|
413
|
+
// durationMs: 1035,
|
|
414
|
+
// }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The MCP consumer (`consumers/mcp.js`) already wires `aquifer.init()` before `server.connect()` and exits non-zero if `ready=false` under `apply` mode.
|
|
418
|
+
|
|
419
|
+
#### `aquifer.listPendingMigrations()` / `aquifer.getMigrationStatus()`
|
|
420
|
+
|
|
421
|
+
Returns `{ required, applied, pending, lastRunAt }` via a `pg_tables` signature probe. No DDL runs. Use it from a health check or from a consumer that wants to surface drift before calling `init()`.
|
|
422
|
+
|
|
380
423
|
#### `aquifer.migrate()`
|
|
381
424
|
|
|
382
|
-
Runs SQL migrations (idempotent). Creates tables, indexes, triggers, and extensions.
|
|
425
|
+
Runs SQL migrations (idempotent). Creates tables, indexes, triggers, and extensions. Uses `pg_try_advisory_lock` with a 250 ms poll and a `lockTimeoutMs` deadline (30 s default); on exhaustion throws with `code: 'AQ_MIGRATION_LOCK_TIMEOUT'`. On success returns `{ ok: true, durationMs, notices, ddlExecuted }`; on failure throws an error whose `err.notices` / `err.failedAt` describe the stage that blew up. Most callers should go through `aquifer.init()` instead.
|
|
426
|
+
|
|
427
|
+
#### `aquifer.ensureMigrated()`
|
|
428
|
+
|
|
429
|
+
Lazy idempotent wrapper — fires `migrate()` once on first call, no-ops afterwards. Honors `migrations.mode`: `check` only probes, `off` marks the instance migrated without touching the DB.
|
|
383
430
|
|
|
384
431
|
#### `aquifer.commit(sessionId, messages, opts)`
|
|
385
432
|
|
|
@@ -463,6 +510,26 @@ const result = await aquifer.bootstrap({
|
|
|
463
510
|
|
|
464
511
|
Cross-session dedup on open loops and decisions, sentinel filtering (removes 無/none/n/a), and maxChars truncation.
|
|
465
512
|
|
|
513
|
+
#### `aquifer.insights.commitInsight(opts)` / `recallInsights(query, opts)` / `markStale(id)` / `supersede(oldId, newId)`
|
|
514
|
+
|
|
515
|
+
Higher-order reflections distilled from session windows (preferences, patterns, frustrations, workflows). Split into two identities: a **canonical key** that describes what the insight is *about* (stable across rewordings), and an **idempotency key** that describes which revision of that claim was written.
|
|
516
|
+
|
|
517
|
+
```javascript
|
|
518
|
+
await aquifer.insights.commitInsight({
|
|
519
|
+
agentId: 'main',
|
|
520
|
+
type: 'preference',
|
|
521
|
+
canonicalClaim: 'mk prefers checking context before coding', // required — short declarative claim
|
|
522
|
+
title: 'Context-first discipline', // best-effort display
|
|
523
|
+
body: '…',
|
|
524
|
+
entities: ['mk', 'claude code'],
|
|
525
|
+
sourceSessionIds: ['sess-a', 'sess-b'],
|
|
526
|
+
evidenceWindow: { from: isoString, to: isoString },
|
|
527
|
+
importance: 0.9,
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Write rules: **duplicate** (same idempotency key → return existing), **revision** (same canonical key + newer evidence → INSERT + inline supersede of prior active), **back-fill revision** (same canonical key + older evidence → INSERT without supersede), **stale replay** (same canonical + same body → return existing). Old pre-1.5.6 rows are not retrofitted; their `canonical_key_v2` stays `NULL` and they age out naturally.
|
|
532
|
+
|
|
466
533
|
#### `aquifer.close()`
|
|
467
534
|
|
|
468
535
|
Closes the PostgreSQL connection pool (only if Aquifer created it).
|
|
@@ -498,9 +565,19 @@ createAquifer({
|
|
|
498
565
|
access: 0.10, // access frequency weight
|
|
499
566
|
entityBoost: 0.18, // entity match boost
|
|
500
567
|
},
|
|
568
|
+
migrations: {
|
|
569
|
+
mode: 'apply', // 'apply' | 'check' | 'off'
|
|
570
|
+
lockTimeoutMs: 30000, // abort init() if advisory lock held this long
|
|
571
|
+
startupTimeoutMs: 60000, // overall init() deadline (plan probe + DDL combined)
|
|
572
|
+
onEvent: null, // (e) => void — lifecycle hook, see below
|
|
573
|
+
},
|
|
501
574
|
});
|
|
502
575
|
```
|
|
503
576
|
|
|
577
|
+
### Startup observability
|
|
578
|
+
|
|
579
|
+
Set `migrations.onEvent` to observe the lifecycle without parsing logs. Event names: `init_started`, `check_completed`, `apply_started`, `apply_succeeded`, `apply_failed`. Each payload carries `schema`, `mode`, the plan, `ddlExecuted`, `durationMs`, and on failure the `error` / `failedAt` / `notices`. No listener → zero cost.
|
|
580
|
+
|
|
504
581
|
### Entity Scope
|
|
505
582
|
|
|
506
583
|
`entities.scope` defines the namespace for entity identity. The unique constraint is `(tenant_id, normalized_name, entity_scope)` — the same entity name in different scopes creates separate entities. This decouples entity identity from `agentId`, allowing multiple agents to share an entity namespace.
|
|
@@ -542,6 +619,22 @@ Key indexes: trigram on entity names, GiST on embeddings, unique on `(tenant_id,
|
|
|
542
619
|
|
|
543
620
|
Also adds `trust_score` column to `session_summaries` (default 0.5, range 0–1).
|
|
544
621
|
|
|
622
|
+
### 005-entity-state-history.sql *(entities enabled)*
|
|
623
|
+
|
|
624
|
+
| Table | Purpose |
|
|
625
|
+
|-------|---------|
|
|
626
|
+
| `entity_state_history` | Temporal state-change log with partial `UNIQUE (tenant, agent, entity, attribute) WHERE valid_to IS NULL` to enforce at-most-one-current. Out-of-order backfill is supported via predecessor/successor overlap checks |
|
|
627
|
+
|
|
628
|
+
Opt-in pipeline (`createAquifer({stateChanges: {enabled, whitelist, confidenceThreshold, timeoutMs, ...}})`) extracts temporal state transitions from session text during `enrich()`; off by default to control LLM cost.
|
|
629
|
+
|
|
630
|
+
### 006-insights.sql
|
|
631
|
+
|
|
632
|
+
| Table | Purpose |
|
|
633
|
+
|-------|---------|
|
|
634
|
+
| `insights` | Higher-order reflections with TSTZRANGE evidence window, importance, GIN on source_session_ids, HNSW on 1024-dim embedding, and a non-unique partial index on `canonical_key_v2` for the canonical/revision dedup contract |
|
|
635
|
+
|
|
636
|
+
Key indexes: `idx_insights_canonical_v2_active` (partial on active rows with canonical key set), `idx_insights_idempotency_key` (unique on revision key).
|
|
637
|
+
|
|
545
638
|
---
|
|
546
639
|
|
|
547
640
|
## Troubleshooting
|
|
@@ -556,6 +649,10 @@ Also adds `trust_score` column to `session_summaries` (default 0.5, range 0–1)
|
|
|
556
649
|
|
|
557
650
|
**Embedding provider connection refused** — Verify your `AQUIFER_EMBED_BASE_URL` is reachable. For local Ollama, make sure the server is running and the model is pulled (`ollama pull bge-m3`).
|
|
558
651
|
|
|
652
|
+
**`AQ_MIGRATION_LOCK_TIMEOUT` on startup** — another process holds the migration advisory lock for `aquifer:<schema>`. Either it is a concurrent `aquifer.init()` racing yours (expected; one will win, the other re-runs and finds `pending=[]`) or a crashed worker left the lock held. Raise `migrations.lockTimeoutMs`, or drop the stale backend via `SELECT pg_terminate_backend(pid) FROM pg_locks WHERE locktype='advisory'` after you have confirmed which pid is dead.
|
|
653
|
+
|
|
654
|
+
**MCP process exits non-zero at startup** — expected when `migrations.mode=apply` and `aquifer.init()` returns `ready=false`. Read the `[aquifer-mcp] startup aborted` line on stderr for the `error.code` / `failedAt`. If you need the old lazy-migrate-on-first-tool-call behaviour instead, set `AQUIFER_MIGRATIONS_MODE=check` (and run `migrate()` out of band) or `=off`.
|
|
655
|
+
|
|
559
656
|
---
|
|
560
657
|
|
|
561
658
|
## Dependencies
|
package/consumers/cli.js
CHANGED
|
@@ -99,6 +99,7 @@ async function cmdRecall(aquifer, args) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const showExplain = !!args.flags.explain;
|
|
102
103
|
for (let i = 0; i < results.length; i++) {
|
|
103
104
|
const r = results[i];
|
|
104
105
|
const ss = r.structuredSummary || {};
|
|
@@ -107,6 +108,18 @@ async function cmdRecall(aquifer, args) {
|
|
|
107
108
|
console.log(`${i + 1}. [${r.score?.toFixed(3)}] ${title} (${date}, ${r.agentId})`);
|
|
108
109
|
if (ss.overview) console.log(` ${ss.overview.slice(0, 200)}`);
|
|
109
110
|
if (r.matchedTurnText) console.log(` > ${r.matchedTurnText.slice(0, 150)}`);
|
|
111
|
+
if (showExplain && r._debug) {
|
|
112
|
+
const d = r._debug;
|
|
113
|
+
const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
|
|
114
|
+
const parts = [
|
|
115
|
+
`rrf=${f(d.rrf)}`, `td=${f(d.timeDecay)}`, `access=${f(d.access)}`,
|
|
116
|
+
`entity=${f(d.entityScore)}`, `trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
|
|
117
|
+
`ol=${f(d.openLoopBoost)}`, `\u2192 hybrid=${f(d.hybridScore)}`,
|
|
118
|
+
];
|
|
119
|
+
if (d.rerankApplied) parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
|
|
120
|
+
else parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
|
|
121
|
+
console.log(` ${parts.join(' ')}`);
|
|
122
|
+
}
|
|
110
123
|
console.log();
|
|
111
124
|
}
|
|
112
125
|
}
|
|
@@ -133,6 +146,22 @@ async function cmdFeedback(aquifer, args) {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
async function cmdFeedbackStats(aquifer, args) {
|
|
150
|
+
const stats = await aquifer.feedbackStats({
|
|
151
|
+
agentId: args.flags['agent-id'] || undefined,
|
|
152
|
+
dateFrom: args.flags['date-from'] || undefined,
|
|
153
|
+
dateTo: args.flags['date-to'] || undefined,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (args.flags.json) {
|
|
157
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`);
|
|
160
|
+
console.log(`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`);
|
|
161
|
+
console.log(`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
async function cmdBackfill(aquifer, args) {
|
|
137
166
|
const limit = parsePositiveInt(args.flags.limit, 100);
|
|
138
167
|
const dryRun = !!args.flags['dry-run'];
|
|
@@ -318,6 +347,7 @@ Commands:
|
|
|
318
347
|
migrate Run database migrations
|
|
319
348
|
recall <query> Search sessions (requires embed config)
|
|
320
349
|
feedback Record trust feedback on a session
|
|
350
|
+
feedback-stats Show trust feedback statistics and coverage
|
|
321
351
|
backfill Enrich pending sessions
|
|
322
352
|
stats Show database statistics
|
|
323
353
|
export Export sessions as JSONL
|
|
@@ -336,6 +366,7 @@ Options:
|
|
|
336
366
|
--session-id ID Session ID (feedback)
|
|
337
367
|
--verdict helpful|unhelpful Feedback verdict (feedback)
|
|
338
368
|
--note TEXT Feedback note (feedback)
|
|
369
|
+
--explain Show score breakdown per result (recall)
|
|
339
370
|
--json JSON output
|
|
340
371
|
--dry-run Preview only (backfill)
|
|
341
372
|
--output PATH Output file (export)
|
|
@@ -410,6 +441,9 @@ Options:
|
|
|
410
441
|
case 'feedback':
|
|
411
442
|
await cmdFeedback(aquifer, args);
|
|
412
443
|
break;
|
|
444
|
+
case 'feedback-stats':
|
|
445
|
+
await cmdFeedbackStats(aquifer, args);
|
|
446
|
+
break;
|
|
413
447
|
case 'backfill':
|
|
414
448
|
await cmdBackfill(aquifer, args);
|
|
415
449
|
break;
|
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, session_feedback, feedback_stats,
|
|
11
|
+
* session_bootstrap, memory_stats, memory_pending
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
13
14
|
* npx aquifer mcp
|
|
@@ -32,8 +33,8 @@ function getAquifer() {
|
|
|
32
33
|
|
|
33
34
|
const { formatRecallResults } = require('./shared/recall-format');
|
|
34
35
|
|
|
35
|
-
function formatResults(results, query) {
|
|
36
|
-
return formatRecallResults(results, { query, showScore: true });
|
|
36
|
+
function formatResults(results, query, explain) {
|
|
37
|
+
return formatRecallResults(results, { query, showScore: true, showExplain: !!explain });
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
@@ -74,6 +75,7 @@ async function main() {
|
|
|
74
75
|
entities: z.array(z.string()).optional().describe('Entity names to match'),
|
|
75
76
|
entityMode: z.enum(['any', 'all']).optional().describe('"any" (default, boost) or "all" (only sessions with every entity)'),
|
|
76
77
|
mode: z.enum(['fts', 'hybrid', 'vector']).optional().describe('Recall mode: "fts" (keyword only, no embed needed), "hybrid" (default, FTS + vector), "vector" (vector only)'),
|
|
78
|
+
explain: z.boolean().optional().describe('Include per-result score breakdown (diagnostic)'),
|
|
77
79
|
},
|
|
78
80
|
async (params) => {
|
|
79
81
|
try {
|
|
@@ -93,7 +95,7 @@ async function main() {
|
|
|
93
95
|
if (params.mode) recallOpts.mode = params.mode;
|
|
94
96
|
|
|
95
97
|
const results = await aquifer.recall(params.query, recallOpts);
|
|
96
|
-
const text = formatResults(results, params.query);
|
|
98
|
+
const text = formatResults(results, params.query, params.explain);
|
|
97
99
|
return { content: [{ type: 'text', text }] };
|
|
98
100
|
} catch (err) {
|
|
99
101
|
return {
|
|
@@ -106,7 +108,7 @@ async function main() {
|
|
|
106
108
|
|
|
107
109
|
server.tool(
|
|
108
110
|
'session_feedback',
|
|
109
|
-
'
|
|
111
|
+
'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.',
|
|
110
112
|
{
|
|
111
113
|
sessionId: z.string().min(1).describe('Session ID to give feedback on'),
|
|
112
114
|
verdict: z.enum(['helpful', 'unhelpful']).describe('Was the recalled session useful?'),
|
|
@@ -133,6 +135,37 @@ async function main() {
|
|
|
133
135
|
}
|
|
134
136
|
);
|
|
135
137
|
|
|
138
|
+
server.tool(
|
|
139
|
+
'feedback_stats',
|
|
140
|
+
'Return trust feedback statistics: total feedback count, helpful/unhelpful breakdown, trust score distribution, and coverage.',
|
|
141
|
+
{
|
|
142
|
+
agentId: z.string().optional().describe('Filter by agent ID'),
|
|
143
|
+
dateFrom: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
144
|
+
dateTo: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
145
|
+
},
|
|
146
|
+
async (params) => {
|
|
147
|
+
try {
|
|
148
|
+
const aquifer = getAquifer();
|
|
149
|
+
const stats = await aquifer.feedbackStats({
|
|
150
|
+
agentId: params.agentId || undefined,
|
|
151
|
+
dateFrom: params.dateFrom || undefined,
|
|
152
|
+
dateTo: params.dateTo || undefined,
|
|
153
|
+
});
|
|
154
|
+
const lines = [
|
|
155
|
+
`Feedback: ${stats.totalFeedback} total (${stats.helpfulCount} helpful, ${stats.unhelpfulCount} unhelpful)`,
|
|
156
|
+
`Coverage: ${stats.feedbackSessions}/${stats.totalSessions} sessions rated`,
|
|
157
|
+
`Trust score: avg=${stats.trustScoreAvg} min=${stats.trustScoreMin} max=${stats.trustScoreMax}`,
|
|
158
|
+
];
|
|
159
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: 'text', text: `feedback_stats error: ${err.message}` }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
136
169
|
server.tool(
|
|
137
170
|
'memory_stats',
|
|
138
171
|
'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
|
|
@@ -14,6 +14,7 @@ function buildSessionContext({ today, agentId, focusText, todoItems, moodLine, h
|
|
|
14
14
|
const parts = [];
|
|
15
15
|
parts.push('你是 Miranda。以下是你已經知道的現況,直接用來回應,不需要讀檔或搜尋。像做 briefing——帶現況也帶判斷和建議。用散文段落,最後一句必須是結論或建議,不能是問句。若草稿有 bullet、標題、表格或問句收尾,改寫再送出。');
|
|
16
16
|
parts.push('回答任何關於過去做過什麼、討論過什麼、決策過什麼的問題時,第一步用 session_recall MCP tool 查,不要用 grep、讀 log、翻檔案。工具在手上就用。');
|
|
17
|
+
parts.push('用完 session_recall 後,如果某筆結果實際幫助了你的回答,呼叫 session_feedback(sessionId, verdict="helpful");如果結果明顯過時或錯誤,呼叫 session_feedback(sessionId, verdict="unhelpful"),帶簡短 note 說明原因。只對實際影響回答的結果回饋,不要每次 recall 都打分。');
|
|
17
18
|
|
|
18
19
|
if (focusText) parts.push(`現在的焦點是${focusText}。`);
|
|
19
20
|
if (handoffText) parts.push(`上一段的交接:${handoffText}`);
|
|
@@ -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,6 +30,15 @@ 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 },
|
|
34
43
|
rerank: {
|
|
35
44
|
enabled: false,
|
|
@@ -75,6 +84,9 @@ const ENV_MAP = [
|
|
|
75
84
|
['AQUIFER_LLM_TEMPERATURE', 'llm.temperature', Number],
|
|
76
85
|
['AQUIFER_ENTITIES_ENABLED', 'entities.enabled', Boolean],
|
|
77
86
|
['AQUIFER_ENTITY_SCOPE', 'entities.scope'],
|
|
87
|
+
['AQUIFER_INSIGHTS_DEDUP_MODE', 'insights.dedup.mode'],
|
|
88
|
+
['AQUIFER_INSIGHTS_DEDUP_COSINE', 'insights.dedup.cosineThreshold', Number],
|
|
89
|
+
['AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM', 'insights.dedup.closeBandFrom', Number],
|
|
78
90
|
['AQUIFER_RERANK_ENABLED', 'rerank.enabled', Boolean],
|
|
79
91
|
['AQUIFER_RERANK_PROVIDER', 'rerank.provider'],
|
|
80
92
|
['AQUIFER_RERANK_BASE_URL', 'rerank.baseUrl'],
|
|
@@ -165,6 +177,14 @@ function loadConfig(opts = {}) {
|
|
|
165
177
|
config = deepMerge(config, opts.overrides);
|
|
166
178
|
}
|
|
167
179
|
|
|
180
|
+
// insights.dedup shorthand: true → enforce, false → off
|
|
181
|
+
if (config.insights && typeof config.insights.dedup === 'boolean') {
|
|
182
|
+
config.insights.dedup = {
|
|
183
|
+
...DEFAULTS.insights.dedup,
|
|
184
|
+
mode: config.insights.dedup ? 'enforce' : 'off',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
return config;
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -66,6 +66,30 @@ const defaultRenderers = {
|
|
|
66
66
|
if (!showScore) return null;
|
|
67
67
|
return `Score: ${typeof result.score === 'number' ? result.score.toFixed(3) : '?'}`;
|
|
68
68
|
},
|
|
69
|
+
explain(result, { showExplain }) {
|
|
70
|
+
if (!showExplain) return null;
|
|
71
|
+
const d = result._debug;
|
|
72
|
+
if (!d) return null;
|
|
73
|
+
const f = (v) => typeof v === 'number' ? v.toFixed(3) : '?';
|
|
74
|
+
const parts = [
|
|
75
|
+
`rrf=${f(d.rrf)}`,
|
|
76
|
+
`td=${f(d.timeDecay)}`,
|
|
77
|
+
`access=${f(d.access)}`,
|
|
78
|
+
`entity=${f(d.entityScore)}`,
|
|
79
|
+
`trust=${f(d.trustScore)}(\u00d7${f(d.trustMultiplier)})`,
|
|
80
|
+
`ol=${f(d.openLoopBoost)}`,
|
|
81
|
+
`\u2192 hybrid=${f(d.hybridScore)}`,
|
|
82
|
+
];
|
|
83
|
+
if (d.rerankApplied) {
|
|
84
|
+
parts.push(`rerank=${f(d.rerankScore)}(${d.rerankReason || '?'})`);
|
|
85
|
+
} else {
|
|
86
|
+
parts.push(`[rerank: off (${d.rerankReason || '?'})]`);
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(d.searchErrors) && d.searchErrors.length > 0) {
|
|
89
|
+
parts.push(`errors: ${d.searchErrors.map(e => (e && e.path) || '?').join(',')}`);
|
|
90
|
+
}
|
|
91
|
+
return ` ${parts.join(' ')}`;
|
|
92
|
+
},
|
|
69
93
|
separator() {
|
|
70
94
|
return '';
|
|
71
95
|
},
|
|
@@ -102,6 +126,8 @@ function createRecallFormatter(overrides = {}) {
|
|
|
102
126
|
if (matched) lines.push(matched);
|
|
103
127
|
const score = r.score(res, { showScore: !!opts.showScore, ...ctx });
|
|
104
128
|
if (score) lines.push(score);
|
|
129
|
+
const explain = r.explain(res, { showExplain: !!opts.showExplain, ...ctx });
|
|
130
|
+
if (explain) lines.push(explain);
|
|
105
131
|
const sep = r.separator(i, ctx);
|
|
106
132
|
if (sep !== null && sep !== undefined) lines.push(sep);
|
|
107
133
|
}
|
package/core/aquifer.js
CHANGED
|
@@ -1558,6 +1558,17 @@ function createAquifer(config = {}) {
|
|
|
1558
1558
|
});
|
|
1559
1559
|
},
|
|
1560
1560
|
|
|
1561
|
+
async feedbackStats(opts = {}) {
|
|
1562
|
+
await ensureMigrated();
|
|
1563
|
+
return storage.getFeedbackStats(pool, {
|
|
1564
|
+
schema,
|
|
1565
|
+
tenantId,
|
|
1566
|
+
agentId: opts.agentId || undefined,
|
|
1567
|
+
dateFrom: opts.dateFrom || undefined,
|
|
1568
|
+
dateTo: opts.dateTo || undefined,
|
|
1569
|
+
});
|
|
1570
|
+
},
|
|
1571
|
+
|
|
1561
1572
|
// --- admin ---
|
|
1562
1573
|
|
|
1563
1574
|
async getSession(sessionId, opts = {}) {
|
|
@@ -1837,6 +1848,7 @@ function createAquifer(config = {}) {
|
|
|
1837
1848
|
recallWeights: (config.insights && config.insights.recallWeights) || null,
|
|
1838
1849
|
recencyWindowDays: config.insights && Number.isFinite(config.insights.recencyWindowDays)
|
|
1839
1850
|
? config.insights.recencyWindowDays : undefined,
|
|
1851
|
+
dedup: config.insights && config.insights.dedup ? config.insights.dedup : undefined,
|
|
1840
1852
|
});
|
|
1841
1853
|
|
|
1842
1854
|
return aquifer;
|