Skip to content

mcpx(catalog): VectorIndex silently drops non-384-dim vectors on reload — the remote-embedder cache vanishes on restart and mismatched dims score 0 #1643

Closed

Description

@dezren39

Summary

mcpx's catalog.VectorIndex accepts embedding vectors of any dimension at write time — Put returns nil, Import counts them as imported, and the daemon's semantic search stores whatever the configured embedder produces — but the SQLite reload path silently discards every stored vector whose dimension is not exactly VectorDim (384). With a remote embedder configured (the default model, text-embedding-3-small, returns 1536 dimensions), every tool vector the daemon caches is thrown away on the next start, with no error and no log. In-process, the same dimension gap silently zeroes semantic scores: CosineSimilarity returns exactly 0 for mismatched lengths, so a query embedded by one backend against tool vectors cached under another scores every tool 0 and semantic search degrades to keyword-only without any signal.

Evidence (verified on main ed8e5910)

  • pkgs/mcpx/internal/catalog/vector.go:159-182 — loadFromDB loads a row only when len(blob) == VectorDim*4 (:172); any other dimension is skipped silently (no log, no error surfaced — NewVectorIndex ignores the outcome entirely).
  • pkgs/mcpx/internal/catalog/vector.go:204-222 — Put stores a vector of any length in memory and on disk and never validates len(vec); its error return is never used for this.
  • pkgs/mcpx/internal/catalog/embeddings.go:346-376 — Import accepts and counts vectors of any length and persists them as blobs that loadFromDB will drop on the next open; Export (embeddings.go:324-343) stamps Dim: VectorDim regardless of the dimensions actually stored.
  • pkgs/mcpx/internal/catalog/embeddings.go:67-127 — RemoteEmbedder.Embed L2-normalizes but never checks the returned dimension: the default model text-embedding-3-small returns 1536 floats, not 384. CascadeEmbedder.Embed (embeddings.go:268-302) tries remote first in auto mode.
  • pkgs/mcpx/internal/catalog/vector.go:20-29 — CosineSimilarity returns 0 when len(v) != len(other). The daemon's hybrid search (internal/daemon/registry.go:534) combines that 0 into the score with no mismatch signal; a tool with no keyword score is then dropped by the combined > 15.0 || kwScore > 0 gate at registry.go:542.
  • The write path that populates the cache with embedder-dimension vectors is live: internal/daemon/registry.go:524-531 embeds each uncached tool with the configured embedder and calls vIdx.Put(t.Function, tVec) (:531, ignoring Put's error).

Probe (laptop, real internal/catalog package)

Probe test in a detached laptop worktree at the same HEAD (vector.go sha256 5e901780, embeddings.go sha256 19044242 — byte-identical to this clone), run twice with identical results (probe + log: hidden_files/probe-2026-10-28-laneB-vecdim/):

  • CTRL: Put a 384-dim vector → close → reopen → present.
  • BUG-A: Put a 1536-dim vector returns err=nil and is readable in-process; after close → reopen it is missing — silently dropped by loadFromDB.
  • BUG-B: Import of a payload carrying one 1536-dim vector reports count=1, err=nil; after reopen it is missing.
  • BUG-C: CosineSimilarity(1536-dim, 384-dim) = 0 exactly, while identical 384-dim vectors score > 0 — the silent in-process half of the same gap.

Why it matters

The vector index is the semantic-search cache for mcpx search: with a remote (or WASM/Ollama) embedder configured, the daemon pays to embed every tool once, persists the results, and then silently loses all of them on restart — and any mid-flight backend switch (config change, remote outage falling back to local in the cascade) zeroes the semantic half of the hybrid score for already-cached tools. Nothing logs, errors, or tests the mismatch: existing tests (embeddings_test.go, vector_test.go) only ever exercise 384-dim local vectors.

Acceptance criteria

  • A vector whose dimension is not the index's dimension cannot silently enter the persistent index: Put/Import either reject it with an error that names the expected and actual dimensions, or the index records and reloads vectors together with their dimension — one behaviour, pinned by tests.
  • loadFromDB no longer drops rows silently: unloadable rows are surfaced (error or logged count).
  • A restart round-trip test with a non-384 embedder vector exists and passes (either the vector survives, or the write failed loudly at write time).
  • The daemon search path does not score a dimension-mismatched pair as a plain 0 without a signal (fallback to re-embed, or an explicit debug/log trace).
  • Existing catalog tests pass unchanged for the 384-dim local path.

Filed by new-issue-creator-code-b. Whether to pin the index to one dimension per DB (reject at write) vs store per-vector dimensions is the triage call — the defect is the silence, which either ruling removes.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    on Oct 28, 2026
  2. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b cron run 2026-10-28 04:50 CDT
    • Found via: filed this run
    • Task: Filed after verifying on main ed8e591 with a probe test in the real internal/catalog package on the laptop (detached worktree, removed after; vector.go/embeddings.go sha256 identical both clones; probe + log in the goal's hidden_files/probe-2026-10-28-laneB-vecdim/, run twice identical): Put and Import accept 1536-dim vectors with err=nil, both are silently missing after a VectorIndex reopen (loadFromDB keeps only len == VectorDim*4 blobs), the 384-dim control survives, and CosineSimilarity across mismatched dims is exactly 0.
    • Expectation: effort/s — triage pins reject-at-write vs per-vector dimensions; the defect being filed is the silence, which either ruling removes.
    • Links: queue packet packet-1643.json (repo-health-code-pilot, triage)
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  6. 18 remaining items

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions