@shadowforge0/aquifer-memory 1.6.0 → 1.8.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.
Files changed (44) hide show
  1. package/.env.example +8 -0
  2. package/README.md +72 -0
  3. package/README_CN.md +17 -0
  4. package/README_TW.md +4 -0
  5. package/aquifer.config.example.json +19 -0
  6. package/consumers/cli.js +259 -12
  7. package/consumers/codex-active-checkpoint.js +186 -0
  8. package/consumers/codex-current-memory.js +106 -0
  9. package/consumers/codex-handoff.js +551 -6
  10. package/consumers/codex.js +209 -25
  11. package/consumers/mcp.js +144 -6
  12. package/consumers/shared/config.js +60 -1
  13. package/consumers/shared/factory.js +10 -3
  14. package/core/aquifer.js +357 -838
  15. package/core/backends/capabilities.js +89 -0
  16. package/core/backends/local.js +430 -0
  17. package/core/legacy-bootstrap.js +140 -0
  18. package/core/mcp-manifest.js +66 -2
  19. package/core/memory-bootstrap.js +20 -8
  20. package/core/memory-consolidation.js +365 -11
  21. package/core/memory-promotion.js +157 -26
  22. package/core/memory-recall.js +341 -22
  23. package/core/memory-records.js +347 -11
  24. package/core/memory-serving.js +132 -0
  25. package/core/postgres-migrations.js +533 -0
  26. package/core/public-session-filter.js +40 -0
  27. package/core/recall-runtime.js +115 -0
  28. package/core/scope-attribution.js +279 -0
  29. package/core/session-checkpoint-producer.js +412 -0
  30. package/core/session-checkpoints.js +432 -0
  31. package/core/session-finalization.js +98 -2
  32. package/core/storage-checkpoints.js +546 -0
  33. package/core/storage.js +121 -8
  34. package/docs/getting-started.md +6 -0
  35. package/docs/setup.md +66 -3
  36. package/package.json +8 -4
  37. package/schema/014-v1-checkpoint-runs.sql +349 -0
  38. package/schema/015-v1-evidence-items.sql +92 -0
  39. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  40. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  41. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  42. package/scripts/codex-checkpoint-commands.js +464 -0
  43. package/scripts/codex-checkpoint-runtime.js +520 -0
  44. package/scripts/codex-recovery.js +246 -1
@@ -0,0 +1,533 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SCHEMA_RE = /^[a-zA-Z_]\w{0,62}$/;
7
+
8
+ function validateSchema(schema) {
9
+ if (!SCHEMA_RE.test(schema)) {
10
+ throw new Error(`Invalid schema name: "${schema}". Must match /^[a-zA-Z_]\\w{0,62}$/`);
11
+ }
12
+ }
13
+
14
+ function qi(identifier) {
15
+ return `"${identifier}"`;
16
+ }
17
+
18
+ function loadSql(filename, schema) {
19
+ const filePath = path.join(__dirname, '..', 'schema', filename);
20
+ const raw = fs.readFileSync(filePath, 'utf8');
21
+ return raw.replace(/\$\{schema\}/g, qi(schema));
22
+ }
23
+
24
+ const MIGRATION_PLAN = [
25
+ { id: '001-base', file: '001-base.sql', always: true, signature: 'sessions' },
26
+ { id: '002-entities', file: '002-entities.sql', gate: 'entities', signature: 'entities' },
27
+ { id: '003-trust-feedback', file: '003-trust-feedback.sql', always: true, signature: 'session_feedback' },
28
+ { id: '004-facts', file: '004-facts.sql', gate: 'facts', signature: 'facts' },
29
+ { id: '004-completion', file: '004-completion.sql', always: true, signature: 'narratives' },
30
+ { id: '005-entity-state-history',file: '005-entity-state-history.sql',gate: 'entities', signature: 'entity_state_history' },
31
+ { id: '006-insights', file: '006-insights.sql', always: true, signature: 'insights' },
32
+ { id: '007-v1-foundation', file: '007-v1-foundation.sql', always: true, signature: 'memory_records' },
33
+ { id: '008-session-finalizations',file: '008-session-finalizations.sql',always: true, signature: 'session_finalizations' },
34
+ { id: '009-v1-assertion-plane', file: '009-v1-assertion-plane.sql', always: true, signature: 'fact_assertions_v1' },
35
+ { id: '010-v1-finalization-review',file: '010-v1-finalization-review.sql',always: true, signature: 'finalization_candidates' },
36
+ { id: '011-v1-compaction-claim', file: '011-v1-compaction-claim.sql', always: true, signature: { table: 'compaction_runs', column: 'apply_token' } },
37
+ { id: '012-v1-compaction-lease', file: '012-v1-compaction-lease.sql', always: true, signature: { table: 'compaction_runs', column: 'lease_expires_at' } },
38
+ { id: '013-v1-compaction-lineage', file: '013-v1-compaction-lineage.sql', always: true, signature: 'compaction_candidates' },
39
+ {
40
+ id: '014-v1-checkpoint-runs',
41
+ file: '014-v1-checkpoint-runs.sql',
42
+ always: true,
43
+ signature: [
44
+ { table: 'session_finalizations', column: 'scope_snapshot' },
45
+ { table: 'checkpoint_runs', column: 'scope_id' },
46
+ { table: 'checkpoint_run_sources', column: 'finalization_id' },
47
+ ],
48
+ },
49
+ {
50
+ id: '015-v1-evidence-items',
51
+ file: '015-v1-evidence-items.sql',
52
+ always: true,
53
+ signature: [
54
+ 'evidence_items',
55
+ { table: 'evidence_refs', column: 'evidence_item_id' },
56
+ ],
57
+ },
58
+ {
59
+ id: '016-v1-evidence-ref-multi-item',
60
+ file: '016-v1-evidence-ref-multi-item.sql',
61
+ always: true,
62
+ signature: [
63
+ { index: 'idx_evidence_refs_source_dedupe' },
64
+ { index: 'idx_evidence_refs_evidence_item_dedupe' },
65
+ ],
66
+ },
67
+ {
68
+ id: '017-v1-memory-record-embeddings',
69
+ file: '017-v1-memory-record-embeddings.sql',
70
+ always: true,
71
+ signature: [
72
+ { table: 'memory_records', column: 'embedding' },
73
+ ],
74
+ },
75
+ {
76
+ id: '018-v1-finalization-candidate-envelope',
77
+ file: '018-v1-finalization-candidate-envelope.sql',
78
+ always: true,
79
+ signature: [
80
+ { table: 'session_finalizations', column: 'candidate_envelope' },
81
+ { table: 'finalization_candidates', column: 'candidate_hash' },
82
+ ],
83
+ },
84
+ ];
85
+
86
+ function createPostgresMigrationRuntime(opts = {}) {
87
+ const {
88
+ pool,
89
+ schema,
90
+ migrations = {},
91
+ getEntitiesEnabled = () => false,
92
+ getFactsEnabled = () => false,
93
+ initialFtsConfig = null,
94
+ } = opts;
95
+
96
+ let migrated = false;
97
+ let migratePromise = null;
98
+ let ftsConfig = initialFtsConfig;
99
+
100
+ const migrationsMode = (() => {
101
+ const raw = migrations.mode;
102
+ if (raw === 'apply' || raw === 'check' || raw === 'off') return raw;
103
+ if (raw === undefined || raw === null) return 'apply';
104
+ throw new Error(`config.migrations.mode must be 'apply' | 'check' | 'off' (got ${JSON.stringify(raw)})`);
105
+ })();
106
+ const migrationLockTimeoutMs = Number.isFinite(migrations.lockTimeoutMs)
107
+ ? Math.max(0, migrations.lockTimeoutMs) : 30000;
108
+ const migrationStartupTimeoutMs = Number.isFinite(migrations.startupTimeoutMs)
109
+ ? Math.max(0, migrations.startupTimeoutMs) : 60000;
110
+ const migrationOnEvent = typeof migrations.onEvent === 'function' ? migrations.onEvent : null;
111
+
112
+ function emitMigrationEvent(name, payload) {
113
+ if (!migrationOnEvent) return;
114
+ try {
115
+ migrationOnEvent({ name, schema, ...payload });
116
+ } catch (err) {
117
+ console.warn(`[aquifer] migrations.onEvent handler threw: ${err.message}`);
118
+ }
119
+ }
120
+
121
+ function requiredMigrations() {
122
+ return MIGRATION_PLAN
123
+ .filter(m => m.always
124
+ || (m.gate === 'entities' && getEntitiesEnabled())
125
+ || (m.gate === 'facts' && getFactsEnabled()))
126
+ .map(m => m.id);
127
+ }
128
+
129
+ async function readAppliedMigrations(queryRunner) {
130
+ const required = MIGRATION_PLAN.filter(m => m.always
131
+ || (m.gate === 'entities' && getEntitiesEnabled())
132
+ || (m.gate === 'facts' && getFactsEnabled()));
133
+ const normalizedSignatures = required.flatMap((m) => {
134
+ if (Array.isArray(m.signature)) return m.signature;
135
+ return [m.signature];
136
+ });
137
+ const tableSignatures = normalizedSignatures
138
+ .filter(signature => typeof signature === 'string');
139
+ const columnSignatures = normalizedSignatures
140
+ .filter(signature => signature && typeof signature === 'object' && signature.table && signature.column);
141
+ const indexSignatures = normalizedSignatures
142
+ .filter(signature => signature && typeof signature === 'object' && signature.index);
143
+ const presentTables = new Set();
144
+ const presentColumns = new Set();
145
+ const presentIndexes = new Set();
146
+ if (tableSignatures.length > 0) {
147
+ const r = await queryRunner.query(
148
+ `SELECT tablename FROM pg_tables
149
+ WHERE schemaname = $1 AND tablename = ANY($2::text[])`,
150
+ [schema, tableSignatures]
151
+ );
152
+ for (const row of r.rows) presentTables.add(row.tablename);
153
+ }
154
+ if (columnSignatures.length > 0) {
155
+ const tables = [...new Set(columnSignatures.map(signature => signature.table))];
156
+ const r = await queryRunner.query(
157
+ `SELECT table_name, column_name
158
+ FROM information_schema.columns
159
+ WHERE table_schema = $1 AND table_name = ANY($2::text[])`,
160
+ [schema, tables]
161
+ );
162
+ for (const row of r.rows) presentColumns.add(`${row.table_name}.${row.column_name}`);
163
+ }
164
+ if (indexSignatures.length > 0) {
165
+ const indexes = indexSignatures.map(signature => signature.index);
166
+ const r = await queryRunner.query(
167
+ `SELECT indexname FROM pg_indexes
168
+ WHERE schemaname = $1 AND indexname = ANY($2::text[])`,
169
+ [schema, indexes]
170
+ );
171
+ for (const row of r.rows) presentIndexes.add(row.indexname);
172
+ }
173
+ return required
174
+ .filter(m => {
175
+ const signatures = Array.isArray(m.signature) ? m.signature : [m.signature];
176
+ return signatures.every((signature) => {
177
+ if (typeof signature === 'string') return presentTables.has(signature);
178
+ if (signature && signature.index) return presentIndexes.has(signature.index);
179
+ return presentColumns.has(`${signature.table}.${signature.column}`);
180
+ });
181
+ })
182
+ .map(m => m.id);
183
+ }
184
+
185
+ async function buildMigrationPlan(queryRunner) {
186
+ const required = requiredMigrations();
187
+ const applied = await readAppliedMigrations(queryRunner);
188
+ const appliedSet = new Set(applied);
189
+ const pending = required.filter(id => !appliedSet.has(id));
190
+ return { required, applied, pending };
191
+ }
192
+
193
+ async function ensureMigrated() {
194
+ if (migrated) return;
195
+ if (migratePromise) return migratePromise;
196
+ if (migrationsMode === 'off') {
197
+ migrated = true;
198
+ return;
199
+ }
200
+ if (migrationsMode === 'check') {
201
+ const plan = await buildMigrationPlan(pool).catch(() => null);
202
+ if (plan && plan.pending.length === 0) migrated = true;
203
+ return;
204
+ }
205
+ migratePromise = migrate().finally(() => { migratePromise = null; });
206
+ return migratePromise;
207
+ }
208
+
209
+ async function migrate() {
210
+ const t0 = Date.now();
211
+ const lockKey = Buffer.from(`aquifer:${schema}`).reduce((h, b) => (h * 31 + b) & 0x7fffffff, 0);
212
+
213
+ emitMigrationEvent('init_started', { mode: migrationsMode });
214
+
215
+ const supportsCheckout = typeof pool.connect === 'function';
216
+ const client = supportsCheckout ? await pool.connect() : pool;
217
+ const releasesClient = supportsCheckout && typeof client.release === 'function';
218
+ const notices = [];
219
+ const onNotice = (n) => {
220
+ notices.push({ severity: n.severity || 'NOTICE', message: n.message || String(n) });
221
+ };
222
+ const hasEvents = typeof client.on === 'function' && typeof client.off === 'function';
223
+ if (hasEvents) client.on('notice', onNotice);
224
+
225
+ const ddlExecuted = [];
226
+ let lockAcquired = false;
227
+
228
+ try {
229
+ const planBefore = await buildMigrationPlan(client).catch(() => null);
230
+ emitMigrationEvent('check_completed', {
231
+ required: planBefore ? planBefore.required : requiredMigrations(),
232
+ applied: planBefore ? planBefore.applied : [],
233
+ pending: planBefore ? planBefore.pending : requiredMigrations(),
234
+ });
235
+
236
+ const lockDeadline = Date.now() + migrationLockTimeoutMs;
237
+ const pollMs = 250;
238
+ while (true) {
239
+ const r = await client.query('SELECT pg_try_advisory_lock($1) AS ok', [lockKey]);
240
+ const row = r && r.rows ? r.rows[0] : null;
241
+ if (row && row.ok === false) {
242
+ if (Date.now() >= lockDeadline) break;
243
+ await new Promise(res => setTimeout(res, pollMs));
244
+ continue;
245
+ }
246
+ lockAcquired = true;
247
+ break;
248
+ }
249
+ if (!lockAcquired) {
250
+ const err = new Error(`aquifer: failed to acquire migration advisory lock within ${migrationLockTimeoutMs}ms for schema "${schema}"`);
251
+ err.code = 'AQ_MIGRATION_LOCK_TIMEOUT';
252
+ err.failedAt = 'acquire_lock';
253
+ throw err;
254
+ }
255
+
256
+ emitMigrationEvent('apply_started', {
257
+ pending: planBefore ? planBefore.pending : requiredMigrations(),
258
+ });
259
+
260
+ try {
261
+ await client.query(loadSql('001-base.sql', schema));
262
+ ddlExecuted.push('001-base');
263
+
264
+ if (getEntitiesEnabled()) {
265
+ await client.query(loadSql('002-entities.sql', schema));
266
+ ddlExecuted.push('002-entities');
267
+ }
268
+
269
+ await client.query(loadSql('003-trust-feedback.sql', schema));
270
+ ddlExecuted.push('003-trust-feedback');
271
+
272
+ if (getFactsEnabled()) {
273
+ await client.query(loadSql('004-facts.sql', schema));
274
+ ddlExecuted.push('004-facts');
275
+ }
276
+
277
+ await client.query(loadSql('004-completion.sql', schema));
278
+ ddlExecuted.push('004-completion');
279
+
280
+ if (getEntitiesEnabled()) {
281
+ await client.query(loadSql('005-entity-state-history.sql', schema));
282
+ ddlExecuted.push('005-entity-state-history');
283
+ }
284
+
285
+ for (const migration of [
286
+ ['006-insights.sql', '006-insights'],
287
+ ['007-v1-foundation.sql', '007-v1-foundation'],
288
+ ['008-session-finalizations.sql', '008-session-finalizations'],
289
+ ['009-v1-assertion-plane.sql', '009-v1-assertion-plane'],
290
+ ['010-v1-finalization-review.sql', '010-v1-finalization-review'],
291
+ ['011-v1-compaction-claim.sql', '011-v1-compaction-claim'],
292
+ ['012-v1-compaction-lease.sql', '012-v1-compaction-lease'],
293
+ ['013-v1-compaction-lineage.sql', '013-v1-compaction-lineage'],
294
+ ['014-v1-checkpoint-runs.sql', '014-v1-checkpoint-runs'],
295
+ ['015-v1-evidence-items.sql', '015-v1-evidence-items'],
296
+ ['016-v1-evidence-ref-multi-item.sql', '016-v1-evidence-ref-multi-item'],
297
+ ['017-v1-memory-record-embeddings.sql', '017-v1-memory-record-embeddings'],
298
+ ['018-v1-finalization-candidate-envelope.sql', '018-v1-finalization-candidate-envelope'],
299
+ ]) {
300
+ await client.query(loadSql(migration[0], schema));
301
+ ddlExecuted.push(migration[1]);
302
+ }
303
+
304
+ migrated = true;
305
+ } finally {
306
+ await client.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch((err) => {
307
+ console.warn(`[aquifer] failed to release migration advisory lock for schema "${schema}": ${err.message}`);
308
+ });
309
+ }
310
+ } catch (err) {
311
+ err.notices = Array.isArray(err.notices) ? err.notices : notices.slice();
312
+ err.failedAt = err.failedAt || 'apply_ddl';
313
+ emitMigrationEvent('apply_failed', {
314
+ error: { code: err.code || null, message: err.message },
315
+ failedAt: err.failedAt,
316
+ notices: err.notices,
317
+ durationMs: Date.now() - t0,
318
+ });
319
+ throw err;
320
+ } finally {
321
+ if (hasEvents) client.off('notice', onNotice);
322
+ if (releasesClient) client.release();
323
+ }
324
+
325
+ for (const n of notices) {
326
+ const sev = (n.severity || 'NOTICE').toUpperCase();
327
+ const msg = n.message || '';
328
+ const line = `[aquifer] migration ${sev.toLowerCase()}: ${msg}`;
329
+ if (sev === 'WARNING' || sev === 'ERROR') {
330
+ console.warn(line);
331
+ } else if (sev === 'NOTICE' && msg.startsWith('[aquifer]')) {
332
+ process.stderr.write(line + '\n');
333
+ }
334
+ }
335
+
336
+ if (!ftsConfig) {
337
+ try {
338
+ const r = await pool.query(
339
+ `SELECT 1 FROM pg_ts_config
340
+ WHERE cfgname = 'zhcfg' AND cfgnamespace = 'public'::regnamespace
341
+ LIMIT 1`);
342
+ ftsConfig = r.rowCount > 0 ? 'zhcfg' : 'simple';
343
+ } catch {
344
+ ftsConfig = 'simple';
345
+ }
346
+ }
347
+
348
+ try {
349
+ const f = await pool.query(`
350
+ SELECT
351
+ EXISTS(SELECT 1 FROM pg_extension WHERE extname='pg_jieba') AS have_jieba,
352
+ EXISTS(SELECT 1 FROM pg_extension WHERE extname='zhparser') AS have_zhparser,
353
+ (SELECT p.prsname FROM pg_ts_config c
354
+ JOIN pg_ts_parser p ON c.cfgparser = p.oid
355
+ WHERE c.cfgname='zhcfg' AND c.cfgnamespace='public'::regnamespace
356
+ LIMIT 1) AS zhcfg_parser
357
+ `);
358
+ const row = f.rows[0] || {};
359
+ const backend = row.zhcfg_parser
360
+ ? `zhcfg(parser=${row.zhcfg_parser})`
361
+ : `simple (no zhcfg in public namespace)`;
362
+
363
+ let warmupMs = null;
364
+ if (row.zhcfg_parser) {
365
+ const t0Warmup = Date.now();
366
+ await pool.query(`SELECT to_tsvector('zhcfg', $1)`, ['warmup 記憶系統 aquifer'])
367
+ .catch(() => {});
368
+ warmupMs = Date.now() - t0Warmup;
369
+ }
370
+
371
+ const warmupNote = warmupMs !== null ? ` warmup=${warmupMs}ms` : '';
372
+ process.stderr.write(
373
+ `[aquifer] FTS post-flight: backend=${backend} ` +
374
+ `jieba=${row.have_jieba} zhparser=${row.have_zhparser} ` +
375
+ `selected=${ftsConfig}${warmupNote}\n`
376
+ );
377
+ if (warmupMs !== null && warmupMs > 500) {
378
+ process.stderr.write(
379
+ `[aquifer] Note: first FTS call paid ~${warmupMs}ms for tokenizer init ` +
380
+ `(dictionary mmap). Subsequent calls on the same backend are cached.\n`
381
+ );
382
+ }
383
+ } catch (err) {
384
+ console.warn(`[aquifer] FTS post-flight check failed: ${err.message}`);
385
+ }
386
+
387
+ const durationMs = Date.now() - t0;
388
+ emitMigrationEvent('apply_succeeded', {
389
+ ddlExecuted,
390
+ durationMs,
391
+ notices: notices.slice(),
392
+ });
393
+ return { ok: true, durationMs, notices: notices.slice(), ddlExecuted };
394
+ }
395
+
396
+ async function listPendingMigrations() {
397
+ const plan = await buildMigrationPlan(pool);
398
+ return { ...plan, lastRunAt: null };
399
+ }
400
+
401
+ async function init() {
402
+ const t0 = Date.now();
403
+ const mode = migrationsMode;
404
+
405
+ let deadlineTimer = null;
406
+ const startupDeadline = migrationStartupTimeoutMs > 0
407
+ ? new Promise((_, reject) => {
408
+ deadlineTimer = setTimeout(() => {
409
+ const err = new Error(`aquifer: init() exceeded startupTimeoutMs=${migrationStartupTimeoutMs}ms`);
410
+ err.code = 'AQ_MIGRATION_STARTUP_TIMEOUT';
411
+ reject(err);
412
+ }, migrationStartupTimeoutMs);
413
+ if (typeof deadlineTimer.unref === 'function') deadlineTimer.unref();
414
+ })
415
+ : null;
416
+ const withDeadline = (p) => startupDeadline ? Promise.race([p, startupDeadline]) : p;
417
+ const clearDeadline = () => {
418
+ if (deadlineTimer) {
419
+ clearTimeout(deadlineTimer);
420
+ deadlineTimer = null;
421
+ }
422
+ };
423
+
424
+ try {
425
+ let plan;
426
+ try {
427
+ plan = await withDeadline(buildMigrationPlan(pool));
428
+ } catch (err) {
429
+ const durationMs = Date.now() - t0;
430
+ emitMigrationEvent('apply_failed', {
431
+ error: { code: err.code || null, message: err.message },
432
+ failedAt: 'plan_probe',
433
+ notices: [],
434
+ durationMs,
435
+ });
436
+ return {
437
+ ready: false,
438
+ memoryMode: 'off',
439
+ migrationMode: mode,
440
+ pendingMigrations: [],
441
+ appliedMigrations: [],
442
+ error: { code: err.code || 'AQ_MIGRATION_PROBE_FAILED', message: err.message },
443
+ durationMs,
444
+ };
445
+ }
446
+
447
+ if (mode === 'off') {
448
+ return {
449
+ ready: true,
450
+ memoryMode: 'rw',
451
+ migrationMode: mode,
452
+ pendingMigrations: plan.pending,
453
+ appliedMigrations: plan.applied,
454
+ error: null,
455
+ durationMs: Date.now() - t0,
456
+ };
457
+ }
458
+
459
+ if (mode === 'check') {
460
+ const ready = plan.pending.length === 0;
461
+ if (ready) migrated = true;
462
+ return {
463
+ ready,
464
+ memoryMode: ready ? 'rw' : 'ro',
465
+ migrationMode: mode,
466
+ pendingMigrations: plan.pending,
467
+ appliedMigrations: plan.applied,
468
+ error: null,
469
+ durationMs: Date.now() - t0,
470
+ };
471
+ }
472
+
473
+ if (plan.pending.length === 0) {
474
+ migrated = true;
475
+ return {
476
+ ready: true,
477
+ memoryMode: 'rw',
478
+ migrationMode: mode,
479
+ pendingMigrations: [],
480
+ appliedMigrations: plan.applied,
481
+ error: null,
482
+ durationMs: Date.now() - t0,
483
+ };
484
+ }
485
+
486
+ try {
487
+ const result = await withDeadline(migrate());
488
+ const planAfter = await buildMigrationPlan(pool).catch(() => null);
489
+ return {
490
+ ready: true,
491
+ memoryMode: 'rw',
492
+ migrationMode: mode,
493
+ pendingMigrations: planAfter ? planAfter.pending : [],
494
+ appliedMigrations: planAfter ? planAfter.applied : plan.required,
495
+ error: null,
496
+ durationMs: result.durationMs || (Date.now() - t0),
497
+ };
498
+ } catch (err) {
499
+ return {
500
+ ready: false,
501
+ memoryMode: 'ro',
502
+ migrationMode: mode,
503
+ pendingMigrations: plan.pending,
504
+ appliedMigrations: plan.applied,
505
+ error: { code: err.code || 'AQ_MIGRATION_FAILED', message: err.message },
506
+ durationMs: Date.now() - t0,
507
+ };
508
+ }
509
+ } finally {
510
+ clearDeadline();
511
+ }
512
+ }
513
+
514
+ return {
515
+ buildMigrationPlan,
516
+ ensureMigrated,
517
+ getFtsConfig: () => ftsConfig,
518
+ init,
519
+ isMigrated: () => migrated,
520
+ listPendingMigrations,
521
+ loadSql: filename => loadSql(filename, schema),
522
+ migrate,
523
+ requiredMigrations,
524
+ };
525
+ }
526
+
527
+ module.exports = {
528
+ MIGRATION_PLAN,
529
+ createPostgresMigrationRuntime,
530
+ loadSql,
531
+ qi,
532
+ validateSchema,
533
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const PUBLIC_PLACEHOLDER_SUMMARY_PATTERN = '(空測試會話|測試會話無實質內容|x 字元填充|placeholder)';
4
+ const PUBLIC_PLACEHOLDER_SUMMARY_RE = new RegExp(PUBLIC_PLACEHOLDER_SUMMARY_PATTERN, 'i');
5
+
6
+ function publicPlaceholderSummarySql(alias = 'ss') {
7
+ return `(
8
+ COALESCE(${alias}.summary_text, '') ~* '${PUBLIC_PLACEHOLDER_SUMMARY_PATTERN}'
9
+ OR COALESCE(${alias}.structured_summary::text, '') ~* '${PUBLIC_PLACEHOLDER_SUMMARY_PATTERN}'
10
+ )`;
11
+ }
12
+
13
+ function isPublicPlaceholderSessionMaterial(row = {}) {
14
+ const summaryText = String(row.summary_text || row.summaryText || '').trim();
15
+ if (summaryText && PUBLIC_PLACEHOLDER_SUMMARY_RE.test(summaryText)) return true;
16
+
17
+ const structuredSummary = row.structured_summary ?? row.structuredSummary ?? null;
18
+ if (structuredSummary === null || structuredSummary === undefined) return false;
19
+
20
+ if (typeof structuredSummary === 'string') {
21
+ return PUBLIC_PLACEHOLDER_SUMMARY_RE.test(structuredSummary);
22
+ }
23
+
24
+ try {
25
+ return PUBLIC_PLACEHOLDER_SUMMARY_RE.test(JSON.stringify(structuredSummary));
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function filterPublicPlaceholderSessionRows(rows = []) {
32
+ return rows.filter(row => !isPublicPlaceholderSessionMaterial(row));
33
+ }
34
+
35
+ module.exports = {
36
+ PUBLIC_PLACEHOLDER_SUMMARY_PATTERN,
37
+ filterPublicPlaceholderSessionRows,
38
+ isPublicPlaceholderSessionMaterial,
39
+ publicPlaceholderSummarySql,
40
+ };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const { createEmbedder } = require('../pipeline/embed');
4
+
5
+ function buildRerankDocument(row, maxChars) {
6
+ const ss = row.structured_summary || null;
7
+ const parts = [];
8
+ if (ss) {
9
+ if (ss.title) parts.push(String(ss.title).trim());
10
+ if (ss.overview) parts.push(String(ss.overview).trim());
11
+ if (Array.isArray(ss.topics)) {
12
+ const topics = ss.topics
13
+ .map(t => typeof t === 'string' ? t : (t && t.name ? `${t.name}${t.summary ? ': ' + t.summary : ''}` : ''))
14
+ .filter(Boolean).join(' / ');
15
+ if (topics) parts.push(topics);
16
+ }
17
+ if (Array.isArray(ss.decisions)) {
18
+ const decisions = ss.decisions
19
+ .map(d => typeof d === 'string' ? d : (d && d.decision ? d.decision : ''))
20
+ .filter(Boolean).join(' / ');
21
+ if (decisions) parts.push(`Decisions: ${decisions}`);
22
+ }
23
+ if (Array.isArray(ss.open_loops)) {
24
+ const loops = ss.open_loops
25
+ .map(l => typeof l === 'string' ? l : (l && l.item ? l.item : ''))
26
+ .filter(Boolean).join(' / ');
27
+ if (loops) parts.push(`Open loops: ${loops}`);
28
+ }
29
+ }
30
+ if (!parts.length) {
31
+ const bare = (row.summary_text || row.summary_snippet || '').trim();
32
+ if (bare) parts.push(bare);
33
+ }
34
+ const turn = (row.matched_turn_text || '').replace(/\s+/g, ' ').trim();
35
+ if (turn) {
36
+ const joined = parts.join(' \n ');
37
+ if (!joined.includes(turn)) parts.push(`Matched turn: ${turn}`);
38
+ }
39
+
40
+ let text = parts.join('\n\n').replace(/[ \t]+/g, ' ').trim();
41
+ if (text.length > maxChars) text = text.slice(0, maxChars);
42
+ return text;
43
+ }
44
+
45
+ function resolveEmbedFn(embedConfig, env) {
46
+ if (embedConfig && typeof embedConfig.fn === 'function') {
47
+ return embedConfig.fn;
48
+ }
49
+ if (embedConfig && embedConfig.provider) {
50
+ const embedder = createEmbedder(embedConfig);
51
+ return (texts) => embedder.embedBatch(texts);
52
+ }
53
+ const provider = env.EMBED_PROVIDER;
54
+ if (!provider) return null;
55
+
56
+ const opts = { provider };
57
+ if (provider === 'ollama') {
58
+ opts.ollamaUrl = env.OLLAMA_URL || env.AQUIFER_EMBED_BASE_URL || 'http://localhost:11434';
59
+ opts.model = env.AQUIFER_EMBED_MODEL || 'bge-m3';
60
+ } else if (provider === 'openai') {
61
+ opts.openaiApiKey = env.OPENAI_API_KEY;
62
+ if (!opts.openaiApiKey) {
63
+ throw new Error('EMBED_PROVIDER=openai requires OPENAI_API_KEY');
64
+ }
65
+ opts.openaiModel = env.AQUIFER_EMBED_MODEL || 'text-embedding-3-small';
66
+ if (env.AQUIFER_EMBED_DIM) opts.openaiDimensions = Number(env.AQUIFER_EMBED_DIM);
67
+ } else {
68
+ throw new Error(`EMBED_PROVIDER=${provider} not supported by autodetect (use 'ollama' or 'openai', or pass config.embed.fn explicitly)`);
69
+ }
70
+ const embedder = createEmbedder(opts);
71
+ return (texts) => embedder.embedBatch(texts);
72
+ }
73
+
74
+ function shouldAutoRerank({ query, mode, ranked, hasEntities, autoTrigger }) {
75
+ if (!autoTrigger.enabled) return { apply: false, reason: 'auto_disabled' };
76
+
77
+ if (hasEntities && autoTrigger.alwaysWhenEntities) {
78
+ return { apply: true, reason: 'entities_present' };
79
+ }
80
+
81
+ const len = ranked.length;
82
+ if (len < autoTrigger.minResults) return { apply: false, reason: 'too_few_results' };
83
+ if (len > autoTrigger.maxResults) return { apply: false, reason: 'too_many_results' };
84
+
85
+ const q = String(query || '').trim();
86
+ const tokenCount = q.split(/\s+/).filter(Boolean).length;
87
+ if (q.length < autoTrigger.minQueryChars && tokenCount < autoTrigger.minQueryTokens) {
88
+ return { apply: false, reason: 'query_too_short' };
89
+ }
90
+
91
+ if (mode === 'fts') {
92
+ if (len > autoTrigger.ftsMinResults) return { apply: true, reason: 'fts_wide_shortlist' };
93
+ return { apply: false, reason: 'fts_shortlist_too_narrow' };
94
+ }
95
+
96
+ if (!autoTrigger.modes.includes(mode)) {
97
+ return { apply: false, reason: 'mode_not_in_autotrigger_modes' };
98
+ }
99
+
100
+ if (len >= 2) {
101
+ const s0 = ranked[0]?._score ?? 0;
102
+ const s1 = ranked[1]?._score ?? 0;
103
+ if (s0 - s1 <= autoTrigger.maxTopScoreGap) {
104
+ return { apply: true, reason: 'top_score_gap_close' };
105
+ }
106
+ }
107
+
108
+ return { apply: false, reason: 'top_score_gap_wide' };
109
+ }
110
+
111
+ module.exports = {
112
+ buildRerankDocument,
113
+ resolveEmbedFn,
114
+ shouldAutoRerank,
115
+ };