@@ 1,6 1,6 @@
# lethe-server
-**Status:** Plan
+**Status:** Execute (verify pending)
**Module:** `sourcecraft.dev/bigbes/lethe`
**Branch:** master
**Worktree:** none
@@ 410,5 410,15 @@ Greenfield — no compat surface. Wire format is the only forward-compat concern
- Each phase from 2 onward must remove the dep it adopts from `internal/deps/deps.go`; Phase 9 deletes the file.
- README's Caddy/Authelia snippets use `auth.example.com` placeholders; replace with phoebe-specific values when the production deploy lands (out of scope for this task).
- **Phase 4 finding (steward unwind gap)**: `steward.Manager.Init` returns on the first failing `CallInit` and does **not** iterate back over previously-initialized assets to call `Destroy`. The canary test `TestStewardUnwindsOnInitFailure` (in `internal/platform/health/steward_unwind_test.go`) is intentionally red on master to document this. **Phase 9 main must compensate**: track each component as it init's and, on `Init` error, walk the list in reverse calling `Destroy` directly on each (don't try `mgr.Stop`/`mgr.Destroy` — those panic unless the manager has reached Started). Once Phase 9 lands the explicit unwind, either delete the canary test or convert it to assert the new compensating behavior.
-- **Phase 5 consistency fix (folded into commit `1b215bc` via amend)**: chi's default 404/405 handlers wrote text/plain, violating the invariant "errors leaving any HTTP handler are rendered as RFC 7807". Added explicit `chi.Router.NotFound`/`MethodNotAllowed` handlers that call `apierror.Render` with `NOT_FOUND` / `METHOD_NOT_ALLOWED` codes. Added `METHOD_NOT_ALLOWED → 405` entry to the apierror code-status map. Added two regression tests (`TestRouter_NotFoundReturnsProblemJSON`, `TestRouter_MethodNotAllowedReturnsProblemJSON`).
+- **Phase 5 consistency fix (folded into commit `3c45b48` via amend)**: chi's default 404/405 handlers wrote text/plain, violating the invariant "errors leaving any HTTP handler are rendered as RFC 7807". Added explicit `chi.Router.NotFound`/`MethodNotAllowed` handlers that call `apierror.Render` with `NOT_FOUND` / `METHOD_NOT_ALLOWED` codes. Added `METHOD_NOT_ALLOWED → 405` entry to the apierror code-status map. Added two regression tests.
+- **Phase 7**: added `UNSUPPORTED_MEDIA_TYPE → 415` entry to `apierror.codeStatus` (ingest handler enforces `application/x-ndjson` Content-Type). Repository simulates DB-down by closing the underlying `*sql.DB` (cleaner than service-faking, mirrors real driver-disconnect failure). Service-level FK test omitted because the schema makes it unreachable through the Service path (parent session is upserted in the same chunk); equivalent Repository-level test pins the wrap-and-classify code path.
+- **Phase 8**: introduced `JSONText` (sql.Scanner wrapper) for nullable TEXT-JSON columns — `json.RawMessage` cannot Scan NULL directly. External JSON shape unchanged. If Phase #3 (search) wants the same scan-safety, factor up to `internal/pkg/sqljson`.
+- **Phase 9**: refactored `Server.Start` to `net.Listen` first then `http.Serve(listener)` plus added `Server.Addr()` so `:0` binds report the kernel-assigned port — enables the e2e smoke to bind to a random port without races. `cmd/lethe/main.go` uses a `run() int` shell so tests can drive it. Steward unwind canary `internal/platform/health/steward_unwind_test.go` deleted; `main.go`'s reverse-order `unwindOnError` compensator is now the production guarantee. Bootstrap stderr slog handler installed before any asset registration so the unwind path always has a logger.
+
+### Final state
+
+- All 9 phases committed (`4ca03be` → `53221c9`).
+- `go test ./... -race -count=1` fully green; no allowed-red exception.
+- `go vet`, `gofmt -l`, `go mod tidy`, `golangci-lint run ./...` all clean.
+- Manual smoke: lethe binary built and ran against `config.example.yaml`; `/healthz`, `/readyz`, `/metrics`, unauthed `/api/v1/sessions` (401), authed `/api/v1/sessions` with `Remote-User: bigbes` (200) all behaved as designed; SIGTERM triggered clean shutdown via the steward graph.
- **Phase 3 → Phase 7 contract pin**: `INSERT … ON CONFLICT … DO UPDATE` fires the UPDATE trigger on SQLite (verified by `TestUpsertFiresUpdateTriggerAndKeepsFTSCoherent`). Regular FTS5 (not contentless / external content) was chosen so `WHERE owner = ?` works on the FTS table without a join — accepted the storage cost (`content` duplicated in real table + FTS shadow). Composite key order is `(owner, tool, host, session_id[, turn_id])` everywhere; ingest INSERT/UPDATE/ON CONFLICT clauses must match. `started_at`/`ended_at`/`source_file` are `NOT NULL` — ingest derives `started_at` from `MIN(turn.timestamp)` when `SessionMeta.StartedAt` is absent.