Intelligence
Intelligence is the recommendations feature that surfaces stale items, possible duplicates, grouping suggestions, and other gentle nudges to keep lists clean. It runs an LLM under the hood, so it depends on a configured AI provider.
The user-facing version of this feature is documented at Suggestions. This page is the operator’s reference.
Quick Start
Section titled “Quick Start”- Configure an AI provider.
- Go to Admin > Intelligence.
- Toggle Enabled on.
- (Optional) Pick an
intelligenceModelOverrideif you want a different model than the global AI config uses elsewhere. - Let the daily cron tick (
0 4 * * *) populate recs, or runpnpm intelligence:run-onceto generate immediately for testing.
Analyzers
Section titled “Analyzers”Four analyzers ship in v1:
| Analyzer | What it does | Calls the LLM? |
|---|---|---|
| Primary list | Picks the user’s primary “active” wishlist and surfaces it on the dashboard. Pure heuristic. | No |
| Stale items | Flags items that haven’t been touched in a long time. | Yes |
| Duplicates | Detects probable duplicate items within a list (or across the user’s lists). | Yes |
| Grouping | Suggests or / order item groups from clusters of similar items. The only analyzer that mutates state via an apply action. | Yes |
Each analyzer can be individually enabled/disabled via intelligencePerAnalyzerEnabled. Adding a new analyzer ships enabled by default; the per-analyzer map only stores admin overrides.
| Setting | Default | What it does |
|---|---|---|
intelligenceEnabled | false | Master kill switch. |
intelligenceRefreshIntervalDays | 7 | Cron regenerates a user’s recs when their last successful run is at least this old AND no active recs are un-reviewed. |
intelligenceManualRefreshCooldownMinutes | 60 | Per-user rate limit on manual “refresh recs” clicks. |
intelligenceCandidateCap | 50 | Max items per analyzer prompt. Caps token usage on power users. |
intelligenceConcurrency | 3 | Max parallel users in one cron tick. Avoid hitting provider rate limits. |
intelligenceUsersPerInvocation | 25 | Distinct users processed per cron tick before bailing. Lower if cron times out. |
intelligenceStaleRecRetentionDays | 30 | How long to keep recommendations rows before the sweep deletes them. |
intelligenceRunStepsRetentionDays | 30 | How long to keep recommendation_run_steps (per-step debug rows). |
intelligenceDryRun | false | When true, runs go through the full pipeline (model calls, step logging) but persist no recommendations rows. Run + run-step rows still write, so admins can debug without affecting users. |
intelligenceModelOverride | null | Optional model ID override. Provider/key/baseUrl still come from the global AI config; only the model name swaps. |
Skip States
Section titled “Skip States”Every run path (cron, manual, CLI) records a row in recommendation_runs with a skip_reason if it didn’t actually generate recs. Common values:
skip_reason | When |
|---|---|
disabled | Master toggle off. |
no-provider | AI config not resolvable. |
unread-recs-exist | Cron only. User has active recs from a prior batch; we don’t churn tokens before they engage with them. |
unchanged-input | Cron only. The combined per-analyzer input hash matches the last successful run AND we’re inside the refresh window. |
cooldown | Manual only. User hit intelligenceManualRefreshCooldownMinutes. |
lock-held | Another run is in flight for this user. |
The Postgres advisory lock is keyed by user, so concurrent triggers for the same user collide rather than double-spending tokens.
Dismissal Stickiness
Section titled “Dismissal Stickiness”Each rec has a stable fingerprint (analyzer + kind + sorted target IDs). When a new batch produces a rec with the same fingerprint as a prior dismissed (or applied) rec, the runner carries the status forward instead of creating it as active.
So if you dismiss a “duplicate item pair” rec, that exact pair stays dismissed across regenerations until one of the items is deleted or renamed enough that the fingerprint changes.
Spoiler Protection
Section titled “Spoiler Protection”Intelligence honors GiftWrapt’s spoiler-protection rule:
giftedItems(claim) data never appears in any prompt sent to a model.- Rec body text is human-readable rationale only; never references claim data.
items.isArchived = trueitems are excluded from candidate sets (those are revealed gifts, not list hygiene).giftideasandtodoslists are filtered out at the SQL level - gift ideas are spoiler surfaces for someone else, todos have no gift fields to analyze.
Deployment Shapes
Section titled “Deployment Shapes”Three ways to run the analyzer pipeline; pick whichever fits your stack. They all share the same runner and can run alongside each other safely thanks to the per-user advisory lock.
| Shape | Where it runs | When to pick it |
|---|---|---|
| Bundled HTTP cron | /api/cron/intelligence-recommendations, CRON_SECRET-gated | Default. Works with Vercel Cron, Render Cron, system crontab, the bundled docker compose sidecar. |
| External scheduler | Same endpoint, hit by your own scheduler | When you want central scheduling outside the app. Same secret. |
| Standalone worker | pnpm intelligence:worker | When you want a separate Docker service that owns the analyzer drain. Keep the HTTP cron running too if you like - the lock prevents double-spend. |