feat(b1): repoint the upload route to /ingest (PR B of 3, register #51) - #78
Merged
Merged
Conversation
**Re-authored on current `main`, not rebased off the pre-split branch.** That branch is based on `140bc31`, before PR #74, and a rebase would have taken its side of two hunks that main has since changed — silently reverting register #54's upstream-cause capture and #74's `getServiceUrl()` production guard, at the exact moment production starts depending on a new path. Both are preserved here. **The post-forward error write is conditional on `status='processing'`, and the asymmetry is deliberate.** Once the request leaves for the ingestion service, that service owns the terminal status — it writes `ready` before it acks. A failure on our side after the forward must therefore never blind-write `error`: the service may have succeeded and had its ack lost to a timeout, and stomping a correctly-finished `ready` row would destroy a document that is fine, reporting failure to the user while its chunks sit orphaned in the Ask path. The `.eq('status','processing')` filter makes the write a no-op in exactly that case and still rescues the row when the service never got far enough to write anything. **Pre-forward failures keep their unconditional writes** because nothing else can have touched the row yet — there is no correct state to protect. **#54's capture survives the move.** When the service failed after it could reach the database it has already written `error` with its own, more specific message, and the guard correctly declines to overwrite a better diagnosis with a worse one. When it could not — 401 at its auth gate, 404 from version skew, 503, crash — the row is still `processing` and the `${status}: ${detail}` string lands exactly as before. The `console.error` is unconditional either way. Also: `document_id`/`kb_id` in the form body and the end user's token in `X-Supabase-Token` (the service is not given a service-role key — register #45); the local chunk batch-insert, the local `ready` write and the in-memory materialization of every 1024-float embedding plus the full markdown are deleted; the forward is wrapped in try/catch, killing the orphan-stuck-at-`processing` class that previously fell to the outer catch untouched; and a 200 is not taken as success until the ack narrows to `status === 'ready'` with a numeric `chunk_count`, so a version-skewed service cannot report a document ready while nothing was persisted. RLS identity is unchanged — the writes move process, not principal. **NOT MERGEABLE YET.** §10 rows V6, V7, V8, V9, V10 and V12 are blank and V12 forbids merging while any PR-A/PR-B row is. They are filled by a second commit on this branch, from THIS PR's preview deployment (§5.1 — PR A's preview still calls the shim and would green all five while exercising `/ingest` zero times), and only after the Vercel Preview scope is confirmed to carry a current `INGESTION_SERVICE_URL` and `INGESTION_TOKEN`. **Accepted, not fixed:** merging swaps the production ingestion image (register #55, empty Watch Paths) at the same moment production first depends on `/ingest`, the Railway rollback is dead (#56), and N10's digest pin is not due until PR C — so a post-merge upload failure has two candidate causes and no surviving artifact to separate them. The detector is the monitor's tick, at a MEASURED mean of ~68 min daytime and ~113 min full-cycle, not 15. This PR's own abort path is unaffected: `route.ts` is a Vercel deployment, Vercel retains its deployments, and the shim is still live. Scope: one file under `src/`. ZERO under `services/`. `tsc --noEmit` clean, `eslint` clean, `grep -rn "/convert" src/` returns zero hits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Written BEFORE the V6-V10 run so these read as conditions, not as post-hoc justification. §5.1 is not edited; corrections go in §2, per this file. - §2.6.1 "Confirm email" is now ENABLED, established by evidence: a confirmation mail was delivered and its link consumed. §5.1 step 1's parenthetical justification is therefore FALSE. Cause recorded: Supabase SMTP pointed at a deleted Resend account, so the stored key was dead and no confirmation mail was leaving the system at all. DNS was never implicated. - §2.6.2 The consequent /login claim, CORRECTED. Its earlier unscoped form was wrong as stated. It applies to the form-submit step only; the confirmation link is observed to land on the dashboard with a live session. The scoped version is labelled an UNOBSERVED CODE READING, because it is one. - §2.6.3 The throwaway account was created on production, not on PR B's preview. The claim stated precisely: the only file differing between main and PR B that Next compiles or serves is src/app/api/ingest/route.ts, and this commit adds no runtime file -- which is what makes signup/page.tsx byte-identical on both. Corroborated independently of production's SHA: signup/page.tsx unchanged since 48ac550, 2026-07-03. - §2.6.4 THE UPLOAD ORDER IS LOAD-BEARING and is now written next to the step. V9 is satisfied by nearly every post-insert failure path in route.ts, not only by a conversion failure: :218-221, :255 and :292 each write a non-null error_message with zero chunks WITHOUT /ingest processing anything. So corrupt.pdf first against a misconfigured preview yields a false V9 PASS. QORVANTHIL first is the discriminator. The :196-200 unset-INGESTION_TOKEN branch is recorded as the exception -- it writes no error_message and so FAILS V9 rather than faking it; naming it as the example would have been wrong. - §2.6.5 The profiles precondition is kept per register #23, and became LESS redundant on 2026-08-08: confirmation-enabled signup makes the client-side upsert at signup/page.tsx:42 run with no session, so RLS rejects it with 42501 and the :48 guard, which swallows 23505 only, routes it to console.error. The row now rests entirely on the on_auth_user_created trigger. - §2.6.6 V6-V10 will be owner-attested, not machine-verified: no agent here holds a database credential, so §4 runs in the SQL editor and is pasted back for adjudication. Same class as V3 and V5. Recorded now so §10's Provenance column is not decided at write-up time. A pointer to §2.6 is added at §5.1 step 1; the step's own text is unedited. Docs only. No runtime file touched. §10 remains blank; PR B still DO NOT MERGE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…red (register #51) V9 did not fail. It did not occur. The 9-byte "not a pdf" file mandated by §2.1 was accepted, converted, chunked and embedded, reaching status='ready' with 1 chunk holding exactly those 9 characters. §2.1's clause "reaches MarkItDown, which raises" is false, and every link after it was conditional on it. The agent running this verification repeated that claim as the expected code path and told the owner which exception to watch for, having never run MarkItDown against that input. §2.7.1 records that, because the defect is not a wrong document -- it is an unverified assertion passed along a second time inside the run whose whole purpose is to stop exactly that. §2.7.2 records the METHOD ahead of the result, because the method is the transferable part. A local markitdown[all]==0.1.5 harness proves nothing on its own; it is a different machine with a different dependency resolution, which is §8.1.1's own finding about this package. What makes it evidence is a CONTROL: candidate 1 was the 9-byte payload whose production result was already known, and the discard rule -- if the harness misses the control, throw everything out -- was fixed before the probe ran. It reproduced production exactly. Six other payloads then RAISED, every one of them through the extension-driven converter, which is what identifies the mechanism: MarkItDown sniffs content, and the plain-text converter is a universal accept for text-like bytes. §2.1's fixture was defeated by its own readability. Limit stated rather than left to be found: a calibrated local harness is not the Railway container, and the control agreeing is evidence about the harness, not proof about Railway. §2.7.3 replaces the fixture with 2048 null bytes, md5 c99a74c555371a433d121f551d6c6398, reproducible by `head -c 2048 /dev/zero`. Random bytes raised identically and were rejected as the fixture precisely because a future reader cannot regenerate them. Filename deviates to corrupt2.pdf, because corrupt.pdf now names the junk ready row the invalid fixture left behind; Q6a's target changes accordingly, per §4's standing warning about retries putting more than one candidate in range. §2.7.4 is a BINDING STOP RULE: if the replacement also converts, the run stops. No third fixture. Two have now been proposed on reasoning and one was wrong. §2.7.5 records a real product finding -- extension+MIME-only gating at route.ts:70-88 against a content-sniffing converter, pre-existing and identical on main, data integrity rather than security -- and corrects the owner's framing of it BY MEASUREMENT rather than softening it: a structurally destroyed PDF RAISES as designed, so the hole is type-mismatch, not damage. Docs only. No runtime file touched. V9 remains unfilled, so §10 still has a blank PR-B row and V12 cannot be checked. PR B still DO NOT MERGE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cover (register #51) V9 passes. The replacement fixture from §2.7 -- 2048 null bytes as corrupt2.pdf -- raised exactly as the calibrated harness predicted, including the PDFSyntaxError text, and the row landed status=error / embedding_status=error / chunk_rows=0 with Q6b stuck_processing=0. THE ROW WORTH READING IS V9's, and not for its four conditions. The error_message in the database is _mark_error's own string (main.py:257-275) and is NONE of route.ts's. An unconditional post-forward write would have replaced it with "ingestion service returned 500: ...". It did not. So the guarded .eq('status','processing') write at route.ts:179-189 stood down as designed -- the premise of this entire PR observed on the wire rather than read from the code. The cell is careful about what that means: the guard's EFFECT is observed; the zero-row UPDATE itself is not directly observable and is not claimed. V8 came out stronger than its bar. The Ask reproduced the ingested document's distinguishing account of the batch size -- chunk rows per insert call, chosen to stay under the REST request-size ceiling rather than for throughput -- which CONTRADICTS the substitute document authored for this run and never ingested. The answer could only have come from the artifact actually in the database. V10 keeps its absolute form: before_count was read, not assumed, and n=1 with first_at = last_at. It carries an emit-timing corroboration (2.124s after the document's created_at, consistent with the emit firing on the service's ack at route.ts:329) and explicitly declines to claim anything about failed uploads, which were never re-read. Two artifacts in the throwaway account are documented against misreading. The QORVANTHIL file is the one authored in an EARLIER session, not the one written at the start of this run; §2.3 mandates properties rather than bytes, the substitution was caught before any row was recorded, and V8 was adjudicated against documents.markdown_content and nothing else. And there is a corrupt.pdf row sitting at ready with nine bytes of content: that is §2.1's mandated fixture FAILING TO FAIL, retained deliberately so the finding is inspectable, and it is why the working fixture had to be named corrupt2.pdf. §7.4 is the counterweight to a green §10 and is the reason this commit is not just good news. V9 exercised ONE failure branch. The RESCUE half of the same guard -- the orphan-stuck-at-processing killer this whole change exists for -- has never executed anywhere, nor have :255, :310, :196-200 or :218-221. Accepted as untested; PR B is not held on it; recorded so V1-V12 cannot be read as more than they are. Also: §2.1's line references drifted (:59-76 to :59-88, :71 to :83), no substantive change. All five rows are owner-attested per §2.6.6. Two Railway deployment IDs remain ⟨FILL-IN: dashboard⟩ and §10 forbids PR B moving until they are resolved. Docs only. No runtime file touched. V12's row is filled by the follow-up commit, since it cannot precede itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ister #51) V12 cannot precede itself, so its cell names 1fed6e5 as the commit that filled V6-V10. Recorded as CONDITIONAL, not PASS: V12's stated condition is that the table is committed to main, and this is a branch. Two Railway serving-deployment IDs are still (FILL-IN: dashboard), which §10 says must be resolved before PR B moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gister #51) Every other transition in this sequence carries a check: PR A's image going live has V1/V2/V4/V5/N4/N9, PR C's shim deletion has N5/N6. PR B's has none - and PR B is the merge at which production changes which endpoint it calls. The next production upload check is N6, at PR C post-merge, so the window between them is verified only by inference from a preview. production-monitor does not close it: it probes /health and /convert's auth posture and never uploads. The absence is asymmetric with the rest of this file, which is what makes it a gap rather than an addition. N11 IS NOT A MERGE GATE, and §3.1 says so in its first paragraph. §8's binary stays V1-V12 unchanged. N11 is a post-merge observation in the class of N1/N4/N9, and it cannot gate a merge it is defined to run after. Its remedy is named concretely rather than delegated to TRIGGER 2, whose remedy is Railway-shaped and does not apply: PR B changes a Vercel deployment, Vercel RETAINS its deployments, so the rollback is to the immediately preceding production deployment - a genuine return to a known-good artifact, unlike the Railway rebuild §8.1.1 documented. Followed by a git revert PR and a §7 entry, per register #39. Docs only. No runtime file touched. Nothing merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… ALIVE (register #51) V6 and V9 both name Railway deployment 6e03e8bd (service knowflow, project athletic-miracle, environment production), built from 365ac24, created 2026-08-03T13:17Z. V7, V8 and V10 read "Same as V6" and resolve with it. The §10 preamble's tally is corrected from two FILL-IN markers to four: two were resolved 2026-08-02 by B8, these two today. No FILL-IN remains, which is the condition §10 states must hold before PR B moves. THE MANDATORY MARKER IS WRITTEN IN THE FUTURE TENSE, BECAUSE THE PAST TENSE WOULD HAVE BEEN A LIE. Every other row in §10 carries ARTIFACT REAPED as an accomplished fact. This deployment read ACTIVE today -- "Deployment successful", 1 replica, US East, every deployment below it in HISTORY marked REMOVED -- so the cell records ARTIFACT REAPED - SCHEDULED, NOT YET TRUE AT TIME OF WRITING, and names PR #78's own merge as the thing that reaps it (empty Watch Paths, register #55). The point of saying so in the cell is that a later reader will find 6e03e8bd marked REMOVED and must be able to tell a PREDICTED end state from a falsified record. THE IDENTIFICATION IS REPO-ENTAILED, NOT MERELY ATTESTED, AND THAT IS THE PART WORTH KEEPING. 365ac24's commit timestamp is 2026-08-03T13:17:54Z -- the same minute as the deployment's creation -- and 365ac24 is STILL main's head, so no commit landed on main between the build and the 2026-08-08 run. With Watch Paths empty every push rebuilds; there was no push, so no supersession was possible and exactly one deployment could have served those rows. That argument is checkable from a clone. The dashboard read agrees with it rather than carrying it. LIMIT STATED RATHER THAN LEFT TO BE FOUND. The monitor's paired probes (GET /health 200, POST /convert 401) repeat in this deployment's live log and corroborate CONTINUOUS SERVING across the window. The log was NOT read for the V6-V10 upload events themselves. So this cell deliberately claims less than the 16ea983d cell, whose deploy log carried the very events its row asserts. NOT CAPTURED, AND RECORDED AS NOT CAPTURED: build duration and trigger label. §10 names the deployment ID as the only handle on both. After this PR merges they are no longer readable from a live deployment. ONE EDIT BEYOND THE BRIEF, NAMED RATHER THAN SLIPPED IN. V12's row enumerated TWO blockers, one of which was the FILL-IN cells this commit just removed. Its result stays CONDITIONAL and V12 is NOT resolved -- its condition is "committed to main" and this is a branch. Only the count of what remains is corrected, from two blockers to one (the merge). Leaving it would have shipped a row asserting a blocker the same commit eliminated, which is the defect this table exists to catch. Docs only. One file. No runtime file touched, zero files under services/. PROGRESS.md is deliberately untouched -- the #55/#56 register rows, #54's stale monitoring claim and the header refresh are all step 4, after N11, so that exactly one unaudited ingestion rebuild sits between the last known-good state and the first real production upload through the new path. V12 is deliberately NOT resolved: its condition is "committed to main", and this is a branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tornidomaroc-web
added a commit
that referenced
this pull request
Aug 10, 2026
…B9-2 (register #51) (#79) **(b1) register #51 — DOCS-ONLY. ZERO files under `src/`. ZERO under `services/`.** No migration, no SQL, no dashboard change, no workflow change, no Watch Paths change, no base-image pin. **PR B (#78) merged at `3cd91bd` and N11 PASSES.** This PR is the record of that, plus the two findings the run itself produced. --- ## 1. N11 — PASS, and it is an observation, not an eleventh gate Document `bfd6e9d5-9e95-4739-8579-7fca784f81dc` (`xilvaroth-n11-20260810.md`, 1177 bytes), subject `43387669-8189-4970-96aa-9499464291df`, **created empty with its id recorded before any file existed in it**. All five Q3 conditions: `status='ready'`, `embedding_status='ready'`, `chunk_count=1` = `actual_chunk_rows=1`, `has_markdown=true`, `error_message` null. **Adjudicated by `document_id`, not by position.** **V12 was discharged by the merge itself**, exactly as its CONDITIONAL cell predicted. **N11 is a post-merge observation with a rollback condition (§3.1)** — PR B merged on V1–V12 and nothing else. **Corroboration, not a condition:** `markdown_len` 1175 from 1177 source bytes is the **same 2-character delta V6 saw** (1169 from 1171). The converter behaved identically in production and preview. **The drift check passed and is recorded as a pass, not as an absence.** `/health` read immediately before the upload was byte-identical to the pre-merge capture on all four fields, including `embed_provider` and `embed_model`, which the monitor does not assert. ## 2. Two findings from the run — the reason this PR is not just a filled cell **Register #57 opened — "the real account" is not a defined term.** N11's first Q3 ran against `72ebd7b5-…`, derived from V4's document `6c0ff12c` exactly as this protocol suggests, and **returned zero rows**. The N11 subject and document are owned by `d6fff4eb-…`. It was our record that was wrong, not the product, and **the upload was not repeated** — the finding was reached by widening the search. **The hazard is a false rollback arrived at by obeying the document.** A zero-row Q3 is indistinguishable from a failed upload, and §3.1's remedy for a failed N11 is a **Vercel production rollback**. The next reader who runs a production check against the wrong UID rolls back production on a query artefact. **Which account is canonical is unknown and is not guessed at; both UUIDs are recorded and neither is blessed.** The row opens with a binding rule: **every production check must record the `user_id` it ran against.** **The UI said `ready` with a chunk count before any query ran.** Adjudicating on the screen would have passed N11 on `chunk_count` — a column the pipeline **writes** — without ever comparing it to `actual_chunk_rows`, which is what the chunk table **contains**. A green screen is the pipeline reporting on itself, the same shape as V9's nine-byte `not a pdf` reaching `ready`. ## 3. Register #55 confirmed a second time, and `6e03e8bd` reaped on schedule PR #78 touched `src/` and `docs/` with **zero files under `services/`** and still reaped `6e03e8bd` in favour of **`35cbf150`**. **This is corroboration, not a first observation** — #55's mechanism was already confirmed on 2026-08-02 via PR #74. What is new is the **before/after deployment pair** and a **machine-readable GitHub commit status** on `3cd91bd` carrying the new deployment id, reproducible with `gh api`, rather than a dashboard read alone. `6e03e8bd`'s `ARTIFACT REAPED — SCHEDULED, NOT YET TRUE` marker **came true on schedule**, which is the convention working as designed. `35cbf150` now carries the same SCHEDULED marker — and **this PR's own merge is the push that discharges it**, since a docs-only merge rebuilds the ingestion image under empty Watch Paths exactly as #78's did. **N11's artifact is therefore reaped by the very PR that records N11.** The §10 cell says "the next push to `main` reaps it" and this is that push; a later reader finding `35cbf150` REMOVED is seeing the predicted end state, not a falsified record. ## 4. Scope beyond N11, and why each item is here rather than deferred - **Registers #55 and #56 get §4 rows for the first time.** Both were opened in the 2026-08-02 §7 block and **never had a row**, so for eight days they were invisible to anyone reading the register as a register. A register that exists only inside a changelog paragraph is not findable. - **B9-2 CLOSED.** All four `b1-verification-protocol.md` cadence sites corrected to the measured figure; the fifth, immutable `PROGRESS.md:217`, is **superseded by the new §7 block rather than edited** — the mechanism B9-2 itself specified. **Included here rather than deferred because leaving four known-false sentences in place, inside the very PR whose §7 block records the sixth instance of false claims surviving in records, was not defensible.** Their line numbers had drifted by PR #78's +360 lines and were re-located by content, not by number. - **B9-4 OPENED.** `production-monitor` asserts `ok`, `embed_dim` and `supabase_configured` but **never `embed_provider` or `embed_model`**. Under #56's mutable base tag and #55's every-push rebuild, a model swap that kept the dimension at 1024 would write foreign vectors into the same column, pass all three assertions, and silently degrade retrieval against every chunk already stored. Not fixed here — it edits a workflow file, and this is docs-only. - **B9-3 recorded as OVERDUE**, six days readable and unread, against a row whose own closing sentence forbids exactly that. B9-2's sites were corrected using the **baseline** figures, not the `*/30` result, because that result does not exist yet. ## 5. The PR #78 title incident — the sixth instance, and the count is checkable PR #78's title carried `— DO NOT MERGE, §10 blank` into the merge window. This repo squash-merges with the PR title as the commit subject, so the string was one command away from being permanent in `main`'s history; the body's opening paragraph and §5 table said the same. **§10 had been filled by `1fed6e5`, `25578be` and `3ee4099` and read "No `⟨FILL-IN⟩` remains" at the merge SHA.** The title was corrected and the body rewritten to **strike the hold and explain it**, so the history does not read as a bypassed gate. The five priors are enumerated in the §7 block so "sixth" is checkable rather than asserted: the 07-23 #45 rotation claim; §8.1's no-rebuild rollback; §5.1's "Confirm email is disabled"; §2.1's V9 fixture; the `~15 minute` cadence claims. **The common mechanism: every one was true when written, and none had an owner when it stopped being true.** ## 6. Verification honesty **N11 is `owner-attested`, not machine-verified.** The repository owner ran the SQL and pasted the raw row; **no agent here queried the database**, and none could — no agent holds credentials for it. The merge, the deployment identities, the commit statuses and the fixture bytes **are** machine-verified and reproducible with `gh api`, `git` and a hash. The `/health` comparison is owner-attested. **`XILVAROTH` was verified absent from the entire repo; it was NOT verified absent from the database**, and that limit was stated before the upload rather than after. `tsc` and `db-types` **passed vacuously** — confirmed green on `d2e0fc1`, along with `ingestion-image` and both Vercel checks (5/5) — because this PR contains zero TypeScript and zero SQL. Their passing is not evidence for anything in it. ## 7. Merge-order warning, not a request **`chore/b1c-remove-convert-shim` is 2 ahead and 6 behind `3cd91bd`, and it falls one further behind with this merge and with every merge after it.** It touches `docs/PROGRESS.md` and `docs/b1-verification-protocol.md` — **both files this PR edits.** (Stated against a fixed SHA rather than against `main`, because "6 behind `main`" is false the moment this PR lands, and a merge-order warning that expires at the merge is worth nothing.) Rebasing or merging PR C's branch without re-authoring would take its side of these hunks and silently revert N11's row and this §7 block, which is **exactly the failure PR B avoided by re-authoring rather than rebasing** (#78 §1). PR C should be re-authored on `main` after this lands. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(b1) PR B of 3, register #51.
src/app/api/ingest/route.tsstops calling the shim and calls/ingest, which has been live since PR A (ee45958). One file undersrc/. ZERO files underservices/. No migration, no SQL, no dashboard change.DO NOT MERGE. §10 rows V6, V7, V8, V9, V10, V12 are blank, and V12 forbids merging PR B while any PR-A/PR-B row is blank. They are filled by a second commit on this branch after the throwaway run against this PR's preview deployment — not PR A's, per §5.1, because PR A's preview still calls the shim and would green all five while exercising
/ingestzero times.1. Re-authored on current
main, not rebased — and that is the pointThe pre-split branch
feat/b1-railway-owns-chunk-persistenceis based on140bc31, before PR #74. Main has since gained two things that live exactly where this change edits:getServiceUrl()(src/lib/ingestion.ts), which throws in production instead of silently rewriting an unsetINGESTION_SERVICE_URLtohttp://localhost:8000. Preserved here, unchanged.${status}: ${detail}string that ended nine days of a 401 outage being indistinguishable from every other failure. Preserved here, folded inside the new guarded write (§2).Rebasing the old branch would have taken its side of both hunks and silently reverted register #54's fix at the exact moment production starts depending on a new path. That is why this is a re-author.
2. The guarded error write, and why the asymmetry is deliberate
Every post-forward failure writes
errorthroughfailIfStillProcessing(), which carries.eq('status', 'processing').Once the request leaves for the ingestion service, that service owns the terminal status — it writes
readybefore it acks us. So a failure on our side after the forward must never blind-writeerror. The service may have succeeded and had its ack lost to a timeout, a socket reset, or a platform request cutoff. Stomping a correctly-finishedreadyrow would destroy a document that is fine: the row would report failure to the user while its chunks sat orphaned in the Ask path. The.eq('status','processing')filter makes the write a no-op in exactly that case — the service has already moved the row offprocessing— while still rescuing the row when the service never got far enough to write anything. PostgREST reports a filtered-out UPDATE as success with zero rows, so "did nothing" and "worked" return identically here, which is correct: both mean the row is already in the state it should be in.Pre-forward failures keep their unconditional writes, and a reviewer should not read that as an oversight. Before the forward, nothing else can have touched the row — this route inserted it moments ago and no other writer exists. There is no correct state to protect, so an unconditional write is the honest one. The asymmetry tracks exactly one question: can something other than this function have written this row yet?
#54's capture is not weakened by moving inside the guard, and the case split is the reason this is safe:
_mark_errorran)error, with the service's own specific messageprocessing${status}: ${detail}string lands exactly as it did beforeAnd the
console.errorabove it is unconditional in both cases. The status code reaches the logs always, and the database whenever nothing better is already there.3. What else changes in the route
document_idandkb_idtravel in the form body; the end user's Supabase access token travels inX-Supabase-Token, becauseAuthorizationalready carriesINGESTION_TOKEN. Two credentials, two jobs — service-to-service identity, and the user's database authority. The service is not given a service-role key (register docs: advance the three stale header lines to reality (main @ 63d498c, PR #44) #45: that service was publicly duplicable once, and an RLS-bypassing credential in it turns any exposure into a full-database breach). RLS identity is unchanged by this PR — the writes move process, not principal.readywrite, and the materialization of every 1024-float embedding plus the full markdown in this function's memory. The service persists all of it.try/catcharound the forward. The in-flight death class previously fell to the outer catch, which logged and returned 500 without touching the row — leaving it atprocessingforever, with no reaper and no UI path out.status === 'ready'and a numericchunk_countare required before thematerial_uploadedemit, so a version-skewed service answering 200 with an unrecognised body cannot report a document ready to a user while nothing was persisted.readyafter both its chunk inserts and itsdocumentsupdate landed, and it verifies the update matched a row (an RLS-filtered UPDATE returns 200 with zero rows, not an error).4. THE TWO-CAUSE HAZARD — accepted, not fixed
Merging this PR swaps the production ingestion image at the same moment production first depends on
/ingest. Railway's Watch Paths are empty (register #55), so a merge touching onlysrc/rebuilds and redeploysservices/ingestionfrom a mutable base tag with unpinned transitive dependencies. The Railway rollback is dead (§8.1.1, register #56): every deployment image is REMOVED, so "roll back" rebuilds from source and restores source, not state. N10's digest pin and hash-locked lockfile are not due until PR C.This is accepted, not fixed. Stated plainly so nobody discovers it during an incident: if production uploads break after this merge, there are two candidate causes — the route change, and an image rebuilt from different bytes — and no artifact survives to tell them apart.
The detector is the monitor's tick, and its real cost is now measured rather than assumed.
production-monitorre-probes/healthand the unauthenticated shim on a schedule. Requested cadence is*/30; delivered cadence, measured over 18.8 hours, is a mean of ~68 minutes daytime and ~113 minutes full-cycle, with overnight intervals past 3.5 hours (PR #77,docs/b9-queue.mdB9-2/B9-3). Not 15 minutes. That is the window in which an ingestion-service regression introduced by this merge stays invisible if it does not surface as a failed upload first.What is NOT at risk: this PR's own abort path.
route.tsis a Vercel deployment and Vercel retains its deployments — abort is Instant Rollback (seconds, no rebuild, exact bundle) orgit revert, and the shim is still live because PR C has not run. Neither touches a Railway image. Prefer Instant Rollback: a revert push is itself anothermainpush, and therefore another #55 rebuild.5. §10 rows
/ingestsatisfies Q3QORVANTHIL; Q5 returns rowscorrupt.pdfreacheserror, 0 chunks; Q6b = 0material_uploaded= Q7-BEFORE + 1All PR-A rows (V1, V2, V3, V4, V5, V11, N4, N9) are filled and PASS as of PR #76. N5/N6/N8/N10 belong to PR C or after it.
Preconditions before the throwaway run, owner-side:
INGESTION_SERVICE_URLandINGESTION_TOKENmust be present and current in Vercel's Preview scope. Preview deployments build withNODE_ENV=production, sogetServiceUrl()throws if the URL is unset — and a stale preview token produces a 401 that reads exactly like a code defect. This PR's Vercel check passing proves nothing about either:getServiceUrl()is a function read at request time, not a module-scope const read at build time.6. Accepted / untested
/ingesthas still never processed a request anywhere (§7.1). The preview run in §5 is its first-ever exercise, on any environment._persist's idempotency guard remains unexercised (§7.2) — no flow re-ingests the samedocument_id.errorrow with the service's own message, not a silent partial write.tsc,db-typesandingestion-imageare vacuous on this PR in the sense that none of them exercises the new call path;tscdoes check the ack shape and the removed chunk-insert types. Local:tsc --noEmitclean,eslintclean, andgrep -rn "/convert" src/returns zero hits (N5's future check is not tripped by a comment — see the note atroute.ts:11).