---
name: Storage settings, error log, feedback, implementation rules
type: architecture-map-page
slice: 05-core-storage-hotkey-build
last_verified: 2026/05/09
---

# Storage settings, error log, feedback, implementation rules

> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Settings and miscellaneous CRUD

**Plain English summary.** The smaller persistence surfaces. Settings is a key-value store. Error log captures structured errors. Feedback captures HITL thumbs and corrections so the LLM prompt builder can inject few-shot examples. Implementation rules persist the if-then automation rules from Phase 12.

## Settings (key-value)

### Functions — `crates/storage/src/database.rs:754`

```rust
pub async fn set_setting(pool: &SqlitePool, key: &str, value: &str) -> Result<()>;
pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result<Option<String>>;
```

`set_setting` is `INSERT OR REPLACE INTO settings`, so it is idempotent. `get_setting` returns `Ok(None)` for a missing key (not `Err`).

### Known keys

The `settings` table is keyed `TEXT PRIMARY KEY, value TEXT NOT NULL`. The conventions are:

- **`lumotia_preferences`** — JSON blob. The frontend's full preferences object. Read at boot, written on every preference change. This is the bridge between the frontend's reactive preferences store (slice 1) and persistence (slice 5). See [Slice 1 preferences store](../01-frontend/README.md) and [Slice 2 preferences command](../02-tauri-runtime/README.md).
- **`active_profile_id`** — string. The profile id the user is actively transcribing into.
- Other keys are added ad-hoc per feature; no enumeration in code today.

## Error log

### Schema (migration v1)

```sql
CREATE TABLE error_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp TEXT NOT NULL DEFAULT (datetime('now')),
    context TEXT NOT NULL,
    error_code TEXT,
    message TEXT NOT NULL,
    metadata TEXT
);
CREATE INDEX idx_error_log_context ON error_log(context);
```

### Public type — `crates/storage/src/database.rs:1149`

```rust
pub struct ErrorLogRow {
    pub id: i64,
    pub timestamp: String,
    pub context: String,
    pub error_code: Option<String>,
    pub message: String,
    pub metadata: Option<String>,
}
```

### Functions

- **`log_error(pool, context, error_code, message, metadata)`** — `crates/storage/src/database.rs:1126`. Append-only insert. Called from anywhere a meaningful structured error happens (audio capture failure, model download failure, transcription engine error, etc).
- **`prune_error_log(pool, keep_days) -> Result<u64>`** — `crates/storage/src/database.rs:1166`. `DELETE FROM error_log WHERE timestamp < datetime('now', '-{keep_days} days')`. Called once on app startup. 90 days is the default (slice 2). Returns the affected row count for logging.
- **`list_recent_errors(pool, limit)`** — `crates/storage/src/database.rs:1178`. Used by the diagnostic-report bundler in Settings → About to capture recent errors into the export.

## Feedback (HITL)

Phase 2 of the feature-complete roadmap. Captures thumbs + corrections on AI-generated output so the prompt builder can inject recent examples as few-shot exemplars. Storage-only here; the prompt-conditioning logic lives in `lumotia-llm` (slice 4).

### Schema (migration v10)

```sql
CREATE TABLE feedback (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    target_type TEXT NOT NULL,
    target_id TEXT,
    rating INTEGER NOT NULL,  -- -1 thumbs down, 0 correction (neutral), +1 thumbs up
    original_text TEXT,
    corrected_text TEXT,
    context_json TEXT,
    profile_id TEXT NOT NULL DEFAULT '<DEFAULT_PROFILE_ID>',
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_feedback_target_type_rating ON feedback(target_type, rating);
CREATE INDEX idx_feedback_profile ON feedback(profile_id);
```

### Public types — `crates/storage/src/database.rs:1210, 1241, 1253`

```rust
pub enum FeedbackTargetType { MicroStep, TaskExtraction, Cleanup }
impl FeedbackTargetType {
    pub fn as_str(self) -> &'static str;       // "microstep" | "task_extraction" | "cleanup"
    pub fn parse(s: &str) -> Option<Self>;     // (named to avoid the FromStr trait)
}

pub struct RecordFeedbackParams {
    pub target_type: FeedbackTargetType,
    pub target_id: Option<String>,
    pub rating: i8,            // -1, 0, or +1
    pub original_text: Option<String>,
    pub corrected_text: Option<String>,
    pub context_json: Option<String>,
    pub profile_id: Option<String>,  // None falls back to DEFAULT_PROFILE_ID
}

pub struct FeedbackRow {
    pub id: i64,
    pub target_type: String,
    pub target_id: Option<String>,
    pub rating: i64,
    pub original_text: Option<String>,
    pub corrected_text: Option<String>,
    pub context_json: Option<String>,
    pub profile_id: String,
    pub created_at: String,
}
```

### Functions

- **`record_feedback(pool, params) -> Result<i64>`** — `crates/storage/src/database.rs:1265`. Validates `rating ∈ -1..=1`; falls `profile_id` back to `DEFAULT_PROFILE_ID`; returns the inserted row id.
- **`list_feedback_examples(pool, target_type, limit, min_rating, profile_id)`** — `crates/storage/src/database.rs:1303`. Returns the most recent rows scoped to the active profile, ordered `created_at DESC, id DESC` (recency wins ties). `min_rating` filters out thumbs-down examples when the caller wants positive reinforcement only; pass `-1` to include everything.

The "scoped to the active profile" behaviour exists so feedback does not cross profiles — corrections you made while transcribing for Work do not leak into Personal.

## Implementation rules (Phase 12)

If-then automation rules. Each rule has a trigger kind and value (eg "phrase contains 'remind me'"), a JSON-encoded list of actions, and a `last_fired_key` so a rule does not double-fire on the same trigger.

### Schema (migration v12)

```sql
CREATE TABLE implementation_rules (
    id TEXT PRIMARY KEY,
    enabled INTEGER NOT NULL,
    trigger_kind TEXT NOT NULL,
    trigger_value TEXT NOT NULL,
    actions_json TEXT NOT NULL,
    last_fired_key TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_implementation_rules_enabled_trigger
    ON implementation_rules(enabled, trigger_kind);
```

### Public type — `crates/storage/src/database.rs:845`

```rust
pub struct ImplementationRuleRow {
    pub id: String,
    pub enabled: bool,
    pub trigger_kind: String,
    pub trigger_value: String,
    pub actions_json: String,
    pub last_fired_key: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}
```

### Functions

- **`insert_implementation_rule(pool, id, enabled, trigger_kind, trigger_value, actions_json, last_fired_key)`** — `crates/storage/src/database.rs:636`.
- **`list_implementation_rules(pool)`** — `crates/storage/src/database.rs:667`.
- **`get_implementation_rule(pool, id)`** — `crates/storage/src/database.rs:680`.
- **`set_implementation_rule_enabled(pool, id, enabled)`** — `crates/storage/src/database.rs:697`. Bumps `updated_at`.
- **`mark_implementation_rule_fired(pool, id, fired_key)`** — `crates/storage/src/database.rs:720`. Sets `last_fired_key = ?` so the dispatcher knows not to fire again on the same trigger.
- **`delete_implementation_rule(pool, id)`** — `crates/storage/src/database.rs:743`.

## Watch-outs

- **`lumotia_preferences` is a JSON blob in TEXT.** Schema-on-read. A typo in the frontend store can land in the database and silently round-trip through future loads. A periodic schema-validation pass would catch it.
- **`error_log` has no severity column.** Every error is created equal. Worth a future migration if the diagnostic-report bundler needs to highlight critical errors.
- **`feedback.profile_id` defaults to `DEFAULT_PROFILE_ID` at the column level.** Combined with the `record_feedback` fallback, this means feedback can never end up profile-orphaned. Good.
- **`implementation_rules.actions_json` is unschematised TEXT.** The dispatcher logic owns the parsing. A future schema for actions would catch malformed JSON at the storage layer.
- **`set_setting` is unbounded.** A user with a huge `lumotia_preferences` blob (which has happened in dogfooding) drags every read of that key. Today the preferences blob is small (KB-scale).

## See also

- [Schema and migrations](storage-schema-and-migrations.md) — v1, v10, v12.
- [Storage overview](storage-overview.md)
- [Slice 4 LLM prompt builder](../04-llm-formatting-mcp/README.md) — caller of `list_feedback_examples`.
- [Slice 2 preferences command](../02-tauri-runtime/README.md) — caller of `set_setting("lumotia_preferences", ...)`.
