diff --git a/persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-6-complete.md b/persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-6-complete.md new file mode 100644 index 0000000..ef77efd --- /dev/null +++ b/persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-6-complete.md @@ -0,0 +1,105 @@ +# worldtree-sdk cutover — SLICE-6 COMPLETE (admin: bifrost inspection + admin-events SSE) + +`[2026-07-19]` Slice 6 of 7 of the worldtree-sdk consumer cutover (issue #20; +contract `docs/contracts/worldtree_sdk_cutover.contract.md`). Full House Code +Discipline; both heid panels cleared. Suite **494 green**. The meatiest slice — +SSE stream + admin auth + an event-shape decision. + +## Commits (v0.21.19–.20, on `main`, PUSHED this session) + +- **`de9a5ba`** feat — the two admin routes onto `ratatoskr.wt`, hand-rolled deleted. +- **`bba57e1`** fix — heid-code-review fixups (stale docstring + None-cursor test). + **NO version bump** (docs + test only, SemVer skip rule). +- **`11ae2f0`** fix — heid-bug-hunt fixups (admin-stream + bifrost hardening). + +## What migrated (WEB-only) + +`get_session_bifrost` → `client.admin.sessions.bifrost(id)` (open-world dict verbatim, +any `ApiError` → `SessionApiFailed` default — no new Error-map row). `stream_admin_events` +→ `client.admin.stream_events(last_event_id=…)`. **Both consumed ONLY by `web/server.py`** +(`_session_bifrost_endpoint` + `_admin_events_endpoint`) — the coverage-map's `tui.py` +rows were STALE (the grep found zero TUI callers), so the TUI-deprecation wrinkle was +moot. Corrected the coverage-map rows to `web/server.py`. + +Deleted the hand-rolled `sessions.get_session_bifrost` + `sse_client.stream_admin_events` +(+ ruff-cleaned the orphaned `httpx`/`httpx_sse`/`json`/`AsyncIterator` imports). Retired +`test_sse_client.py` WHOLESALE (its last test was the admin stream; slice-2 had already +removed the turn-stream tests) + `test_sessions.py`'s `TestGetSessionBifrost`. `sessions.py` +is now down to `endpoint_for_plane` + exception classes; `sse_client.py` to `SseId` + +`AdminEvent` + exception classes. + +## Decisions made at TDD (contract § slice-6 notes) + +- **Admin auth moves from a per-call `Authorization` header to the client's `admin_auth`.** + The SDK's `admin.*` routes use `admin_auth` (set via `build_client(admin_key=…)`), NOT a + header. So `_wt_client(client, *, admin_key=None, …)` was extended, and the two web + endpoints pass `admin_key`. The web already guards `if not admin_key: 400`, so the SDK's + pre-HTTP `ConfigurationError` (W-5) is unreachable from the surface. +- **`AdminEvent` re-wrap (chosen over yield-through).** The SDK's `AdminEvent` diverges + from ratatoskr's: `admin_id: int|float` (`nan` for id-less) vs `id: int`; None-able + `type`/`data` vs a dotted-str / `{}`-default dict. The web filter + SSE formatter read + `ev.id`/`ev.type`/`ev.data`. The adapter re-wraps at the boundary — `id = admin_id if + int else 0`, `type = ev.type if isinstance str else ""`, `data = dict if Mapping else {}` + — degrading the open-world None/nan ONCE and keeping the web endpoint + filter + the + `AdminEvent` domain type UNCHANGED (preserves the web surface). **Rejected:** yield SDK + events through + rewire the web filter (heavier churn; scatters the None/nan hardening). + This is implementation-level (reversible, no module-boundary change), decided + autonomously + flagged to the operator with the rejected alternative. + +## The ApiError-not-ConnectFailed gotcha (TDD → integration test) + +First mapped the admin-stream non-200 as `ConnectFailed`. The web INTEGRATION test +(respx mocking a real 500) exposed that the SDK admin stream raises +`ApiError("admin_stream_failed", status=…)` on a non-200 open — the unit fake couldn't +model it. Fixed to `ApiError → SseConnectFailed`. Lesson: a web integration test catches +what the adapter unit fake structurally can't. + +## LIVE SMOKE (:8081, readonly-admin key — INV-CUT-5 / DEC-4 cleared) + +Drove the WEB surface (via `httpx.ASGITransport` over `create_app(client_factory, +admin_key=…)`) against real :8081. The bifrost endpoint returned an admin-authed clean +404 `session_not_bifrost_bound` envelope (auth + route + mapping proven — a 404 not a +401/403 = the admin key authenticated). :8081's admin stream is idle (no heartbeats in +15s raw), so I generated activity: streamed admin events while concurrently creating a +session (`POST /sessions {agent_id: mimir, end_user_id: …}` → 201) and observed the real +`session.created` admin event (id=32/34) re-wrapped cleanly (id int, type str, data dict); +threwaway session cleaned up (DELETE → 204). NOTE: a raw `POST /sessions` needs +`end_user_id` (422 without it). + +## heid-code-review (thread 01KXXYRNNY…) — 3/3 no drift + +Gróa + Regin zero; Hulda "no slice-6 implementation drift." Only minor doc/test looseness: +a stale `_session_bifrost_endpoint` docstring ("overrides the Authorization header" → +corrected to "rides on the client's admin_auth"), and an admin-stream None-cursor test-gap +(added). Hulda's "web endpoints under-tested" was **source-VOIDED by Heid** — those tests +live in `test_web_server.py`, which wasn't in the consult embed (excerpt-elides-tests trap). + +## heid-bug-hunt (thread 01KXXZBW74…) — 4 real findings, all fixed + +The cold spec-free hunt earned its keep: the CR found the admin surface CONFORMANT, but +judging against the general `ConnectFailed` floor + the degrade-never-crash promise it +surfaced 4 hardening gaps: +- **[bug, 3/3] `stream_admin_events` never mapped `ConnectFailed`** — the SDK admin-stream + open DOES raise it (connect-time / auth-resolution; confirmed in SDK source), `stream_turn` + + the bifrost GET both catch it, and this endpoint's OWN `:633` comment claimed it did. + An unmapped ConnectFailed escaped the web gen's `except (Sse*)` → aborted SSE with no + `stream_error`. Now mapped → `SseConnectFailed`. +- **[bug, 2/3] non-str `type` crashed the web filter** — `ev.type or ""` (falsy-only) let a + truthy non-str `type` (123) reach `.startswith` → AttributeError. Now `isinstance`-guarded + (matches admin_id/data). Same container-type class as the slice-5 bug-hunt. +- **[robustness] `dict(bstate)` 500 on a non-mapping bifrost body** — I introduced it in + slice-6 (`JSONResponse(bstate)` → `dict(bstate)`). Now degrades to `{}`. +- **[robustness] transport leak** — `_wt_client` ran before the try/finally in the SSE gen; + a construction failure would leak the httpx transport. Moved inside the try. +Voided (Heid): Regin's `dict(ev.data)` TypeError — the `isinstance(_, Mapping)` guard +already handles it. + +## Next: slice-7 (teardown, the LAST slice) + +Drop the `httpx-sse` dep from `pyproject.toml` (SDK owns SSE parsing — verify nothing else +imports it), retire wire contracts #2/#15, relocate the `AdminEvent`/exception classes if +`sse_client.py`/`sessions.py` end up ~empty, final coverage-map re-anchor, and the **MINOR +bump per DEC-6 (needs operator approval)** publishing the cutover milestone. + +See also [[2026-07-19-worldtree-sdk-cutover-slice-5-complete]] (the container-type +degrade-not-crash lesson) and [[2026-07-19-worldtree-sdk-cutover-slice-4-complete]]. diff --git a/persistent-memory.md b/persistent-memory.md index ba4bf91..186f949 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -46,7 +46,7 @@ upstream API key stays server-side (INV-003). _As of 2026-07-19:_ -**🔨 ACTIVE MIGRATION — worldtree-sdk cutover (issue #20): SLICE-1–5 COMPLETE, slice-6 (admin) next.** +**🔨 ACTIVE MIGRATION — worldtree-sdk cutover (issue #20): SLICE-1–6 COMPLETE, slice-7 (teardown, the LAST) next.** Operator ruled ADOPT (2026-07-18): ratatoskr cuts its CONSUMER client layer over to **worldtree-sdk (Python) 1.0.0**, retiring the hand-rolled httpx wrappers behind a thin `ratatoskr.wt` adapter. Design locked (6 DECs, vor-cross'd, heid-panel-reviewed); contract `docs/contracts/worldtree_sdk_cutover.contract.md`. **SLICE-1+2 @@ -61,6 +61,15 @@ onto `client.me`/`.capabilities`/`.models`/`.characters.*` (all open-world reads **NO new Error-map rows**), rewired `--whoami`/`--characters` (**CLI-only; no web caller**), and DELETED the 6 hand-rolled `sessions.py` wrappers (`endpoint_for_plane`+`get_session_bifrost` [slice-6]+exceptions stay). Full arc → `persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-5-complete.md`. +**SLICE-6 (admin: bifrost inspection + admin-events stream) ✅ DONE** — `de9a5ba` (feat) + `bba57e1` +(CR-fixups, no-bump docs+test) + `11ae2f0` (BH-fixups), v0.21.19–.20; suite **494 green**; both heid gates +cleared. Migrated `get_session_bifrost`→`client.admin.sessions.bifrost` + `stream_admin_events`→`client.admin. +stream_events` (**WEB-only** — coverage-map's tui.py rows were STALE); admin auth moved to the client's +`admin_auth` (`_wt_client(admin_key=…)`); the admin-events adapter **re-wraps** the SDK's `AdminEvent`→ratatoskr's +(nan→0, non-str/None type→"", None/non-mapping data→{}) to keep the web filter + AdminEvent domain type stable; +deleted `sessions.get_session_bifrost` + `sse_client.stream_admin_events`, retired `test_sse_client.py` whole. +**LIVE SMOKE :8081**: web bifrost admin-authed 404 clean envelope + a real `session.created` event (id=34) +re-wrapped end-to-end. Full arc → `persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-6-complete.md`. **KEY ADAPTER FACTS (foot-guns, cumulative for slices 6-7):** SDK reads = **open-world dicts** — presenters MUST degrade not crash, guarded at THREE levels (slice-5 needed all three): **container-type** (a scalar `123` is non-iterable → `for x in 123` TypeError; the `or []` idiom catches null/absent but NOT a truthy non-iterable @@ -71,9 +80,10 @@ failure to `ConnectFailed(status=0)`** (NOT raw httpx) — every adapter caller **caller-semantic exceptions the adapter raises + a `-m` CLI catches must NOT live in the `-m` module** (double- module class-identity split → uncaught traceback; live smoke catches it, unit tests can't); `TurnEvent` `turn_id` ABSENT on text/thinking frames; `consumer_key` is BOUND-create-only; envelope parser prefers nested `detail`. -**NEXT = slice-6** (admin: `admin.sessions.bifrost` + `admin.stream_events`, admin_auth — `get_session_bifrost` -in `sessions.py` + `stream_admin_events` in `sse_client.py`); then slice-7 (teardown: retire contracts #2/#15, -drop `httpx-sse`, MINOR bump per DEC-6 w/ operator approval). Scope: consumer layer ONLY; Bifrost provider +**NEXT = slice-7 (teardown, the LAST slice):** delete any residual hand-rolled paths, drop the `httpx-sse` dep +from `pyproject.toml` (SDK owns SSE parsing now — verify nothing else imports it), retire wire contracts #2/#15, +relocate the `AdminEvent`/exception classes if `sse_client.py`/`sessions.py` end up ~empty, final coverage-map +re-anchor, and the **MINOR bump per DEC-6 (needs operator approval)** publishing the cutover milestone. Scope: consumer layer ONLY; Bifrost provider untouched. Full design → auto-memory `project_worldtree_sdk_cutover`. **✅ RESOLVED — tier3 agents `model`→`role` (scope B) folded into cutover slice-4** (`c62b4ee`, v0.21.13). The @@ -122,14 +132,14 @@ Full record → `persistent-memory.d/2026-07-18-368-silo-test-passed.md`. Siblin (2) R39 Phase-2 **matched-quartets rebuild** (confirmatory, "whenever"); (3) bifrost **snapshot-cursor adoption** (ruled normative, not blocking → `persistent-memory.d/2026-07-16-bifrost-cursor-conformance.md`). -**Substrate / environment:** branch `main` at **v0.21.18** — slice-4 arc `c62b4ee`→`477d98f` + slice-5 arc -`deab762`→`4e20030` + this snapshot **COMMITTED, not-yet-pushed** (tags v0.21.13–.18; push is the operator's -call); slice-1–3 (v0.21.3–.12, `b1fbadd`→`4f92a21`) PUSHED to origin earlier. origin +**Substrate / environment:** branch `main` at **v0.21.20** — slices 1-5 (`b1fbadd`→`4e20030`, v0.21.3–.18) +PUSHED to origin; **slice-6 arc `de9a5ba`→`11ae2f0` (v0.21.19–.20) + this snapshot** committed then PUSHED this +session (tags through v0.21.20). origin `git@gitea.phasefinal.com:vh/ratatoskr.git`. **NEW core dep: `worldtree-sdk==1.0.0`** (gitea PyPI, `[tool.uv.sources]`; `httpx-sse` retires at slice-7). bifrost **`==1.1.4`** / wire v0.7; WT openapi vendored 2.3.0, **conversation-api-spec re-synced to v1.1** (`b4a278c`); -**suite 488 green** (slice-5 added the characters/me/caps adapter tests + heid code-review/bug-hunt fixup -tests, ~offset by the deleted hand-rolled character/me/caps tests). Personal WT on **b128** +**suite 494 green** (slice-6 added the admin adapter tests + heid fixup tests, ~offset by the retired +hand-rolled admin tests + the whole `test_sse_client.py`). Personal WT on **b128** (`http://10.250.50.152:8081`; #368 silo + #364 promotion-hygiene live both instances). The combined **:8392** provider (memory+affect) + **:8765** web are THE surfaces, dev-box BACKGROUND SHELLS — restart via `scratchpad/relaunch_by_pid.py ` (pid via `ss -ltnp | grep `). `env.sh` sets @@ -284,6 +294,8 @@ decision. Captures rationale that won't be obvious from code alone. - `[2026-07-19]` **worldtree-sdk cutover SLICE-5 COMPLETE (characters + me/capabilities/models, `deab762`+`d86d6df`+`4e20030`, v0.21.16–.18, 488 green).** Last consumer reads + transient-character CRUD onto the wt adapter (CLI-only rewire, NO new Error-map rows — all six routes → SessionApiFailed default); live-smoke-proven on :8081/b128; both heid gates cleared — code-review caught the null/element open-world-presenter degrade holes, the cold bug-hunt caught the **container-type layer below** them (guard container-type + element-type + top-level-mapping). Slice-6 (admin) next. → `persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-5-complete.md` +- `[2026-07-19]` **worldtree-sdk cutover SLICE-6 COMPLETE (admin: bifrost inspection + admin-events SSE, `de9a5ba`+`bba57e1`+`11ae2f0`, v0.21.19–.20, 494 green).** The two admin routes onto `client.admin.*` (**WEB-only** — the coverage-map's tui.py rows were stale); admin auth moved from a per-call header to the client's `admin_auth`; the admin-events adapter **re-wraps** the SDK's divergent `AdminEvent`→ratatoskr's (nan/None degraded) to preserve the web surface. Live-smoke-proven (real `session.created` event id=34 re-wrapped end-to-end). Both heid gates cleared — code-review 3/3 no-drift (only a stale docstring + a test-gap), the cold bug-hunt caught 4 real hardening gaps the CR couldn't (ConnectFailed unmapped on the admin stream; non-str type crash; `dict(non-mapping)` bifrost 500; a transport leak). Slice-7 (teardown, LAST) next. → `persistent-memory.d/2026-07-19-worldtree-sdk-cutover-slice-6-complete.md` + - `[2026-07-19]` **wyrd-dev #368 silo-enforcement consult delivered — ratatoskr's store-side silo is CONVENTIONAL (query-time filter); wyrd going STRUCTURAL off the framing.** Answered artifact-only from the provider memory-store code; wyrd folded my foot-guns into their unit-4 contract + committed to one-DB-file-per-campaign + first-class partition columns. OPEN LOOP: I'll eyeball their data model once the contract's cut (they'll ping). → `persistent-memory.d/2026-07-19-wyrd-368-silo-consult-delivered.md` _67 older entries (2026-05-* debug-TUI/web era + the 2026-06-14 → 06-18 Bifrost-provider build / #17+#18 / #295-296 era) archived to archival-memory.md._