2.19.0
Released
Features
Investigations on a paid Anthropic or OpenAI persona can now search the web, up to three times per question, so questions such as finding local businesses, prices or listings are answered from current pages. The worker lists the pages the answer cites under a Sources heading. If an organisation has web search turned off, the worker answers without it. The model is now told whether it can search. A model that cannot search is told not to imply it looked anything up and to suggest pasting page links instead. The Investigate dialog shows the credit range for the question (for example "Uses 4–35 credits on Claude Opus"), updates it as links and files are added, says whether the model can search the web, and states that the charge never exceeds the top of the range. When the balance is below the top of the range, it says the quest's model will be replaced by the free model, or, for a model picked in the workspace, that the question will be refused. A finished investigation shows how many searches it ran and the credits it used. (#622)
A reference link to a web page can now be part of an investigation. The route asks each link's host what it serves: images still go to the model as pictures, HTML and plain-text pages are stored as page references, and the worker fetches each page, extracts its readable text — without scripts, styles, navigation, headers, footers, forms or content hidden with
hidden,aria-hiddenor an inline style — and appends it to the prompt under the page's URL, marked as untrusted reference material and cut at 20,000 characters with the cut stated. A page costs the same as an image or an attachment. Links of any other type are still refused before any credits are spent, and a link whose host gives no usable answer is still treated as an image. A page that cannot be read — an error status, a redirect, a timeout, more than 2 MiB, or no text without JavaScript — fails the investigation with a message naming the link, and the failure refunds the charge. Because pages reach the model as text, they work with every persona, DeepSeek included. The Investigate dialog explains which links are read as pages and what they cost, and the investigation history lists pages read separately from images. The dialog now submits its links aslinks; the route still acceptsimagesfrom a page loaded before the deploy, within the same ten-link budget. (#601)/admin/ai-spendnow shows each account's estimated AI spend for a chosen month beside the existing platform-funded feature report: metered actions, user-funded and platform-funded credits, refunds recorded that month, and gross and net credits with their US$ estimates, plus the account's current plan and subscription status and totals for all accounts. Accounts without metered spend are listed, net spend can be negative when refunds for earlier charges exceed new spend, and each report shows its own error instead of zeroes when its read fails. No revenue or margin is calculated. The platform-funded feature report now shows one month at a time with the same month buttons instead of stacking every month, and each report keeps its own month in the URL. Both month pickers start at August 2026. (#608)With
NEXT_PUBLIC_FEATURE_PARTY_EMAILSon, a person added to a Party Quest by invite, or seated from a guild roster, is emailed who added them and a link to the quest. Nobody is emailed about their own action, seats without an account are skipped, and at most four emails are sent at a time however large the roster. The profile's Email preferences section now always appears, with a new Feedback update emails toggle and, when the flag is on, a Party Quest emails toggle; timezone and quiet hours appear only when a reminder category does, and saving the form without them leaves the stored values alone. Each email's unsubscribe link turns off only its own category. (#571)
Fixes
Investigation answers no longer stop mid-sentence as often. The response budget was 1,200 tokens including the model's thinking, and a reported answer used 970 of them before it began writing. Investigations now have a 4,800-token budget and are charged for the tokens they use. The model is asked to keep its answer under about 600 words. An answer that still reaches the limit now carries a notice that its end may be missing. The worker no longer reads
AI_MAX_RESPONSE_TOKENS, which still sets the quest narrative budget in the web app. Its newAI_INVESTIGATION_MAX_RESPONSE_TOKENSis only an upper limit on the investigation budget and defaults to 4,800. (#622)A task's investigations now span the full width of the task card instead of sitting in the narrow column between the status button and the action buttons. (#622)
Feedback status emails now honour the submitter's preferences and are sent only to a confirmed, active account; they previously went to whatever address the Auth admin API returned. They now carry an unsubscribe link and
List-Unsubscribeheader, andnotification_sent_atis still stamped only when an email was sent. (#571)A feedback status email is now sent at most once when two admins resolve the same report at the same time. The row is claimed before the email goes out, and an email that is skipped or fails releases the claim, so a later status change can still send it. (#628)
Security
The worker fetches a page only from a public address: every DNS answer is validated and the connection is pinned to it, redirects are refused rather than followed, compressed responses are refused so the size cap cannot be bypassed, DNS and the download share one ten-second deadline, and a response is capped at 2 MiB. The public-address policy mirrors the route's link probe from #611. Both the fetch and the probe now send a
Questify-ReferenceReader/1.0User-Agent, because sites such as Wikipedia refuse a client without one, which had left their pages unclassifiable. When one page of an investigation fails, the worker cancels the fetches still running for its other pages instead of leaving them open until their own deadlines. (#601)
Technical changes· 12 entries for database, tooling, CI, tests and docs
Database
Investigations are now charged a hold when queued and settled when they finish.
ai_credit_cost()prices an investigation at its hold — eight times the persona weight, plus the per-reference surcharge, plus one credit per allowed web search — so the charge triggers, the reprice and the balance check all reserve the most an investigation can cost. The new service-role-onlysettle_investigation_credits()works out the real cost from the provider's token counts and search count (ai_investigation_credit_settlement(): one persona weight per 7,000 tokens of input plus five times output, plus one credit per search, never below the old flat price and never above the hold) and returns the rest as a refund linked to the charge through the newai_credit_entries.settles_entry_id. Settling twice refunds nothing more, and a failure after settling still refunds the rest. Newtask_investigations.pricing,max_output_tokensandweb_search_limitcolumns record the budget each charge covered. A newset_investigation_budget()trigger sets them from the persona whenever a row is queued or queued again (4,800 tokens, and three searches for a paid Anthropic, OpenAI or OpenRouter persona) and ignores values a client sends. Rows queued before this migration keep the flat price, a 1,200-token budget and no search. The claim now also requires ametered_pricingcapability for a metered row (investigation_row_capabilities()), so a worker built before this change leaves those rows inpendinginstead of never settling them. Apply the migration before deploying the web app. (#622)Added the admin-only
user_ai_spend_report()function. It totals the credit ledger per account for one of the last six UTC calendar months, by when each entry was recorded, with a bounded page and totals taken over every account before the page is cut. A month is accepted with five minutes' tolerance for clock differences between the web server and the database, and an account's subscription status is its entitling subscription's, as billing chooses it.ai_credit_entries.created_atis now indexed for it. Neither this report norsystem_ai_spend_report()goes back before August 2026, when AI spend metering began, so the platform report no longer lists earlier months as zero spend. (#608)Added
task_investigations.page_urls, the web pages an investigation references. A CHECK constraint (investigation_page_urls_are_valid()) accepts only distinct http(s) URLs without whitespace, up to 2,048 characters, none of them also listed inimages, and at most ten links acrossimagesandpage_urlstogether — the same budget the route enforces, repeated here because the table is writable through PostgREST.ai_credits_charge_investigation()counts pages in the per-reference surcharge alongside images and attachments. A newguard_investigation_references()trigger refuses a signed-in user's change toimages,attachment_idsorpage_urlsunless the same update queues the row again, so the worker can no longer be handed references that were added to a pending row after it was charged; that gap already existed for images and attachments. Bothpage_urlsandattachment_idsmust now be one-dimensional, since a multidimensional array was priced on its first dimension only.claim_next_capable_task_investigation(text[])hands a row to a worker only when the worker lists every capability the row needs (investigation_required_capabilities():page_urlsfor a row with pages), and the no-argumentclaim_next_task_investigation()now claims only rows that need none, so a worker deployed before this change leaves page investigations inpendinginstead of answering them without the pages. Apply the migration before deploying the web app; the web app and the worker can then deploy in either order. (#601)Added
party_emailsandfeedback_emailstonotification_preferences, both on by default.update_my_notification_preferences()accepts both, and the one-click unsubscribe function accepts thepartyandfeedbackcategories. The new service-role-onlyprepare_notification_email()tells the web app whether it may email an account now — only confirmed, live, non-anonymous, unbanned accounts with an address that have not turned the category off — and returns the address, display name and unsubscribe token to use, creating the preference row the first time it is needed. The newadd_guild_to_quest_party_seats()seats a guild roster with the same checks asadd_guild_to_quest_party()but returns the seats that call created, so the app emails exactly those accounts even when another add or invite runs at the same time;add_guild_to_quest_party()still returns a count, for deployments built before it. Apply both migrations before deploying the web app. (#571)Added
feedback.notification_claimed_at, a service-role-only reservation for the feedback status email. A request claims the row before sending, so two concurrent triage updates can no longer both email the submitter; a claim older than ten minutes can be taken over. Apply the migration before deploying the web app. (#628)
Tests
investigation_page_urls.sqlcovers the page shape rules, the charge for every mix of images, attachments and pages, refusals of invalid page lists written straight through PostgREST, the reference guard in both directions, and the refund when the worker fails a page investigation. pgTAP also covers one-dimensional reference lists and the capability-gated claim, including that the legacy claim never hands out a page investigation. Worker tests cover compressed responses, cancellation, page text extraction, character-set decoding, ordering, truncation, unreadable pages and the prompt the model receives; route and dialog tests cover link classification, pricing, the shared budget and the legacy field. (#601)pgTAP covers the report's admin check, month window, UTC boundaries, both funding origins, refunds, excluded top-ups and adjustments, idle accounts, ordering, pagination bounds and totals; Vitest covers the query and page states; and a live Playwright spec covers the admin view and the non-admin redirect. (#608)
party_and_feedback_emails.sqlcovers whoprepare_notification_email()will and will not email, token creation and reuse, the two new categories through the preference function and their unsubscribe links, and the function's privileges, and that a guild add reports only the seats it created;rls.sqladds the privilege boundary. Vitest covers the Resend wrapper, the template registry,sendNotification()with an opted-out recipient, missing configuration and provider failures, the bounded concurrency ofsendNotifications(), the feedback path refactored onto it, both Party Quest add paths, and the preference form and parser. The Playwright preference spec covers the new unsubscribe categories and the profile in each flag state. (#571)rls.sqlcovers the new claim column and that an admin client cannot write it; Vitest covers the claim, the lost race, a failed claim, the release after a skipped, failed or thrown send, and the stamp that clears the claim. (#628)
Documentation
docs/ai-investigations.mddescribes how a reference link becomes an image or a page, what the worker refuses to fetch, and how pages are priced and refunded. (#601)docs/admin-user-ai-spend.mdsets out the report's accounting rules: estimated spend only, refunds counted in the month they are recorded, top-ups and adjustments excluded, and plan status as it stands today. It also lists what reporting actual cost and revenue would require. (#608)docs/notifications.mddescribes both email senders, the per-category preferences, the web app's send path and how to add an email;.env.exampleanddocs/ops/deployments.mddocumentNEXT_PUBLIC_FEATURE_PARTY_EMAILSand the web app's use ofRESEND_API_KEYandFROM_EMAIL. (#571)