Amended 2026-07-31 (v2) after PR #16228 closed unmerged via Drop+Supersede — this body is the successor authority the replacement PR resolves against. Salvage map: the closing review on #16228. Source-boundary authority: the correction comment on #16208 (2026-07-31T03:52Z). The v1 body scoped receipts + bounded retry only; v2 carries the complete contract the review found missing.
Context
The #16208 rebuild's memory leg reported unrecoverable=7562 (~24%): diff-verified as per-pass embed failures under provider saturation (400/400 sampled rows carry full documents), not content loss. The v1 fix (fate-stamped receipts + per-range retry) shipped in PR #16228 and failed review on five executable falsifiers plus a source-authority mismatch; that PR is closed unmerged and this ticket now defines the successor.
Composition rule (settled): the runner composes with, never re-decides, #16208's source boundary — a fresh Compose-owned target populated by graph+WAL replay/re-embed, or a supported version-migration against a clone. The legacy vector store is never the document authority. Everything below is source-agnostic hardening of the runner/extractor that the chosen source feeds.
The Problem (v2 — the five falsifiers)
- Fate without cause: all embed failures collapse to
embedding-provider-error → retryable: true. A permanent 401 reads "resumable"; grouping drops the HTTP status, so the receipt cannot answer "resume or fix configuration?".
- Unbounded amplification: every binary-split range gets a fresh retry budget — a persistent 8-doc outage costs 45 outer calls — and the fail-fast worker pool starts the next attempt while orphaned requests from the previous one are still in flight, exceeding configured concurrency exactly under saturation.
- Sparse results pass: the embed fn pre-sizes its output array, so a provider response missing indexes passes the length check and emits
undefined vectors with zero failure receipts.
- Identity and reconciliation are nominal: source/target identity is a URL-string comparison (two spellings of one host pass); reconciliation is count-only, so a same-store no-op or a target contaminated with foreign ids can return
ok: true.
- Dry-run mutates:
getOrCreateCollection runs before the dry-run branch.
The Fix (successor contract)
- Status-class fate map, classifier-owned: provider failures carry their cause (
http-401/403/404 → terminal-config; http-408/429/5xx, network, timeout → retryable; malformed/sparse result → retryable with its own reason; unknown → terminal, never optimistic). The grouped receipt keys on the full reason so cause survives aggregation.
- Globally bounded, drained retry: one attempt budget for the whole operation (not per range); a failed batch attempt drains its in-flight siblings before any retry fires; backoff between attempts; concurrency never exceeds its configured bound. Split isolates content, retry absorbs transients, and sustained failure STOPS with a
stopped-early receipt naming the budget spent.
- Result validation: per-element vector presence + dimension check against the expected model dimension; sparse/duplicate indexes are per-document failures with receipts, never silent undefineds.
- Provenance + exact reconciliation: semantic identity check (collection UUID, not URL strings) refuses source==target; the receipt records target endpoint, collection UUID, embedding model id, and dimension; reconciliation is id-set equality (planned ⊆ target, foreign ids reported), not counts.
- CLI/receipt contract (operator surface): documented flags with validated ranges, stderr = progress, stdout = exactly one receipt JSON, exit 0 only on
ok: true, and a true no-write dry-run (no collection creation).
Salvaged from the closed PR (base of the replacement): classifier-owned retryable stamp with unknown=false, grouped reason receipts with capped sampleIds, attempts: 1 compatibility default on the shared extractor, injected wait/backoff seams, transient-recovery coverage.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| failure entries |
extractor classifier |
{id, reason(status-classed), retryable, message}; unknown → terminal |
consumers ignoring new fields unaffected |
module JSDoc |
fate-map spec incl. 401/429/timeout |
| retry engine |
extractor |
global attempt budget, drained pool, backoff, stopped-early receipt |
attempts: 1 reproduces historical behavior |
JSDoc |
persistent-outage bound spec (call-count ceiling), drain spec |
| embed result validation |
embed fn + extractor |
per-element presence + dimension check |
invalid element → per-doc failure receipt |
JSDoc |
sparse/duplicate-index specs |
| runner receipt |
rebuild runner |
provenance block (endpoint, collection UUID, model, dims) + id-set reconciliation + per-reason failed array |
ok: false on any foreign id, identity overlap, or stop |
runner JSDoc |
same-store refusal, contaminated-target, reconciliation specs |
| CLI |
runner entrypoint |
validated flags, stdout receipt / stderr progress, exit-code contract, no-write dry-run |
invalid flag → usage error, exit non-zero |
runner JSDoc + runbook |
dry-run no-write spec, flag-validation spec |
| source boundary |
#16208 (correction comment) |
runner consumes the decided source; never reads the legacy store as authority |
n/a — composition rule |
this body |
replacement-PR review gate |
Decision Record impact: none (script/helper layer; no config leaves — runner stays AiConfig-free by design).
Acceptance Criteria
Out of Scope
- Deciding the
#16208 source boundary (graph+WAL vs clone migration) — that decision is consumed here, made there.
- The shared
ai/-wide retry primitive consolidation (#16224-family) beyond what the extractor already carries.
- Cutover blessing, host-stopgap removal, live-plane reconciliation (
#16208).
Avoided Traps
- One
retryable bit as the whole fate story — a fate bit is only as useful as its cause classifier; collapsing 401/429/timeout/5xx into one reason recreates "unrecoverable" ambiguity under a new label (review retrospective, adopted verbatim).
- Per-range fresh retry budgets — retries must be globally bounded or sustained failure multiplies pressure on the already-saturated provider.
- Count-based reconciliation — counts bless no-ops and contamination; only id-sets prove the rebuild.
- Two named fate fields in the receipt schema — array-of-receipts with a fate attribute stays (v1 convergence, still right).
Related
- #16208 — source-boundary + cutover authority; the correction comment (2026-07-31T03:52Z) is binding on any successor runner
- PR #16228 — closed unmerged (Drop+Supersede); its closing review carries the salvage map and falsifier harnesses
- #16222, #16224 — bounded-retry family siblings
Origin Session ID: 5814af6b-fe4e-41ba-819f-e1aeb5558643
Retrieval Hint: "successor runner fate map status class drained bounded retry id-set reconciliation provenance dry-run no-write"
Context
The
#16208rebuild's memory leg reportedunrecoverable=7562(~24%): diff-verified as per-pass embed failures under provider saturation (400/400 sampled rows carry full documents), not content loss. The v1 fix (fate-stamped receipts + per-range retry) shipped in PR#16228and failed review on five executable falsifiers plus a source-authority mismatch; that PR is closed unmerged and this ticket now defines the successor.Composition rule (settled): the runner composes with, never re-decides,
#16208's source boundary — a fresh Compose-owned target populated by graph+WAL replay/re-embed, or a supported version-migration against a clone. The legacy vector store is never the document authority. Everything below is source-agnostic hardening of the runner/extractor that the chosen source feeds.The Problem (v2 — the five falsifiers)
embedding-provider-error → retryable: true. A permanent 401 reads "resumable"; grouping drops the HTTP status, so the receipt cannot answer "resume or fix configuration?".undefinedvectors with zero failure receipts.ok: true.getOrCreateCollectionruns before the dry-run branch.The Fix (successor contract)
http-401/403/404→terminal-config;http-408/429/5xx, network, timeout →retryable; malformed/sparse result →retryablewith its own reason; unknown → terminal, never optimistic). The grouped receipt keys on the full reason so cause survives aggregation.stopped-earlyreceipt naming the budget spent.ok: true, and a true no-write dry-run (no collection creation).Salvaged from the closed PR (base of the replacement): classifier-owned
retryablestamp with unknown=false, grouped reason receipts with cappedsampleIds,attempts: 1compatibility default on the shared extractor, injected wait/backoff seams, transient-recovery coverage.Contract Ledger Matrix
{id, reason(status-classed), retryable, message}; unknown → terminalstopped-earlyreceiptattempts: 1reproduces historical behaviorfailedarrayok: falseon any foreign id, identity overlap, or stop#16208(correction comment)Decision Record impact: none (script/helper layer; no config leaves — runner stays AiConfig-free by design).
Acceptance Criteria
stopped-earlyreceipt; no attempt starts while prior in-flight requests are undrained.#16228closing review's salvage map and the#16208correction comment, and resolves THIS ticket only —#16208closes on its own authority (source boundary + cutover + live reconciliation), not through this runner.Out of Scope
#16208source boundary (graph+WAL vs clone migration) — that decision is consumed here, made there.ai/-wide retry primitive consolidation (#16224-family) beyond what the extractor already carries.#16208).Avoided Traps
retryablebit as the whole fate story — a fate bit is only as useful as its cause classifier; collapsing 401/429/timeout/5xx into one reason recreates "unrecoverable" ambiguity under a new label (review retrospective, adopted verbatim).Related
Origin Session ID: 5814af6b-fe4e-41ba-819f-e1aeb5558643 Retrieval Hint: "successor runner fate map status class drained bounded retry id-set reconciliation provenance dry-run no-write"