Prefs: owner defaults — model, backend, persona_id, reel span/length (new columns) #127

Closed
opened 2026-09-27 15:04:56 +00:00 by cameron · 2 comments
Owner

Part of #113

What to build

Second wave of the prefs surface (#126 built the endpoints over the existing
columns; this adds the recurring per-user picks that are currently
re-sent on every request). One migration grows user_ai_prefs, and the
existing PUT /prefs / GET /prefs surface exposes the new fields with the
same partial-update semantics.

New columns on user_ai_prefs (all nullable — NULL = keep the current
per-request/default behavior):

  • default_model — last/explicit choice for insight + chat model
    (local id or OpenRouter id).
  • default_backend — "local" | "hybrid".
  • default_persona_id — active persona for persona chat (currently
    hard-defaults to "default").
  • reel_span — "day" | "week" | "month".
  • reel_max_segments — i32.

Resolution order on every consumer that accepts these per request:
request param → stored pref → current hard default. The per-request
knobs stay per-request — sampling params (temperature/top_p/top_k/min_p),
system_prompt, num_ctx, max_iterations, enable_thinking, amend
deliberately do NOT become prefs (ephemeral or per-object).

Where the pref is consulted: insight generation (model/backend), persona
chat turn (persona), reel creation (span/max_segments). Capture: either an
explicit "save as default" flag on the create request, or the PUT /prefs
surface (client's choice — the FV settings page owns it); do not silently
overwrite a stored pref on every reel request (that's what capture_prefs
does for the existing columns today and is fine there because those fields
were always "last value wins"; the model/backend pick is different — an
experiment with a hybrid model must not clobber the stored default).

Acceptance criteria

  • Migration adds the five columns; schema.rs updated by hand (no diesel CLI).
  • GET /prefs returns the new fields; PUT /prefs partial-update semantics
    apply identically (omitted = unchanged, explicit null = cleared).
  • Each consumer resolves request → pref → default; request always wins.
  • Reel creation does not silently overwrite stored model/backend prefs.
  • default_backend validated against the known backend names; invalid → 400.
  • Endpoint tests cover resolution order for at least model and persona.

Blocked by

  • ImageApi #126 (prefs endpoints surface)
Part of #113 ## What to build Second wave of the prefs surface (#126 built the endpoints over the existing columns; this adds the recurring per-user picks that are currently re-sent on every request). One migration grows `user_ai_prefs`, and the existing `PUT /prefs` / `GET /prefs` surface exposes the new fields with the same partial-update semantics. New columns on `user_ai_prefs` (all nullable — NULL = keep the current per-request/default behavior): - `default_model` — last/explicit choice for insight + chat model (local id or OpenRouter id). - `default_backend` — `"local"` | `"hybrid"`. - `default_persona_id` — active persona for persona chat (currently hard-defaults to `"default"`). - `reel_span` — `"day"` | `"week"` | `"month"`. - `reel_max_segments` — `i32`. Resolution order on every consumer that accepts these per request: **request param → stored pref → current hard default.** The per-request knobs stay per-request — sampling params (temperature/top_p/top_k/min_p), `system_prompt`, `num_ctx`, `max_iterations`, `enable_thinking`, `amend` deliberately do NOT become prefs (ephemeral or per-object). Where the pref is consulted: insight generation (model/backend), persona chat turn (persona), reel creation (span/max_segments). Capture: either an explicit "save as default" flag on the create request, or the PUT /prefs surface (client's choice — the FV settings page owns it); do not silently overwrite a stored pref on every reel request (that's what `capture_prefs` does for the existing columns today and is fine there because those fields were always "last value wins"; the model/backend pick is different — an experiment with a hybrid model must not clobber the stored default). ## Acceptance criteria - [ ] Migration adds the five columns; schema.rs updated by hand (no diesel CLI). - [ ] `GET /prefs` returns the new fields; `PUT /prefs` partial-update semantics apply identically (omitted = unchanged, explicit null = cleared). - [ ] Each consumer resolves request → pref → default; request always wins. - [ ] Reel creation does not silently overwrite stored model/backend prefs. - [ ] `default_backend` validated against the known backend names; invalid → 400. - [ ] Endpoint tests cover resolution order for at least model and persona. ## Blocked by - ImageApi #126 (prefs endpoints surface)
Author
Owner

Implemented on feature/prefs-owner-defaults — PR #129.

Five nullable owner-default columns on user_ai_prefs (default_model, default_backend, default_persona_id, reel_span, reel_max_segments), exposed through the existing GET/PUT /prefs surface with the same three-state partial-update semantics (omitted = unchanged, null = clear, value = set). Resolution order at every consumer: request param → stored pref → hard default — insight generation (standard + agentic), all three chat-turn variants (model/backend/persona), and reel creation (span/max_segments via resolve_reel_params).

Notable: capture_prefs (reel passive mirror) now reads the existing row and carries the AI owner-defaults through unchanged — the upsert overwrites all columns, so the original shape would have wiped the owner defaults on every reel creation. Regression-tested. Validation runs before any write and delegates to the canonical BackendKind::parse.

1463/0 tests (+27 vs master), clippy clean, two-axis subagent review: spec all-conformant, standards zero findings.

Implemented on `feature/prefs-owner-defaults` — **PR #129**. Five nullable owner-default columns on `user_ai_prefs` (`default_model`, `default_backend`, `default_persona_id`, `reel_span`, `reel_max_segments`), exposed through the existing `GET/PUT /prefs` surface with the same three-state partial-update semantics (omitted = unchanged, null = clear, value = set). Resolution order at every consumer: **request param → stored pref → hard default** — insight generation (standard + agentic), all three chat-turn variants (model/backend/persona), and reel creation (span/max_segments via `resolve_reel_params`). Notable: `capture_prefs` (reel passive mirror) now reads the existing row and carries the AI owner-defaults through unchanged — the upsert overwrites all columns, so the original shape would have wiped the owner defaults on every reel creation. Regression-tested. Validation runs before any write and delegates to the canonical `BackendKind::parse`. 1463/0 tests (+27 vs master), clippy clean, two-axis subagent review: spec all-conformant, standards zero findings.
Author
Owner

Cross-client audit (FileViewer-React, subagent, read-only): SAFE-WITH-CAVEATS.

  • Client never calls PUT /prefs and never sends empty strings for model/backend/persona_id/span/max_segments (AsyncStorage clear = key removal → field omitted) → pref fill-in and three-state semantics work as designed; no explicit-null clear risk.
  • No stored pref row = byte-identical behavior for the mobile client.
  • One caveat: the client never sends backend="local" (only explicit "hybrid"), so a stored default_backend=hybrid would silently reroute mobile agentic generation/chat to OpenRouter, and a locally-picked model id sent alongside it would 404 on OpenRouter. If mobile should stay pinned to local, that is a FileViewer-React change (send backend="local" explicitly) — belongs in FVR #33, not this ticket.
  • Stored default_model / default_persona_id / reel_max_segments cause silent-but-benign output changes only (reel_span is inert for this client — it always sends span explicitly).
Cross-client audit (FileViewer-React, subagent, read-only): SAFE-WITH-CAVEATS. - Client never calls PUT /prefs and never sends empty strings for model/backend/persona_id/span/max_segments (AsyncStorage clear = key removal → field omitted) → pref fill-in and three-state semantics work as designed; no explicit-null clear risk. - No stored pref row = byte-identical behavior for the mobile client. - One caveat: the client never sends backend="local" (only explicit "hybrid"), so a stored default_backend=hybrid would silently reroute mobile agentic generation/chat to OpenRouter, and a locally-picked model id sent alongside it would 404 on OpenRouter. If mobile should stay pinned to local, that is a FileViewer-React change (send backend="local" explicitly) — belongs in FVR #33, not this ticket. - Stored default_model / default_persona_id / reel_max_segments cause silent-but-benign output changes only (reel_span is inert for this client — it always sends span explicitly).
cameron added the wayfinder:task label 2026-09-30 00:47:23 +00:00
Sign in to join this conversation.