Questify

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.sql installs pgvector (vector in extensions) and adds content_embeddings, which holds one 1536-dimension vector per quest, task, succeeded investigation summary, Year Theme and active Quest Pack. AFTER triggers on quests, tasks, task_investigations, year_themes, quest_packs and quest_pack_tasks queue 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 to anon or authenticated, 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) and recommend_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 through claim_/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 new embeddings ledger reason and system budget (500 credits). charge_embedding_credits() meters texts per month in embedding_usage, books one credit per 10,000, and refuses with 53000 before the provider call at the ceiling. Each reservation is recorded in embedding_reservations under the caller's reference, and release_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/ui and apps/web), so a component can mark itself @container and style its children with @xs: / @md: variants on its own width. The stall banner is the first user (#947).

  • NEXT_PUBLIC_FEATURE_SEMANTIC_SIMILARITY (default false) gates the three similarity surfaces and the worker's new embeddings loop. It is registered in .env.example, both env.ts schemas, the web Dockerfile, docker-compose.yml, turbo.json, the Playwright web server, and the release workflow's Vercel→Fly flag copy and shadow guard, pinned in scripts/worker-deploy.test.mjs. The loop embeds through OpenRouter with the existing OPENROUTER_API_KEY; there is no new credential. Its settings are EMBEDDING_MODEL (default openai/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) and EMBEDDING_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.tsx covers 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.ts pins the title read (one in, explicit columns, newest first, an unreadable quest dropped, a failure thrown), body.test.ts that 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.tsx the section's place between Due this week and the preview, quest-card.test.tsx and quest-list.test.tsx the stallDisplay marker against the banner, and stall-banner.test.tsx the new title link and onResolved callback. apps/web/e2e/stall-check-in.spec.ts seeds 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.tsx pins the banner's two layouts by class — the @container root, 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.tsx now checks the 32px target as min-h-8, since the buttons may wrap; the tab order it pins (proposal, Check in, Snooze, then Dismiss) is unchanged. The apps/web functions coverage floor rose from 95.6 to 95.8 (measured 96.12) (#947).

  • supabase/tests/semantic_embeddings.sql covers 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.sql pins that content_embeddings is unreadable by anon and authenticated, 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.sql and supabase/advisor-allowlist.txt record 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.ts covers 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 needs OPENROUTER_API_KEY (#549).

Documentation

  • docs/schema.md documents content_embeddings, embedding_usage and the similarity RPCs. docs/pricing.md explains why embeddings are metered per 10,000 texts against the usual 500-credit ceiling. AGENTS.md adds the rule that vectors are service-role only and reach clients solely through definer RPCs with explicit columns (#549).