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 <noreply@openai.com>

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
cybernaut6404
2026-07-27 14:13:08 -07:00
committed by GitHub
co-authored by OpenAI Codex
parent 9690140bf3
commit 7a65f182aa
9 changed files with 50 additions and 6 deletions
+7
View File
@@ -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.**
+1 -1
View File
@@ -1 +1 @@
0.42.66.0
0.42.66.1
+2
View File
@@ -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`.
+1 -1
View File
@@ -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",
+15 -3
View File
@@ -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)"
+1 -1
View File
@@ -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."
+7
View File
@@ -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);`,
+5
View File
@@ -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));
}
+11
View File
@@ -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`;