Skip to content

fix(health): expõe indexOnly — o orphans do vectorCoverage é cego para vetor fora do map - #25

Merged
totobusnello merged 1 commit into
mainfrom
fix/health-index-only-vectors
Aug 28, 2026
Merged

totobusnello merged 1 commit into
mainfrom
fix/health-index-only-vectors

Conversation

@totobusnello

Copy link
Copy Markdown
Owner

O quê

vectorCoverage passa a expor indexOnly ao lado de orphans. Mudança aditiva, 21 linhas, um arquivo.

Por quê

orphans é totalMap − embeddedos dois lados vêm de vec_chunk_map. Um vetor válido no índice vec0 que perdeu a linha de map não pode aparecer nessa conta. E a mesma cegueira existe no prune-orphan-vectors (varre FROM vec_chunk_map LEFT JOIN chunks) e em qualquer alerta que consuma o campo: três instrumentos, um único ângulo.

Medido num deploy de produção em 2026-08-27:

vec_chunks_rowids (vetores VÁLIDOS no índice) = 69.261
vec_chunk_map (linhas)                        = 67.187
map cujo chunk NÃO existe                     =      0   <- o que orphans procura
                                    excedente =  2.074

São 2.074 vetores inalcançáveis pela busca, imprunáveis pela ferramenta que existe para isso, e reportados como 0 por todos os guardas. O tamanho (~25 MB) não é o ponto — a classe inteira é invisível, e se o mecanismo que a produziu ainda existir, ela cresce em silêncio.

⚠️ Origem não identificada, e declarada como tal: nem o prune nem trg_chunks_delete_cascade podem tê-los criado (os dois são atômicos). Este PR não tenta consertar a causa nem apagar os vetores — apagar sem saber a origem é apagar evidência. Ele só torna a classe medível.

Lição generalizável, que motivou o campo: um guarda cujo predicado exige o dado que falta não cobre a falta desse dado. O teste antes de aceitar um nothing to do: em que estado do mundo este guarda ficaria calado por não ter o dado, em vez de por não haver problema?

Como testei

  • em produção, no lineage da VPS onde a mesma mudança já roda: /api/health devolve {"embedded": 67187, "total": 67187, "orphans": 0, "indexOnly": 2074};
  • npm run build limpo (0 erros TS) e suíte em 377/380 — as 2 falhas são pré-existentes em edge-typing (KG), reproduzem isoladas e não tocam este arquivo;
  • 5 testes novos no lineage da VPS cobrindo a classe construída de propósito, a cegueira do detector antigo sobre ela (assert que falha se a premissa mudar), a disjunção dos dois contadores e o −1 de shadow ausente;
  • vec_chunks_rowids, que é tabela normal — não exige o módulo vec0 carregado, ao contrário de vec_chunks (que falha com no such module: vec0 no CLI). Envolvido em try/catch com −1, então instalação sem vec0 não regride.

Escopo, e o que deliberadamente não vem aqui

Os lineages são intencionalmente diferentes (docs/REDEPLOY-VPS-LINEAGE-PLAN.md: "3.2.1 é SUBSET do lineage da VPS — não unificar"), e a direção registrada é promover fix por fix. Duas correções irmãs ficaram fora de propósito:

  • o detector countIndexOnlyVectors() no prune-orphan-vectorso módulo não existe neste repo (é do core-kit trim), então não há o que corrigir aqui;
  • religar o escritor de search_telemetry.top_chunk_ids — o schema deste repo não tem essas colunas; adicioná-las é decisão de produto, não correção.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FXG3rN3mBLHSwKtiGSRktV

… para vetor fora do map

`orphans` é `totalMap − embedded`: os dois lados vêm de `vec_chunk_map`. Logo um
vetor válido no índice vec0 que perdeu a linha de map não pode aparecer nessa
conta — e o mesmo vale para o `prune-orphan-vectors`, que varre a partir do map, e
para qualquer alerta que consuma o campo. Três instrumentos, mesma cegueira.

Medido num deploy de produção em 2026-08-27: `orphans: 0` com **2.074** vetores
nessa condição (`vec_chunks_rowids` = 69.261 contra `vec_chunk_map` = 67.187, e
zero linhas de map cujo chunk não exista). Inalcançáveis pela busca, imprunáveis
pela ferramenta que existe para isso, e invisíveis a todos os guardas.

A mudança é aditiva: `indexOnly` sai ao lado de `orphans`, lendo a shadow table
`vec_chunks_rowids` direto — ela é tabela normal e não exige o módulo vec0
carregado, ao contrário de `vec_chunks`. `−1` sinaliza shadow ausente (vec0 nunca
criado), que não é zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXG3rN3mBLHSwKtiGSRktV

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 577a848ce6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread nox-mem/src/api-server.ts
embeddingOrphans = Math.max(0, totalMap - embedded);
try {
const idx = (db.prepare("SELECT COUNT(*) AS c FROM vec_chunks_rowids").get() as { c: number }).c;
indexOnly = Math.max(0, idx - totalMap);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count unmatched rowids instead of subtracting table totals

When vec_chunk_map also contains any stale vec_rowid absent from the vector index, subtracting the two table sizes lets those map-only rows cancel real index-only vectors. This state is reachable because upsertEmbedding deletes and inserts the vector and map rows in separate autocommit statements, so failures can leave either side unmatched; for example, one stale map row plus one unmapped vector makes this report indexOnly: 0. Query vec_chunks_rowids with an anti-join on vec_chunk_map.vec_rowid so the new health field measures the advertised set difference.

Useful? React with 👍 / 👎.

Comment thread nox-mem/src/api-server.ts
*
* −1 = shadow table ausente (vec0 nunca criado), que NÃO é zero.
*/
vectorCoverage: { embedded, total, orphans: embeddingOrphans, indexOnly },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include index-only vectors in observability health

When monitoring uses the advertised /api/observability/health endpoint rather than /api/health, this new metric is never exposed or incorporated into the vector indicator: handleObsHealth still derives vector_coverage and vectorHealth(...) solely from embedded, total, and map-based orphans. Consequently the production state described here (embedded === total, orphans === 0, but thousands of index-only vectors) still reports the vector indicator as green; propagate indexOnly into that endpoint and its health threshold.

Useful? React with 👍 / 👎.

@totobusnello
totobusnello merged commit b1da30b into main Aug 28, 2026
1 check passed
@totobusnello
totobusnello deleted the fix/health-index-only-vectors branch August 28, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant