@shadowforge0/aquifer-memory 1.0.3 → 1.3.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/README.md +37 -29
- package/consumers/claude-code.js +117 -0
- package/consumers/cli.js +28 -1
- 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/profile.json +145 -0
- package/consumers/miranda/prompts/summary.js +303 -0
- package/consumers/miranda/recall-format.js +74 -0
- package/consumers/miranda/render-daily-md.js +186 -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 +209 -71
- package/core/artifacts.js +174 -0
- package/core/bundles.js +400 -0
- package/core/consolidation.js +340 -0
- package/core/decisions.js +164 -0
- package/core/entity.js +1 -3
- package/core/errors.js +97 -0
- package/core/handoff.js +153 -0
- package/core/mcp-manifest.js +131 -0
- package/core/narratives.js +212 -0
- package/core/profiles.js +171 -0
- package/core/state.js +163 -0
- package/core/storage.js +86 -28
- package/core/timeline.js +152 -0
- package/docs/postprocess-contract.md +132 -0
- package/index.js +23 -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-completion.sql +375 -0
- 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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Shared recall formatter — turns aquifer.recall() rows into human-readable
|
|
5
|
+
// text. The default is English and markdown-ish; consumers with a persona
|
|
6
|
+
// (Miranda: zh-TW narrative) can override individual renderers.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function truncate(s, n) {
|
|
10
|
+
if (!s) return '';
|
|
11
|
+
const str = String(s);
|
|
12
|
+
return str.length > n ? `${str.slice(0, n)}...` : str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatDateIso(value) {
|
|
16
|
+
if (!value) return 'unknown';
|
|
17
|
+
const d = new Date(value);
|
|
18
|
+
return Number.isNaN(d.getTime()) ? 'unknown' : d.toISOString().slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Default English renderers --------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const defaultRenderers = {
|
|
24
|
+
header({ results, query }) {
|
|
25
|
+
if (!query) return null;
|
|
26
|
+
return `Found ${results.length} result(s) for "${query}":`;
|
|
27
|
+
},
|
|
28
|
+
empty({ query }) {
|
|
29
|
+
return query ? `No results found for "${query}".` : 'No matching sessions found.';
|
|
30
|
+
},
|
|
31
|
+
title(result, index) {
|
|
32
|
+
const ss = result.structuredSummary || {};
|
|
33
|
+
const title = ss.title || truncate(result.summaryText, 60) || '(untitled)';
|
|
34
|
+
const date = formatDateIso(result.startedAt);
|
|
35
|
+
const agent = result.agentId || 'default';
|
|
36
|
+
return `### ${index + 1}. ${title} (${date}, ${agent})`;
|
|
37
|
+
},
|
|
38
|
+
body(result) {
|
|
39
|
+
const ss = result.structuredSummary || {};
|
|
40
|
+
const text = ss.overview || result.summaryText || '';
|
|
41
|
+
return text ? truncate(text, 300) : null;
|
|
42
|
+
},
|
|
43
|
+
matched(result) {
|
|
44
|
+
return result.matchedTurnText ? `Matched turn: ${truncate(result.matchedTurnText, 200)}` : null;
|
|
45
|
+
},
|
|
46
|
+
score(result, { showScore }) {
|
|
47
|
+
if (!showScore) return null;
|
|
48
|
+
return `Score: ${typeof result.score === 'number' ? result.score.toFixed(3) : '?'}`;
|
|
49
|
+
},
|
|
50
|
+
separator() {
|
|
51
|
+
return '';
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a formatter with optional per-renderer overrides.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} [overrides] — renderers to override: header/empty/title/body/matched/score/separator
|
|
59
|
+
* @returns {(results: any[], opts?: object) => string}
|
|
60
|
+
*/
|
|
61
|
+
function createRecallFormatter(overrides = {}) {
|
|
62
|
+
const r = { ...defaultRenderers, ...overrides };
|
|
63
|
+
|
|
64
|
+
return function format(results, opts = {}) {
|
|
65
|
+
const safeResults = Array.isArray(results) ? results : [];
|
|
66
|
+
const ctx = { query: opts.query || null, results: safeResults };
|
|
67
|
+
|
|
68
|
+
if (safeResults.length === 0) {
|
|
69
|
+
return r.empty(ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lines = [];
|
|
73
|
+
const header = r.header(ctx);
|
|
74
|
+
if (header) { lines.push(header); lines.push(''); }
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < safeResults.length; i++) {
|
|
77
|
+
const res = safeResults[i];
|
|
78
|
+
const title = r.title(res, i, ctx);
|
|
79
|
+
if (title) lines.push(title);
|
|
80
|
+
const body = r.body(res, i, ctx);
|
|
81
|
+
if (body) lines.push(body);
|
|
82
|
+
const matched = r.matched(res, i, ctx);
|
|
83
|
+
if (matched) lines.push(matched);
|
|
84
|
+
const score = r.score(res, { showScore: !!opts.showScore, ...ctx });
|
|
85
|
+
if (score) lines.push(score);
|
|
86
|
+
const sep = r.separator(i, ctx);
|
|
87
|
+
if (sep !== null && sep !== undefined) lines.push(sep);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Trim trailing empty separator
|
|
91
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
92
|
+
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Pre-built default English formatter
|
|
98
|
+
const defaultFormatter = createRecallFormatter();
|
|
99
|
+
|
|
100
|
+
function formatRecallResults(results, opts = {}) {
|
|
101
|
+
return defaultFormatter(results, opts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
createRecallFormatter,
|
|
106
|
+
formatRecallResults,
|
|
107
|
+
truncate,
|
|
108
|
+
formatDateIso,
|
|
109
|
+
defaultRenderers,
|
|
110
|
+
};
|
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';
|
|
@@ -207,9 +264,23 @@ function createAquifer(config) {
|
|
|
207
264
|
const trustSql = loadSql('003-trust-feedback.sql', schema);
|
|
208
265
|
await pool.query(trustSql);
|
|
209
266
|
|
|
267
|
+
// 4. Facts / consolidation (opt-in)
|
|
268
|
+
if (factsEnabled) {
|
|
269
|
+
const factsSql = loadSql('004-facts.sql', schema);
|
|
270
|
+
await pool.query(factsSql);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 5. Completion foundation (always, additive): narratives,
|
|
274
|
+
// consumer_profiles, sessions.consolidation_phases. Pure additive DDL
|
|
275
|
+
// with IF NOT EXISTS guards — safe on every migrate() call.
|
|
276
|
+
const completionSql = loadSql('004-completion.sql', schema);
|
|
277
|
+
await pool.query(completionSql);
|
|
278
|
+
|
|
210
279
|
migrated = true;
|
|
211
280
|
} finally {
|
|
212
|
-
await pool.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch(() => {
|
|
281
|
+
await pool.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch((err) => {
|
|
282
|
+
console.warn(`[aquifer] failed to release migration advisory lock for schema "${schema}": ${err.message}`);
|
|
283
|
+
});
|
|
213
284
|
}
|
|
214
285
|
},
|
|
215
286
|
|
|
@@ -225,7 +296,7 @@ function createAquifer(config) {
|
|
|
225
296
|
sources.set(name, {
|
|
226
297
|
type: opts.type || 'custom',
|
|
227
298
|
search: opts.search || null,
|
|
228
|
-
weight: opts.weight !==
|
|
299
|
+
weight: opts.weight !== undefined && opts.weight !== undefined ? opts.weight : 1.0,
|
|
229
300
|
});
|
|
230
301
|
},
|
|
231
302
|
|
|
@@ -238,6 +309,32 @@ function createAquifer(config) {
|
|
|
238
309
|
}
|
|
239
310
|
},
|
|
240
311
|
|
|
312
|
+
async enableFacts() {
|
|
313
|
+
factsEnabled = true;
|
|
314
|
+
// Run the facts DDL (idempotent — all CREATE/ALTER use IF NOT EXISTS).
|
|
315
|
+
// Safe to call repeatedly; also safe to call before migrate() (will no-op
|
|
316
|
+
// until base schema exists, which enrich/commit will materialize).
|
|
317
|
+
await ensureMigrated();
|
|
318
|
+
const factsSql = loadSql('004-facts.sql', schema);
|
|
319
|
+
await pool.query(factsSql);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
async consolidate(sessionId, opts = {}) {
|
|
323
|
+
if (!factsEnabled) throw new Error('aquifer.consolidate() requires enableFacts() first');
|
|
324
|
+
await ensureMigrated();
|
|
325
|
+
const { applyConsolidation } = require('../pipeline/consolidation');
|
|
326
|
+
const agentId = opts.agentId || 'agent';
|
|
327
|
+
return applyConsolidation(pool, {
|
|
328
|
+
actions: opts.actions || [],
|
|
329
|
+
agentId,
|
|
330
|
+
sessionId,
|
|
331
|
+
schema,
|
|
332
|
+
tenantId,
|
|
333
|
+
normalizeSubject: opts.normalizeSubject || null,
|
|
334
|
+
recapOverview: opts.recapOverview || '',
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
|
|
241
338
|
// --- write path ---
|
|
242
339
|
|
|
243
340
|
async commit(sessionId, messages, opts = {}) {
|
|
@@ -302,17 +399,19 @@ function createAquifer(config) {
|
|
|
302
399
|
const postProcess = opts.postProcess || null; // async (ctx) => void
|
|
303
400
|
const optModel = 'model' in opts ? opts.model : undefined; // undefined = no override
|
|
304
401
|
|
|
305
|
-
// 1. Optimistic lock: claim session for processing
|
|
306
|
-
// Also reclaim stale 'processing' sessions (
|
|
307
|
-
|
|
402
|
+
// 1. Optimistic lock: claim session for processing.
|
|
403
|
+
// Also reclaim stale 'processing' sessions (likely killed worker).
|
|
404
|
+
// Stale window is config.staleEnrichMinutes (default 10).
|
|
308
405
|
const claimResult = await pool.query(
|
|
309
406
|
`UPDATE ${qi(schema)}.sessions
|
|
310
407
|
SET processing_status = 'processing', processing_started_at = NOW()
|
|
311
408
|
WHERE session_id = $1 AND agent_id = $2 AND tenant_id = $3
|
|
312
409
|
AND (processing_status IN ('pending', 'failed')
|
|
313
|
-
OR (processing_status = 'processing'
|
|
410
|
+
OR (processing_status = 'processing'
|
|
411
|
+
AND (processing_started_at IS NULL
|
|
412
|
+
OR processing_started_at < NOW() - make_interval(mins => $4))))
|
|
314
413
|
RETURNING *`,
|
|
315
|
-
[sessionId, agentId, tenantId]
|
|
414
|
+
[sessionId, agentId, tenantId, staleEnrichMinutes]
|
|
316
415
|
);
|
|
317
416
|
const session = claimResult.rows[0];
|
|
318
417
|
if (!session) {
|
|
@@ -333,34 +432,46 @@ function createAquifer(config) {
|
|
|
333
432
|
// 2. Extract user turns
|
|
334
433
|
const turns = storage.extractUserTurns(normalized);
|
|
335
434
|
|
|
435
|
+
// Collected across pre-tx and tx phases; any non-empty warnings demote
|
|
436
|
+
// the final status from 'succeeded' to 'partial' (see step 8 below).
|
|
437
|
+
const warnings = [];
|
|
438
|
+
|
|
336
439
|
// 3. Summarize (custom or built-in)
|
|
337
440
|
let summaryResult = null;
|
|
338
441
|
let entityRaw = null;
|
|
339
442
|
let extra = null;
|
|
340
443
|
|
|
341
444
|
if (!skipSummary && normalized.length > 0) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
445
|
+
// Pre-transaction failures (customSummaryFn / summarize throws) would
|
|
446
|
+
// otherwise bubble out and leave the session stuck in 'processing'
|
|
447
|
+
// until stale reclaim. Capture as a warning so status ends 'partial',
|
|
448
|
+
// keeping parity with how embed/entity-extract failures are treated.
|
|
449
|
+
try {
|
|
450
|
+
if (customSummaryFn) {
|
|
451
|
+
// Custom pipeline: caller handles LLM call and parsing
|
|
452
|
+
summaryResult = await customSummaryFn(normalized);
|
|
453
|
+
if (summaryResult && summaryResult.entityRaw) entityRaw = summaryResult.entityRaw;
|
|
454
|
+
if (summaryResult && summaryResult.extra) extra = summaryResult.extra;
|
|
455
|
+
} else {
|
|
456
|
+
// Built-in pipeline
|
|
457
|
+
const doMergeEntities = entitiesEnabled && mergeCall && !skipEntities;
|
|
458
|
+
summaryResult = await summarize(normalized, {
|
|
459
|
+
llmFn,
|
|
460
|
+
promptFn: summarizePromptFn,
|
|
461
|
+
mergeEntities: doMergeEntities,
|
|
462
|
+
});
|
|
463
|
+
if (summaryResult.entityRaw) {
|
|
464
|
+
entityRaw = summaryResult.entityRaw;
|
|
465
|
+
}
|
|
357
466
|
}
|
|
467
|
+
} catch (e) {
|
|
468
|
+
warnings.push(`summary step failed: ${e.message}`);
|
|
469
|
+
summaryResult = null;
|
|
358
470
|
}
|
|
359
471
|
}
|
|
360
472
|
|
|
361
473
|
// 4. Pre-compute all LLM/embed results BEFORE opening transaction
|
|
362
474
|
// (avoids holding pool connection during slow LLM/embed calls)
|
|
363
|
-
const warnings = [];
|
|
364
475
|
let summaryEmbedding = null;
|
|
365
476
|
let turnVectors = null;
|
|
366
477
|
let parsedEntities = [];
|
|
@@ -494,7 +605,11 @@ function createAquifer(config) {
|
|
|
494
605
|
await client.query('ROLLBACK').catch(() => {});
|
|
495
606
|
try {
|
|
496
607
|
await storage.markStatus(pool, session.id, 'failed', err.message, { schema });
|
|
497
|
-
} catch (
|
|
608
|
+
} catch (markErr) {
|
|
609
|
+
// Secondary failure: session is stuck in 'processing' until stale reclaim.
|
|
610
|
+
// Surface so operators notice and don't silently rely on the timeout.
|
|
611
|
+
console.warn(`[aquifer] enrich failed for session ${sessionId} AND markStatus('failed') also failed: ${markErr.message}`);
|
|
612
|
+
}
|
|
498
613
|
throw err;
|
|
499
614
|
} finally {
|
|
500
615
|
client.release();
|
|
@@ -692,7 +807,7 @@ function createAquifer(config) {
|
|
|
692
807
|
entityScoreBySession.set(row.session_id, parseInt(row.entity_count) / maxCount);
|
|
693
808
|
}
|
|
694
809
|
}
|
|
695
|
-
} catch
|
|
810
|
+
} catch { /* entity search failure non-fatal */ }
|
|
696
811
|
}
|
|
697
812
|
|
|
698
813
|
// 3. Run search paths in parallel (conditioned on mode)
|
|
@@ -747,7 +862,7 @@ function createAquifer(config) {
|
|
|
747
862
|
for (const r of [...filteredFts, ...filteredEmb, ...filteredTurn]) {
|
|
748
863
|
const sid = r.session_id || String(r.id);
|
|
749
864
|
const ss = typeof r.structured_summary === 'string'
|
|
750
|
-
? (() => { try { return JSON.parse(r.structured_summary); } catch
|
|
865
|
+
? (() => { try { return JSON.parse(r.structured_summary); } catch { return null; } })()
|
|
751
866
|
: r.structured_summary;
|
|
752
867
|
if (ss && Array.isArray(ss.open_loops) && ss.open_loops.length > 0) {
|
|
753
868
|
openLoopSet.add(sid);
|
|
@@ -760,7 +875,7 @@ function createAquifer(config) {
|
|
|
760
875
|
const externalPromises = [];
|
|
761
876
|
for (const [name, sourceConfig] of sources) {
|
|
762
877
|
if (typeof sourceConfig.search === 'function') {
|
|
763
|
-
const w = sourceConfig.weight !==
|
|
878
|
+
const w = sourceConfig.weight !== undefined && sourceConfig.weight !== undefined ? sourceConfig.weight : 1.0;
|
|
764
879
|
externalPromises.push(
|
|
765
880
|
Promise.race([
|
|
766
881
|
sourceConfig.search(query, opts),
|
|
@@ -831,7 +946,7 @@ function createAquifer(config) {
|
|
|
831
946
|
if (sessionRowIds.length > 0) {
|
|
832
947
|
try {
|
|
833
948
|
await storage.recordAccess(pool, sessionRowIds, { schema });
|
|
834
|
-
} catch
|
|
949
|
+
} catch { /* access recording non-fatal */ }
|
|
835
950
|
}
|
|
836
951
|
|
|
837
952
|
// 8. Format results
|
|
@@ -842,7 +957,6 @@ function createAquifer(config) {
|
|
|
842
957
|
startedAt: r.started_at,
|
|
843
958
|
summaryText: r.summary_text || null,
|
|
844
959
|
structuredSummary: r.structured_summary || null,
|
|
845
|
-
summarySnippet: r.summary_snippet || null,
|
|
846
960
|
matchedTurnText: r.matched_turn_text || null,
|
|
847
961
|
matchedTurnIndex: r.matched_turn_index || null,
|
|
848
962
|
score: r._rerankScore ?? r._score,
|
|
@@ -913,35 +1027,31 @@ function createAquifer(config) {
|
|
|
913
1027
|
return { id: result.rows[0].id, sessionId, agentId, status: 'skipped' };
|
|
914
1028
|
},
|
|
915
1029
|
|
|
916
|
-
async getSessionFull(sessionId) {
|
|
917
|
-
const result = await pool.query(
|
|
918
|
-
`SELECT * FROM ${qi(schema)}.sessions
|
|
919
|
-
WHERE session_id = $1 AND tenant_id = $2
|
|
920
|
-
LIMIT 1`,
|
|
921
|
-
[sessionId, tenantId]
|
|
922
|
-
);
|
|
923
|
-
const session = result.rows[0];
|
|
924
|
-
if (!session) return null;
|
|
925
|
-
|
|
926
|
-
const sumResult = await pool.query(
|
|
927
|
-
`SELECT * FROM ${qi(schema)}.session_summaries
|
|
928
|
-
WHERE session_row_id = $1
|
|
929
|
-
LIMIT 1`,
|
|
930
|
-
[session.id]
|
|
931
|
-
);
|
|
932
|
-
|
|
933
|
-
return {
|
|
934
|
-
session,
|
|
935
|
-
summary: sumResult.rows[0] || null,
|
|
936
|
-
};
|
|
937
|
-
},
|
|
938
|
-
|
|
939
1030
|
// --- public config accessor ---
|
|
940
1031
|
|
|
941
1032
|
getConfig() {
|
|
942
1033
|
return { schema, tenantId };
|
|
943
1034
|
},
|
|
944
1035
|
|
|
1036
|
+
// v1.2.0: expose the internal pool so host persona layers can reuse it
|
|
1037
|
+
// for host-owned tables (e.g. daily_entries). Read-only — callers should
|
|
1038
|
+
// not call pool.end() on it; use aquifer.close() for that.
|
|
1039
|
+
getPool() {
|
|
1040
|
+
return pool;
|
|
1041
|
+
},
|
|
1042
|
+
|
|
1043
|
+
// v1.2.0: expose resolved LLM function. May be null if no llm.fn was
|
|
1044
|
+
// supplied and AQUIFER_LLM_PROVIDER env is unset. Persona layers that
|
|
1045
|
+
// implement custom summaryFn can reuse this instead of wiring their own.
|
|
1046
|
+
getLlmFn() {
|
|
1047
|
+
return llmFn;
|
|
1048
|
+
},
|
|
1049
|
+
|
|
1050
|
+
// v1.2.0: expose resolved embed function (may be null same as LLM).
|
|
1051
|
+
getEmbedFn() {
|
|
1052
|
+
return embedFn;
|
|
1053
|
+
},
|
|
1054
|
+
|
|
945
1055
|
// --- admin query helpers ---
|
|
946
1056
|
|
|
947
1057
|
async getStats() {
|
|
@@ -974,7 +1084,7 @@ function createAquifer(config) {
|
|
|
974
1084
|
[tenantId]
|
|
975
1085
|
);
|
|
976
1086
|
entityCount = entResult.rows[0]?.count || 0;
|
|
977
|
-
} catch
|
|
1087
|
+
} catch { /* entities table may not exist */ }
|
|
978
1088
|
|
|
979
1089
|
return {
|
|
980
1090
|
sessions: Object.fromEntries(sessions.rows.map(r => [r.processing_status, r.count])),
|
|
@@ -1033,7 +1143,11 @@ function createAquifer(config) {
|
|
|
1033
1143
|
const maxChars = opts.maxChars || 4000;
|
|
1034
1144
|
const format = opts.format || 'structured';
|
|
1035
1145
|
|
|
1036
|
-
|
|
1146
|
+
// 'partial' sessions have a summary but recorded warnings during enrich;
|
|
1147
|
+
// they are user-visible content, not in-progress — bootstrap must include
|
|
1148
|
+
// them alongside 'succeeded'. 'pending' / 'processing' have no summary
|
|
1149
|
+
// yet and are correctly excluded.
|
|
1150
|
+
const where = [`s.tenant_id = $1`, `s.processing_status IN ('succeeded', 'partial')`];
|
|
1037
1151
|
const params = [tenantId];
|
|
1038
1152
|
|
|
1039
1153
|
if (agentId) {
|
|
@@ -1046,7 +1160,10 @@ function createAquifer(config) {
|
|
|
1046
1160
|
}
|
|
1047
1161
|
|
|
1048
1162
|
params.push(lookbackDays);
|
|
1049
|
-
|
|
1163
|
+
// upsertSession sets ended_at on every commit but started_at / last_message_at
|
|
1164
|
+
// only when the caller supplies them — fall back through both so sessions
|
|
1165
|
+
// committed without explicit timestamps remain reachable.
|
|
1166
|
+
where.push(`COALESCE(s.last_message_at, s.ended_at, s.started_at) > now() - ($${params.length} || ' days')::interval`);
|
|
1050
1167
|
|
|
1051
1168
|
params.push(limit);
|
|
1052
1169
|
|
|
@@ -1056,7 +1173,7 @@ function createAquifer(config) {
|
|
|
1056
1173
|
FROM ${qi(schema)}.sessions s
|
|
1057
1174
|
JOIN ${qi(schema)}.session_summaries ss ON ss.session_row_id = s.id
|
|
1058
1175
|
WHERE ${where.join(' AND ')}
|
|
1059
|
-
ORDER BY s.started_at DESC
|
|
1176
|
+
ORDER BY COALESCE(s.last_message_at, s.ended_at, s.started_at) DESC
|
|
1060
1177
|
LIMIT $${params.length}`,
|
|
1061
1178
|
params
|
|
1062
1179
|
);
|
|
@@ -1122,6 +1239,29 @@ function createAquifer(config) {
|
|
|
1122
1239
|
},
|
|
1123
1240
|
};
|
|
1124
1241
|
|
|
1242
|
+
// Completion-capability surfaces (P2). All methods return AqResult envelope;
|
|
1243
|
+
// DDL materialised in schema/004-completion.sql (migrated unconditionally,
|
|
1244
|
+
// additive only). See core/errors.js for envelope shape.
|
|
1245
|
+
const { createNarratives } = require('./narratives');
|
|
1246
|
+
const { createTimeline } = require('./timeline');
|
|
1247
|
+
const { createState } = require('./state');
|
|
1248
|
+
const { createHandoff } = require('./handoff');
|
|
1249
|
+
const { createProfiles } = require('./profiles');
|
|
1250
|
+
const { createDecisions } = require('./decisions');
|
|
1251
|
+
const { createArtifacts } = require('./artifacts');
|
|
1252
|
+
const { createConsolidation } = require('./consolidation');
|
|
1253
|
+
const { createBundles } = require('./bundles');
|
|
1254
|
+
const qSchema = qi(schema);
|
|
1255
|
+
aquifer.narratives = createNarratives({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1256
|
+
aquifer.timeline = createTimeline({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1257
|
+
aquifer.state = createState({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1258
|
+
aquifer.handoff = createHandoff({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1259
|
+
aquifer.profiles = createProfiles({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1260
|
+
aquifer.decisions = createDecisions({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1261
|
+
aquifer.artifacts = createArtifacts({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1262
|
+
aquifer.consolidation = createConsolidation({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1263
|
+
aquifer.bundles = createBundles({ pool, schema: qSchema, defaultTenantId: tenantId });
|
|
1264
|
+
|
|
1125
1265
|
return aquifer;
|
|
1126
1266
|
}
|
|
1127
1267
|
|
|
@@ -1135,8 +1275,6 @@ function formatBootstrapText(data, maxChars) {
|
|
|
1135
1275
|
}
|
|
1136
1276
|
|
|
1137
1277
|
let truncated = false;
|
|
1138
|
-
const parts = [];
|
|
1139
|
-
|
|
1140
1278
|
// Build session lines (newest first, truncate from oldest if over budget)
|
|
1141
1279
|
const sessionLines = [];
|
|
1142
1280
|
for (const s of data.sessions) {
|