mixdog 0.9.79 → 0.9.80
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/package.json
CHANGED
|
@@ -146,12 +146,6 @@ export async function searchRelevantHybrid(db, query, options = {}) {
|
|
|
146
146
|
const queryTokenCount = countQueryTokens(clean)
|
|
147
147
|
const minExactHits = exactTerms.length >= 8 ? 3 : exactTerms.length >= 4 ? 2 : 1
|
|
148
148
|
|
|
149
|
-
// For very short queries (< 3 chars) the trigram operator still works but
|
|
150
|
-
// we relax the server-side threshold via set_limit() — however that requires
|
|
151
|
-
// a separate round-trip. Instead we fall back to a plain ILIKE scan for
|
|
152
|
-
// short text (rare edge case; sequential scan is acceptable for < 3 chars).
|
|
153
|
-
const isShortQuery = clean.length < 3
|
|
154
|
-
|
|
155
149
|
// $5 onward are the filter params for the entries legs (non-MV path).
|
|
156
150
|
// Each CTE leg duplicates the same positional params because they live in
|
|
157
151
|
// independent SELECT scopes. When useHotActive=true, the trgm leg still uses
|
|
@@ -269,50 +263,26 @@ sparse AS (
|
|
|
269
263
|
),`) : `
|
|
270
264
|
sparse AS (SELECT NULL::bigint AS id, NULL::float8 AS lex, NULL::bigint AS sparse_rank WHERE $2::text IS NOT NULL AND false),`
|
|
271
265
|
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
const trgmCte =
|
|
266
|
+
// Portable substring leg. The curated Unix PG runtimes include pgvector but
|
|
267
|
+
// not the optional pg_trgm contrib extension, so fuzzy similarity cannot be
|
|
268
|
+
// a startup/runtime requirement. FTS and dense vector search retain broad
|
|
269
|
+
// matching while this leg gives exact substrings a deterministic rescue.
|
|
270
|
+
// It always queries entries because mv_hot_active omits display text + ts.
|
|
271
|
+
const trgmCte = `
|
|
278
272
|
trgm AS (
|
|
279
273
|
SELECT id,
|
|
280
|
-
0
|
|
274
|
+
1.0::float8 AS trg_sim,
|
|
281
275
|
ROW_NUMBER() OVER (ORDER BY ts DESC) AS trgm_rank
|
|
282
276
|
FROM entries
|
|
283
|
-
WHERE (
|
|
277
|
+
WHERE (
|
|
278
|
+
content ILIKE '%' || $3 || '%'
|
|
279
|
+
OR coalesce(element, '') ILIKE '%' || $3 || '%'
|
|
280
|
+
OR coalesce(summary, '') ILIKE '%' || $3 || '%'
|
|
281
|
+
)
|
|
284
282
|
${trgmFilterClause}
|
|
285
283
|
ORDER BY ts DESC
|
|
286
284
|
LIMIT $4
|
|
287
|
-
),`
|
|
288
|
-
trgm AS (
|
|
289
|
-
SELECT id,
|
|
290
|
-
GREATEST(
|
|
291
|
-
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
292
|
-
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
293
|
-
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
294
|
-
) AS trg_sim,
|
|
295
|
-
ROW_NUMBER() OVER (ORDER BY GREATEST(
|
|
296
|
-
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
297
|
-
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
298
|
-
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
299
|
-
) DESC) AS trgm_rank
|
|
300
|
-
FROM entries
|
|
301
|
-
WHERE (
|
|
302
|
-
content % $3 OR element % $3 OR summary % $3
|
|
303
|
-
OR content ILIKE '%' || $3 || '%'
|
|
304
|
-
OR coalesce(element, '') ILIKE '%' || $3 || '%'
|
|
305
|
-
OR coalesce(summary, '') ILIKE '%' || $3 || '%'
|
|
306
|
-
)
|
|
307
|
-
AND GREATEST(
|
|
308
|
-
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
309
|
-
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
310
|
-
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
311
|
-
) >= 0.10
|
|
312
|
-
${trgmFilterClause}
|
|
313
|
-
ORDER BY trg_sim DESC
|
|
314
|
-
LIMIT $4
|
|
315
|
-
),`
|
|
285
|
+
),`
|
|
316
286
|
|
|
317
287
|
const exactCte = exactTerms.length > 0 ? `
|
|
318
288
|
exact AS (
|
|
@@ -134,17 +134,14 @@ export async function init(db, dims) {
|
|
|
134
134
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_phase_sweep ON entries(status, is_root, error_count, reviewed_at, id)`)
|
|
135
135
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_promoted_at ON entries(promoted_at) WHERE promoted_at IS NOT NULL`)
|
|
136
136
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_tsv ON entries USING GIN (search_tsv)`)
|
|
137
|
-
// Recall CTEs (memory-recall-store.mjs dense/
|
|
137
|
+
// Recall CTEs (memory-recall-store.mjs dense/text legs) intentionally match
|
|
138
138
|
// BOTH root and leaf/chunk rows, so their SQL has NO `is_root = 1` predicate
|
|
139
|
-
// (only `embedding IS NOT NULL` /
|
|
139
|
+
// (only `embedding IS NOT NULL` / portable substring text filters).
|
|
140
140
|
// The old root-only PARTIAL indexes therefore could not be used by those
|
|
141
141
|
// queries — the planner fell back to a Seq Scan + top-N heapsort over every
|
|
142
|
-
// embedding (verified via EXPLAIN ANALYZE). Broaden the
|
|
143
|
-
// the query shape
|
|
144
|
-
//
|
|
145
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)`)
|
|
146
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL`)
|
|
147
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_summary_trgm ON entries USING GIN (summary gin_trgm_ops) WHERE summary IS NOT NULL`)
|
|
142
|
+
// embedding (verified via EXPLAIN ANALYZE). Broaden the HNSW predicate to
|
|
143
|
+
// match the query shape. Substring rescue intentionally stays index-free:
|
|
144
|
+
// bundled Unix PG runtimes do not include the optional pg_trgm extension.
|
|
148
145
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
|
|
149
146
|
|
|
150
147
|
// BEFORE INSERT/UPDATE trigger keeps score in sync with category + last_seen_at
|
|
@@ -392,8 +389,6 @@ async function _migrateRecallIndexesIfStale(db) {
|
|
|
392
389
|
// Each entry: [indexName, newDefTailPredicate] where the presence of
|
|
393
390
|
// "is_root" in the live indexdef signals the stale root-only shape.
|
|
394
391
|
const targets = [
|
|
395
|
-
{ name: 'idx_entries_content_trgm', create: `CREATE INDEX idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)` },
|
|
396
|
-
{ name: 'idx_entries_element_trgm', create: `CREATE INDEX idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL` },
|
|
397
392
|
{ name: 'idx_entries_embedding_hnsw', create: `CREATE INDEX idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL` },
|
|
398
393
|
]
|
|
399
394
|
try {
|
|
@@ -403,8 +398,7 @@ async function _migrateRecallIndexesIfStale(db) {
|
|
|
403
398
|
const r = await db.query(`SELECT indexdef FROM pg_indexes WHERE indexname = $1`, [t.name])
|
|
404
399
|
def = r.rows?.[0]?.indexdef ?? null
|
|
405
400
|
} catch { def = null }
|
|
406
|
-
// Missing
|
|
407
|
-
// indexes; embedding_hnsw is also re-ensured by ensureCurrentSchemaExtensions.
|
|
401
|
+
// Missing embedding_hnsw is re-ensured by ensureCurrentSchemaExtensions.
|
|
408
402
|
if (def == null) continue
|
|
409
403
|
// Already broadened (no is_root predicate) → no-op, no rebuild.
|
|
410
404
|
if (!/is_root/i.test(def)) continue
|
|
@@ -416,26 +410,6 @@ async function _migrateRecallIndexesIfStale(db) {
|
|
|
416
410
|
__mixdogMemoryLog(`[memory] recall index migration for ${t.name} failed: ${err?.message || err}\n`)
|
|
417
411
|
}
|
|
418
412
|
}
|
|
419
|
-
// Self-heal trgm recall indexes on already-bootstrapped DBs: init() will
|
|
420
|
-
// not re-run, and a missing index is not handled by the stale-shape loop
|
|
421
|
-
// above (def == null → continue). These IF-NOT-EXISTS ensures recreate any
|
|
422
|
-
// trgm index that never existed or whose migrate-create failed. No-op (cheap
|
|
423
|
-
// catalog check) when the broadened index is already present.
|
|
424
|
-
try {
|
|
425
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)`)
|
|
426
|
-
} catch (err) {
|
|
427
|
-
__mixdogMemoryLog(`[memory] idx_entries_content_trgm ensure failed: ${err?.message || err}\n`)
|
|
428
|
-
}
|
|
429
|
-
try {
|
|
430
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL`)
|
|
431
|
-
} catch (err) {
|
|
432
|
-
__mixdogMemoryLog(`[memory] idx_entries_element_trgm ensure failed: ${err?.message || err}\n`)
|
|
433
|
-
}
|
|
434
|
-
try {
|
|
435
|
-
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_summary_trgm ON entries USING GIN (summary gin_trgm_ops) WHERE summary IS NOT NULL`)
|
|
436
|
-
} catch (err) {
|
|
437
|
-
__mixdogMemoryLog(`[memory] idx_entries_summary_trgm ensure failed: ${err?.message || err}\n`)
|
|
438
|
-
}
|
|
439
413
|
} catch (err) {
|
|
440
414
|
__mixdogMemoryLog(`[memory] _migrateRecallIndexesIfStale failed: ${err?.message || err}\n`)
|
|
441
415
|
}
|
|
@@ -139,8 +139,6 @@ async function _initClient(client, schema) {
|
|
|
139
139
|
? 'scheduler, public'
|
|
140
140
|
: 'memory, public'
|
|
141
141
|
await client.query(`SET search_path = ${sp}`)
|
|
142
|
-
// pg_trgm similarity threshold: session-local, must be set per connection.
|
|
143
|
-
await client.query(`SELECT set_limit(0.10)`)
|
|
144
142
|
await client.query(`SET default_transaction_isolation TO 'read committed'`)
|
|
145
143
|
// Mark seen only after all init statements succeed; failure leaves client
|
|
146
144
|
// unmarked so the next checkout retries init.
|
|
@@ -400,7 +398,6 @@ async function bootstrapInstance(pgPool, dataDirKey) {
|
|
|
400
398
|
const client = await pgPool.connect()
|
|
401
399
|
try {
|
|
402
400
|
await client.query(`CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public`)
|
|
403
|
-
await client.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public`)
|
|
404
401
|
await client.query(`CREATE SCHEMA IF NOT EXISTS memory`)
|
|
405
402
|
await client.query(`CREATE SCHEMA IF NOT EXISTS trace`)
|
|
406
403
|
await client.query(`CREATE SCHEMA IF NOT EXISTS scheduler`)
|
|
@@ -108,9 +108,8 @@ async function findFreePort(preferred) {
|
|
|
108
108
|
|
|
109
109
|
const MIXDOG_CONF_V2_MARKER = '# mixdog overrides v2 — bgwriter / checkpoint distribution'
|
|
110
110
|
|
|
111
|
-
// Lines emitted into postgresql.conf for v2. effective_io_concurrency
|
|
112
|
-
// posix_fadvise
|
|
113
|
-
// parameter error on every reload. Skip it there.
|
|
111
|
+
// Lines emitted into postgresql.conf for v2. effective_io_concurrency requires
|
|
112
|
+
// posix_fadvise; PostgreSQL rejects non-zero values on Windows and macOS.
|
|
114
113
|
function buildV2Block() {
|
|
115
114
|
const lines = [
|
|
116
115
|
'',
|
|
@@ -119,17 +118,16 @@ function buildV2Block() {
|
|
|
119
118
|
'bgwriter_lru_maxpages = 1000', // up to 1000 dirty pages per round (default 100)
|
|
120
119
|
'bgwriter_delay = 50ms', // wake bgwriter every 50ms (default 200ms)
|
|
121
120
|
]
|
|
122
|
-
if (process.platform
|
|
123
|
-
lines.push('effective_io_concurrency = 32')
|
|
121
|
+
if (process.platform === 'linux') {
|
|
122
|
+
lines.push('effective_io_concurrency = 32')
|
|
124
123
|
}
|
|
125
124
|
return lines.join('\n') + '\n'
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
// Migration: an earlier v2
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (process.platform !== 'win32') return conf
|
|
127
|
+
// Migration: an earlier v2 emitted effective_io_concurrency on every POSIX
|
|
128
|
+
// platform. Strip it anywhere except Linux so old macOS clusters can start.
|
|
129
|
+
function stripPlatformInvalidLines(conf) {
|
|
130
|
+
if (process.platform === 'linux') return conf
|
|
133
131
|
return conf.replace(/^effective_io_concurrency\s*=\s*\d+\s*\r?\n/m, '')
|
|
134
132
|
}
|
|
135
133
|
|
|
@@ -137,7 +135,7 @@ function ensureConfV2(pgdataDir) {
|
|
|
137
135
|
const confPath = join(pgdataDir, 'postgresql.conf')
|
|
138
136
|
if (!existsSync(confPath)) return false
|
|
139
137
|
const original = readFileSync(confPath, 'utf8')
|
|
140
|
-
let conf =
|
|
138
|
+
let conf = stripPlatformInvalidLines(original)
|
|
141
139
|
const stripped = conf !== original
|
|
142
140
|
const hasMarker = conf.includes(MIXDOG_CONF_V2_MARKER)
|
|
143
141
|
if (hasMarker && !stripped) return false
|
|
@@ -378,7 +376,14 @@ export async function startPg({
|
|
|
378
376
|
}
|
|
379
377
|
}
|
|
380
378
|
|
|
381
|
-
|
|
379
|
+
let serverLog = ''
|
|
380
|
+
try { serverLog = readFileSync(logFile, 'utf8').trim() } catch {}
|
|
381
|
+
const detail = [
|
|
382
|
+
errText.trim(),
|
|
383
|
+
serverLog ? `postgres log:\n${serverLog}` : '',
|
|
384
|
+
].filter(Boolean).join('\n') || (
|
|
385
|
+
r.timeout ? '(readiness probe timed out after 30s; no pg_ctl or postgres log output)' : '(no captured output)'
|
|
386
|
+
)
|
|
382
387
|
throw new Error(`[pg-process] pg_ctl start failed: ${detail}`)
|
|
383
388
|
}
|
|
384
389
|
|