fix: bootstrap user preferences without invalidating DWH cache

This commit is contained in:
User
2026-07-16 20:32:20 +02:00
parent ccb3cf4aa9
commit 5cacf70a0d
6 changed files with 242 additions and 3 deletions
@@ -0,0 +1,110 @@
# User Preference Bootstrap Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Seed a new principal's private settings from complete legacy settings so a first session can configure and start Pi.
**Architecture:** `buildApp` continues to resolve settings through the principal-bound harness runner. When `preferencesGet()` returns an empty object, it derives the existing effective legacy settings from `SETTINGS_FILE`, persists them once via `preferencesSet()`, and returns that same object. A non-empty private object remains authoritative.
**Tech Stack:** TypeScript, Fastify, Vitest.
## Global Constraints
- Never overwrite a non-empty private preference object.
- Persist only provider, model, thinking, and workspace values; no credentials enter preferences.
- Keep storage failures fail-closed through the existing 503 route contract.
- Follow TDD: observe the regression test fail before adding implementation.
---
### Task 1: Bootstrap legacy settings for an empty private profile
**Files:**
- Modify: `backend/test/routes-settings.test.ts`
- Modify: `backend/src/app.ts`
**Interfaces:**
- Consumes: `ThtRunner.preferencesGet(): Promise<Record<string, unknown>>`, `ThtRunner.preferencesSet(settings): Promise<void>`, `loadSettings(config)`, and `effectiveSettings(config, settings)`.
- Produces: `getSettings(principal): Promise<Settings>` that returns a complete persisted profile for first-time principals.
- [ ] **Step 1: Write the failing regression tests**
```ts
test("GET /settings seeds an empty private profile from complete legacy settings once", async () => {
// Seed SETTINGS_FILE with local-qwen/qwen3.6-35b-a3b/low.
// Make preferencesGet return {} and record preferencesSet calls.
// Assert the first GET returns and persists all four settings, and a second GET does not write again.
});
test("GET /settings keeps a non-empty private profile authoritative", async () => {
// Seed different legacy settings, return an existing private profile,
// and assert no preferencesSet call occurs.
});
```
- [ ] **Step 2: Run the focused test file and verify RED**
Run: `cd backend && npx vitest run test/routes-settings.test.ts`
Expected: the first test fails because the current resolver returns only defaults and never calls `preferencesSet`.
- [ ] **Step 3: Implement the minimal resolver change**
```ts
const stored = await runner.preferencesGet();
if (Object.keys(stored).length === 0) {
const seeded = effectiveSettings(config, loadSettings(config));
await runner.preferencesSet(seeded);
return seeded;
}
return effectiveSettings(config, stored);
```
- [ ] **Step 4: Run focused tests and TypeScript verification**
Run: `cd backend && npx vitest run test/routes-settings.test.ts && npx tsc --noEmit -p .`
Expected: exit 0.
- [ ] **Step 5: Run backend regression suite**
Run: `cd backend && npx vitest run && npm run build`
Expected: exit 0.
### Task 2: Preserve DWH artifact ownership across the optional session-storage field
**Files:**
- Modify: `harness/tests/test_dwh_preprocess_job.py`
- Modify: `harness/tht/jobs/dwh_pipeline.py`
**Interfaces:**
- Consumes: `config_dwh_binding(cfg)` and Pydantic's `Config.model_dump(mode="json")`.
- Produces: a DWH binding whose `config_fingerprint` excludes only `session_storage`.
- [ ] **Step 1: Write the failing regression test**
```python
def test_session_storage_does_not_change_the_dwh_artifact_binding(tmp_path):
assert config_dwh_binding(config()) == config_dwh_binding(config(session_storage={...}))
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `cd harness && .venv/bin/pytest tests/test_dwh_preprocess_job.py::test_session_storage_does_not_change_the_dwh_artifact_binding -q`
Expected: FAIL because the full configuration JSON currently includes `session_storage`.
- [ ] **Step 3: Implement the minimal DWH-only fingerprint**
```python
payload = cfg.model_dump(mode="json")
payload.pop("session_storage", None)
config_fingerprint = fingerprint(json.dumps(payload, separators=(",", ":"), ensure_ascii=False))
```
- [ ] **Step 4: Run focused and complete harness verification**
Run: `cd harness && .venv/bin/pytest tests/test_dwh_preprocess_job.py -q && .venv/bin/pytest -q && .venv/bin/ruff check tht/jobs/dwh_pipeline.py tests/test_dwh_preprocess_job.py`
Expected: exit 0.
@@ -0,0 +1,34 @@
# User preference bootstrap design
## Problem
The user-owned-session branch reads settings only from the current principal's
repository preferences. Existing deployments still have their provider, model,
thinking level, and workspace only in the legacy backend settings JSON file.
For a principal with no private preference record, a new session is therefore
created without a provider or model and Pi is rejected before it can spawn.
## Decision
On the first settings read for a principal whose private preference object is
empty, the backend computes the complete effective legacy settings from
`SETTINGS_FILE` and the configured environment defaults, writes that complete
object to the principal's repository preferences, and returns it.
After this one-time bootstrap, the private preference object is the sole
source for that principal. A non-empty private object is never replaced with
the legacy values. If either reading or writing the private preferences fails,
the request remains fail-closed with the existing 503 response.
The addition of optional session storage must not change DWH artifact
ownership. The DWH binding therefore excludes `session_storage` while retaining
every schema, retrieval, vector, and execution setting in its fingerprint.
## Scope and verification
The change is confined to the backend settings resolver. Vitest coverage must
prove that an empty private profile is seeded exactly once with the complete
legacy settings, and that an existing private profile is neither changed nor
replaced. Harness coverage must also prove that adding session storage leaves
the DWH binding unchanged. Existing session-route coverage continues to prove
that new-session creation consumes the resolved settings.