"LLM not configured" behaves 8 different ways across 27 call sites, and one of them strands a session in extracting forever #287
Closed
opened 2026-08-05 20:33:49 +00:00 by claude-bot
·
3 comments
No Branch/Tag specified
main
feat/1328-fill-it-in
feat/1328-fill-frontend
feat/1328-fill-backend
feat/9-foundry-push
fix/9-foundry-live
feat/9-foundry-push-frontend
renovate/celery-5.x
feat/5-foundry-connect
feat/9-foundry-push-backend
feat/9-foundry-push-module
feat/5-foundry-frontend
feat/5-foundry-backend
feat/5-foundry-module
release/v4.8.0-rc1
docs/d341-convert-draft
feat/958-update-to-version
feat/958-convert-sheet
feat/958-pathbuilder-import-frontend
fix/1360-heading-order
feat/958-pathbuilder-import-backend
feat/11-weekly-digest
feat/1341-auto-update
docs/d332-superseded
chore/1345-names-cleanup
feat/1342-mentioned-pages
feat/1328-recipes
fix/1346-pgdg-key
perf/1247-shared-recording-stream
chore/132-gitleaks-allowlist
release/v4.7.1
spike/foundry-module
release/v4.7.1-rc1
docs/decisions-d332-d337
fix/bot-certifi
chore/release-4.7.0
chore/release-4.7.0-rc4
chore/deps-4.7.0
chore/release-4.7.0-rc3
fix/1280-quote-must-say-it
fix/1257-names-row-squeeze
chore/release-4.7.0-rc2
fix/1245-pg-upgrade-count
chore/release-4.7.0-rc1
feat/1257-names-in-prep
feat/1257-be
feat/1257-fe
feat/1257-rating-fixes
design/1328-fill-it-in
feat/1257-name-splits
fix/1326-redis-503
feat/1280-fact-suggestions
feat/1280-be
feat/1280-fe
design/1280-fact-suggestions
feat/1279-infobox
feat/1279-be
feat/1279-fe
feat/1321-shelf-copies
design/1279-infobox
feat/1243-table-find
feat/1257-who-rated
feat/1243-prep-drawer
feat/1243-sheet-pickers
design/1243-browse-in-place
feat/1315-hotwords-default
feat/1242-system-filters
feat/1257-more-name-styles
feat/506-leg3-hotwords
design/1242-system-filters
fix/1302-packs-polish
feat/1241-rules-library
design/1241-rules-library
feat/1245-postgres-18
integ/4.6.2
fix/1263-map-ui-2
fix/1263-map-server
fix/1263-map-ui-1
fix/1261-editor-link-menu
fix/1229-axe-4.14-name-mismatch
renovate/axllent-mailpit-1.x
renovate/eslint-monorepo
renovate/authlib-1.x
chore/release-4.6.0
chore/release-4.6.0-rc2
fix/460-rc2-screens
fix/460-rc2-server
fix/release-prerelease-exit
chore/release-4.6.0-rc1
feat/125-campaign-templates
feat/126-calendar
feat/1181-pictures-and-files
feat/1190-event-people
integ/4.6.0
feat/1191-line-of-the-night
feat/1200-starter-packs
chore/bot-multidict-and-playwright-group
fix/1202-this-week-answered-count
fix/122-124-leftovers
fix/1178-get-routes-that-write
fix/1203-journeys-check-names
design/125-campaign-templates
feat/126-calendar-engine
design/126-in-world-calendar
fix/1189-cited-stamp-in-range
fix/1189-v4-looser-check
fix/1189-richer-beats
fix/1113-export-bind-lists
fix/1154-release-notes-by-file
chore/release-4.5.0
chore/release-4.5.0-rc12
fix/1135-wiki-markup-as-text
fix/demo-usage-cost-in-window
chore/release-4.5.0-rc11
fix/1121-sheet-consistency
fix/1121-sheet-5e-terms-and-card-hp
copy/1122-busiest-pages
fix/1121-relevant-choices
fix/1121-wiki-create-npc-save-sheet-links
fix/1029-menu-stays-on-screen
docs/prototype-transcript-bin-wording
fix/1023-editor-save-row-back-scroll
fix/1020-search-exact-first
fix/1020-table-prep
fix/1020-shell-wordmark-footer-bell
fix/1020-wiki-article-party
fix/872-admin-kept-until-phone-tables
feat/l13-milestone-level-waiting
fix/1000-1001-bell-older-setup-context
fix/999-pronouns-in-prompts
fix/l19-other-class-feats
fix/1010-transcript-window-ordinary-confirm
docs/d254-d269-decisions
fix/702-member-search
fix/680-transcript-bin
fix/993-unidentified-prompts
fix/841-redis-timeouts
fix/990-binned-not-exported
fix/830-export-prep-shelf
fix/deps-audit-2026-10-01
fix/877-title-reads
fix/798-audio-received-at
build/lane11-17-j7
build/lane11-16-levelup
build/lane11-14-seams
build/lane11-14-seed
build/lane11-15-start
build/lane11-13-old-block-out
fix/980-review-r3
build/lane11-12-block
build/lane11-8-packs
fix/demo-seed-canonical-names
build/lane11-11-hot-half
docs/l11-decisions
build/lane11-7-convert
build/lane11-10-editing
fix/npm-audit-brace-expansion
build/lane11-9-sheet-page
fix/bot-pyjwt
build/lane11-6-content
fix/lane11-rank-names
build/lane11-2b-language
build/lane11-4-js-evaluator
build/lane11-3-server
build/lane11-5a-pf2e
build/lane11-5b-5e
feat/design-admin
build/lane11-2-evaluator
build/lane11-1-sheet-pieces
build/lane11-0-drawing
fix/943-quest-objectives-fk
fix/prepsheet-shelf-flake
fix/945-leftovers
build/demo-gm-copy
fix/901-email-landings
build/lane10-8-journeys
build/demo-seed-lanes-8-10
fix/887-gm-own-confirmation
build/lane9-5-j1-admin
fix/918-audience-click
fix/useasync-overtaken
ci/939-critical-path
test/937-backend-suite-speed
chore/claude-orchestration
fix/924-xp-gm-only
fix/session-stale-poll
docs/decisions-d239-d242
fix/930-pypdf
build/lane8-journeys
build/925-quest-editor
fix/894-private-page-pictures
build/lane8-p4-drafts
build/lane8-p7-map
build/lane8-p6-handouts-rules
fix/wiki-fixture-names
build/lane10-d227-test-usage
fix/rsvp-horizon-test-tuesday
fix/media-volume-owner
build/lane8-p3-editor
fix/912-unidentified-rename
build/lane8-p5-party-quests
build/lane8-handout-takeback
build/lane10-pickers
build/lane8-p2-article
build/lane8-6-map
build/lane10-7b-email-pages
build/lane8-5-handouts
build/lane10-7a-test-the-bot-pages
build/lane8-p1-pages
build/lane8-4-quests
build/lane8-2b-feeds
build/lane8-3-parties
build/lane8-2a-links
build/lane10-5-commands
build/lane9-6-seed
build/lane10-4b-reactions
build/lane9-4d-pages
build/lane9-4c-pages
build/lane10-4a-posts
build/lane10-3-email
fix/891-web-fonts
build/lane9-4b-pages
build/lane10-6-test-the-bot
build/lane10-2-notify-out
build/lane8-1-wiki-text
build/lane9-4a-frame
design/lane10-drawings
fix/j6-budget
fix/j5-phone-flake
fix/869-test-leaks
fix/843-record-start-followups
build/lane7-all
build/lane6-all
build/lane9-b2-server
chore/changelog-fragments
docs/decisions-d218
build/lane9-a-owner-overview
fix/rule-card-level-label
fix/834-retention-semantics
fix/o3c-record-without-announcement
docs/decisions-d179
docs/decisions-d131-d170
fix/851-commit-before-response
build/lane6-j3-seed
build/lane9-b1-d67-identity
fix/841-blocking-loop
build/lane9-c-backups-audit
fix/vote-race-and-start-commit
fix/recording-announcement
fix/proposal-read-leak
fix/last-admin-guard
build/lane6-p4c-planning
build/lane6-p4b-margin
build/lane6-p4a-sheet
build/lane5-j4-seed
build/lane6-base
build/lane7-pages-talk
build/lane5-pages
build/lane7-pages-safety
build/lane7-p3-safety
build/lane7-p2-notify
build/lane6-p3-planning
build/lane6-p2-history
build/lane6-p1b-items
build/lane7-p1-threads
build/lane5-p6-faces
build/lane6-p1a-sheet
build/lane5-p5-find
build/lane5-p4a-board
build/lane5-p4b-scenes
build/lane5-p3-live
build/lane5-base
build/lane5-p2-cards
docs/lane5-decisions
build/review-r2-wide
fix/review-r1
build/lane5-p1-scenes
build/demo-seed
build/lane4-journeys-j5
build/lane4-page-review-b
build/lane4-page-review-a
build/lane4-page-read
build/lane4-journeys
build/lane4-page-sess2
build/lane4-page-sess1
build/lane4-notes
build/lane4-discord-on-approval
build/lane4-scheduling
build/lane4-versions
build/760-session-number
build/lane4-decisions
build/797-erase-no-regenerate
build/test-race-audit
build/lane3-completions
build/794-dark-only
build/796-titles-scroll
build/791-retention-anchor
build/a11y-sweep
build/shell-followups
build/retention-preview
build/753-backup-readback
docs/d91-d93
build/lane3-j1
build/ci-harness-paths-selftest
fix/useauth-test-race
build/ci-harness-paths
build/777-ai-settings-read
build/lane3-settings-c
build/lane3-settings-b
build/lane3-j6
build/lane3-sessionform
build/lane3-settings-a
build/lane3-members
build/lane3-thisweek
build/lane3-story
chore/87-align-python-3.12
v4.8.0-rc1
v4.7.1
v4.7.1-rc1
v4.7.0
v4.7.0-rc4
v4.7.0-rc3
v4.7.0-rc2
v4.7.0-rc1
v4.6.2
v4.6.2-rc4
v4.6.2-rc3
v4.6.2-rc2
v4.6.2-rc1
v4.6.1
v4.6.1-rc1
v4.6.0
v4.6.0-rc2
v4.6.0-rc1
v4.5.3
v4.5.3-rc1
v4.5.2
v4.5.2-rc1
v4.5.1
v4.5.1-rc2
v4.5.1-rc1
v4.5.0
v4.5.0-rc12
v4.5.0-rc11
v4.5.0-rc10
v4.5.0-rc9
v4.5.0-rc8
v4.5.0-rc7
v4.5.0-rc6
v4.5.0-rc5
v4.5.0-rc4
v4.5.0-rc3
v4.5.0-rc2
v4.5.0-rc1
v4.4.1
v4.4.0
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.2
v4.1.1
v4.1.0
v4.0.1
v4.0.0
v3.11.4
v3.11.3
v3.11.2
v3.11.1
v3.11.0
v3.10.0
v3.9.1
v3.9.0
v3.8.0
v3.7.0
v3.6.1
v3.6.0
v3.5.0
v3.4.1
v3.4.0
v3.3.0
v3.2.0
v3.1.0
v3.0.0
v2.1.0
v2.0.0
v1.1.0
v1.0.0
v0.12.0
v0.11.0
v0.10.0
v0.9.1
v0.3.3
v0.9.0
v0.8.0
v0.3.2
v0.3.1
v0.3.0
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
backend
Issue related to the backend
bot
Issue related to the bot
bug
Something is not working
duplicate
This issue or pull request already exists
enhancement
New feature
frontend
Issue related to the frontend
help wanted
Need some help
invalid
Something is wrong
question
More information is needed
recording
Issue related to the recording system
security
Security hardening / vulnerability work
wontfix
This won't be fixed
No labels
backend
bot
bug
duplicate
enhancement
frontend
help wanted
invalid
question
recording
security
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Quest-Board#287
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the LLM trigger audit that produced #278 and #279. Two related problems: an inconsistency that makes the system unpredictable, and one concrete stuck-state bug that falls out of it.
The inconsistency
settings_service.get_llm_configis resolved at 27 call sites across 7 files:tasks/reminder_tasks.pyrouters/campaigns.pyrouters/bot.pytasks/planning_tasks.pyrouters/sessions.pyservices/lore_service.pyrouters/users.pyThere is no shared "resolve or bail" helper, and the
Nonebranch was written independently each time. The result is eight distinct behaviours for the same condition:reminder_tasks.py:563,:648,:747,lore_service.py:790)failed, error stored, bot notified (reminder_tasks.py:2036):3219,:3781,:4106,:4258):3324,:3704)sessions.py:281,campaigns.py:4251,:4293,:5285,:5368,:5414){"skipped": True}(planning_tasks.py:91,:183)lore_service.py:594-595, reached fromcampaigns.py:2745passing an uncheckedllm_cfg)sessions.py:339)A GM who hasn't configured an LLM — or whose endpoint is down — cannot predict what any given feature will do. Some fail loudly, some fail into a stored error, some pretend to succeed. #279 was exactly this class of problem (a silent failure indistinguishable from success) and it survived a full release.
The concrete bug this produces
lore_chunk_extractraisesRuntimeError("LLM endpoint not configured.")(reminder_tasks.py:562-563), which is caught by the blanket handler at:588-591and converted intoself.retry(...)with exponential backoff. Aftermax_retriesthe task dies withMaxRetriesExceededError.But
lore_generation_statuswas set toextractingback at:488, and nothing on the retry-exhaustion path sets it tofailed. So the session sits inextractingindefinitely — the UI shows generation in progress forever, and the GM has no signal that it died. The same shape applies to thematchingphase (:672).Retrying a missing configuration is itself questionable: an unconfigured LLM will still be unconfigured 4, 8, and 16 seconds later. It is a permanent condition being handled as a transient one.
Fix direction
with_llm_config(db)helper returning either the config or a typed "not configured" outcome, so each call site makes an explicit, visible choice rather than an accidental one. Roughly three sensible policies: fail the request (503), fail the job and record it on the domain row, or skip with a warning. Any given site should pick one deliberately.extracting/matching/processingmust have a terminal-failure path that clears it, including retry exhaustion (Celery'son_failurehook, or an explicit try/finally).asyncio.runwrapper →task_session()→db.get→ None-check →get_llm_config→ None-branch → local import → try/except log-and-swallow). Five near-identical skeletons at:3190-3260,:3751-3862,:4060-4192,:4228-4338,:3661-3730. An@llm_taskdecorator would delete most of it and make the not-configured policy uniform by construction. Optional, but it is the thing that would stop this drifting again.Acceptance
extracting/matchingafter its task has permanently failedLabels: backend
Reopening. This was closed on 2026-08-06 when the stuck-state bug shipped in v3.11.3, but the harmonisation this issue is named for was never tracked anywhere — so the trail went cold on the larger half. Picking that up now.
Re-survey against current
main(26d63a5)24 call sites, not 27 — the v3.11.x fixes removed three (
bot.pywent 4 → 2).What v3.11.3 already delivered:
extracting/matching—on_failurerecords a terminal status and refuses to clobber one a later phase already reached.LlmNotConfiguredErrorexists (settings_service.py:25) with a docstring making the permanent-vs-transient distinction explicit, andreminder_tasks.py:627-629excludes it from retry.So the typed error and the no-retry rule are real — but they reach only 3 of the 24 sites (the lore chunk pipeline at
:662,:751,:854). Everything else still hand-rolls itsNonebranch.Current behaviour census:
LlmNotConfiguredError— fail fast, no retryreminder_tasks662/751/854RuntimeError— will burn retriesreminder_tasks:2142,lore_service:886campaigns×5,sessions:282,bot.py×1returnreminder_tasks3367/3931/4256/4409reminder_tasks3474/3854{"skipped": True}planning_tasks91/182lore_service:678viacampaigns:2745bot.py/meta,users.pytest endpointTwo corrections to the original body, from reading the current code:
planning_tasks:75is not unchecked. The check is simply deferred to:91, and it returns{"skipped": True}consistently with:182. Not a defect.bot.py/metaandusers.py's test endpoint are correctly different. They report whether an LLM is configured rather than consuming one;llm.endpoint_url if llm else Noneis the right answer there. These should be documented as deliberate, not harmonised away.The one live defect left
approve_lore_proposal(lore_service.py:667-679) falls back toexisting_body + "\n\n" + new_bodywhenllm_cfg is None— a blind append instead of an LLM merge, with no log and no signal to the GM.campaigns.py:2745reaches it with an unchecked config.Worth noting
get_llm_configreturnsNonefor a malformed endpoint URL too (it swallows thenormalize_service_urlValueError), so a typo'd URL degrades silently by the same path. This is the "indistinguishable from success" class that produced #279 and #285.Scope I'm taking
require_llm_config(db)— resolve or raiseLlmNotConfiguredError.get_llm_configstays for the two sites that genuinely want the optional form.RuntimeErrorraises so they stop burning retries.Deliberately deferring the
@llm_taskdecorator (fix direction 4). It is the right long-term answer and I am not doing it in the same change as the behavioural fixes — it rewrites five task skeletons, and bundling a structural refactor with semantic changes makes any regression much harder to attribute. Worth its own issue once this settles.Harmonisation landed in
0a8910e. CI green. All four acceptance items now met.Acceptance
extracting/matching— v3.11.3, viaon_failure.settings_service's module docstring, with the non-obvious sites annotated inline.test_lore_generation_failure.py.What landed
require_llm_config(db)— resolve or raise the typed, non-retryableLlmNotConfiguredError. The three lore-pipeline sites that hand-rolled the identical raise now share it, so the message lives in one place.The four legitimate policies are documented so a new call site has to pick one deliberately rather than invent a ninth behaviour: require it / record the failure on the domain row / skip with a warning / report configuration status. What is explicitly ruled out is producing a worse result and saying nothing.
Two visibility fixes — the only behavioural changes:
approve_lore_proposalwas degrading silently. With no usable LLM it appended both bodies verbatim instead of merging, giving the GM a visibly worse entry that was indistinguishable from a successful merge. The fallback stays — it beats refusing the approval — but it now logs that it happened.get_llm_configswallowednormalize_service_url'sValueError, so a configured but invalid endpoint reported as "not configured" and sent the GM to a settings page that already had a value in it. Now logged: the reason only, never the URL, which is admin-supplied and may embed credentials.896 backend tests (8 new in
test_llm_config_policy.py), ruff check + format clean on the pinned 0.4.4.Corrections to this issue's census
Both from reading the current code rather than trusting the original write-up:
RuntimeErrorsite was burning retries.process_audiosetsmax_retries=0;generate_lore_proposalscatches the exception and recordsfailedwithout retrying. My earlier comment repeated the claim before verifying it. The typed error is still worth having for uniformity — but it fixed no retry bug, because there was none. Acceptance item 2 was already satisfied.planning_tasks:75is not an unchecked site (deferred to:91, matching:182), andbot.py's/metaand the admin connectivity test are correctly different — they report whether an LLM is configured rather than consuming one, soNoneis data there.Net: one real defect, one diagnostic gap, and the rest consistency work. Less dramatic than the title implies — the headline stuck-state bug was already fixed in v3.11.3.
Remaining, not done
Fix direction 4, the
@llm_taskdecorator — ~15 lines of identical preamble across five task skeletons. The issue marks it optional, and it is the thing that would stop this drifting again, but rewriting five task bodies alongside semantic changes would make any regression much harder to attribute. It is not currently tracked anywhere — worth its own issue, which is how this one's larger half got lost in the first place.Closing on acceptance.
Follow-up filed as #294 — the
@llm_taskdecorator, with the duplication re-measured against0a8910erather than this issue's original line numbers.