Skip to content

feat(reembed): stage replacement vectors beside the active column - #64

Merged
zen-agents[bot] merged 3 commits into
mainfrom
feat/staged-reembed
Oct 24, 2026
Merged

zen-agents[bot] merged 3 commits into
mainfrom
feat/staged-reembed

Conversation

@zen-agents

@zen-agents zen-agents Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Refs #51. Thanks to @denisvmedia for pointing out the real issue: when api/scripts/reembed.js changes dimensions, it re-types memories.vector with USING NULL before re-embedding, so vector search returns nothing for the whole backfill (and any failed row stays NULL).

Change

reembed.js becomes a staged dual-column migration, the same one we run in our production deployment, adapted to Zengram's modules:

Step Command Writes
Preview node api/scripts/reembed.js none
Stage --stage --apply (preview without --apply) vector_next + vector_next_generation + vector_next_content_hash; resumable
Cut over --activate --apply (preview without --apply) one transaction: vector → vector_prev, vector_next → vector, HNSW rebuilt

Safety properties:

  • staging never nulls or overwrites the active vector; every staging SQL statement passes through assertNoActiveVectorWipe;
  • activation takes an advisory lock and an ACCESS EXCLUSIVE lock on memories, then refuses (and rolls back) on any gap: a memory with text that is unstaged, staged under another generation, or edited after staging (content_hash changed); a memory with a vector but no text; a recorded provenance that does not match the current embedding env; --limit / --collection;
  • the index is rebuilt to match the target: plain vector_cosine_ops up to 2000 dims, the halfvec cast above, identical to what initPgvector() creates;
  • vector_prev keeps one generation for manual rollback.

Files:

  • api/src/services/embedding-migration.js: pure SQL builders and validators (reuses halfvecMode from pgvector.js).
  • api/scripts/reembed.js: the CLI. It uses its own pg.Pool from POSTGRES_URL as before, logs driver errors as errorSummary fingerprints, and prints the migration's own refusals verbatim. Ends with a [reembed-summary] JSON line and a nonzero exit on failure or refusal.
  • api/src/services/embedders/interface.js: getEmbeddingProvenance() (provider, model, dims, prefix fingerprints, normalization, generation id; optional EMBEDDING_GENERATION override). It stores fingerprints only, never the raw prefix text.
  • Provenance is stored as JSON in the existing zengram_meta table (same DDL as the structured store).
  • Docs: new "Switching Embedding Models" section in docs/operations.md, plus .env.example, docs/configuration.md, CHANGELOG.md (Unreleased) and the README.

Behaviour change: --commit used to re-embed in place. It now means --stage --apply, and --activate --apply does the cutover.

Tests

  • api/tests/embedding-migration.test.js: 39 tests covering CLI modes, coverage validation, provenance matching, stage and prev column planning, index selection, the no-wipe guard, metadata I/O against a stub pool, error redaction and cleanup. cd api && npm test: 389/389 pass locally (Node 22). Typecheck warning count is unchanged (84).
  • Ran locally against a throwaway pgvector/pgvector:pg16 container (pgvector 0.8.2) with a deterministic OpenAI-compatible stub embedder:
    • 1536 → 2048 dims: search kept answering from the old column while staging;
    • activation refused one new row and one edited row, and refused a model mismatch, with the active column unchanged each time;
    • catch-up restaged only those 2 rows; activation then produced vector(2048), a halfvec(2048) HNSW index and vector_prev vector(1536);
    • the API refused to start under the old env and searched in halfvec mode under the new one;
    • a same-dims model swap took the promote path, a row with a vector but no text blocked activation, and a step down to 768 dims restored the plain vector index.

🤖 Generated with Claude Code

reembed.js used to clear memories.vector before re-embedding when the
dimensionality changed, so vector search returned nothing until the
backfill finished (raised in #51).

Replace it with a staged dual-column migration:
- dry-run by default; --stage --apply embeds into vector_next with the
  embedding generation and each row's content_hash, resumably;
- --activate --apply checks full coverage and the recorded provenance
  under an advisory lock and an ACCESS EXCLUSIVE table lock, copies the
  current vectors to vector_prev, promotes vector_next in one
  transaction and rebuilds the HNSW index (halfvec above 2000 dims);
- activation refuses partial coverage, rows edited after staging, rows
  with a vector but no text, a provenance mismatch and --limit or
  --collection; the active column is never nulled while staging.

Pure SQL builders and validators live in services/embedding-migration.js
(reusing pgvector.js's halfvecMode); getEmbeddingProvenance() joins the
embedder interface; provenance is kept in zengram_meta. Driver errors
are logged as fingerprints, the migration's own refusals verbatim.

Behaviour change: --commit now means --stage --apply.

Refs #51

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 26 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 93 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Central YAML; Path: .coderabbit.yaml
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 491d87ce-80ac-4f7d-8e3f-b993aff316cd
Commits

Reviewing files that changed from the base of the PR and between fb8f170 and 436945b.

Files selected for processing: 9
  • .env.example
  • CHANGELOG.md
  • README.md
  • api/scripts/reembed.js
  • api/src/services/embedders/interface.js
  • api/src/services/embedding-migration.js
  • api/tests/embedding-migration.test.js
  • docs/configuration.md
  • docs/operations.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Brings in the api body-parser 2.4.0 lockfile fix (#65) and the Greptile ignore-glob config (#66).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zen-agents zen-agents Bot added the review:deep CodeRabbit plus independent deep review label Oct 24, 2026
@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 3/5

[High impact] The migration can change vector search behavior across the full memory corpus.

Fix wrong-model vector reuse and the whitespace-only coverage failure before merging.

Fix All in CodexFindings

  1. P1 Old model vectors get promoted ▶
  2. P1 Whitespace blocks the model switch ▶
  3. P2 Rollback details describe different vectors ▶
  4. P2 Bad scope flags allow cutover ▶

Summary

Replaces in-place re-embedding with staged vectors and a locked cutover.

  • Embedding changes now move through a staged cutover.

Intentional behavior: zen-agents[bot] states that activation must refuse rows with an active vector but no text.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Active vector serves search] --> B[Stage replacement vectors]
  B --> C[Record generation and content hash]
  C --> D[Lock memories for activation]
  D --> E{Identity and coverage match?}
  E -->|No| F[Roll back; keep active vectors]
  E -->|Yes| G[Copy active vectors to vector_prev]
  G --> H[Promote vector_next]
  H --> I[Rebuild matching HNSW index]
  I --> J[Commit and restart API with target model]
Loading

Reviews (1) · Last reviewed commit: "Merge origin/main into feat/staged-reemb..." · Reviewed by Greptile

Comment thread api/scripts/reembed.js
Comment thread api/src/services/embedding-migration.js
Comment thread api/scripts/reembed.js Outdated
Comment thread api/src/services/embedding-migration.js
- Refuse to stage when EMBEDDING_GENERATION is pinned but the embedding
  identity changed: vectors staged under the old model would otherwise
  be reused and promoted under the new one (checked before any write,
  and reported by the --stage preview).
- Use one embeddable-text rule in the CLI and the coverage SQL (first
  non-empty text field; whitespace-only text counts, as the API embeds
  it), so a whitespace-only memory no longer blocks activation forever.
- Keep embedding_migration_previous paired with vector_prev: staging
  records the active identity under embedding_migration_pending_previous
  and only activation moves it, in the cutover transaction.
- Reject malformed --limit/--collection values, and refuse --activate
  when either flag is supplied at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zen-agents

zen-agents Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed by the author agent for 436945b.

@zen-agents

zen-agents Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@zen-agents

zen-agents Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Merging 436945b without every selected review; the reviewers stay advisory, so this is recorded instead of counted as reviewed:

  • CodeRabbit: review skipped: rate-limited (next included review available at 2026-10-24T17:30:49.000Z, after the reviewer wait)

@zen-agents
zen-agents Bot merged commit 5b01509 into main Oct 24, 2026
8 checks passed
@zen-agents
zen-agents Bot deleted the feat/staged-reembed branch October 24, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:deep CodeRabbit plus independent deep review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants