Troubleshooting
Each entry is a message you might see, what it means, and the fix.
ptah inference works against PostgreSQL with pgvector
Section titled “ptah inference works against PostgreSQL with pgvector”Cause. The --db-url names another engine. This feature is a PostgreSQL
vertical: the run state and the vectors have no dialect-agnostic form.
Fix. Point it at a PostgreSQL database. There is no workaround.
preprocessing.null_policy "" is not one this build acts on
Section titled “preprocessing.null_policy "" is not one this build acts on”Cause. The field is absent. Thirteen fields are required, and each one is
refused by name with the values it accepts – the same message appears for
empty_policy, unicode_normalization, truncate, model.endpoint_class,
model.normalization and target.metric. The refusal happens before any verb
does work, describe included, so it is not about your database.
Fix. Write the field. The specification
reference marks which fields a specification is
refused without; truncate is the one most often missed, because it is required
even where max_input_bytes names no cap for it to act at.
the target database has no pgvector
Section titled “the target database has no pgvector”Cause. The vector extension is not installed in that database.
Fix. CREATE EXTENSION vector, which needs a privilege Ptah does not assume
on your behalf. The refusal names the statement.
column "embedding" on articles holds generation ... and this run is generation ...
Section titled “column "embedding" on articles holds generation ... and this run is generation ...”Cause. You changed the model but not target.column, so a new generation
would overwrite the one your queries read.
Fix. Give the new generation its own column. Keeping the previous one is what makes a rollback possible.
live generations sharing one source must use the same ordered key fields ...
Section titled “live generations sharing one source must use the same ordered key fields ...”Cause. The new outbox specification changes how a shared event identifies or versions a row while another run over that source is still live. One source has one outbox event format, even when generations embed different input fields or apply different filters.
Fix. Keep source.key_fields, version_strategy, and version_field
identical until the earlier run is abandoned or its generation is retired.
Input and filter columns may differ; Ptah makes the shared trigger watch their
union. The refusal occurs before target DDL and leaves the existing trigger in
place.
catch-up needs a consistency mode that records changes
Section titled “catch-up needs a consistency mode that records changes”Cause. The specification selects immutable or nothing, and you ran
catchup.
Fix. There is nothing to catch up on under those modes, and the run does not
need one: a completed backfill is what carries it to caught_up, so index,
verify and cutover follow directly. If the source is changing after all,
switch to outbox and run prepare again to install the triggers and record a
fresh boundary.
catch-up needs a backfill that reached the end of its snapshot
Section titled “catch-up needs a backfill that reached the end of its snapshot”Cause. catchup was run on a generation whose backfill has not finished.
The message names the phase the run is actually at — boundary_captured for a
run that was prepared and never backfilled, backfilling for one whose walk was
interrupted.
Fix. Run backfill until it reports the walk complete, then catchup.
Catch-up covers what changed after the snapshot the backfill walked, so before
that walk finishes there is no such range: the changes it would read are ones
the backfill still owes.
Nothing is spent to reach this refusal. It is raised before the first provider request, so no vector is written and the catch-up watermark does not move.
Running catchup again once the run is past backfilled — after an index, a
verification, or a cutover — is unaffected. That is ordinary, because the source
keeps moving, and the phase is a high-water mark.
the outbox keeps events this run has processed: floor ... is held by run ...
Section titled “the outbox keeps events this run has processed: floor ... is held by run ...”Cause. Another usable live feeder over the same source has not reached those
events. One source table has one outbox, so catchup may delete only what every
positioned, source-matched feeder has passed. The message names each run at the
minimum position and the generation it belongs to.
Fix. Inspect the named run:
ptah inference status --spec old-spec.yaml --db-url "$DB" --run-id old-runIf the migration is still wanted, catch it up. If that attempt is permanently over but its vectors should remain available for inspection, release only its outbox position:
ptah inference abandon --db-url "$DB" --run-id old-run \ --reason "superseded by run 2026-09-articles-v2"The next catch-up prunes events the remaining runs have all passed. The
abandoned run cannot resume. Ptah refuses this action when it would leave an
active or maintained generation without another usable live feeder: for outbox
mode, that means a nonterminal, source-matched run with a readable durable
resume position. Start a positioned replacement, move queries elsewhere, or
finish the maintenance window first. Use retire instead only when the
generation and its vectors should be destroyed.
Two specifications over one table have separate outboxes
Section titled “Two specifications over one table have separate outboxes”Cause. They were prepared by a Ptah that keyed the source on the spelling
each document used rather than on the relation it names. table: docs and
schema: public, table: docs are one pg_class row when search_path is
public, so one physical source ended up with two outbox tables, two trigger
pairs, two advisory-lock domains and two reader floors that did not know about
each other.
Ptah now resolves both halves of source and target against the connected
session before it derives any of that, so the two spellings share one outbox,
one floor, one lock and one target pointer. An omitted schema resolves through
the session’s search_path rather than defaulting to public, and two
genuinely different schemas holding the same table name stay separate.
The generation identity is deliberately not resolved. It is a content address
of the document, which ptah inference describe computes with no database at
all, so two spellings remain two generations over one source — which is the
ordinary state during a generation change.
Fix. Rows recorded before this are not reinterpreted: an outbox created
under the old key keeps its name and its runs. Finish or abandon the runs on
the outbox you no longer want, then retire its generation; the remaining
specification’s next prepare installs against the resolved source. Pick one
spelling for new documents so a reader can see at a glance that two
specifications are about one table.
provider: embedding endpoint unreachable
Section titled “provider: embedding endpoint unreachable”Cause. The endpoint is down, the address is wrong, or the network is not there.
Fix. Everything committed stays committed. Fix the endpoint and run the same command again; it resumes from its checkpoint.
The run stops repeatedly with provider errors
Section titled “The run stops repeatedly with provider errors”Cause. Usually rate limiting. Ptah does not implement backoff against a provider’s limiter.
Fix. Lower --batch-inputs, raise --provider-timeout, and run the backfill
in sessions. See
Plan provider capacity.
the state changed underneath this write: run ... is fenced at token N
Section titled “the state changed underneath this write: run ... is fenced at token N”Cause. Another worker took over the run. The lease says who should work; the token says who may still commit.
Fix. If the other worker is real, let it finish. If it is a process that died, run the command again — the new invocation becomes the holder.
verification found N blocking findings
Section titled “verification found N blocking findings”Cause. Depends on the layer named in the finding.
Fix. See the table in
Resume and recover.
freshness and coverage almost always mean a phase has more to do rather than
something being wrong.
this policy requires an approval and none was given
Section titled “this policy requires an approval and none was given”Cause. policy.require_exact_approval is on, which is the sensible default.
Fix. Run cutover once to see the plan digest, then approve that exact
digest with --approve and --approver.
the approval is bound to plan X and this plan is Y
Section titled “the approval is bound to plan X and this plan is Y”Cause. Something the plan rests on changed between you reading the digest and approving it — the pointer moved, the findings changed, the source moved.
Fix. Read the new plan and approve that. The refusal is the mechanism working: it stopped an approval being applied to a different plan.
N rows are stale and this policy allows 0
Section titled “N rows are stale and this policy allows 0”Cause. The previous generation stopped being caught up. It drifted from the source, and going back to it would answer queries from a corpus that no longer matches your data.
Fix. There is no way to roll back to a drifted generation, and that is the honest answer. Catch it up first if it is close enough to be worth it; otherwise fix forward.
Prevention. Run catchup --maintain-for on a schedule for the length of the
window. See Rollback and retire.
no stabilization window was asked for
Section titled “no stabilization window was asked for”Cause. cutover ran without --stabilize-for, so nothing is keeping the
previous generation current and there is no rollback to it.
Fix. Nothing, after the fact. Decide the window before the cutover.
Search results got worse after a cutover
Section titled “Search results got worse after a cutover”Cause. The new model is not better for your data, the index parameters changed, or your query is using the wrong operator for the metric.
Fix. Check the operator first — <=> for cosine, <-> for l2, <#> for
inner product — because a mismatch returns plausible-looking wrong rows without
erroring. Then measure with evaluate --baseline rather than by impression.
Prevention. Run the evaluation with a regression gate before the cutover, not after.
not found: run <id>
Section titled “not found: run <id>”Cause. The run identifier is wrong, or you are pointed at a different database. Run identifiers are yours to choose and are not derived from anything.
The running verbs say the same thing behind the operation they were doing:
claim run <id>: not found: run <id>.
Fix. Check both. A typo looks exactly like a run that was never prepared.
The plan says unknown for something
Section titled “The plan says unknown for something”Cause. Ptah could not establish that fact. A source it could not count, a capability it could not ask about.
Fix. It is not necessarily an error — a migration can run with an uncounted source. It is telling you that a decision rests on something nobody checked, and whether that matters is yours to judge.