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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.79",
3
+ "version": "0.9.80",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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
- // trgm CTE: pg_trgm similarity path. For short queries (< 3 chars) the %
273
- // operator is unreliable (trigrams need at least 3 chars); use ILIKE instead.
274
- // NOTE: trgm always queries entries regardless of useHotActive mv_hot_active
275
- // lacks the content column (trgm/ILIKE) and ts column (short-query ORDER BY).
276
- // Uses trgmFilterClause whose $N offsets are aligned to activeBindParams.
277
- const trgmCte = isShortQuery ? `
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.5::float8 AS trg_sim,
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 (content ILIKE '%' || $3 || '%' OR element ILIKE '%' || $3 || '%')
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/trgm legs) intentionally match
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` / content|element|summary text filters).
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 predicates to match
143
- // the query shape so HNSW/GIN are actually used. A `summary` trgm index is
144
- // added because the trgm leg also filters on `summary` but had no index.
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 index post-loop IF-NOT-EXISTS ensures below self-heal trgm
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 is
112
- // posix_fadvise-only; PG rejects non-zero values on Windows with an invalid-
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 !== 'win32') {
123
- lines.push('effective_io_concurrency = 32') // I/O concurrency hint (POSIX only)
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 always emitted effective_io_concurrency. On Windows
129
- // that line is invalid strip it so pg_ctl reload stops logging a parse error
130
- // every cycle.
131
- function stripWindowsInvalidLines(conf) {
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 = stripWindowsInvalidLines(original)
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
- const detail = errText || (r.timeout ? '(readiness probe timed out after 30s; no pg_ctl output)' : '(no captured output)')
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