[Backend] /ask is unprotected on every provider — no context budget, no num_ctx, no truncation check #426
Closed
opened 2026-08-27 23:28:53 +00:00 by claude-bot
·
2 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#426
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 by the v4.0.0 acceptance-criteria pass, while verifying #336, #337 and #338. Each of those three has a gap, and they turn out to be the same gap seen from three angles: the
/askquestion-answering path received none of the context hygiene the summarisation path did.The four
/askprovider functionsmax_tokens_qa_anthropic(audio_service.py:3994)_qa_openai(audio_service.py:4020)_qa_llamacpp(audio_service.py:3970)_qa_ollama(audio_service.py:3955)optionskey at all —num_ctxnever setNone of them calls
_reject_if_truncated. None passescontext_tokens._qa_ollamabuilds its body with nooptionsdict whatsoever, so Ollama applies its 2048–4096 default and truncates the prompt silently — precisely the defect #337 was filed to fix, still live on this path._qa_anthropicwas named explicitly in #338's body as in scope and was not touched.Why it matters
/askis how a GM queries their own campaign. #340 bounded what goes into the prompt (and that part is done — closed), but nothing checks what comes back. A truncated answer is indistinguishable from a complete one: it is prose, so it simply stops, and there is nofinish_reasoncheck to notice. On Ollama the prompt is silently cut instead, so the model answers a question it was only shown part of.Lower severity than #425 or #397 — this degrades answers rather than destroying data — but it is the one remaining place where the pipeline trusts a provider response without checking it.
Related, and deliberately left in the parent issues
generate_structured_textcall sites./askis one of the 15 without it.num_ctxis set only where a caller passescontext_tokens;_qa_ollamanever does._summarise_*but not_qa_*.Those three stay open on their own merits; this issue exists so the
/askcluster is not lost inside them.A design question worth answering first
_reject_if_truncatedcurrently only raises underjson_mode(llm_service.py:243-248), and there is a test asserting truncated prose is returned rather than raised (test_llamacpp_truncated_prose_is_returned). That directly contradicts #338's last acceptance criterion, which asks for a raise.Both positions are defensible — half an answer may beat no answer for a GM asking a question — but the code and the issue currently disagree, and one of them should change. Decide that before implementing, because it determines whether this is "raise" or "return, flagged".
Acceptance criteria
_qa_*function sets an explicit, non-trivialmax_tokens(1024 is small for a prose answer over ten sessions of context)_qa_ollamasetsoptions.num_ctxfrom the resolved context window_qa_*function checks the provider's truncation signal, and the raise-versus-flag question above is settled and consistent/askanswer is not silently presented as complete#336, #337 and #338 are now closed.
/askwas left untouched, as this issue intended — recording what changed underneath it and what did not, so the next person does not re-derive it.The design question is settled
This issue asks for the raise-versus-flag question to be decided before implementing. Ryan decided it on #338: prose returns, flagged; structured output raises. Half a summary — or half an answer — a GM can read and correct beats none at all, whereas a partial JSON object cannot be trusted or repaired. The rationale and an explicit warning not to make the two halves consistent now live in
_reject_if_truncated(llm_service.py:240-280).So this issue's third criterion resolves to flag, not raise, and its fourth ("a truncated answer is not silently presented as complete") is the real work.
What changed under
/askfor freegenerate_structured_textnow resolves an undeclared window to a conservative default rather than switching budgeting off, so any path routing through it is preflighted and getsnum_ctxon Ollama. That does not cover_qa_*: those four functions build their own request bodies and bypass the transport entirely, exactly as this issue's table says.answer_question→_qa_ollamastill sends nooptionsdict at all.The table is still accurate, with one addition
_summarise_ollamahad the same defect as_qa_ollama— no_reject_if_truncated, no output cap — and was fixed in65eebb4. It is worth copying that shape rather than inventing a new one:optionsis merged, not assigned, sonum_ctxandnum_predictcoexist. Assigning drops one silently;test_ollama_greedy_decoding_does_not_clobber_num_ctxexists because that already happened once._reject_if_truncatedreturns whether it fired, so a caller can record it rather than only logging._PROSE_MAX_TOKENS(4096) is the shared prose cap. The 1024 in_qa_anthropicand_qa_openaishould become that.Where a flagged
/askanswer should goUnlike the summary path there is no
SummarisationRunto hang a flag on —/askreturns a response to a live request rather than persisting a run. So the flag likely belongs in the response body, and the front end has to render it. That is a genuine design decision this issue still owns, and it is the reason/askwas not simply swept up in #338: the mechanism that made "return, flagged" defensible for summaries does not exist here.Done in PR #486 (merged, CI green on all 7 jobs). All five criteria met — closing.
_qa_*sets an explicit, non-trivialmax_tokens—_PROSE_MAX_TOKENS(4096) on all four, replacing 1024 on Anthropic/OpenAI and nothing at all on llama.cpp._qa_ollamasetsoptions.num_ctxfrom the resolved window — together withnum_predict, in one literal rather than two assignments, for the reasontest_ollama_greedy_decoding_does_not_clobber_num_ctxexists._qa_*checks the truncation signal, consistently — through_check_usage, the same helper the structured paths use. Settled as flag, not raise, per the decision recorded on #338.QaAnswer→AskResponse.truncated→ the Discord embed.The Ollama half was worse than "degraded answers"
Worth recording plainly: with no
optionsdict, Ollama applied its own 2–4k default to the prompt, so/askon a self-hosted Ollama was answering from a fraction of the context it had assembled. That is #337's defect, still live on this path a release later, on the provider a self-hoster is most likely to run. The other three degrade the answer; this one degraded the question.The design question this issue owned
Resolved as: the flag travels in the response body, and the bot renders it in the embed description, not the footer. A GM may act on a partial answer and footer text is small grey print. The answer budget shrinks to 3900 characters so the notice cannot push the embed past Discord's 4096-character cap — that failure would turn a degraded answer into no answer, which is strictly worse than the bug being fixed. There is a test for it.
Additive field, so no
BOT_CONTRACT_VERSIONbump; a test covers the older-backend direction wheretruncatedis absent.What this did not do
The four
_qa_*transports are still near-duplicates — folding them into the shared one is #484, and doing it here would have been smuggling a refactor into a bugfix. What did change is that the checks are now shared, so the specific failure mode that produced this issue (one of four getting a fix the others did not) cannot recur even while the transports stay separate.Two test doubles were fixed rather than worked around, both of which were passing while diverging from reality (#441's concern):
test_recap_email'stype("Cfg", ...)shim lackedcontext_tokensand so failed on anAttributeErrorraised before the code under test, andtest_context_boundsmocked_qa_llamacppwith a bare"ok"that would have unpacked into two characters against the new tuple return and passed silently.