feature/ollama-fix #13

Merged
aureus merged 12 commits from feature/ollama-fix into main 2026-05-26 04:20:26 +00:00
Owner
No description provided.
The Ollama code path had three bugs:
1. refreshForUser() skipped all candidates already in user_discovery, so
   Ollama was never called after initial population with fallback reasons.
   Fixed by a re-generation pass that upgrades 'template'/'stale'/null rows
   to 'ollama', capped at OLLAMA_REGEN_LIMIT (default 5) per run.
2. generateReason() accepted only 2 sources; buildSourceTitlesMap() capped
   at 2. Both now accept and pass all matching source titles.
3. 15 s timeout was too short for CPU-only inference; now driven by
   OLLAMA_TIMEOUT_MS env var (default 60 s, 90 s in dev).

Migration 004 adds reason_source (ollama/template/stale/null) and
reason_source_count to user_discovery so staleness can be detected when
a user rates a new source item for an existing recommendation.

Ollama added to dev/docker-compose.dev.yml (CPU-only, no GPU flags).
bootstrap.ts pulls llama3.2:3b and writes OLLAMA_URL to .env.dev.
Full unit + integration test coverage (79 unit, 94 integration passing).
ollama/ollama:latest is Ubuntu-based and several GB; alpine/ollama is ~70 MB
compressed and CPU-only, which is all the dev setup needs.
Multiple ratings in quick succession each fired a background refresh,
causing parallel Ollama calls for the same candidate. An in-process Set
lock skips any refresh that is already running for a given userId:audienceId.

Also exclude dist/ from Vitest to prevent compiled test files from being
discovered alongside source tests.
Blurbs are audience-level data (derived from collective member ratings),
not per-user. user_discovery now tracks only per-user status; reasons
live in audience_candidates(audience_id, tmdb_id).

- Migration 005 creates audience_candidates, migrates existing reason
  data out of user_discovery, and drops the reason columns from user_discovery
- refreshForUser writes blurbs to audience_candidates once per
  audience+candidate; concurrent member refreshes skip Ollama if a blurb
  already exists for that audience+candidate pair
- getDiscoveryMap joins user_discovery with audience_candidates to surface
  the reason in the feed without duplicating it per user
- Staleness detection and re-generation pass operate on audience_candidates
The discover queue was empty because feed visibility was gated on
user_discovery rows that were never written. This redesign removes that
dependency entirely.

Key changes:
- New migration 006 creates user_candidate_actions (explicit per-user
  skip/request actions; "pending" is now implicit — absence of a row)
  and migrates existing skipped/requested rows from user_discovery, then
  drops user_discovery.
- refreshForUser now writes template reasons to audience_candidates
  synchronously (no Ollama wait). upgradeReasons() is a new exported
  function that does the Ollama upgrade pass in the background.
- Feed (getAudienceCandidateMap) reads audience_candidates directly
  for visibility; per-user filter reads user_candidate_actions.
- cron fetchTmdbRecs fires upgradeReasons once per unique audience
  after all refreshForUser calls complete; per-audience calls are
  parallelised with Promise.allSettled.
- discover, rate, and settings routes updated to use
  user_candidate_actions; unskip now deletes the row (pending = no row).
- All 174 tests updated and passing.
When no mode is stored in session (e.g. initial login), land on
curate instead of discover. Curate prompts rating of library items
which is the prerequisite for the rec engine to produce results.
When upgradeReasons runs in parallel for multiple audiences, the same
(candidate, sources) tuple was dispatched to Ollama once per audience.

ollamaInFlight tracks in-flight calls keyed by
'tmdb_id:sorted-sources'. If a call for the same prompt is already
in-flight, concurrent upgradeReasons invocations await the same
promise and write the shared result to their own audience_candidates
row — one Ollama request instead of N.
Prompt fixes:
- System prompt now instructs the model to address the viewer as 'you'
- Solo audience prompt: 'Why would you enjoy X if you loved Y?'
- Group audience prompt: 'Why would you and your group enjoy X if you all loved Y?'
- Eliminates 'Someone who loved...' third-person output

Architecture:
- upgradeReasons(db) is now global — no audienceId param
- Iterates all audiences in one pass: staleness detection,
  candidate collection, then a single capped upgrade loop
- Solo vs group tone determined from user_profiles member count
- Cron fires one rec.upgradeReasons(db).catch() instead of
  N per-audience calls; seenAudienceIds tracking removed
TMDB and Ollama upgrades now run as persistent EventEmitter-based worker
pools instead of fire-and-forget cron jobs. Each pool has a configurable
worker count and poll interval; the dispatcher deduplicates work so the
same audience is never processed by two workers simultaneously.

TMDB: polls all audience_profiles every TMDB_POLL_INTERVAL_MS (default 5s),
calls refreshAudience(db, audienceId) which uses no viewer-specific
exclusions — the feed layer handles those at display time.

Ollama: polls for audience_candidates needing upgrade every
OLLAMA_POLL_INTERVAL_MS (default 5s), calls upgradeCandidate per item.

Ollama prompts now include per-member rating context with labels
(hated/disliked/watched/liked/loved) and address the audience owner as
"you", other members by username.

Routes no longer trigger refreshes directly; dispatchers are the sole
source of work.
loadWork now queries only audiences that have at least one signal-generating
rating (1/2/4/5★) with no fresh rec_links, so the dispatcher returns an empty
list when everything is up to date and workers stay idle.

Dispatcher now logs found/added counts on every poll cycle at debug level so
the idle-vs-busy state is visible without enabling trace logging.
fix(workers): log dispatcher poll at trace level
Some checks failed
PR Checks / test (pull_request) Failing after 2m23s
7b8cea6049
aureus force-pushed feature/ollama-fix from 7b8cea6049
Some checks failed
PR Checks / test (pull_request) Failing after 2m23s
to c532b7b0c4
Some checks failed
PR Checks / test (pull_request) Failing after 2m20s
2026-05-26 03:36:03 +00:00
Compare
fix(migrations): fix cross-database compatibility in migrations 005–006
All checks were successful
PR Checks / test (pull_request) Successful in 4m3s
bf6bf04a80
Three issues caused PG and MariaDB failures:

- migration 005: INSERT OR IGNORE is SQLite-only; replaced with Knex
  query builder + .onConflict().ignore() with JS dedup for the
  GROUP BY/HAVING logic that is also non-portable
- migrations 005/006: FK columns referencing auto-increment PKs need
  .unsigned() on MariaDB 11.4, which enforces strict type matching
  between signed int and unsigned int (int vs int unsigned)
- rec/index.ts: GROUP BY tmdb_id without aggregating media_type is
  invalid on PG/MariaDB strict mode; replaced with .distinct()

Also adds user_discovery to the MariaDB test helper's transient-table
drop list (it exists mid-migration but is dropped by migration 006)
to prevent TRUNCATE failures on a clean DB.
aureus merged commit 314b2f7411 into main 2026-05-26 04:20:26 +00:00
aureus referenced this pull request from a commit 2026-05-26 04:20:27 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
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
aureus/watchcraft!13
No description provided.