3.10.0
Released
Features
Starting a new quest now warns you when it reads like one of your own earlier quests, abandoned ones included. Matches are by meaning, not just matching words, so "get fit" can find "start running again". It never stops you creating the quest. Public quest pages list related public quests. Onboarding puts the Quest Pack closest to your own quests and Year Theme first and marks it "Suggested for you"; with no history yet, the usual order stays. Behind
NEXT_PUBLIC_FEATURE_SEMANTIC_SIMILARITY, off by default (#549).
Fixes
Stalled quests get their own "Needs a nudge" section on the dashboard, between Due this week and Recently updated, instead of a full check-in squeezed inside a quest card. Each row names and links the quest and has the idle count, the written nudge, the suggestion and Check in / Snooze / dismiss, laid out side by side at that width. It lists every quest with an open stall — before, only stalls on the four most recently updated quests appeared, and a stalled quest is the one least likely to be among them — newest first, three at a time with "Show N more". Dismissing, snoozing or applying a suggestion removes the row, and the section is gone when nothing is stalled or stall detection is off. The dashboard's quest cards now show only a one-line "No activity · Nd" marker, so one stalled quest no longer stretches its whole row of cards to the banner's height. Cards on Quests and Year Themes keep the full banner (#946).
The dashboard's "Recently updated" preview is a 2×2 of full-size quest cards on desktop instead of four narrow cards in a row. Each card was about 180px wide on every screen size, so titles ran three lines deep, the schedule wrapped and the theme and party lines were cut off mid-word; cards are now about twice as wide, with the status badge back beside the title and the details shown in full (#948).
The "No activity" stall banner on quest cards no longer squeezes its message into a narrow strip or pushes its buttons past its edge. The warning icon and the dismiss button now sit only in a header row with the idle count, and the written nudge, your check-in note, the suggestion and the buttons use the banner's full width underneath — in a dashboard card the nudge had about 52px of a 148px banner and now has all 122px. A long suggestion button such as "Snooze this quest" wraps instead of running 60px past the banner, Check in and Snooze share a row that wraps cleanly, and when the banner is at least 28rem wide it lays out side by side, the nudge on the left and the suggestion and buttons on the right. The banner switches on its own width rather than the screen's, so the same page gives the narrow layout in a dashboard card and the wide one on a wider card (#947).
Technical changes· 7 entries for database, tooling, CI, tests and docs
Database
20261204120000_semantic_embeddings.sqlinstalls pgvector (vectorinextensions) and addscontent_embeddings, which holds one 1536-dimension vector per quest, task, succeeded investigation summary, Year Theme and active Quest Pack. AFTER triggers onquests,tasks,task_investigations,year_themes,quest_packsandquest_pack_tasksqueue a row whenever the text's md5 changes. The migration backfills every existing row. A soft-deleted quest, archived theme, inactive pack or blank text loses its vector. The table has RLS on with no policies and no grants toanonorauthenticated, because a vector is a lossy copy of private text. Clients reach it only through three definer RPCs that return explicit columns:find_similar_own_quests()(the caller's own quests, signed in only),get_related_public_quests()(public, non-deleted quests nearest a public quest, callable signed out, empty for a private source) andrecommend_quest_packs()(active pack ids nearest the caller's own history, signed in only). None of them returns a vector, a task, an investigation or a theme. The worker drains the queue throughclaim_/complete_/fail_/defer_embedding_batch()(service role only). A row edited while its vector was being computed keeps waiting for a vector of the new text. Spend is platform-funded under a newembeddingsledger reason and system budget (500 credits).charge_embedding_credits()meters texts per month inembedding_usage, books one credit per 10,000, and refuses with53000before the provider call at the ceiling. Each reservation is recorded inembedding_reservationsunder the caller's reference, andrelease_embedding_texts()gives a failed call's reservation back to the month it was made in, reverses any credit it booked, and does nothing if repeated, so the ledger always holds one credit per 10,000 texts actually counted (#549).
Tooling & Configuration
The shared Tailwind preset (
packages/ui/tailwind.config.ts) now loads@tailwindcss/container-queries(^0.1.1, a dev dependency of@questify/uiandapps/web), so a component can mark itself@containerand style its children with@xs:/@md:variants on its own width. The stall banner is the first user (#947).NEXT_PUBLIC_FEATURE_SEMANTIC_SIMILARITY(defaultfalse) gates the three similarity surfaces and the worker's newembeddingsloop. It is registered in.env.example, bothenv.tsschemas, the webDockerfile,docker-compose.yml,turbo.json, the Playwright web server, and the release workflow's Vercel→Fly flag copy and shadow guard, pinned inscripts/worker-deploy.test.mjs. The loop embeds through OpenRouter with the existingOPENROUTER_API_KEY; there is no new credential. Its settings areEMBEDDING_MODEL(defaultopenai/text-embedding-3-small, which must answer in 1536 dimensions and is shared with the web app's draft check),EMBEDDING_BATCH_SIZE(default 32, at most 64) andEMBEDDING_POLL_INTERVAL_MS(default one minute). With the flag on and no OpenRouter key the worker logs the loop as misconfigured. The admin AI-spend page labels the new feature "Semantic embeddings" (#549).
Tests
needs-a-nudge.test.tsxcovers the section's states (none, one, overflow and Show fewer, a dismissed, snoozed or applied row leaving and the next moving up, the last row taking the heading with it, a refused call keeping the row).stalled-quests.test.tspins the title read (onein, explicit columns, newest first, an unreadable quest dropped, a failure thrown),body.test.tsthat it runs only with stall detection on and that a failed read hides the section without touching the rest of the page,dashboard-body.test.tsxthe section's place between Due this week and the preview,quest-card.test.tsxandquest-list.test.tsxthestallDisplaymarker against the banner, andstall-banner.test.tsxthe new title link andonResolvedcallback.apps/web/e2e/stall-check-in.spec.tsseeds a stalled quest behind three fresher ones and checks the section, the marker and that the four preview cards stay within 50px of each other's height at 1280px and 1500px against a live project; its generated-nudge case now looks for the nudge copy under the idle line, where the #947 banner layout moved it (#946).stall-banner.test.tsxpins the banner's two layouts by class — the@containerroot, the header row that alone carries the icon and dismiss, the full-width body, the wrapping suggestion button and action row, the@md:side-by-side columns, the right-anchored snooze menu — and adds the dismiss and snooze-menu paths it did not cover.quest-card.test.tsxnow checks the 32px target asmin-h-8, since the buttons may wrap; the tab order it pins (proposal, Check in, Snooze, then Dismiss) is unchanged. Theapps/webfunctions coverage floor rose from 95.6 to 95.8 (measured 96.12) (#947).supabase/tests/semantic_embeddings.sqlcovers the embedding triggers (enqueue, unchanged-hash no-op, soft delete, archive, pack task edits), the lease, a mid-flight edit, the failure backoff, the metered budget and its refusal, and each similarity RPC with hand-built vectors.rls.sqlpins thatcontent_embeddingsis unreadable byanonandauthenticated, that dedupe never returns another user's private quest, that a private quest neither has related quests nor appears as one, and that recommendation returns only pack ids.function_execute_grants.sql,security_advisors.sqlandsupabase/advisor-allowlist.txtrecord the three new client grants. Worker Vitest covers the OpenRouter embeddings call (order, width, errors), the batch handler (charge-before-call, budget deferral, failure release) and the loop. Web Vitest covers the draft check (flag, length, signed-out, budget, provider failure), the warning's debounce and dismissal, the related-quests section by flag and visibility, and the personalised pack order with its fallback.apps/web/e2e/semantic-similarity.spec.tscovers all three surfaces in the browser, with the flag on and off, against a live project; vectors are seeded through the service role, and the duplicate-warning case also needsOPENROUTER_API_KEY(#549).
Documentation
docs/schema.mddocumentscontent_embeddings,embedding_usageand the similarity RPCs.docs/pricing.mdexplains why embeddings are metered per 10,000 texts against the usual 500-credit ceiling.AGENTS.mdadds the rule that vectors are service-role only and reach clients solely through definer RPCs with explicit columns (#549).