Troubleshooting
Each of these has happened. They are in rough order of how often.
Nothing is being fetched from one source
karani prints the outcome per source, so a zero-row source is visible in the pass rather than hidden in the total.
- Try the endpoint by hand with
curl. A slug that changed ATS 404s quietly. - Look at the
rawpayload on a recent row from that source. Feed schemas drift, andrawis kept precisely so you can write a reparser instead of losing the data. - If several sources are empty at once, it is the network, not the feeds.
Everything is coming back as maybe
Almost always a resume-and-prompt disconnect rather than a model problem.
- Is
data/resume.mdactually current? It is the thing every verdict is written against. - Read the qualification for one posting. If
recommended_positioningis generic, the model did not have enough to work with. - Isolate model from prompt: run the same row on a different provider with
--provider. If both are vague, it is the prompt or the resume.
Nothing passes the pre-filter any more
Read the dropped lines from karani run. They say which gate ate the
pass.
role not engineering-adjacenteating most of it means[profile] rolesis narrower than you meant.region-lockedeating most of it, when you do have a relocation shape, means the shape is not matching — checkdestinationsandrequires.comp below the flooreating a lot meansMIN_COMP_USDis above the band for the roles you are actually hunting.
Then karani refilter, which re-judges every stored row for free.
The autopilot drafts nothing, or drafts everything
You changed the qualify model and left [autopilot] min_fit alone.
A fit score is only comparable within the model that produced it. Measured over 598 rows, one model put 2 rows above 80 and another put 31 there — a floor of 85 is a different filter under each (ADR 0022).
Look at the new distribution with karani funnel before picking a number.
A pass takes hours
Three local-model settings, in the order they bite:
OLLAMA_NUM_PARALLELabove 1 splits the context window per slot. karani matched its fan-out to it after a pass took 13.5 hours with the mismatch.[hunt] qualify_limittoo high for the model. At 75-170 seconds a row, 15 rows is about half an hour.- JSON grammar mode, if somebody added it back. It fights thinking models at roughly 60 times slower, which is why karani does not ask for it.
Infrastructure has all three in full.
Qualification results look truncated or confused
OLLAMA_CONTEXT_LENGTH is at its 4096 default and silently truncating
your prompt, so the model is judging against a resume it never saw the end
of.
launchctl setenv OLLAMA_CONTEXT_LENGTH 16384
killall Ollama ollama
curl -s localhost:11434/api/ps # confirm context_lengthAnd remember launchctl setenv does not survive a reboot.
The scheduler shows exit 127
The binary is gone — almost always a deleted project virtualenv.
uv tool install --editable ".[slack,orchestrator,artifacts,memory,anthropic]"
karani huntNothing is lost. See Scheduling for why it must not run from a project venv.
The scheduler shows exit 78
A relative path in a launchd plist. launchd has no working directory, so
every path has to be absolute. karani hunt renders absolute paths; a
hand-edited plist is the usual cause.
The scheduler looks healthy and nothing is happening
This is the 40-hour failure. Check the marker file:
cat logs/.last-passIf it is hours stale while launchctl list shows exit 0, a pass is wedged
and launchd will not start another while it lives. The pass deadline now
kills it (ADR 0025); if you
are seeing this, check KARANI_PASS_DEADLINE_SECONDS is not set absurdly
high.
“Another run holds the lock” straight after a deadline kill
A pass killed at the deadline still holds its advisory lock until the database connection drops. Postgres releases session-scoped locks when the session ends, and the unlock tolerates a dead session, so this self-heals — but it is the first thing to check if the message appears right after a deadline kill.
The watcher reports invalid_grant
The Gmail refresh token expired. An OAuth app left in Google’s Testing mode expires them after seven days.
karani watch --authorizeThen paste the new token into .env. Publishing the app would stop this
and cost you a Google review you do not need for a single-user tool.
The watcher matches almost nothing
That is often correct, and it is the designed behaviour. An ATS sender domain never matches on its own, two tracked companies in one message is a refusal, and two open applications at one company need the role title to separate them.
A wrong match writes a rejection onto the wrong company and poisons the
only outcome labels the learning loop will ever have — and unlike a missed
match, you never find out. Read a few passes with --no-slack and enter
the unmatched ones by hand.
Memory calls log a spaCy warning
uv run python -m spacy download en_core_web_smRecall degrades rather than failing without it, so this is a warning to clear, not an outage.
Tests wrote to a real service
They should not be able to: the suite strips every external credential
before any module loads, and re-strips per test. Best-effort integrations
read the environment at call time, so without the strip a developer’s .env
makes a “deterministic” test write to a live service — which happened once,
as thirty fixture pages on the real Notion board.
If you are adding a test, do not undo the strip. Pass dummy credentials explicitly where you need them.
The test suite got slow
The default suite is deterministic and runs in about forty seconds, most of which is five subprocess tests. The in-process ones take about six.
When those six seconds climb, something is doing network I/O. Treat it as a failure even when every test passes.
A migration failed
It cannot have. Every schema change is ADD COLUMN IF NOT EXISTS and the
whole block is idempotent. A migration failure means somebody broke the
rule — revert it (CLAUDE.md
4.7 ).
Starting over
# Re-qualify everything against an updated resume (normally automatic).
psql "$DATABASE_URL" -c \
"UPDATE jobs SET qualification_resume_hash = NULL WHERE prefilter_passed = TRUE;"
karani qualify
# Never suggest a company again.
psql "$DATABASE_URL" -c \
"UPDATE jobs SET active = FALSE, closed_at = NOW() WHERE company_display = 'X';"