@shadowforge0/aquifer-memory 1.0.2 → 1.2.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/README.md +29 -20
- package/consumers/claude-code.js +117 -0
- package/consumers/cli.js +17 -0
- package/consumers/default/daily-entries.js +196 -0
- package/consumers/default/index.js +282 -0
- package/consumers/default/prompts/summary.js +153 -0
- package/consumers/mcp.js +3 -23
- package/consumers/miranda/context-inject.js +119 -0
- package/consumers/miranda/daily-entries.js +224 -0
- package/consumers/miranda/index.js +353 -0
- package/consumers/miranda/instance.js +55 -0
- package/consumers/miranda/llm.js +99 -0
- package/consumers/miranda/prompts/summary.js +303 -0
- package/consumers/miranda/recall-format.js +74 -0
- package/consumers/miranda/workspace-files.js +91 -0
- package/consumers/openclaw-ext/index.js +38 -0
- package/consumers/openclaw-ext/openclaw.plugin.json +9 -0
- package/consumers/openclaw-ext/package.json +10 -0
- package/consumers/openclaw-plugin.js +66 -74
- package/consumers/opencode.js +21 -24
- package/consumers/shared/autodetect.js +64 -0
- package/consumers/shared/entity-parser.js +119 -0
- package/consumers/shared/ingest.js +148 -0
- package/consumers/shared/llm-autodetect.js +137 -0
- package/consumers/shared/normalize.js +129 -0
- package/consumers/shared/recall-format.js +110 -0
- package/core/aquifer.js +200 -82
- package/core/entity.js +29 -17
- package/core/storage.js +116 -45
- package/docs/postprocess-contract.md +132 -0
- package/index.js +9 -1
- package/package.json +23 -2
- package/pipeline/_http.js +1 -1
- package/pipeline/consolidation/apply.js +176 -0
- package/pipeline/consolidation/index.js +21 -0
- package/pipeline/extract-entities.js +2 -2
- package/pipeline/rerank.js +1 -1
- package/pipeline/summarize.js +4 -1
- package/schema/001-base.sql +61 -24
- package/schema/002-entities.sql +17 -3
- package/schema/004-facts.sql +67 -0
- package/scripts/diagnose-fts-zh.js +168 -134
- package/scripts/diagnose-vector.js +188 -0
- package/scripts/install-openclaw.sh +59 -0
- package/scripts/smoke.mjs +2 -2
package/core/aquifer.js
CHANGED
|
@@ -9,6 +9,7 @@ const entity = require('./entity');
|
|
|
9
9
|
const { hybridRank } = require('./hybrid-rank');
|
|
10
10
|
const { summarize } = require('../pipeline/summarize');
|
|
11
11
|
const { extractEntities } = require('../pipeline/extract-entities');
|
|
12
|
+
const { createEmbedder } = require('../pipeline/embed');
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Schema name validation
|
|
@@ -54,48 +55,104 @@ function buildRerankDocument(row, maxChars) {
|
|
|
54
55
|
return text;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// resolveEmbedFn — v1.2.0 embed autodetect (explicit > object > env > null)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function resolveEmbedFn(embedConfig, env) {
|
|
63
|
+
if (embedConfig && typeof embedConfig.fn === 'function') {
|
|
64
|
+
return embedConfig.fn;
|
|
65
|
+
}
|
|
66
|
+
if (embedConfig && embedConfig.provider) {
|
|
67
|
+
const embedder = createEmbedder(embedConfig);
|
|
68
|
+
return (texts) => embedder.embedBatch(texts);
|
|
69
|
+
}
|
|
70
|
+
const provider = env.EMBED_PROVIDER;
|
|
71
|
+
if (!provider) return null;
|
|
72
|
+
|
|
73
|
+
const opts = { provider };
|
|
74
|
+
if (provider === 'ollama') {
|
|
75
|
+
opts.ollamaUrl = env.OLLAMA_URL || env.AQUIFER_EMBED_BASE_URL || 'http://localhost:11434';
|
|
76
|
+
opts.model = env.AQUIFER_EMBED_MODEL || 'bge-m3';
|
|
77
|
+
} else if (provider === 'openai') {
|
|
78
|
+
opts.openaiApiKey = env.OPENAI_API_KEY;
|
|
79
|
+
if (!opts.openaiApiKey) {
|
|
80
|
+
throw new Error('EMBED_PROVIDER=openai requires OPENAI_API_KEY');
|
|
81
|
+
}
|
|
82
|
+
opts.openaiModel = env.AQUIFER_EMBED_MODEL || 'text-embedding-3-small';
|
|
83
|
+
if (env.AQUIFER_EMBED_DIM) opts.openaiDimensions = Number(env.AQUIFER_EMBED_DIM);
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`EMBED_PROVIDER=${provider} not supported by autodetect (use 'ollama' or 'openai', or pass config.embed.fn explicitly)`);
|
|
86
|
+
}
|
|
87
|
+
const embedder = createEmbedder(opts);
|
|
88
|
+
return (texts) => embedder.embedBatch(texts);
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
// ---------------------------------------------------------------------------
|
|
58
92
|
// createAquifer
|
|
59
93
|
// ---------------------------------------------------------------------------
|
|
60
94
|
|
|
61
|
-
function createAquifer(config) {
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
function createAquifer(config = {}) {
|
|
96
|
+
// v1.2.0: db falls back to DATABASE_URL / AQUIFER_DB_URL env so hosts can
|
|
97
|
+
// call createAquifer() with zero args for install-and-go.
|
|
98
|
+
const dbInput = config.db !== undefined
|
|
99
|
+
? config.db
|
|
100
|
+
: (process.env.DATABASE_URL || process.env.AQUIFER_DB_URL || null);
|
|
101
|
+
|
|
102
|
+
if (!dbInput) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'Aquifer requires a database: pass config.db (pg.Pool or connection string), '
|
|
105
|
+
+ 'or set DATABASE_URL / AQUIFER_DB_URL in the environment.'
|
|
106
|
+
);
|
|
64
107
|
}
|
|
65
108
|
|
|
66
|
-
const schema = config.schema || 'aquifer';
|
|
109
|
+
const schema = config.schema || process.env.AQUIFER_SCHEMA || 'aquifer';
|
|
67
110
|
validateSchema(schema);
|
|
68
111
|
|
|
69
112
|
if (config.tenantId === '') throw new Error('config.tenantId must not be empty');
|
|
70
|
-
const tenantId = config.tenantId || 'default';
|
|
113
|
+
const tenantId = config.tenantId || process.env.AQUIFER_TENANT_ID || 'default';
|
|
71
114
|
|
|
72
115
|
// Pool management
|
|
73
116
|
let pool;
|
|
74
117
|
let ownsPool = false;
|
|
75
|
-
if (typeof
|
|
76
|
-
pool = new Pool({ connectionString:
|
|
118
|
+
if (typeof dbInput === 'string') {
|
|
119
|
+
pool = new Pool({ connectionString: dbInput });
|
|
77
120
|
ownsPool = true;
|
|
78
121
|
} else {
|
|
79
|
-
pool =
|
|
122
|
+
pool = dbInput;
|
|
80
123
|
ownsPool = !!config.ownsPool; // allow factory to claim ownership
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
// Embed config (lazy — only required for recall/enrich)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
// v1.2.0 fallback chain:
|
|
128
|
+
// 1. config.embed.fn (explicit function)
|
|
129
|
+
// 2. config.embed.provider (build via createEmbedder)
|
|
130
|
+
// 3. EMBED_PROVIDER env + provider-specific key (zero-arg install-and-go)
|
|
131
|
+
// 4. null — defer to requireEmbed() at call time
|
|
132
|
+
const embedFn = resolveEmbedFn(config.embed, process.env);
|
|
87
133
|
function requireEmbed(op) {
|
|
88
|
-
if (!embedFn) throw new Error(`Aquifer.${op}() requires config.embed.fn (async (texts) => number[][])`);
|
|
134
|
+
if (!embedFn) throw new Error(`Aquifer.${op}() requires config.embed.fn or EMBED_PROVIDER env (async (texts) => number[][])`);
|
|
89
135
|
}
|
|
90
136
|
|
|
91
137
|
// LLM config (optional — only needed for enrich with built-in summarize)
|
|
92
|
-
|
|
138
|
+
// v1.2.0: falls back to AQUIFER_LLM_PROVIDER env + provider-specific key.
|
|
139
|
+
const { resolveLlmFn } = require('../consumers/shared/llm-autodetect');
|
|
140
|
+
const llmFn = resolveLlmFn(config.llm, process.env);
|
|
93
141
|
|
|
94
142
|
// Summarize config
|
|
95
143
|
const summarizePromptFn = config.summarize && config.summarize.prompt ? config.summarize.prompt : null;
|
|
96
144
|
|
|
145
|
+
// Enrich stale-claim window: a 'processing' session older than this is
|
|
146
|
+
// reclaimable by a concurrent enrich() caller (covers crashed workers).
|
|
147
|
+
const staleEnrichMinutes = Number.isFinite(config.staleEnrichMinutes)
|
|
148
|
+
? Math.max(1, Math.floor(config.staleEnrichMinutes))
|
|
149
|
+
: 10;
|
|
150
|
+
|
|
97
151
|
// Entity config
|
|
98
152
|
let entitiesEnabled = config.entities && config.entities.enabled === true;
|
|
153
|
+
|
|
154
|
+
// Facts config (opt-in consolidation lifecycle)
|
|
155
|
+
let factsEnabled = config.facts && config.facts.enabled === true;
|
|
99
156
|
const mergeCall = config.entities && config.entities.mergeCall !== undefined ? config.entities.mergeCall : true;
|
|
100
157
|
const entityPromptFn = config.entities && config.entities.prompt ? config.entities.prompt : null;
|
|
101
158
|
const entityScope = (config.entities && config.entities.scope) || 'default';
|
|
@@ -187,21 +244,38 @@ function createAquifer(config) {
|
|
|
187
244
|
// --- lifecycle ---
|
|
188
245
|
|
|
189
246
|
async migrate() {
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
247
|
+
// Advisory lock prevents concurrent migrations across processes.
|
|
248
|
+
// Lock key is derived from schema name to allow parallel migration
|
|
249
|
+
// of different schemas in the same database.
|
|
250
|
+
const lockKey = Buffer.from(`aquifer:${schema}`).reduce((h, b) => (h * 31 + b) & 0x7fffffff, 0);
|
|
251
|
+
await pool.query('SELECT pg_advisory_lock($1)', [lockKey]);
|
|
252
|
+
try {
|
|
253
|
+
// 1. Run base DDL
|
|
254
|
+
const baseSql = loadSql('001-base.sql', schema);
|
|
255
|
+
await pool.query(baseSql);
|
|
256
|
+
|
|
257
|
+
// 2. If entities enabled, run entity DDL
|
|
258
|
+
if (entitiesEnabled) {
|
|
259
|
+
const entitySql = loadSql('002-entities.sql', schema);
|
|
260
|
+
await pool.query(entitySql);
|
|
261
|
+
}
|
|
193
262
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await pool.query(entitySql);
|
|
198
|
-
}
|
|
263
|
+
// 3. Trust + feedback (always, not gated by entities)
|
|
264
|
+
const trustSql = loadSql('003-trust-feedback.sql', schema);
|
|
265
|
+
await pool.query(trustSql);
|
|
199
266
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
// 4. Facts / consolidation (opt-in)
|
|
268
|
+
if (factsEnabled) {
|
|
269
|
+
const factsSql = loadSql('004-facts.sql', schema);
|
|
270
|
+
await pool.query(factsSql);
|
|
271
|
+
}
|
|
203
272
|
|
|
204
|
-
|
|
273
|
+
migrated = true;
|
|
274
|
+
} finally {
|
|
275
|
+
await pool.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch((err) => {
|
|
276
|
+
console.warn(`[aquifer] failed to release migration advisory lock for schema "${schema}": ${err.message}`);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
205
279
|
},
|
|
206
280
|
|
|
207
281
|
async close() {
|
|
@@ -216,7 +290,7 @@ function createAquifer(config) {
|
|
|
216
290
|
sources.set(name, {
|
|
217
291
|
type: opts.type || 'custom',
|
|
218
292
|
search: opts.search || null,
|
|
219
|
-
weight: opts.weight !==
|
|
293
|
+
weight: opts.weight !== undefined && opts.weight !== undefined ? opts.weight : 1.0,
|
|
220
294
|
});
|
|
221
295
|
},
|
|
222
296
|
|
|
@@ -229,6 +303,32 @@ function createAquifer(config) {
|
|
|
229
303
|
}
|
|
230
304
|
},
|
|
231
305
|
|
|
306
|
+
async enableFacts() {
|
|
307
|
+
factsEnabled = true;
|
|
308
|
+
// Run the facts DDL (idempotent — all CREATE/ALTER use IF NOT EXISTS).
|
|
309
|
+
// Safe to call repeatedly; also safe to call before migrate() (will no-op
|
|
310
|
+
// until base schema exists, which enrich/commit will materialize).
|
|
311
|
+
await ensureMigrated();
|
|
312
|
+
const factsSql = loadSql('004-facts.sql', schema);
|
|
313
|
+
await pool.query(factsSql);
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
async consolidate(sessionId, opts = {}) {
|
|
317
|
+
if (!factsEnabled) throw new Error('aquifer.consolidate() requires enableFacts() first');
|
|
318
|
+
await ensureMigrated();
|
|
319
|
+
const { applyConsolidation } = require('../pipeline/consolidation');
|
|
320
|
+
const agentId = opts.agentId || 'agent';
|
|
321
|
+
return applyConsolidation(pool, {
|
|
322
|
+
actions: opts.actions || [],
|
|
323
|
+
agentId,
|
|
324
|
+
sessionId,
|
|
325
|
+
schema,
|
|
326
|
+
tenantId,
|
|
327
|
+
normalizeSubject: opts.normalizeSubject || null,
|
|
328
|
+
recapOverview: opts.recapOverview || '',
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
|
|
232
332
|
// --- write path ---
|
|
233
333
|
|
|
234
334
|
async commit(sessionId, messages, opts = {}) {
|
|
@@ -293,17 +393,19 @@ function createAquifer(config) {
|
|
|
293
393
|
const postProcess = opts.postProcess || null; // async (ctx) => void
|
|
294
394
|
const optModel = 'model' in opts ? opts.model : undefined; // undefined = no override
|
|
295
395
|
|
|
296
|
-
// 1. Optimistic lock: claim session for processing
|
|
297
|
-
// Also reclaim stale 'processing' sessions (
|
|
298
|
-
|
|
396
|
+
// 1. Optimistic lock: claim session for processing.
|
|
397
|
+
// Also reclaim stale 'processing' sessions (likely killed worker).
|
|
398
|
+
// Stale window is config.staleEnrichMinutes (default 10).
|
|
299
399
|
const claimResult = await pool.query(
|
|
300
400
|
`UPDATE ${qi(schema)}.sessions
|
|
301
401
|
SET processing_status = 'processing', processing_started_at = NOW()
|
|
302
402
|
WHERE session_id = $1 AND agent_id = $2 AND tenant_id = $3
|
|
303
403
|
AND (processing_status IN ('pending', 'failed')
|
|
304
|
-
OR (processing_status = 'processing'
|
|
404
|
+
OR (processing_status = 'processing'
|
|
405
|
+
AND (processing_started_at IS NULL
|
|
406
|
+
OR processing_started_at < NOW() - make_interval(mins => $4))))
|
|
305
407
|
RETURNING *`,
|
|
306
|
-
[sessionId, agentId, tenantId]
|
|
408
|
+
[sessionId, agentId, tenantId, staleEnrichMinutes]
|
|
307
409
|
);
|
|
308
410
|
const session = claimResult.rows[0];
|
|
309
411
|
if (!session) {
|
|
@@ -324,34 +426,46 @@ function createAquifer(config) {
|
|
|
324
426
|
// 2. Extract user turns
|
|
325
427
|
const turns = storage.extractUserTurns(normalized);
|
|
326
428
|
|
|
429
|
+
// Collected across pre-tx and tx phases; any non-empty warnings demote
|
|
430
|
+
// the final status from 'succeeded' to 'partial' (see step 8 below).
|
|
431
|
+
const warnings = [];
|
|
432
|
+
|
|
327
433
|
// 3. Summarize (custom or built-in)
|
|
328
434
|
let summaryResult = null;
|
|
329
435
|
let entityRaw = null;
|
|
330
436
|
let extra = null;
|
|
331
437
|
|
|
332
438
|
if (!skipSummary && normalized.length > 0) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
439
|
+
// Pre-transaction failures (customSummaryFn / summarize throws) would
|
|
440
|
+
// otherwise bubble out and leave the session stuck in 'processing'
|
|
441
|
+
// until stale reclaim. Capture as a warning so status ends 'partial',
|
|
442
|
+
// keeping parity with how embed/entity-extract failures are treated.
|
|
443
|
+
try {
|
|
444
|
+
if (customSummaryFn) {
|
|
445
|
+
// Custom pipeline: caller handles LLM call and parsing
|
|
446
|
+
summaryResult = await customSummaryFn(normalized);
|
|
447
|
+
if (summaryResult && summaryResult.entityRaw) entityRaw = summaryResult.entityRaw;
|
|
448
|
+
if (summaryResult && summaryResult.extra) extra = summaryResult.extra;
|
|
449
|
+
} else {
|
|
450
|
+
// Built-in pipeline
|
|
451
|
+
const doMergeEntities = entitiesEnabled && mergeCall && !skipEntities;
|
|
452
|
+
summaryResult = await summarize(normalized, {
|
|
453
|
+
llmFn,
|
|
454
|
+
promptFn: summarizePromptFn,
|
|
455
|
+
mergeEntities: doMergeEntities,
|
|
456
|
+
});
|
|
457
|
+
if (summaryResult.entityRaw) {
|
|
458
|
+
entityRaw = summaryResult.entityRaw;
|
|
459
|
+
}
|
|
348
460
|
}
|
|
461
|
+
} catch (e) {
|
|
462
|
+
warnings.push(`summary step failed: ${e.message}`);
|
|
463
|
+
summaryResult = null;
|
|
349
464
|
}
|
|
350
465
|
}
|
|
351
466
|
|
|
352
467
|
// 4. Pre-compute all LLM/embed results BEFORE opening transaction
|
|
353
468
|
// (avoids holding pool connection during slow LLM/embed calls)
|
|
354
|
-
const warnings = [];
|
|
355
469
|
let summaryEmbedding = null;
|
|
356
470
|
let turnVectors = null;
|
|
357
471
|
let parsedEntities = [];
|
|
@@ -485,7 +599,11 @@ function createAquifer(config) {
|
|
|
485
599
|
await client.query('ROLLBACK').catch(() => {});
|
|
486
600
|
try {
|
|
487
601
|
await storage.markStatus(pool, session.id, 'failed', err.message, { schema });
|
|
488
|
-
} catch (
|
|
602
|
+
} catch (markErr) {
|
|
603
|
+
// Secondary failure: session is stuck in 'processing' until stale reclaim.
|
|
604
|
+
// Surface so operators notice and don't silently rely on the timeout.
|
|
605
|
+
console.warn(`[aquifer] enrich failed for session ${sessionId} AND markStatus('failed') also failed: ${markErr.message}`);
|
|
606
|
+
}
|
|
489
607
|
throw err;
|
|
490
608
|
} finally {
|
|
491
609
|
client.release();
|
|
@@ -683,7 +801,7 @@ function createAquifer(config) {
|
|
|
683
801
|
entityScoreBySession.set(row.session_id, parseInt(row.entity_count) / maxCount);
|
|
684
802
|
}
|
|
685
803
|
}
|
|
686
|
-
} catch
|
|
804
|
+
} catch { /* entity search failure non-fatal */ }
|
|
687
805
|
}
|
|
688
806
|
|
|
689
807
|
// 3. Run search paths in parallel (conditioned on mode)
|
|
@@ -738,7 +856,7 @@ function createAquifer(config) {
|
|
|
738
856
|
for (const r of [...filteredFts, ...filteredEmb, ...filteredTurn]) {
|
|
739
857
|
const sid = r.session_id || String(r.id);
|
|
740
858
|
const ss = typeof r.structured_summary === 'string'
|
|
741
|
-
? (() => { try { return JSON.parse(r.structured_summary); } catch
|
|
859
|
+
? (() => { try { return JSON.parse(r.structured_summary); } catch { return null; } })()
|
|
742
860
|
: r.structured_summary;
|
|
743
861
|
if (ss && Array.isArray(ss.open_loops) && ss.open_loops.length > 0) {
|
|
744
862
|
openLoopSet.add(sid);
|
|
@@ -751,7 +869,7 @@ function createAquifer(config) {
|
|
|
751
869
|
const externalPromises = [];
|
|
752
870
|
for (const [name, sourceConfig] of sources) {
|
|
753
871
|
if (typeof sourceConfig.search === 'function') {
|
|
754
|
-
const w = sourceConfig.weight !==
|
|
872
|
+
const w = sourceConfig.weight !== undefined && sourceConfig.weight !== undefined ? sourceConfig.weight : 1.0;
|
|
755
873
|
externalPromises.push(
|
|
756
874
|
Promise.race([
|
|
757
875
|
sourceConfig.search(query, opts),
|
|
@@ -822,7 +940,7 @@ function createAquifer(config) {
|
|
|
822
940
|
if (sessionRowIds.length > 0) {
|
|
823
941
|
try {
|
|
824
942
|
await storage.recordAccess(pool, sessionRowIds, { schema });
|
|
825
|
-
} catch
|
|
943
|
+
} catch { /* access recording non-fatal */ }
|
|
826
944
|
}
|
|
827
945
|
|
|
828
946
|
// 8. Format results
|
|
@@ -833,7 +951,6 @@ function createAquifer(config) {
|
|
|
833
951
|
startedAt: r.started_at,
|
|
834
952
|
summaryText: r.summary_text || null,
|
|
835
953
|
structuredSummary: r.structured_summary || null,
|
|
836
|
-
summarySnippet: r.summary_snippet || null,
|
|
837
954
|
matchedTurnText: r.matched_turn_text || null,
|
|
838
955
|
matchedTurnIndex: r.matched_turn_index || null,
|
|
839
956
|
score: r._rerankScore ?? r._score,
|
|
@@ -904,35 +1021,31 @@ function createAquifer(config) {
|
|
|
904
1021
|
return { id: result.rows[0].id, sessionId, agentId, status: 'skipped' };
|
|
905
1022
|
},
|
|
906
1023
|
|
|
907
|
-
async getSessionFull(sessionId) {
|
|
908
|
-
const result = await pool.query(
|
|
909
|
-
`SELECT * FROM ${qi(schema)}.sessions
|
|
910
|
-
WHERE session_id = $1 AND tenant_id = $2
|
|
911
|
-
LIMIT 1`,
|
|
912
|
-
[sessionId, tenantId]
|
|
913
|
-
);
|
|
914
|
-
const session = result.rows[0];
|
|
915
|
-
if (!session) return null;
|
|
916
|
-
|
|
917
|
-
const sumResult = await pool.query(
|
|
918
|
-
`SELECT * FROM ${qi(schema)}.session_summaries
|
|
919
|
-
WHERE session_row_id = $1
|
|
920
|
-
LIMIT 1`,
|
|
921
|
-
[session.id]
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
return {
|
|
925
|
-
session,
|
|
926
|
-
summary: sumResult.rows[0] || null,
|
|
927
|
-
};
|
|
928
|
-
},
|
|
929
|
-
|
|
930
1024
|
// --- public config accessor ---
|
|
931
1025
|
|
|
932
1026
|
getConfig() {
|
|
933
1027
|
return { schema, tenantId };
|
|
934
1028
|
},
|
|
935
1029
|
|
|
1030
|
+
// v1.2.0: expose the internal pool so host persona layers can reuse it
|
|
1031
|
+
// for host-owned tables (e.g. daily_entries). Read-only — callers should
|
|
1032
|
+
// not call pool.end() on it; use aquifer.close() for that.
|
|
1033
|
+
getPool() {
|
|
1034
|
+
return pool;
|
|
1035
|
+
},
|
|
1036
|
+
|
|
1037
|
+
// v1.2.0: expose resolved LLM function. May be null if no llm.fn was
|
|
1038
|
+
// supplied and AQUIFER_LLM_PROVIDER env is unset. Persona layers that
|
|
1039
|
+
// implement custom summaryFn can reuse this instead of wiring their own.
|
|
1040
|
+
getLlmFn() {
|
|
1041
|
+
return llmFn;
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
// v1.2.0: expose resolved embed function (may be null same as LLM).
|
|
1045
|
+
getEmbedFn() {
|
|
1046
|
+
return embedFn;
|
|
1047
|
+
},
|
|
1048
|
+
|
|
936
1049
|
// --- admin query helpers ---
|
|
937
1050
|
|
|
938
1051
|
async getStats() {
|
|
@@ -965,7 +1078,7 @@ function createAquifer(config) {
|
|
|
965
1078
|
[tenantId]
|
|
966
1079
|
);
|
|
967
1080
|
entityCount = entResult.rows[0]?.count || 0;
|
|
968
|
-
} catch
|
|
1081
|
+
} catch { /* entities table may not exist */ }
|
|
969
1082
|
|
|
970
1083
|
return {
|
|
971
1084
|
sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
|
|
@@ -1024,7 +1137,11 @@ function createAquifer(config) {
|
|
|
1024
1137
|
const maxChars = opts.maxChars || 4000;
|
|
1025
1138
|
const format = opts.format || 'structured';
|
|
1026
1139
|
|
|
1027
|
-
|
|
1140
|
+
// 'partial' sessions have a summary but recorded warnings during enrich;
|
|
1141
|
+
// they are user-visible content, not in-progress — bootstrap must include
|
|
1142
|
+
// them alongside 'succeeded'. 'pending' / 'processing' have no summary
|
|
1143
|
+
// yet and are correctly excluded.
|
|
1144
|
+
const where = [`s.tenant_id = $1`, `s.processing_status IN ('succeeded', 'partial')`];
|
|
1028
1145
|
const params = [tenantId];
|
|
1029
1146
|
|
|
1030
1147
|
if (agentId) {
|
|
@@ -1037,7 +1154,10 @@ function createAquifer(config) {
|
|
|
1037
1154
|
}
|
|
1038
1155
|
|
|
1039
1156
|
params.push(lookbackDays);
|
|
1040
|
-
|
|
1157
|
+
// upsertSession sets ended_at on every commit but started_at / last_message_at
|
|
1158
|
+
// only when the caller supplies them — fall back through both so sessions
|
|
1159
|
+
// committed without explicit timestamps remain reachable.
|
|
1160
|
+
where.push(`COALESCE(s.last_message_at, s.ended_at, s.started_at) > now() - ($${params.length} || ' days')::interval`);
|
|
1041
1161
|
|
|
1042
1162
|
params.push(limit);
|
|
1043
1163
|
|
|
@@ -1047,7 +1167,7 @@ function createAquifer(config) {
|
|
|
1047
1167
|
FROM ${qi(schema)}.sessions s
|
|
1048
1168
|
JOIN ${qi(schema)}.session_summaries ss ON ss.session_row_id = s.id
|
|
1049
1169
|
WHERE ${where.join(' AND ')}
|
|
1050
|
-
ORDER BY s.started_at DESC
|
|
1170
|
+
ORDER BY COALESCE(s.last_message_at, s.ended_at, s.started_at) DESC
|
|
1051
1171
|
LIMIT $${params.length}`,
|
|
1052
1172
|
params
|
|
1053
1173
|
);
|
|
@@ -1126,8 +1246,6 @@ function formatBootstrapText(data, maxChars) {
|
|
|
1126
1246
|
}
|
|
1127
1247
|
|
|
1128
1248
|
let truncated = false;
|
|
1129
|
-
const parts = [];
|
|
1130
|
-
|
|
1131
1249
|
// Build session lines (newest first, truncate from oldest if over budget)
|
|
1132
1250
|
const sessionLines = [];
|
|
1133
1251
|
for (const s of data.sessions) {
|
package/core/entity.js
CHANGED
|
@@ -222,27 +222,40 @@ async function upsertEntityRelations(pool, {
|
|
|
222
222
|
}) {
|
|
223
223
|
if (!pairs || pairs.length === 0) return { upserted: 0 };
|
|
224
224
|
const ts = occurredAt || new Date().toISOString();
|
|
225
|
-
let upserted = 0;
|
|
226
225
|
|
|
226
|
+
// Filter and normalize pairs
|
|
227
|
+
const validPairs = [];
|
|
227
228
|
for (const { srcEntityId, dstEntityId } of pairs) {
|
|
228
229
|
if (!srcEntityId || !dstEntityId || srcEntityId === dstEntityId) continue;
|
|
230
|
+
validPairs.push({
|
|
231
|
+
lo: Math.min(srcEntityId, dstEntityId),
|
|
232
|
+
hi: Math.max(srcEntityId, dstEntityId),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
229
235
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
[lo, hi, ts]
|
|
241
|
-
);
|
|
242
|
-
upserted++;
|
|
236
|
+
if (validPairs.length === 0) return { upserted: 0 };
|
|
237
|
+
|
|
238
|
+
// Batch insert: multi-row VALUES
|
|
239
|
+
const valueClauses = [];
|
|
240
|
+
const params = [];
|
|
241
|
+
|
|
242
|
+
for (const { lo, hi } of validPairs) {
|
|
243
|
+
const off = params.length;
|
|
244
|
+
params.push(lo, hi, ts);
|
|
245
|
+
valueClauses.push(`($${off+1}, $${off+2}, 1, $${off+3}, $${off+3})`);
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
|
|
248
|
+
await pool.query(
|
|
249
|
+
`INSERT INTO ${qi(schema)}.entity_relations
|
|
250
|
+
(src_entity_id, dst_entity_id, co_occurrence_count, first_seen_at, last_seen_at)
|
|
251
|
+
VALUES ${valueClauses.join(',\n')}
|
|
252
|
+
ON CONFLICT (src_entity_id, dst_entity_id) DO UPDATE SET
|
|
253
|
+
co_occurrence_count = ${qi(schema)}.entity_relations.co_occurrence_count + 1,
|
|
254
|
+
last_seen_at = GREATEST(${qi(schema)}.entity_relations.last_seen_at, EXCLUDED.last_seen_at)`,
|
|
255
|
+
params
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return { upserted: validPairs.length };
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
// ---------------------------------------------------------------------------
|
|
@@ -373,8 +386,7 @@ async function resolveEntities(pool, {
|
|
|
373
386
|
if (!normQ || seen.has(normQ)) continue;
|
|
374
387
|
seen.set(normQ, true);
|
|
375
388
|
|
|
376
|
-
|
|
377
|
-
const result = await pool.query(
|
|
389
|
+
const result = await pool.query(
|
|
378
390
|
`SELECT id, name, normalized_name
|
|
379
391
|
FROM ${qi(schema)}.entities
|
|
380
392
|
WHERE status = 'active'
|