From 7a65f182aa6b65b7ace5d4bc26a0337368fc0094 Mon Sep 17 00:00:00 2001 From: cybernaut6404 <43730000+cybernaut6404@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:13:08 +0100 Subject: [PATCH] v0.42.66.1 fix: honor pgvector HNSW dimension limits (#3440) * fix(doctor): honor pgvector HNSW dimension limits * fix(ci): stabilize local Docker verification * chore: bump version and changelog (v0.42.66.1) Co-Authored-By: OpenAI Codex --------- Co-authored-by: OpenAI Codex --- CHANGELOG.md | 7 +++++++ VERSION | 2 +- docs/architecture/KEY_FILES.md | 2 ++ package.json | 2 +- scripts/check-wasm-embedded.sh | 18 +++++++++++++++--- scripts/ci-local.sh | 2 +- src/commands/doctor.ts | 7 +++++++ src/core/vector-index.ts | 5 +++++ test/vector-index-lifecycle.test.ts | 11 +++++++++++ 9 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b49e0b8..0a8784a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to GBrain will be documented in this file. +## [0.42.66.1] - 2026-07-27 + +### Fixed + +- `gbrain doctor` now treats embedding columns wider than pgvector's HNSW limit as healthy exact-scan configurations instead of prescribing an index PostgreSQL cannot build. +- Local CI now passes an empty Docker mount list correctly and compiles the embedded-WASM smoke binary from container-local storage on Docker Desktop. + ## [0.42.66.0] - 2026-07-24 **54 verified fixes from the community backlog: background enrichment stops wasting money on dead pages, autopilot stops killing its own healthy runs, and search respects your settings.** diff --git a/VERSION b/VERSION index b079cfa37..bdb592ae4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.42.66.0 \ No newline at end of file +0.42.66.1 diff --git a/docs/architecture/KEY_FILES.md b/docs/architecture/KEY_FILES.md index eddf7f087..842c1c000 100644 --- a/docs/architecture/KEY_FILES.md +++ b/docs/architecture/KEY_FILES.md @@ -502,3 +502,5 @@ T1.5 wiring is partial in v0.40.7.0. Three follow-ups filed in TODOS.md under union widening (`'person' | 'company'` → `string`), facts/eligibility.ts pack-aware `ELIGIBLE_TYPES` wiring, and 3 doctor checks (schema_pack_coverage, schema_pack_writability, schema_pack_mutation_audit). + +- `src/core/vector-index.ts` + `src/commands/doctor.ts:embedding_column_registry` — shared pgvector HNSW eligibility policy. `hnswIndexExpected(columnType, dims)` derives the answer from the canonical `vector`/`halfvec` dimension caps already used by migration index generation. Doctor reports an HNSW-less active embedding column as a healthy exact-scan configuration when its declared width exceeds the applicable pgvector cap, and only emits the index repair recipe when an index is actually supported. Pinned at both cap boundaries by `test/vector-index-lifecycle.test.ts`. diff --git a/package.json b/package.json index 45380ac15..62002a4df 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "bun": ">=1.3.10" }, "license": "MIT", - "version": "0.42.66.0", + "version": "0.42.66.1", "overrides": { "@hono/node-server": "^2.0.5", "fast-uri": "^3.1.4", diff --git a/scripts/check-wasm-embedded.sh b/scripts/check-wasm-embedded.sh index 4b86458e7..a9683ec7f 100755 --- a/scripts/check-wasm-embedded.sh +++ b/scripts/check-wasm-embedded.sh @@ -19,13 +19,25 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" -OUT_BIN="$(mktemp /tmp/gbrain-wasm-check.XXXXXX)" -trap 'rm -f "$OUT_BIN"' EXIT +# Build from a container-local copy. On Docker Desktop, Bun canonicalizes a +# bind-mounted input to /run/host_virtiofs but keeps /app as the output path; +# its final atomic rename then fails with ENOENT even though both names refer +# to the same mount. Keeping inputs and output under /tmp avoids that alias. +BUILD_DIR="$(mktemp -d /tmp/gbrain-wasm-check.XXXXXX)" +OUT_BIN="$BUILD_DIR/chunker-smoketest" +trap 'rm -rf "$BUILD_DIR"' EXIT +mkdir -p "$BUILD_DIR/scripts" +cp -R "$REPO_ROOT/src" "$BUILD_DIR/src" +cp "$REPO_ROOT/scripts/chunker-smoketest.ts" "$BUILD_DIR/scripts/chunker-smoketest.ts" +ln -s "$REPO_ROOT/node_modules" "$BUILD_DIR/node_modules" # Build a minimal smoketest binary that imports the chunker. We compile this # instead of the full gbrain CLI so the failure mode is laser-focused on # chunker + WASM path resolution, not unrelated CLI wiring. -bun build --compile --outfile "$OUT_BIN" scripts/chunker-smoketest.ts >/dev/null 2>&1 +if ! (cd "$BUILD_DIR" && bun build --compile --outfile "$OUT_BIN" scripts/chunker-smoketest.ts >/dev/null); then + echo "[check-wasm-embedded] FAIL: bun could not compile the smoketest binary." >&2 + exit 1 +fi # Run it and capture JSON output. OUTPUT="$("$OUT_BIN" 2>&1)" diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 06ec621d6..1a7670694 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -350,7 +350,7 @@ if [ -f .git ]; then fi echo "[ci-local] Running checks inside runner container..." -docker compose -f "$COMPOSE_FILE" run --rm "${EXTRA_MOUNTS[@]:-}" runner bash -c "$INNER_CMD" +docker compose -f "$COMPOSE_FILE" run --rm "${EXTRA_MOUNTS[@]}" runner bash -c "$INNER_CMD" echo "" echo "[ci-local] All checks passed." diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 04c77d6aa..6c6962f9f 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -52,6 +52,7 @@ import { isUndefinedColumnError } from '../core/utils.ts'; // drift from what search actually filters. import { resolveHardExcludes, DEFAULT_HARD_EXCLUDES } from '../core/search/source-boost.ts'; import { escapeLikePattern, buildVisibilityClause } from '../core/search/sql-ranking.ts'; +import { hnswIndexExpected, hnswMaxDimsForType } from '../core/vector-index.ts'; export interface Check { name: string; @@ -6147,6 +6148,12 @@ export async function buildChecks( continue; } if (engine.kind === 'postgres' && haveIndex.get(colName) === false) { + if (!hnswIndexExpected(entry.type, entry.dimensions)) { + okColumns.push( + `${colName} (exact scan: ${entry.type}(${entry.dimensions}) exceeds HNSW cap ${hnswMaxDimsForType(entry.type)})`, + ); + continue; + } issues.push( `${colName}: no HNSW index. Search works but uses sequential scan. ` + `Fix: CREATE INDEX IF NOT EXISTS idx_chunks_${colName} ON content_chunks USING hnsw (${quoteIdentifier(colName)} ${entry.type}_cosine_ops);`, diff --git a/src/core/vector-index.ts b/src/core/vector-index.ts index b89e9a63a..52fb67092 100644 --- a/src/core/vector-index.ts +++ b/src/core/vector-index.ts @@ -34,6 +34,11 @@ export function hnswMaxDimsForType(columnType: 'vector' | 'halfvec'): number { return columnType === 'halfvec' ? PGVECTOR_HNSW_HALFVEC_MAX_DIMS : PGVECTOR_HNSW_VECTOR_MAX_DIMS; } +/** Whether pgvector can build an HNSW index for this exact column shape. */ +export function hnswIndexExpected(columnType: 'vector' | 'halfvec', dims: number): boolean { + return dims <= hnswMaxDimsForType(columnType); +} + export function applyChunkEmbeddingIndexPolicy(sql: string, dims: number): string { return sql.replaceAll(CHUNK_EMBEDDING_HNSW_INDEX, chunkEmbeddingIndexSql(dims)); } diff --git a/test/vector-index-lifecycle.test.ts b/test/vector-index-lifecycle.test.ts index 267fcfe98..395f8ba8c 100644 --- a/test/vector-index-lifecycle.test.ts +++ b/test/vector-index-lifecycle.test.ts @@ -3,6 +3,8 @@ import { chunkEmbeddingIndexSql, applyChunkEmbeddingIndexPolicy, PGVECTOR_HNSW_VECTOR_MAX_DIMS, + PGVECTOR_HNSW_HALFVEC_MAX_DIMS, + hnswIndexExpected, checkActiveBuild, dropZombieIndexes, dropAndRebuild, @@ -32,6 +34,15 @@ describe('chunkEmbeddingIndexSql — pre-v0.30.1 contract', () => { }); }); +describe('hnswIndexExpected', () => { + test('matches pgvector caps for vector and halfvec columns', () => { + expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS)).toBe(true); + expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS + 1)).toBe(false); + expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS)).toBe(true); + expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS + 1)).toBe(false); + }); +}); + describe('applyChunkEmbeddingIndexPolicy', () => { test('replaces the canonical index SQL', () => { const input = `BEFORE\nCREATE INDEX IF NOT EXISTS idx_chunks_embedding ON content_chunks USING hnsw (embedding vector_cosine_ops);\nAFTER`;