Skip to content

#0584 record extraction provenance so content-free docs are not indexed as success - #79

Open
DevNexsler wants to merge 1 commit into
mainfrom
maint/0584-doc-organizer-a-doc-whose-vision-describe-timed-ou
Open

#0584 record extraction provenance so content-free docs are not indexed as success#79
DevNexsler wants to merge 1 commit into
mainfrom
maint/0584-doc-organizer-a-doc-whose-vision-describe-timed-ou

Conversation

@DevNexsler

Copy link
Copy Markdown
Owner

Summary

Maint ticket #0584. A doc-organizer document whose vision describe timed out was
indexed as a plain success with no description: the only trace was an aggregate
55 docs indexed with degradations WARNING, and because the EXIF/dimensions header made
full_text non-empty, enrichment still ran and fabricated content topics from the file
(topics=image, photo, MPO format for tenant maintenance photos that were never read) —
a different label set on nearly every one of 7 passes over the same unchanged image.

Two defects, both fixed at existing seams:

  1. Content-free docs indexed as success. ExtractionResult now carries
    primary_content, so an EXIF header is no longer mistaken for content, and
    content_status() classifies each doc complete / partial / missing from that plus
    its extraction degradations. This is one path for OCR, describe, transcription and PDF
    text — no per-extractor or per-doc-id special-casing. The pipeline writes
    content_status / content_failure_reasons onto every chunk (so the failure is legible
    in the record a consumer reads back, and slim search hits surface it), skips LLM
    enrichment when primary content is missing, and counts successful incomplete writes into
    a new docs_indexed_incomplete run-summary field.

  2. Same input → different output. Skipping enrichment for content-free docs makes the
    failed-describe path deterministic and drops a wasted per-run LLM call. Labels are also
    canonicalized (imgimage) and deduplicated case-insensitively at the enrichment
    normalization boundary, so identical input maps to identical labels and the taxonomy
    stops accumulating both spellings.

Test result

Full gate at cb156ca tree (python scripts/gate.py):

tier result tests
static (ruff + collect) pass 1671 collected
unit pass 1397 / 0 failures
integration pass 79 / 0 failures
staging-e2e (candidate container) pass 29 / 0 failures
live not run — preflight FAIL (host, see below)

MCP tool coverage 21/21 covered, 21/21 traced.

The live tier does not run on this host: its preflight fails on
comm-data-store-postgres-1 DNS (container not on this host's network) and on
config_test.yaml being absent from linked worktrees. That tier uses real paid providers
and is preflight-guarded by design, so it was not forced.

Candidate-container E2E (the pre-deploy gate): image built from this branch's
Dockerfile via docker-compose.staging.yml — isolated project, ports 17788/19999, own
named volumes, provider simulator, no production state or outbound writes.

  • source SHA: cb156ca24aa4d7a6a07845a0e84fb2f249853311 (squashed to this PR's commit)
  • image digest: sha256:f5dd2159e4f9866078a4acdc9e951b3dc6d5166da9ec1a31ea321db435c36992
  • all three containers reached Healthy; no restarts, no OOM
  • cleanup: gate's down -v removed all containers, volumes and the network (verified)

Regression coverage fails for the right reason before the fix: KeyError: 'content_status'
on the read-back chunk, and expected 'image, photo', got 'img, image, IMAGE, photo, Photo'
at the label boundary.


Maint ticket: #0584 — Maint-Manager tickets/review/0584-doc-organizer-a-doc-whose-vision-describe-timed-ou.md
On merge: verify this ticket's acceptance boxes against production (not tests),
tick them, then move the ticket to tickets/done/ and set status: done.
A merged PR is not a closed ticket — an unmerged PR is not a shipped fix.

…success

A document whose primary-content extractor failed (vision describe timeout,
transcription failure, PDF page OCR failure) landed in the index looking
exactly like a fully-extracted document. The only trace was an aggregate
WARNING in the flow log, so a search consumer — or a health check reading
outcome state — saw a plain success. Worse, the EXIF/dimensions header made
`full_text` non-empty, so enrichment still ran and turned the *file* into
content topics: production emitted `topics=image, photo, MPO format` for
tenant maintenance photos nobody had ever looked at, and a different label
set on nearly every pass.

Recorded at the existing `ExtractionResult` seam rather than per extractor:

- `ExtractionResult.primary_content` marks whether the content path actually
  yielded anything, so the EXIF header is no longer mistaken for content.
  `content_status()` classifies each doc `complete` / `partial` / `missing`
  from that plus the document's extraction degradations — one path for OCR,
  describe, transcription and PDF text, no per-extractor special-casing.
- The indexing pipeline writes `content_status` and
  `content_failure_reasons` onto every chunk, so the failure is legible in
  the record a consumer reads back, and slim search hits surface a
  non-`complete` status.
- LLM enrichment is skipped when primary content is missing, which removes
  the fabricated filler topics and a per-run LLM call for a doc that cannot
  be enriched. Two runs over an unchanged failed-describe file now produce
  identical metadata.
- Enrichment labels are canonicalized (`img` -> `image`) and deduplicated
  case-insensitively at the normalization boundary, so identical input maps
  to identical labels and the taxonomy stops accumulating variants.
- Successful incomplete writes feed a `docs_indexed_incomplete` count in
  the run summary and `index_metadata.json`.

Regression coverage: an integration test drives the real image extractor, a
timing-out vision provider and a real LanceDB upsert, then asserts on the
read-back chunk and re-indexes the unchanged file to prove stability; a
staging-e2e test arms the provider sim's describe fault and asserts the same
through the public MCP boundary.

Refs: Maint-Manager #0584

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DevNexsler added a commit that referenced this pull request Jul 31, 2026
Nightly reconciliation for 2026-07-31. Each defect was re-derived independently
against 7-10 days of production evidence BEFORE reading its fix; each PR's
regression tests were run against origin/main (RED) and this branch (GREEN).

Merged:
  #77  fix(embed): bound embedding inputs to the model context window (#0569/#0731)
  #80  one log record is one physical line (#0546)
  #82  fix(index): never overwrite a good row because the provider was down (#0619)
  #83  fix(index): report interrupted run progress (#0544)
  #85  fix(health): client-invalid 400/413/422 are not provider-critical (#0705)
  #86  fix(ocr): enforce the configured ocr.concurrency (#0732)
  #88  fix(index): ledger unchanged empty files (#0758)
  #76  make the doc-ID sweep converge and report what it did (#0545)

Cluster bake-offs (operator reconcile-notes):
  #0731 embed cluster: kept #77, closed #87. Production's own 400 body states
        "This model's maximum context length is 40960 tokens"; #87 encoded 32768
        and truncated by UTF-8 bytes, discarding ~80% of a valid input.
  #0732 OCR cluster: kept #86, closed #84. Same mechanism; #86 fails fast on a
        non-integer/bool/<1 concurrency instead of int()-coercing it.

Left open (not merged): #75, #79, #81. Closed as superseded by main: #78.

Gate on this branch: static pass, unit 1494/0 (origin/main baseline 1413/0),
integration 77/0, staging-e2e 31/0 across 4 consecutive runs.

NOT DEPLOYED — production has been in an unrelated outage since 08:36 UTC
(#0771, LanceDB ArrowInvalid projection error), so post-deploy verify could
neither confirm a deploy nor safely auto-roll-back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open — the headline claim does not reproduce; the residual is a weaker, different defect

Reviewed by the 2026-07-31 nightly reconciliation. Not merged, not closed.

#0584's central claim is that a doc whose vision describe timed out is indexed as SUCCESS and enrichment fabricates filler topics from the filename/EXIF, with the same image yielding different doc_type/topics on all 7 runs. I went looking for that in the live store before reading the fix.

Across all 2,999 img/video/audio docs in chunks.lance:

media docs with <60 chars of extracted content AND a populated enr_summary:  0

Zero. The fabrication half of the claim does not reproduce at all today. The two docs currently flagged vision_sidecar_failed in the degraded ledger (documents::001Og, 001Oo) are absent from the store entirely — correctly withheld, not indexed as false success.

What I did find is real but narrower: 205 of those 2,999 media docs sit in the index carrying only a context:c:0 chunk — the conversation context — with no description and, notably, enr_summary empty. So there is a genuine "content-free doc recorded as success without provenance" residue, which is what this PR's title actually addresses. But it is not the ticket's defect, and whether an attachment whose image failed but whose conversation context indexed fine should count as a failure is a design question, not a bug I can demonstrate.

This ticket's own most recent nightly note (2026-07-31) reaches the same place from the other direction: "consistent with later self-heal rather than permanent wrong data" and "No identifier reuse, payload contradiction, missing downstream write, or decision flip emerged."

Given that, a 514-line diff touching doc_enrichment.py, extractors.py, flow_index_vault.py and mcp_server.py is not something I will land at 4 AM on a claim I cannot reproduce. The merge gate wants a reproduced defect plus red/green; I have the red/green but not the reproduction.

What would make this mergeable: re-scope it to the residue I can actually measure — 205 media docs indexed with zero extracted content and no provenance marker — and give it a red that fails on that specific state. The extraction provenance idea is sound and probably worth having regardless; it just needs to be justified by the defect that exists rather than the one the ticket was filed for.

Ticket #0584 stays in review/.

@DevNexsler

Copy link
Copy Markdown
Owner Author

Nightly reconciliation 2026-08-01 — verified real, but left open: schema widening into an actively-failing table

I reproduced the defect. Not merging it tonight, and the reason is not about the diff's quality.

The defect is real (independently observed, not taken from the ticket)

/data/index/indexer.log, 1,713 OCR describe failed lines over 2026-07-22..07-31 (by day: 9 / 116 / 292 / 292 / 330 / 315 / 280 / 75 / 4). The content-free-success path is visible end to end, e.g. documents::002qT:

2026-07-28 12:56:36 WARNING extractors: OCR describe failed for ...@002qT@.jpg: timed out
2026-07-28 12:56:46 INFO doc_enrichment: Enriched 'documents::002qT': doc_type=image, photo, topics=image, photo, MPO format
2026-07-28 12:56:46 INFO prefect.task_runs: Upserted 1 chunks: documents::002qT

Describe failed, and the doc is still enriched with filler describing the file and upserted as a success. It finally got a real description on 2026-07-29 20:17 (topics=renovation, flooring, construction, ...) once the vision backend recovered — so the recall hole is transient here, but it lasted ~7 days and nothing downstream could distinguish it from a described image. The run-to-run label instability (img/image/image, photo) is also visible. The fix's theory is right and the approach looks sound.

Why it is not merging tonight

content_status and content_failure_reasons are written into the chunk metadata dict, so they become new sub-fields of the LanceDB chunks metadata struct. Measured on the live table right now:

top-level columns in dataset schema: 5
   struct field 'metadata': 102 sub-fields
fragments: 833

Production is currently down with exactly this class of error, on every index run since 07:04 UTC today:

pyarrow.lib.ArrowInvalid: External error: Invalid user input: The projection specified
the column index 67 but there are only 67 columns in the file
  at lancedb_store.py:331 _probe_table_read -> to_lance().to_table(limit=1)

10 consecutive failed runs; last success 06:57 UTC. That is #0771 recurring after PR #89 was merged and deployed (the merged sparse-fragment narrowing is present in the running image — I checked /app/lancedb_store.py). Widening a 102-sub-field metadata struct by two more fields, producing another generation of fragments with a different column count in a table that already cannot be read across its existing 833 fragments, is not something I am willing to land unattended at 04:00. It risks deepening the outage and it would certainly confuse its diagnosis.

Per the reconciliation brief, schema changes are explicitly not exempt from review and structural changes are better left open with a written reason than merged at 4AM. This is that reason.

What would unblock it

  1. #0771 resolved and the chunks table readable again (full-projection read succeeding across all fragments).
  2. An explicit statement of what happens to the 833 pre-existing fragments when the two new sub-fields appear — do they read back as null, or do they become sparse in the way that is currently breaking reads? A test that adds the fields to a table with older fragments and then does a full projection read would settle it. Note that a full-projection probe is exactly what _probe_table_read does and what is failing, so this is directly in the blast radius.
  3. Re-run the gate on a rebase onto current main — this branch's base predates the degraded-ledger backoff and the #0771 narrowing.

The ticket stays open; I have not moved it. Nothing here is a criticism of the change, which I would merge once the store is healthy.

DevNexsler added a commit that referenced this pull request Aug 1, 2026
… #0515, #0618)

Nightly PR reconciliation pass. Each defect was independently reproduced against
production before its fix was read, and each has a test that is RED on
origin/main and GREEN after — both sides run in this pass.

- #92 (#0785) live preflight fails closed + verifies indexer PID identity.
  Reproduced both directions against the live container: main's heartbeat-only
  check returned ok=True on docker-missing / probe-error / unreadable heartbeat,
  and simultaneously reported 'prod indexer active (heartbeat 104s old)' while
  no indexer process existed. RED 12 failed -> GREEN 48 passed.
- #91 (#0780) + #90 (#0781) exclude .evals/.worktrees/.gitnexus/logs/test_index
  from the Docker context. Measured on the real deploy path: 7.5 GB copied into
  the image before, 13.6 MB after, runtime files intact. RED/GREEN each.
- #75 (#0515) age the indexer heartbeat against the current run. Reproduced in
  the raw indexer.log: 2026-07-31 02:21:47 prev heartbeat, 02:34:33 new run
  registered, first stamp only at 03:42:15 -> /health false-503 'stalled' for
  ~50 minutes, matching a watcher 'unhealthy' recurrence at 02:53:47 UTC.
  RED 1 failed -> GREEN 31 passed.
- #81 (#0618) account for every degraded-ledger entry. Reproduced live: ledger
  held 70 entries while every run logged 'Re-queued 2'; 16 entries permanently
  unresolvable because upstream dedupe set canonical_message_id and the source
  query filters it out. RED/GREEN on a termination-invariant test written here.

Conflict resolution: PR #81 was written before main gained exponential re-queue
backoff and would have silently reverted it. Kept both — a resolvable entry is
only re-queued once its backoff window elapses, in its own report bucket.
Adversarial review also found the escalation could lose entries when the
terminal-ledger write failed; fixed and covered by a test.

Gate: static+unit+integration PASS (unit 1521/0 failures vs 1505/0 on
origin/main). staging-e2e is 1 failed / 30 passed on this branch AND identically
on origin/main and on 20d3a8b, so that failure is pre-existing, not introduced
here. Live tier not run (spends money; another worker held it).

PR #79 (#0584) deliberately left open — it widens the Lance chunks metadata
struct while production is down with a Lance projection error.
@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open again (2026-08-02 reconciliation) — defect re-verified, but the diff no longer applies and its stated blocker has only partly cleared

I verified the defect independently before reading the fix, and I am not closing this as unreproducible.

The symptom is real over a 9-day window, read from dexec:doc-organizer:/data/index/indexer.log1,713 OCR describe failed lines:

07-22   9      07-26  330      07-30    0
07-23 116      07-27  315      07-31    4
07-24 292      07-28  280
07-25 292      07-29   75

The 07-30/07-31 collapse is not remission: indexing was frozen from 08-01 07:04 by #0771, so nothing was describing anything. A quiet window here means the pipeline stopped, not that the defect stopped.

Why it still does not merge tonight, in order of weight:

  1. It is CONFLICTING against main. main moved again tonight (4a9b786). Landing it now means hand-resolving a 514-line diff across doc_enrichment.py, extractors.py, flow_index_vault.py and mcp_server.py at 04:00 and then re-establishing red/green on the resolved result. The merge gate wants a red I ran against the code that would actually ship; a conflict resolution I invent at this hour is not that.

  2. The "do not widen the metadata struct" hold from 08-01 was directionally right but for the wrong mechanism, and I want to correct the record. #0771's live blocker turned out to be one fragment (51330) whose manifest claimed 106 columns over a 67-column file — not a broadly inconsistent fragment set, and not a direct consequence of the struct being 102 fields wide. That is fixed and deployed tonight (4a9b786), and production is indexing again.

    So the original objection is weaker than it read. But it is not gone: sparse fragments are created by widening events, and they are the raw material this class of defect operates on. Measured tonight on the live table, 471 of 833 fragments are sparse. I also confirmed that adding a new metadata key makes _evolve_metadata_schema rebuild the entire table — on a copy of production that collapsed 833 fragments into 2. That is a 13 GB rewrite triggered by this PR's two new sub-fields, on a table that has been unreadable for two days. I am not doing that tonight.

  3. The prevention side is still missing. I deliberately did not touch _compact_data_files_if_due — gitnexus impact rates it HIGH (3 execution flows: ensure_fts_index, _optimize_and_prune, index_vault_flow) — so compaction still has no post-write verification. Until it does, deliberately widening the struct is a worse bet than it needs to be.

What makes this mergeable, unchanged from the 07-31 review plus one item:

  • rebase onto 4a9b786 and resolve the conflicts, then re-run red/green on the resolved tree;
  • keep the re-scoping the 07-31 review asked for — a red that fails on the residue that is actually measurable (media docs indexed with zero extracted content and no provenance marker) rather than on the ticket's original fabrication claim, which still does not reproduce;
  • land the compaction post-verify guard first, so a widening-induced rewrite cannot brick the table again silently.

Ticket #0584 stays in review/. The idea is sound and the defect is real; this is about sequencing and about a diff that no longer applies, not about merit.

@DevNexsler

Copy link
Copy Markdown
Owner Author

2026-08-03 04:00 reconciliation — defect CONFIRMED, merge BLOCKED on a deploy hazard

The defect is real and I reproduced it independently. I am not disputing it. Raw production log /data/index/indexer.log:

2026-07-31 03:49:32 INFO  Processing: email-attachments/cesar/2026-07/...__msg347732__mm0@002tF@.jpg (id=documents::002tF)
2026-07-31 03:52:48 WARN  OCR describe failed for ...@002tF@.jpg: [Errno 111] Connection refused
2026-07-31 03:52:57 ERROR LLM enrichment failed for 'documents::002tF': ConnectError: [Errno 111] Connection refused
2026-07-31 03:52:58 INFO  Upserted 2 chunks: documents::002tF

A doc with no visual content and no enrichment, written to the index as a normal completed write. That is this ticket's signature, 3 days ago, on the code currently running.

The signature is quiet in the last 48h only because describe volume collapsed (300+ describe lines/day 07-24..07-28 → 20 today) as the attachment backlog drained — not because anything was fixed. I am therefore explicitly NOT closing this as not-reproducible.

Why I am not merging it tonight

Deploying this PR would, on the first index run, trigger a full-table rewrite that this container almost certainly cannot survive.

  1. The PR adds content_status and content_failure_reasons to doc_meta in _process_doc_task. Those keys become named sub-fields of the metadata struct column of chunks.lance.
  2. Live production schema, read just now: 5 top-level fields, metadata struct = 102 sub-fields. Neither new field exists.
  3. _write_nodes_unlocked compares raw node metadata keys against the struct fields and, on a miss, calls _evolve_metadata_schema (lancedb_store.py:705). There is no allowlist and no size guard. That function does table.to_arrow() on the entire table, combine_chunks(), rebuilds the struct, create_tables a full second copy, then shutil.moves the live table directory and rebuilds indices.
  4. Measured on the live read-only snapshot: 65,304 rows, 937 fragments, 4096-dim vectors. to_arrow()3.85 GB peak RSS; after the required combine_chunks()4.56 GB peak RSS.
  5. doc-organizer runs under mem_limit: 8g and is currently resident at 5.4–5.6 GiB. An isolated 8 GiB control with matching ballast reproduced a cgroup OOM kill.
  6. "Schema evolved: added metadata fields" appears 0 times in 18 days of production log. (Caveat, and this weakens my point slightly: that message is only emitted after a successful swap, so zero occurrences proves no evolution has ever completed — not that the path has never been entered.)
  7. Context: this same table was unreadable from 2026-07-31 08:21 to 2026-08-03 02:39 — exactly one completed index run in ~66 hours — with ArrowInvalid ... ran out at field 'metadata', a metadata-struct width mismatch (#0771). The repair landed on main yesterday (4a9b786) and the current container has ~5 hours of uptime on it.

I put this to an adversarial second-opinion review (GPT-5.6 Sol, instructed to refute). Verdict: CANNOT REFUTE. It independently ran the materialization against a live snapshot and produced the 3.85/4.56 GB figures above, which are worse than my original estimate. It correctly corrected three points in my supporting narrative (the 12 GB on-disk figure is mostly MVCC history — the live Arrow payload is 1.88 GB; my "62 hours" and "~1 day soak" numbers were both imprecise); those corrections are folded in above and none of them change the conclusion.

What this PR needs before it can land

  1. Rebase. Currently 1 ahead / 55 behind main and CONFLICTING — it predates the entire #0771 repair.
  2. A migration story for the struct widening. Either land the two fields via an explicit, streaming/out-of-process backfill, or raise DOC_ORGANIZER_MEMORY_LIMIT for a one-shot migration window, or make _evolve_metadata_schema chunked instead of whole-table. As written it is an unbounded in-memory rewrite of the production index with no guard — worth fixing on its own merits, independent of this PR.
  3. Land after PR fix(#0814): reject unreadable Lance compactions #95. fix(#0814): reject unreadable Lance compactions #95 adds post-compaction fragment readback; with it deployed, a widening that produces an over-claiming fragment is caught and rolled back at compaction time instead of surfacing as an outage.
  4. Note for the reviewer: this PR also changes an MCP tool contractcontent_status is added to slim file_search hits — which is a protected surface and wants review on its own.

Merging #95 (#0814) and #96 (#0830) tonight; leaving this open. Ticket #0584 stays in review/ with these findings appended.

DevNexsler added a commit that referenced this pull request Aug 3, 2026
Both defects independently reproduced against live production before the
fixes were read; both have a test that is RED on 4a9b786 and GREEN here,
run on both sides in this pass.

#0830 — degraded backoff bypassed on the normal diff (PR #96).
  Prod at 2026-08-03 09:32 UTC: documents::001Og and documents::001Oo each
  carried {"attempts": 0, "transient_attempts": 204} in degraded_docs.json,
  0 rows in chunks.lance, and retried 19 times that day ~16 min apart — the
  scheduler cadence, not the 6h cap their 204 tries earn. A never-indexed doc
  is selected by the normal source/index diff every scan, so reconciliation
  assigned it "already_queued" and never evaluated _degraded_backoff_seconds.
  RED on main: a simulated 24h of 16-minute scans processes the doc 90 times
  (assert 90 <= 4).

#0814 — Lance compaction recorded success without reading anything back (PR #95).
  _compact_data_files_if_due wrote the daily cadence marker as soon as the
  worker exited 0. On 2026-07-31 a compaction committed a fragment whose
  manifest over-claimed its columns; the marker recorded success and the table
  became unreadable. Production ran from 2026-07-31 08:21 to 2026-08-03 02:39
  with exactly one completed index run (ArrowInvalid ... 'ran out at field
  metadata'). RED on main: corrupt output returns True and is recorded;
  readable output receives zero fragment probes.

Gate, this branch vs 4a9b786 run identically: unit 1530 vs 1523 (+7 = the new
tests), integration 77, staging-e2e 31, tool coverage 21/21 covered and traced
— 0 failures on both sides. Live tier did not run on either side (known host
preflight failure on comm-data-store-postgres-1 DNS; it drives paid providers
and was not forced). No new failures.

PR #79 (#0584) deliberately NOT included: defect confirmed, but it widens the
chunks.lance metadata struct 102 -> 104 fields, which on first write triggers
_evolve_metadata_schema — an unguarded whole-table to_arrow() measured at
4.56 GB peak RSS against a container at 5.4 GiB under an 8g limit. Left open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DevNexsler added a commit that referenced this pull request Aug 6, 2026
…ticity

PR #97 verified this pass: reproduced RED on origin/main by pointing pytest
--basetemp at a real 93.8%-full tmpfs (no mocks, no env overrides) — 7 failed,
8 passed in tests/test_mcp_contract.py -k health_probe, including #0515's own
regression test. GREEN on the PR branch against the same filesystem: 15 passed.
The new staging-compose contract test is red against main's compose (KeyError:
'DISK_USAGE_MAX_PERCENT') and green with the pin.

PR #79 (#0584) deliberately NOT included — see the reconciliation report.
@DevNexsler

Copy link
Copy Markdown
Owner Author

2026-08-06 04:00 reconciliation — defect RE-CONFIRMED (fresh, yesterday). Still NOT merged: the deploy hazard got worse.

Verified independently from the raw production log before reading the diff, per rule zero.

The defect is real and fired yesterday

The 2026-08-03 pass noted the signature had gone quiet as describe volume collapsed. It came back. OCR describe failed per day over the full 16-day log (/data/index/indexer.log, 2026-07-21 18:55 .. 2026-08-06 09:13):

07-22   9 | 07-23 116 | 07-24 292 | 07-25 292 | 07-26 330 | 07-27 315
07-28 280 | 07-29  75 | 07-31   4 | 08-05   6

documents::002Ab — an email attachment first seen 2026-08-05 20:01, describe unreachable, indexed anyway:

20:01:25 INFO  Processing: ...__msg493733__mm0@002Ab@.jpg (id=documents::002Ab)
20:01:39 WARN  OCR describe failed for ...@002Ab@.jpg: [Errno 113] No route to host
20:02:00 INFO  Enriched 'documents::002Ab': doc_type=image, photo,
               topics=Property Management, Photo Documentation, Image Metadata, Maintenance, Samsung G…
20:02:00 INFO  Inserted 1 chunks: documents::002Ab

Those topics are the enricher describing the file — "Image Metadata", "Samsung G[alaxy]" is the EXIF camera string. What the image actually contains only became visible at 23:55, when the provider recovered and a real describe ran:

23:55:53 INFO  Enriched 'documents::002Ab': doc_type=receipt, bank_deposit, photograph,
               topics=Banking, Deposit, Receipt, Chase, Transaction, Personal Finance, Cash Deposit…

It is a bank deposit receipt, not a property-maintenance photo. For ~4 hours the index served confident, wholly fabricated metadata for it, as a normal completed write. documents::002Ac is the same story (fabricated "property management, photo documentation, maintenance, real estate" → actually a bank deposit slip / rent payment).

Note which pass leaks: the provider_unavailable guard correctly protected the 20:43 and 22:42 re-runs (keeping the indexed version … instead of overwriting it with a degraded one). It cannot protect the first pass, because an Inserted has no prior row to keep. That is exactly the gap this PR closes, and it is unaddressed on main.

Verdict: not closable as not-reproducible. This PR is solving a real problem.

Why it still cannot be merged tonight

The 2026-08-03 hazard was re-measured, not taken on faith, and it is worse:

2026-08-03 2026-08-06
chunks.lance rows 65,304 66,859
metadata struct sub-fields 102 102 (still no content_status / content_failure_reasons)
doc-organizer resident 5.4–5.6 GiB / 8 GiB 6.95 GiB / 8 GiB (86.8%)

Measured read-only against the live dataset just now; the 6.95 GiB figure is stable across five samples with no index run active, so it is baseline, not a spike. Headroom is ~1.05 GiB.

The trigger is unchanged and still unguarded. flow_index_vault.py in this PR writes content_status / content_failure_reasons into doc_meta; _write_nodes_unlocked (lancedb_store.py:1140-1153) diffs incoming metadata keys against the struct and calls _evolve_metadata_schema on any miss — no allowlist, no size guard, still true on main after the #0771 repair. That function (lancedb_store.py:705) does table.to_arrow() over the whole table, combine_chunks(), then create_tables a full second copy. Previously measured at 3.85 GB / 4.56 GB peak RSS (independently reproduced by GPT-5.6 Sol, which returned CANNOT REFUTE). A 4.5 GB transient into 1.05 GB of headroom is a cgroup OOM kill of the production indexer on the first index run after deploy.

Blockers, updated

  • Land after PR fix(#0814): reject unreadable Lance compactions #95 (#0814) is deployed — merged 5b1b16f, 2026-08-03.
  • Rebase. Now 1 ahead / 61 behind main, still CONFLICTING.
  • A migration story for the struct widening. This is the real blocker and it is nobody's ticket, which is why this PR has now been deferred twice for the same reason. Filed as #0926 so it can be fixed on its own merits (chunked/streaming evolve, or an out-of-process backfill, or a one-shot raised DOC_ORGANIZER_MEMORY_LIMIT window). Unblocking #0926 unblocks this PR.
  • Separate review for the MCP tool contract changemcp_server.py adds content_status to slim file_search hits. A tool-contract change is a protected surface; the operator's gate says prefer leaving it open with a written reason over merging it at 4AM.

Deliberately not rebased or partially merged tonight: splitting the enrichment/normalization half from the metadata-provenance half would land the cosmetic part and leave the recall hole open, and would still need the same review.

Ticket #0584 stays in review/.

@DevNexsler

Copy link
Copy Markdown
Owner Author

2026-08-08 04:00 reconciliation — defect RE-CONFIRMED (3rd time), still not merged

The defect is real. Re-derived from the raw production log before reading this diff, per rule zero. /data/index/indexer.log, first-write path on an unchanged image:

2026-08-05 20:01:39 WARNING extractors: OCR describe failed for .../mm0@002Ab@.jpg: [Errno 113] No route to host
2026-08-05 20:01:40 WARNING extractors: OCR describe failed for .../mm1@002Ac@.jpg: circuit open for http://192.168.68.87:4000/v1 ...
2026-08-05 20:01:56 INFO doc_enrichment: Enriched 'documents::002Ac': doc_type=image, photo, topics=property management, photo documentation, maintenance, real estate, mobile photo
2026-08-05 20:01:57 INFO prefect.task_runs: Inserted 1 chunks: documents::002Ac
2026-08-05 20:02:00 INFO doc_enrichment: Enriched 'documents::002Ab': doc_type=image, photo, topics=Property Management, Photo Documentation, Image Metadata, Maintenance, Samsung G…
2026-08-05 20:02:00 INFO prefect.task_runs: Inserted 1 chunks: documents::002Ab

Describe never succeeded; both docs were enriched from EXIF and written as normal completed writes. The provider_unavailable guard protected the 20:43 and 22:42 re-runs but cannot protect an Inserted — there is no prior row to keep. That first-write gap is still unaddressed on main.

Per-day OCR describe failed over the full 18-day log (2026-07-21 18:55 .. 2026-08-08 08:06): 9 / 116 / 292 / 292 / 330 / 315 / 280 / 75 (07-22..07-29), then 0, 4 (07-31), 0, 0, 0, 6 (08-05), 0, 0, 0. Quiet for the last three days because describe volume collapsed, not because the path changed. Not closable as not-reproducible.

Why it is still not merged

Not a verdict on the fix — the blocker is unchanged and now has an owner.

  1. #0926 is still open and unfixed. Re-read on main tonight: lancedb_store.py:1140-1153 still calls _evolve_metadata_schema(new_fields) on any metadata-key miss, with no allowlist and no size guard. This PR adds content_status / content_failure_reasons to doc_meta, which are exactly such a miss, so the first index run after deploy triggers the unbounded whole-table rewrite.
  2. Headroom is still short. doc-organizer resident 4.28 GiB / 8 GiB, steady across 4 samples with no index run active — better than the 6.95 GiB measured on 08-06, but that leaves ~3.7 GiB against the previously measured ~4.56 GB (4.25 GiB) peak for to_arrow() + combine_chunks(). Still under water, and chunks.lance has grown to 22 GB / 4984 data files.
  3. Still CONFLICTING against main and needs a rebase.
  4. MCP tool-contract change (content_status added to slim file_search hits) still wants its own review — this repo requires e2e coverage and mcp.tool.<name> spans for tool surfaces.

Per the operator's brief, genuinely structural changes are left open with a written reason rather than merged at 4AM. #0926 is the thing to fix; it unblocks this.

Ticket #0584 stays in review/.

@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open by the 2026-08-10 nightly reconciliation. main has moved to f58198c; this PR is still CONFLICTING and needs a rebase.

What I could verify about the defect (it is real, but narrower and quieter than the ticket says):

The most recent instance is 2026-08-05, and it is a clean reproduction of defect 1. documents::002Ab / 002Ac:

20:01:39 WARNING extractors: OCR describe failed for ...__mm0@002Ab@.jpg: [Errno 113] No route to host
20:01:56 INFO doc_enrichment: Enriched 'documents::002Ac': doc_type=image, photo, topics=property management, photo documentation, maintenance, real estate, mobile photo
20:01:57 INFO prefect.task_runs: Inserted 1 chunks: documents::002Ac
20:02:00 INFO doc_enrichment: Enriched 'documents::002Ab': doc_type=image, photo, topics=Property Management, Photo Documentation, Image Metadata, Maintenance, Samsung G...
20:02:00 INFO prefect.task_runs: Inserted 1 chunks: documents::002Ab

Describe failed, the doc was enriched from EXIF alone, and a chunk was inserted — with the run closing green. And the consumer still cannot tell: I read the live chunks.lance schema and neither content_status nor content_failure_reasons exists in the 102-field metadata struct, so nothing downstream distinguishes "image described" from "image never read".

Three things that argue against merging it tonight:

  1. The trigger is largely quiet. OCR describe failed per day across the retained log: 292–330/day on 07-24..07-28, then 75 (07-29), 4 (07-31), 6 (08-05), and 0 on 08-06 through 08-10. The named victims have also healed on their own — documents::002qT now indexes as "Interior renovation progress..." with real topics, not image, photo, MPO format, and 002Ab/002Ac re-described successfully at 23:55 the same evening. Quiet is not disproven (I am not closing this), but it is not the daily bleed the ticket describes.
  2. A partial mitigation already landed on main. On re-passes the pipeline now logs Provider unavailable (ocr_describe_failed) — keeping the indexed version of documents::002Ab instead of overwriting it with a degraded one. The remaining exposure is the first pass, where there is no prior entry to keep.
  3. It is a schema change on a protected surface. It adds content_status and content_failure_reasons to every chunk. Neither exists in the live struct, so the first index run after deploy triggers _evolve_metadata_schema on a 67,918-row / 102-field table. That is exactly ticket #0926, whose fix (PR fix(#0926): bound Lance metadata schema widening #102) just landed in f58198c — so the blocker is now removed, but this PR has not been rebased onto or gated against it.

What the owner should do: rebase onto f58198c, re-run the full gate (the streaming widening path is new underneath you), and confirm the first-pass insert is what the regression actually pins. Per the reconciliation brief I am not merging a +514/-14 structural schema change at 04:00 without that.

@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open by the 2026-08-11 04:00 reconciliation — but for a newly measured reason, not the old one. The reconciliation work is done and pushed to reconcile/pr79-20260811; what blocks this is a deploy hazard I measured tonight, and it is now filed as Maint #1042.

The defect is real — re-derived from the raw log before reading the diff

/data/index/indexer.log, 2026-08-05 (inside a 7-day window):

20:01:39 WARNING extractors: OCR describe failed for ...__mm0@002Ab@.jpg: [Errno 113] No route to host
20:02:00 INFO doc_enrichment: Enriched 'documents::002Ab': doc_type=image, photo,
         topics=Property Management, Photo Documentation, Image Metadata, Maintenance, Samsung G...
20:02:00 INFO prefect.task_runs: Inserted 1 chunks: documents::002Ab

Ground truth at 23:55 the same evening, once the provider recovered: doc_type=receipt, bank_deposit, photograph, topics=Banking, Deposit, Receipt, Chase.... It is a bank deposit receipt, and for ~4h the index served confident "property maintenance photo" metadata for it. main's provider_unavailable guard correctly protected the 20:43 and 22:42 re-runs — it cannot protect an Inserted, because there is no prior row to keep. That first-write gap is what this PR closes, and content_status still does not exist in the deployed /app/flow_index_vault.py.

Red/green, run here

  • RED — this PR's tests against origin/main source: 3 failed (KeyError: 'content_status' on the read-back chunk x2, plus assert 'img, image, IMAGE, photo, Photo' == 'image, photo').
  • GREEN on the reconciled tree: 7 passed.
  • Conflicts resolved (both additive collisions where main's #0619 outage guard landed beside #0584's helper — kept both sides), then re-parented onto current main (5fc6f66) and re-gated so the tested tree is the ship tree: static PASS, unit 1,556 passed, integration 79 passed, candidate-container staging-e2e 33 passed. Live tier not forced (three host-environment preflight failures; it spends real money).

So the merge gate is satisfied. The deploy is what fails.

What actually blocks it (new, measured tonight)

The prior deferrals cited #0926 — the unbounded whole-table rewrite. #0926 landed (37a5ea4), is deployed, and its verify check exits 0. I did not take that as sufficient, and built a production-shaped shadow migration: 68,413 rows, 4,096-dim vectors, a 102-field metadata struct, 1,369 data files — the live shape of chunks.lance — running the deployed _evolve_metadata_schema with _cgroup_memory_limit_bytes pinned to production's 8 GiB.

Adding exactly this PR's two fields:

run duration RSS delta over the migration window disk high-water reader ops / errors
1 65.7 s +3.81 GiB 2.85 GB 2,445 / 1
2 60.0 s +3.81 GiB 2.85 GB 2,444 / 0
3 56.7 s +3.60 GiB

Phase-split: the allocation is in db.create_table(temp_name, reader, ...) — the streaming write — not the scalar-index rebuild (0.0 s, no extra). #0926 bounded the read side; LanceDB buffers the incoming RecordBatchReader, and the max_row_bytes guard only ever inspects one widened batch, so it stays silent.

Production headroom: doc-organizer is at 4.727 GiB of an 8 GiB limit, stable across 5 samples with the indexer idle → 3.27 GiB free against a measured 3.60–3.81 GiB. Deploying this would OOM-kill the indexer on the first index run.

#0926's check misses it because it builds at ROWS=20000, DIM=64, 40 metadata fields — 219x less vector volume than production.

Second opinion

I asked GPT-5.6 Sol to refute my original conclusion (that this should be held). It returned WEAK and was right to: my stated reason was unfalsifiable — waiting buys no evidence — and it ran an 8,000-row control showing 15.7 s and zero reader errors. So I stopped holding on judgement and went and measured at production shape instead. That measurement is what now blocks the deploy, and it is the opposite of the small-table result. Its other objections were also actioned: the tested tree is now re-parented onto current main and re-gated, and the rollback path was exercised (forced mid-write failure → 68,413 rows preserved, no stray chunks__schema_tmp/__schema_backup, field not added). Its security claim — that tool output exposed production credentials — I checked against the retained transcript and found no credential-shaped strings; I have not rotated anything on an unverified claim.

What this needs

  • Maint #1042 — bound the widening's write side, and re-shape the check so a green run means it fits in production headroom. That is the blocker.
  • Alternatively, a deliberate one-shot raised DOC_ORGANIZER_MEMORY_LIMIT window to absorb this specific migration, decided by a human rather than at 04:00.

Branch reconcile/pr79-20260811 carries the resolved merge, the re-parent onto 5fc6f66, and the measurement harness (shadow_widening_probe.py, shadow_phase_probe.py) so none of this has to be redone.

DevNexsler added a commit that referenced this pull request Aug 12, 2026
Verified tonight against production, each with a test that is red on 73905ed and
green here:

- #108 (#1065) a run that logged completion=100.0% at 04:19:51 was marked
  status=lost 27 s later by the instance that came up at 04:20:08. Only
  completed same-run heartbeat evidence is promoted; every weaker form stays
  lost.
- #107 (#1064) credential-shaped message content reached enrichment, the
  OpenRouter embedding call and LanceDB verbatim; the live index holds 214 such
  chunks across 169 docs. Redaction and quarantine now run before every provider
  and storage boundary. Two corrections to the PR are folded in: it redacted
  ordinary English as a Basic/Bearer credential (47 matches over 25 live docs),
  and it missed a digit-free base64 value.
- #106 (#1042) the widening's write side allocated 3.72 GiB against 2.74 GiB of
  production headroom. Bounded writes cost 0.45-0.75 GiB measured at production
  shape and on a copy of the live table under production's own 8 GiB cgroup.
- Plus a data-loss window that adversarial review surfaced and this branch
  closes: dying between the swap's two renames left no active table at all, and
  the next widening deleted the backup as stale. Recovery is marker-gated,
  fail-closed, and proved by killing a real widening with os._exit in that
  window.

PR #79 (#0584) is deliberately NOT in this merge: it is the change that triggers
the metadata struct widening, and it also alters the file_search tool contract.
Reasons on the PR.

Gate on this tree: static pass, unit 1585, integration 78, staging-e2e 32,
21/21 MCP tools covered and traced (.evals/gate-runs/20260812-081511). The live
tier did not run: its comm-store Postgres preflight fails identically on
73905ed because a docker-network hostname does not resolve from the host.
@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open for the fifth time — but the reason is now different, and much narrower.

The defect is real; I re-derived it from the raw log before reading the diff, as every prior pass did. documents::002Ab / 002Ac on 2026-08-05 20:01: OCR describe failed ... [Errno 113] No route to host, enriched from EXIF anyway (topics=Property Management, Photo Documentation, Image Metadata, Maintenance, Samsung G…), written as Inserted 1 chunks. At 23:55 the provider recovered and the real describe returned doc_type=receipt, bank_deposit, photograph — they are bank deposit receipts, served for ~4 h as maintenance photos. content_status still appears 0 times in the deployed /app/flow_index_vault.py. Per-day OCR describe failed over the full 22-day log: 9 / 116 / 292 / 292 / 330 / 315 / 280 / 75 (07-22..29), then 0, 4 (07-31), 0, 0, 0, 6 (08-05), then 0 through 08-12. Quiet because describe volume collapsed, not because the path changed. Not closable as not-reproducible.

The merge gate is satisfied: RED against origin/main with this PR's tests → 4 failed (KeyError: 'content_status' x2, assert 'img, image, IMAGE, photo, Photo' == 'image, photo', and the slim-hit flag), GREEN on a reconciled tree. A full gate on that tree passed static / unit 1577 / integration 80 / staging-e2e 33 with 21/21 MCP tools covered.

The previous blocker is gone. #1042 landed tonight in 666c9c0. Measured on a copy of the live 69,107-row table under production's own 8 GiB cgroup, adding exactly this PR's two fields: 9.7 s, 454 MB RSS delta, every row preserved. The OOM hazard that held this PR three times is fixed and deployed.

What held it tonight instead. Adversarial review of the reconciliation found a data-loss window in the very migration this PR triggers: _evolve_metadata_schema installs the widened table with two shutil.move calls, and dying between them leaves no active table while the next widening deletes the backup as stale. Reproduced on origin/main. Tonight's merge closes it (durable swap marker, cross-process lock across both renames, recovery that never replaces an existing active table, and a fault-injection test that os._exit(9)s a real widening in that window). But that recovery was written, refuted once, and rewritten inside this same pass — shipping the schema migration that first exercises it, in the same pass, on the same night, is the one thing I am not willing to do at 04:00. It costs one nightly cycle of soak and buys a migration path that has been reviewed rather than co-authored with the change that triggers it.

What remains before this lands (all small):

  1. Re-merge onto the new main (666c9c0) and re-gate — the tested tree must be the ship tree; reconcile/20260812-with-pr79 (pushed) carries tonight's version of that merge, gated green at .evals/gate-runs/20260812-073917.
  2. The separate review for the MCP tool-contract change (content_status added to slim file_search hits) that four passes have now asked for. My own check: it is an additional optional key emitted only when extraction was incomplete, and no consumer in this fleet validates strictly — trace-verifier shells out via mcporter call and hands the JSON to a judge, the hermes side declares function file_search(query, top_k), and there is no extra="forbid" or additionalProperties: false anywhere. That is a static argument, not an executed one; a reviewer should run one real consumer against a changed response.
  3. The chunk-metadata struct goes 102 → 104 sub-fields on the first index run after deploy. Worth one operator-aware window rather than an unattended one, now that the migration is bounded and crash-recoverable.

Ticket #0584 stays in review/.

DevNexsler added a commit that referenced this pull request Aug 26, 2026
…ompaction guard

Two changes, both independently reproduced against production before any fix
was read, and both with a test this pass ran RED on origin/main and GREEN here.

  #135  #1254  run the daily Lance compaction in the idle window, bounded by
                cgroup headroom  (reconciled against #118, which landed after it)
  --    #1101/#1110/#1436  key the fragment-repair column layout on the data
                file version  (authored by this pass; supersedes PRs #113, #114
                and #139, all three of which still fail the defect's own test
                when applied to current main under the deployed lance 10)

Test comparison, both sides run by this pass:

  tier                                origin/main        reconcile
  static (ruff + collect)             pass               pass
  unit    (host venv, lance 4.0.0)    1710 passed        1723 passed
  integration (host venv)             90 passed          90 passed
  unit    (deployed image, lance 10)  2 failed / 1707    1722 passed
  integration (deployed image)        90 passed          90 passed
  staging-e2e (compose stack)         -                  39 passed, 21/21 tools
  live                                not runnable       not runnable

The two lance-10 failures on main are the fragment-repair defect; they are gone
here and nothing new failed. The live tier is blocked on both sides by its own
safety preflight, 'FAIL prod_indexer_idle: prod indexer active (pid 176)' — a
5,617-document production run was in flight for this whole pass.

Left open deliberately, with findings written on the PRs: #79 (#0584) — its
defect still reproduces on main, but it adds two persisted fields to every
chunk, which triggers a whole-table Lance metadata-struct widening (production
last did one over 75,001 rows on 2026-08-22) on a container currently sitting at
7.15 GiB of an 8 GiB cap; it should land after this compaction guard is proven,
not beside it. #140 (#1468) — the flake is real and machine-recorded, but the
mechanism the PR closes was measured not to exist (0 of 600 trials at two load
levels).

Contract change flagged for after-the-fact review: ensure_fts_index() loses its
compact_data parameter, restoring the signature core/storage.py::StorageInterface
already declared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open, deliberately, with a reason and a sequencing. The defect is real and still reproduces on current main — this is about when the fix lands, not whether.

The defect reproduces. I ran this PR's own regression file against origin/main (181 commits after this branch's base), on the host venv:

$ python -m pytest tests/test_incomplete_indexing.int.test.py -q
FAILED test_failed_image_description_is_consumer_visible_and_stable
FAILED test_successful_image_description_is_recorded_as_complete
2 failed

So a doc whose vision describe fails still lands as a plain success today. Confirmed too that neither content_status nor docs_indexed_incomplete appears anywhere in 36 days of deployed indexer.log.

The trigger is quiet, but that is not why I am holding it. Extraction failures in the retained log: 88–330/day from 2026-07-22 to 07-31, then 12 on 2026-08-05, then zero for 21 days. The aggregate N docs indexed with degradations WARNING still fires (last 2026-08-22). A provider outage — circuit open for http://192.168.68.87:4000/v1, [Errno 113] No route to host — is what produces the failures, and that recurs.

Why it waits. This PR writes two new keys onto every chunk's doc_meta:

"content_status": content_status,
"content_failure_reasons": ", ".join(content_failures),

In this store a new metadata key triggers _evolve_metadata_schema, which rewrites the whole metadata struct column across the whole table. Production last did one on 2026-08-22:

WARNING lancedb_store: Widening Lance metadata schema: fields=['effective_date'] rows=75001 memory_limit_bytes=8589934592

That is a stored-record schema change on 75k+ rows, and it would run on the first index run after deploy. At reconcile time the container was at memory.current 7.15 GiB of an 8 GiB memory.max, with memory.peak already at the cap. #1254's compaction guard — the thing that stops a second full-memory process being forked into that same cgroup — only landed tonight (2bde0ab, merge 0367ba7) and has not yet been through an idle window in production. Stacking a whole-table metadata widening on top of an unproven memory fix, at 04:00, across a 181-commit rebase that touches the enrichment path where #1097/#1151/#1251/#1330 all landed in the last two days, is the shape of change the reconciliation brief says to leave open with a written reason. This is that reason.

What it needs to land

  1. Rebase onto main (0367ba7) and re-resolve doc_enrichment.py / flow_index_vault.py against the enrichment changes that landed since — in particular the enr_doc_type canonicalisation from #1251 and #1330, which overlaps this PR's own label normalisation and may make part of it redundant.
  2. Let scripts/verify-checks/1254-compaction-does-not-cost-an-index-run.sh go green first (it is on a 14-day soak from 2026-08-26).
  3. Watch the first post-deploy index run for the Widening Lance metadata schema line and its storage_schema memory measurement, on the real table, before calling it done.

Nothing here is a criticism of the change — the two-defect analysis in the description holds up and its regression coverage is the right shape (asserting on the read-back chunk, not on flow state). Ticket #0584 stays in tickets/review/.

@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open by the 2026-08-27 reconciliation — real defect, but this PR no longer matches it

Reviewed against production over a five-week window (/data/index/indexer.log, 629,428 lines,
2026-07-21 to 2026-08-27). Splitting #0584 into its two defects, because they have diverged:

Defect 1 — content-free docs indexed as success: trigger quiet for 22 days

OCR describe failed for ... per day:

07-22    9    07-25  292    07-28  280    07-31    4
07-23  116    07-26  330    07-29   75    08-05    6
07-24  292    07-27  315    ...           (nothing since)

Last occurrence 2026-08-05. Zero in 22 days. The routing moved to cloud describes
(gpt-5.6-terra / gemini-3.5-flash after the LiteLLM migration), and the old
192.168.68.87:4000 failures went with it.

The structural defect is still in mainextract_image sets vision_text = "" on a
describe exception and returns the EXIF header alone as full_text, with no marker that the
image was never read. So I am not closing this as not-reproducible. But it is now partly
mitigated by work that landed after this PR was written: _keep_indexed_version_on_outage
drops the pending write and keeps the previously indexed row when the degradation is
transient, and a describe timeout is transient. The unmitigated case is a new doc, which
has nothing to keep.

Defect 2 — unstable enrichment: live, huge, and NOT fixed by this PR

This is the part that is firing right now. Of the 593 documents re-enriched since 2026-08-20:

  • 86.3 % got a different doc_type on the second run
  • 96.6 % got different topics

on unchanged content. Examples straight out of the log:

08-20  doc_type=email, payment_notification    08-26  doc_type=payment_notification, system_email
08-20  doc_type=email, system_notification     08-26  doc_type=system_notification, email

and one document across five passes: email notification / email, notification /
email_notification / notification, email.

This PR's _LABEL_ALIASES map is {img→image, images→image, photograph→photo, msg→message}.
Not one of the 512 unstable doc_type values I measured is in that map — today's variation
is word order, separator and compound choice, not img vs image. It is a per-instance alias
map against a cause it does not cover, and it will need editing again on the next variation.

Acceptance box 5 — "two runs over an unchanged file produce identical enrichment output" — is
the one that would actually fix this (content-digest keying, which also removes a redundant LLM
call per doc per run), and this PR does not implement it.

Also blocking

+514/-14, single commit from 2026-07-26, base 8463df8184 commits behind main, and
CONFLICTING in flow_index_vault.py and tests/e2e/test_hooks_and_faults.py (6 hunks; they
are additive-vs-additive and resolvable, but the surface has moved a lot underneath). It is a
structural change: a new CONTENT_MISSING sentinel in extractors.py, a new field on the
indexed record, and a changed write_index_metadata_task signature. The brief's guidance is to
prefer leaving genuinely structural changes open with a written reason rather than merging them
at 4 AM, and I am following it.

Suggested split

  1. Rebase the extraction-provenance half onto current main and land it on its own — it is the
    defensible core and it is still needed for new docs.
  2. Drop _LABEL_ALIASES and replace it with the determinism fix (key enrichment on the content
    digest, reuse the stored result). That addresses normalisation, stability and the redundant
    LLM call in one change, and it is the only part of this PR that touches the defect that is
    actually running today.

The measured 86.3 % / 96.6 % instability has been recorded on ticket #0584 so it is not lost.

DevNexsler added a commit that referenced this pull request Aug 27, 2026
…tion

Every defect below was reproduced from production state or from a control run here
before its fix was read, and every red/green was executed on both sides by this pass
rather than taken from a PR body. Where a PR's red was only an ImportError on a new
symbol (#1655, #1657), a behavioural test that runs unchanged on both sides was written
so the red proves wrong behaviour, not an absent name.

Landed (PR -> ticket):
  #154 -> #1625  serve queued source work at bounded checkpoints during a long sweep,
                 and return 503 stale_tail once a request waits past its SLO
  #153 -> #1654  tier tests by location before name; match the _live.py suffix
  #152 -> #1663  stop the unit tier reaching the real detached indexer launcher
  #151 -> #1659  stop the staging gate racing for fixed host ports
  #150 -> #1661  keep Lance stale-read recovery in the store, not in mcp_server
  #149 -> #1662  keep the upstream body at every provider rejection site
  #148 -> #1657  keep the upstream body on a permanent HTTP rejection
  #147 -> #1658  match provider-sim's embedding character cap to the real route
  #146 -> #1650  let the telemetry seam see qwen's discarded first answer
  #145 -> #1655  bound embed inputs by the route's character cap, not tokens alone
  #144 -> #1656  recover Lance reads while a peer writer keeps moving the table
  #143 -> #1649  bound the nested index write lock join for deadlock, not scheduling
  #142 -> #1629  observe the enrichment route contract on every answer

Overlaps resolved rather than merged blind: #149 stacks on #148, so one merge lands
both; #150 depends on #144's table-identity recovery and would be a downgrade alone, so
they land together; #142 and #146 are two fixes to one root cause (the in-request JSON
validation retry) and compose cleanly rather than competing; #145 and #147 are the
client and simulator halves of the same character cap.

One change is this pass's own, written test-first: #1662 left
providers/llm/litellm_llm.py and providers/llm/openrouter_llm.py on the bare
raise_for_status, and litellm_llm is the enrichment route that was observably
discarding 400 bodies in production on 2026-08-26 and 2026-08-27.

Gate on the reconcile branch, all five tiers:
  static PASS | unit 1864 passed | integration 90 passed
  staging-e2e 41 passed (tool coverage 21/21) | live 123 passed, 0 failed
Baseline at origin/main run the same way: unit 1723 passed, integration 90 passed. No
new failures. Because lancedb_store.py changed and the venv resolves lance 4.0.0 while
the production image resolves lance 10.0.0, the deterministic suite was re-run inside
the candidate image: tests/test_store.py 149 passed on lance 10.

PROTECTED SURFACE: #154 changes the /health contract (new 503 stale_tail, new
INDEX_PENDING_MAX_AGE_S in docker-compose.yml). The container healthcheck raises on
503 and the post-deploy verify requires status=ok, so a stale tail can mark the
container unhealthy and can trip an auto-rollback. Status composition was checked:
stale_tail sits between index_failed and degraded, and disk_full still overwrites last,
so #0910's ordering holds.

Left open with written reasons: #141 (#1630 — hardens a path that has executed zero
times in five weeks; the 440 orphan _indices were reclaimed by the existing incremental
finalizer during this pass, confirming it) and #79 (#0584 — a month stale, and its
label alias map matches none of the 512 unstable doc_types measured in production).

Report: Maint-Manager/.briefs/reconcile/20260827/RAG-in-a-Box.md
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