# Session KV Store

PostgreSQL-backed key-value storage for session-scoped data that must survive server restarts.

## Purpose

Provides a simple API for tools to persist per-session state without inventing ad-hoc storage mechanisms.

Current consumers:
- **todo** — task plans (`scope='todo'`)
- **scratchpad** — working notes (`scope='scratchpad'`)

Future consumers: recall context, wizard state, user preferences per session.

## Schema

```sql
CREATE TABLE session_store (
    id         SERIAL PRIMARY KEY,
    user_id    TEXT NOT NULL DEFAULT '',
    session_id TEXT NOT NULL,
    scope      TEXT NOT NULL,
    key        TEXT NOT NULL,
    value      TEXT NOT NULL DEFAULT '',
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (user_id, session_id, scope, key)
);
```

**Note on `user_id`:** `NULL` is normalized to an empty string before every operation. This prevents duplicate rows for anonymous sessions because PostgreSQL treats `NULL` as distinct in unique constraints.

## API (`navi.store.KvStore`)

```python
from navi.store import KvStore

store = KvStore("postgresql://...")

await store.get(user_id, session_id, scope, key)       # str | None
await store.set(user_id, session_id, scope, key, value)  # None
await store.get_all(user_id, session_id, scope)        # dict[str, str]
await store.delete(user_id, session_id, scope, key)      # None
await store.clear_scope(user_id, session_id, scope)      # None
```

- `user_id` — nullable for legacy single-user mode.
- `session_id` — the chat session UUID.
- `scope` — namespace: `todo`, `scratchpad`, or any custom string.
- `key` — arbitrary string identifier within the scope.

## Wiring

`KvStore` is created as a singleton in `navi/api/deps.py` (`get_kv_store()`) and injected into `build_default_registries()`, which passes it to `TodoTool` and `ScratchpadTool` on startup.
