Commit Graph

257 Commits

Author SHA1 Message Date
vh 73478e7628 memory: heid-code-review triaged+fixed (eb0767e); both heid gates complete 2026-08-01 19:48:28 -07:00
vh eb0767e96d fix: heid-code-review fixups — donut voiced-interview slices 2+3
Triaged the heid-code-review panel (3 arms; reconciled against 56dce00 — three
findings already closed by the bug-hunt, and the two firewalled lenses converged
independently on the same three defects). Fixed the real one + contract precision.

Code:
- kb_bridge: no-hit sentinel (F7, the sharpest solo). The consult prompt asks Mimir
  to emit NO_CORPUS_MATCH when nothing is relevant; pin_kb_context drops any extract
  containing it -> [], so a non-empty "no results found" answer is never pinned as the
  character's own memory. Live-proven: a grounding query pins (5.6s), a nonsense query
  -> [] (0.7s); the sentinel is artificial so `in` can't false-positive on a passage.

Contract (the CODE is correct; the spec had drifted/undercounted — kept canonical):
- DEC-8: the custom "donut" voice was registered EARLY (verified live), so mapping
  ratatoskr:donut -> "donut" is right; "preset now" was stale. A live gateway read
  INVERTED the 3-arm remedy (reverting to a preset would have been the regression).
- FN tts_synthesize: declared the `url` swap-seam param (F3); voice membership is
  gateway-enforced not client-asserted (F2); the postcondition is a container-level
  RIFF/WAVE check, not a 16-bit-PCM fmt parse.
- FN tts_endpoint: pad is BROWSER-SENT per DEC-7, not a server PAD lookup (F5);
  documented the 413 text cap.
- FN pin_kb_context: documented the sentinel + the session-delete hygiene.

Tests (real coverage gaps):
- the read_note prompt test asserts the distinguishing "do NOT call read_note" phrase,
  not the bare token an inverted prompt would also carry (#8 mutation-blind).
- extract-bound asserts the literal 2000, not the impl constant it slices by (#9).
- route roster asserts /api/tts + /api/sessions/{id}/messages (#10 undercount).
- new server test: a degraded KB consult ([]) still streams the turn to done (F9).

Accepted (not fixed): caller-supplied agent_id (LAN/no-auth debug-tool trust model);
no DEC-5 concurrency test (asyncio.Lock is trivially correct — a test would test
asyncio, not our code). 545 green.
2026-08-01 19:48:06 -07:00
vh c437388682 memory: heid-bug-hunt triaged+fixed (56dce00); code-review reply pending 2026-08-01 19:40:12 -07:00
vh 56dce00b2b fix: heid-bug-hunt fixups — donut voiced-interview slices 2+3
Triaged the heid-bug-hunt panel (Gróa+Hulda+Regin+Kimi, 11 distinct findings).
Fixed the real ones; the 3-arm "memory_context unverifiable" alarm was refuted
(tests + live SDK verify), and caller-supplied agent_id is accepted under the
LAN/no-auth debug-tool trust model (documented, not fixed).

Constructible crashes (were uncaught HTTP 500s from wire input):
- _tts_endpoint: coerce non-str / unhashable agent_id -> None before the voice-map
  lookup (matches the submit path's guard); an unhashable {} / [] TypeError'd -> 500.
- PadState.from_obj: catch ArithmeticError — float() of a huge-int JSON literal
  raises OverflowError, absent from the except tuple -> 500; now a neutral read.
- _submit_turn_endpoint: require a non-blank STR content — a truthy non-str crashed
  pin_kb_context's question.strip() mid-stream instead of a deterministic 400.
  pin_kb_context also isinstance-guards the question defensively.

Robustness:
- kb_bridge: delete the throwaway Mimir consult session (SDK sessions.delete) on
  success/error/timeout via a caller-owned holder so cleanup survives a mid-stream
  timeout — consults no longer accumulate server-side under the fixed partition.
- _stream_turn_endpoint: emit a ": keepalive" SSE comment BEFORE the (<=20s) KB
  consult so a reverse proxy / EventSource doesn't drop the silent connection into
  a false "WIRE LOST" before the turn starts.
- _tts_endpoint: cap text at 8000 chars (413) before the process-global lock;
  gateway timeout 120s->60s — one huge/stalled body can't starve all TTS.
- tts_synthesize: validate the WAVE form tag (bytes 8:12), not just the RIFF magic.
- index.html: revoke the audio blob URL in cancelTts (removeAttribute+load fires
  neither ended nor error, so the src's own revoke never ran -> per-turn blob leak).

TDD: +11 tests (543 green). Live-smoked on :8765: all five constructible adversarial
inputs now return 200/413/400, never 500.
2026-08-01 19:38:37 -07:00
vh ef76a03bcd memory: donut voiced-interview slices 2+3 built + committed + live-proven; heid gates pending triage 2026-08-01 19:02:42 -07:00
vh 71689142bc feat: Donut voiced-interview slice-3 — retire-ready KB-recall bridge
Grounds the interview character in the ingested corpus while she stays in-voice.
Tier-3 agents are tool-less by design in v1, so this is the consumer-side
workaround (DEC-6, worldtree-dev ruling): per opted-in interview turn, ratatoskr
consults Mimir out-of-band, extracts the passages, and pins them as memory_context
on the character's turn. She frames the pinned corpus as her own memory.

- src/ratatoskr/kb_bridge.py (new, RETIRE-READY): pin_kb_context — THE single seam
  (INV-KB-1). Allowlist-gated (INV-KB-4: ratatoskr:donut only), hard-timeout-bounded,
  degrades to [] on any failure/timeout/empty (INV-KB-3, never raises; CancelledError
  propagates). Imports nothing from the SDK-adapter / TTS core. aclosing() closes the
  SDK stream deterministically on the DoneEvent break.
- wt.stream_turn: memory_context passthrough (defaults None — inert for every other
  caller and for the bridge's own retirement). Seam-review catch: the contract's
  original touch list undercounted wt.py by one file (recorded in the contract).
- web/server.py: TurnHandle.agent_id + the single pin_kb_context call-site on the
  turn path; the browser now sends agent_id so the allowlist can gate.
- web/static/index.html: the turn POST carries agent_id.

Consult prompt tuned live: "search_library EXACTLY ONCE, no read_note" converges
Mimir in ~3-15s (the softer "do one search" phrasing looped past 25s on
conversational questions).

TDD: 12 kb_bridge unit tests + wt memory_context forwarding + 2 server wiring tests
(531 green). Live-smoked on :8081/b128: pin_kb_context grounds in the DCC corpus
(real excerpts, <20s) and Donut answers in-voice; degrades cleanly on a slow consult.
KNOWN LIMIT surfaced (not a bridge defect): DCC's fiction index is weak (failed
backfill, a worldtree-dev item), so grounding is opportunistic — the bridge's real
payoff is a corpus the model does not already know.

Per docs/contracts/donut_voiced_interview.contract.md (slice 3 of 3).
2026-08-01 18:55:58 -07:00
vh 1883214663 feat: Donut voiced-interview slice-2 — auto-TTS via the Zonos gateway
Adds affect-modulated voice to the web console: the completed assistant
response is spoken on SSE `done`, emotion-modulated by the live PAD the persona
pane already shows (DEC-7 — voice as affect OBSERVABILITY, not chat-app TTS).

- src/ratatoskr/tts.py (new): Zonos-gateway client + PAD→emotion-dial mapping.
  tts_synthesize POSTs {input, voice, response_format:"wav", **dials}; wav-only
  (DEC-3 — mp3/opus silently return mislabeled PCM). pad_to_dials is total
  (None/NaN/out-of-range → valid dials, never raises). TtsUnavailable on any
  gateway failure; the single swap seam if we ever move off Zonos.
- web/server.py: POST /api/tts proxy (DEC-4/INV-TTS-1 — the gateway host never
  reaches the browser). Per-character voice map (DEC-8: ratatoskr:donut→donut),
  serialize lock (DEC-5 — shared 3090), 503 degrade (INV-TTS-4).
- web/static/index.html: 🔊 toggle (opt-in, localStorage, default off,
  INV-TTS-2), speak-on-done, AbortController cancel-on-new-turn (INV-TTS-3),
  hidden <audio> sink; PAD read off the pane's current snapshot.
- web/entrypoint.py: RATATOSKR_TTS_URL override (the swap seam).

TDD: 17 tts unit tests + 5 endpoint tests (516 green). Live-smoked end-to-end
against the Zonos gateway (:8890): Donut voice + affect dials → 44.1kHz wav,
missing-text→400, neutral→200, gateway-fail→503.

Per docs/contracts/donut_voiced_interview.contract.md (slice 2 of 3).
2026-08-01 18:38:14 -07:00
vh 3e912b13b3 feat: Donut voiced-interview slice-1 (contract + persona + define) + /snapshot
Slice 1 of the auto-TTS/voiced-KB-character build (operator ask "add auto-tts
to the web gui"): the donut_voiced_interview contract (validated), the Princess
Donut persona (corpus-grounded from a Mimir DCC pull), and ratatoskr:donut
defined on :8081 (server-side; in the picker). Slices 2 (Zonos auto-TTS) + 3
(retire-ready KB-bridge) are TO BUILD.

Snapshot captures the full build state + design (Zonos gateway :8890, voice
"donut" registered, affect-driven emotion dials; the worldtree-dev-ruled
consumer-side retrieval + memory_context pinning bridge, retire-ready) for the
post-clear resume, plus the arcs since v0.22.0 (SDK 1.1.2 repin, bifrost 1.1.5,
canonical sync, release-only versioning, the Sindra saga + local-index
schema-burial foot-gun, the Mimir #382 reference-consumer finding). Handoff at
/tmp/ratatoskr-dev-handoff.md. Release-only cadence: no tag.
2026-08-01 18:17:52 -07:00
vh 14bbc2b48e docs(canonical): add client-spec-v2 pin + sync worldtree conversation-api canonicals
Operator decision A (client-spec tracking): add a first-class pin for
docs/conversation-api-client-spec-v2.md (v2.1, sha 1ff1d73a — the
client-facing spec our #371 parity pass shaped), vendored as a consumer
copy, tolerate_drift=true (prose reference; OpenAPI+SSE stay the gates).

canonical_sync is whole-manifest (no per-pin scope), so it also brought
two independently-drifted pins current:
- OpenAPI hard-gate 36148179 (2.3.0) -> 1c801547. Additive-only within
  the FROZEN v1 wire (Worldtree #326) — no code/SDK breakage; the SDK
  targets 2.3.0 and still works against the newer additive surface.
- conversation-api-spec.md (full prose) -> f6e964ec (v1.3+).

canonical_drift now exits 0. FOLLOW-UP (non-urgent): the OpenAPI moved,
so a coverage-map re-audit against the new surface would surface any new
in-scope I/O points worth consuming (ratatoskr v1 = full WT I/O coverage).
Release-only cadence: no tag.
2026-08-01 16:58:39 -07:00
vh 0058020272 fix(deps): repin worldtree-sdk 1.1.1→1.1.2 (closes the SSE turn-stream traversal)
1.1.2 is the same-day fix for the residual we caught in 1.1.1: the inline
turn-stream URL construction (turn_stream.py:185) now uses
quote(session_id, safe="") — the _build_path fix had missed that parallel
site. Verified present (turn_stream.py:192). Built byte-exact from
python-v1.1.2 (4f5e30f), infra-ops acceptance-verified; provenance
otherwise unchanged from 1.1.1 (openapi 2.3.0, same frozen wire), drop-in.
Suite 494 green — carried over from 1.1.1 unchanged. Release-only cadence:
no tag. Supersedes d3a36f7's known-residual note (now closed upstream).
2026-08-01 00:52:57 -07:00
vh d3a36f7008 fix(deps): repin worldtree-sdk 1.0.0→1.1.1 (path-encoding correctness fix)
worldtree-sdk 1.1.1 fixes the _build_path encoder (quote safe="" — a
caller-shaped `/`/`../` in a path param no longer traverses the URL).
Our 1.0.0 carried the bug and we DO pass caller-shaped params (web
hands request.path_params session_id/agent_id to the SDK; tier3 CLI
takes agent_id as an operator arg) — auth-rejected, but a real client-
correctness bug. Same frozen wire as 1.0.0 (drop-in); suite 494 green.
Also brings additive define_or_reuse + set_persona_state_raw (our
parity #6) + a 64 KiB error-body alloc cap.

KNOWN RESIDUAL (flagged to wtsdk-dev): 1.1.1's fix patched _build_path
(request.py) but MISSED the parallel inline URL construction in
turn_stream.py:185 (`/sessions/{quote(session_id)}/messages`, still bare
quote, default safe="/"). No upstream session_id format validation (only
a non-empty-str type check at :153). So our highest-traffic path — the
SSE turn stream, which the web console feeds a caller-shaped session_id —
remains traversable until wtsdk patches it. Release-only cadence: no tag.
2026-08-01 00:33:57 -07:00
vh 3ef3a5ef83 chore(deps): repin bifrost 1.1.4→1.1.5 + v0.22.1
Adopt bifrost 1.1.5 (reference-impl posture — take the dep's canonical
hardening). 1.1.5 is a clean patch on the frozen v0.6 wire (no schema /
handshake change):

- fix: gate ALL optional store verbs → clean `unsupported_capability`
  (not 500), extending 1.1.4's maintenance-verb hasattr-backstop to the
  full optional-verb set. Directly improves our provider's error
  semantics on any unimplemented optional verb.
- feat: v0.6 memory verb-floor conformance harness (test infra).
- two docs clarifications (no runtime impact).

Verified: uv lock + sync resolved 1.1.5 from the gitea index; suite
494 green on 1.1.5; the :8392 combined provider restarted onto 1.1.5
(clean uvicorn boot, stores preserved). Patch bump v0.22.0→v0.22.1.
v0.22.1
2026-07-19 18:01:21 -07:00
vh 5775ce2210 memory: /snapshot — worldtree-sdk cutover COMPLETE (all 7 slices, v0.22.0)
Slice-7 teardown landed (ec68b1f); the whole #20 cutover is done. Collapsed
the now-complete cutover in-flight block to a COMPLETE summary + foot-gun
carry-forward, added the slice-7 Recent-decisions entry + detail file, and
flagged the two operator-gated post-ship actions (push + althing announce).
Archival drain deferred (file ~33 over soft cap; standing-posture entries
interleave the settled ones — a dedicated pass, not a milestone-checkpoint cut).
2026-07-19 13:47:26 -07:00
vh ec68b1f3a5 feat(#20): worldtree-sdk cutover teardown (slice-7) + v0.22.0
The last slice of the consumer-layer cutover. Teardown only — zero
runtime-logic change; the 494-green suite is the regression gate.

- Drop `httpx-sse` from pyproject + lockfile: slice-6 deleted its last
  user, nothing imports `httpx_sse`, the SDK owns SSE parsing now.
- Module boundary (operator decision): KEEP `sessions.py` + `sse_client.py`
  as pure caller-semantic type/exception homes (no rename, no fold —
  A3 was blocked by the `AgentNotAvailable` name collision + `wt.py`
  would mis-home `endpoint_for_plane`). Docstrings updated to stop
  claiming "client"; the `AdminEvent`/`SseId`/exception homes stay put
  (resolves the slice-6 deferred-home item).
- Retire wire contracts #2 (sessions) + #15 (tier3): DEC-1 phase-2 —
  normative authority already transferred to the cutover contract; the
  code they specified is gone, so the files are deleted. #1 (SSE event
  vocab) and `first_message` stay (ratatoskr-owned, not retired).
- Final coverage-map re-anchor: tools/list_sessions re-homed to `wt.py`;
  the Last-Event-ID SSE-resume sub-gap CLOSED (folded into `stream_turn`
  auto-resume); Surface-2 SSE parsing re-anchored to the SDK.
- Stale doc-rot fix: the cli.py transport comment no longer calls
  `seed_preset_first_message` "not-yet-migrated" (it rides `wt`).
- v0.22.0 (minor, DEC-6, operator-approved): publishes the full 6-slice
  cutover milestone.
v0.22.0
2026-07-19 13:41:34 -07:00
vh 8274ed2d89 memory: /snapshot — worldtree-sdk cutover slice-6 complete (de9a5ba→11ae2f0)
Slice-6 (admin: bifrost inspection + admin-events SSE) done through the full House
Code Discipline, v0.21.19–.20, suite 494 green, live-proven (real session.created
event re-wrapped end-to-end on :8081), both heid gates cleared. Current state advanced
to slice-7 (teardown, LAST) next; Recent-decisions index entry + detail file added;
substrate at v0.21.20. Consumer client layer now fully cut over (6/7 slices).

persistent-memory.md stays ~345 lines (over the ~300 soft cap): dominated by the
non-archivable Current state block + <30-day July entries (guarded), so archival can't
reach the 250 target — left as-is per the stop-where-the-guards-stop rule.
2026-07-19 13:16:58 -07:00
vh 11ae2f056e fix(#20): heid-bug-hunt fixups — admin-stream + bifrost hardening (slice-6)
Cold spec-free panel (Gróa + Hulda + Regin, source-verified by Heid): the adapter's
core re-wrap is sound, but 4 real hardening gaps the conformance CR couldn't see —
all in failure-path normalization + open-world degrade, judged against the general
ConnectFailed floor + the degrade-never-crash promise. All fixed:

- [bug, 3/3] `stream_admin_events` never mapped `ConnectFailed` — the SDK admin-stream
  open raises it on a connect-time / auth-resolution failure (the general transport
  floor; confirmed in the SDK source), and `stream_turn` + the bifrost GET both catch
  it, and this endpoint's OWN comment claimed it did. An unmapped ConnectFailed escaped
  the web gen's `except (Sse*)` and aborted the SSE with no `stream_error`. Now mapped
  → `SseConnectFailed`, mirroring stream_turn.
- [bug, 2/3] non-str `type` crashed the web filter — the re-wrap used `ev.type or ""`
  (falsy-only), so a truthy non-str `type` (123, a list) reached `.startswith` →
  AttributeError. Now `ev.type if isinstance(ev.type, str) else ""` (matches the
  admin_id/data isinstance guards — same container-type class as slice-5).
- [robustness] `_session_bifrost_endpoint` did `dict(bstate)` on the open-world 200
  body — a non-mapping (list/scalar) → TypeError/500. Now degrades to `{}` (I introduced
  this in slice-6 by changing `JSONResponse(bstate)` → `dict(bstate)`).
- [robustness] `_admin_events_endpoint.gen` allocated the transport + built `_wt_client`
  BEFORE the try/finally — a construction failure would leak the httpx transport. Moved
  `_wt_client` inside the try so the finally always closes it.

Voided (Heid): Regin's `dict(ev.data)` TypeError — the `isinstance(_, Mapping)` guard
already routes non-mappings to `{}` before `dict()`.

Added adapter tests (ConnectFailed→SseConnectFailed; non-str type→"") + a web test
(non-mapping bifrost body → 200 {}). Suite 494 green; my code ruff-clean (13 E501/F841
in test_web_server.py are PRE-EXISTING, HEAD-identical, untouched); mypy clean on wt.py.
Live smoke re-run clean (real session.created event re-wrapped; bifrost 404 envelope).
Patch bump 0.21.19 → 0.21.20.
v0.21.20
2026-07-19 13:13:44 -07:00
vh bba57e1b39 fix(#20): heid-code-review fixups — stale docstring + None-cursor test (slice-6)
Panel (Gróa + Hulda + Regin): 3/3 no drift — the admin adapter honors the contract
(route map, re-wrap/degrade, error-map ORDER, admin_auth-on-client, INV-CUT-1).
Only minor doc/test looseness, both fixed:

- Stale docstring: `_session_bifrost_endpoint` still said "the wrapper overrides the
  Authorization header with it" — corrected to "rides on the wt client's admin_auth"
  (slice-6 moved admin auth off the per-call header; line 79 already said the new way).
- Test-gap: the admin-stream ConnectionDropped test only exercised the cursor-set case;
  added the connect-time None-cursor case (ConnectionDropped(None) → last_seen_sse_id
  None) to back the map's "both cursor shapes" claim.

Not acted on: `admin_key`→`admin_auth` unit assertion (the SDK's use of admin_auth is
SDK-internal/private — out of scope per "assess use, not definitions"; the LIVE SMOKE
already proved the wiring end-to-end). Hulda's "web endpoints under-tested" flag was
source-VOIDED by Heid: those endpoints ARE covered in test_web_server.py, which wasn't
in the consult embed (excerpt-elides-tests trap).

Suite 491 green; ruff clean. Docs + test only — no version bump (SemVer skip rule).
2026-07-19 12:58:10 -07:00
vh de9a5baf45 feat(#20): admin (bifrost inspection + admin-events stream) onto the wt adapter (slice-6)
Slice-6 of the worldtree-sdk cutover: migrate the two admin routes off the
hand-rolled paths onto the `ratatoskr.wt` adapter over `client.admin.*`, and delete
the retired code. Both are web-only (the coverage-map's `tui.py` rows were stale —
corrected to `web/server.py`).

Adapter (`wt.py`): `get_session_bifrost` → `client.admin.sessions.bifrost` (open-world
dict verbatim, any error → SessionApiFailed default); `stream_admin_events` →
`client.admin.stream_events`, re-wrapping the SDK's `AdminEvent` → ratatoskr's at the
boundary.

Decisions (contract § slice-6 notes):
- Admin auth moves from a per-call `Authorization` header override to the client's
  `admin_auth` (`_wt_client(admin_key=…)`, extended this slice) — the SDK's admin.*
  routes use the provider, not a header.
- `AdminEvent` re-wrap (chosen over yield-through): the SDK's `admin_id`(nan)/None-able
  `type`/`data` diverge from ratatoskr's `id`/`type`/`data` that the web filter reads;
  re-wrapping (nan→0, None→""/{}) degrades the open-world None/nan ONCE at the adapter
  and keeps the web endpoint + `_admin_event_matches_web` + the `AdminEvent` domain type
  unchanged (preserves the web surface). Rejected: yield SDK events + rewire the web
  filter (heavier churn, scattered hardening).
- Admin-stream error map: a NON-200 open raises `ApiError("admin_stream_failed")`
  (NOT `ConnectFailed`) → SseConnectFailed; `ConnectionDropped` (connect-time OR
  mid-stream/resumable-EOF) → SseConnectionDropped. The web integration test caught the
  ApiError-not-ConnectFailed gotcha the unit fake couldn't.

Web (`web/server.py`): both admin endpoints build the wt client with admin_key and call
`wt.*`; the bifrost endpoint gains ConnectFailed→502 handling (cutover foot-gun); the
admin-events endpoint closes the injected transport (INV-CUT-1), never the wt client.

Deleted the hand-rolled `sessions.get_session_bifrost` + `sse_client.stream_admin_events`
(+ orphaned httpx/httpx_sse/json/AsyncIterator imports); the ratatoskr `AdminEvent`
dataclass stays in `sse_client.py` (re-wrap target, imported by wt + web) until slice-7.
Retired `test_sse_client.py` entirely (its last test was the admin stream) and the
`test_sessions.py` `TestGetSessionBifrost`; added the slice-6 adapter tests.

LIVE SMOKE (:8081, readonly-admin key) — INV-CUT-5 / DEC-4 cleared: the web bifrost
endpoint returned an admin-authed clean 404 envelope (auth + route + mapping proven);
a real `session.created` admin event (id=32) re-wrapped cleanly on live wire (driven by
a session-create, throwaway session cleaned up).

Suite 490 green; ruff clean; mypy net-improved on web/server.py (16→12 pre-existing, no
new). Patch bump 0.21.18 → 0.21.19 (the cutover MINOR is deferred to slice-7, DEC-6).
v0.21.19
2026-07-19 12:48:04 -07:00
vh 5bc39a092e memory: /snapshot — worldtree-sdk cutover slice-5 complete (deab762→4e20030)
Slice-5 (characters + me/capabilities/models) done through the full House Code
Discipline, tags v0.21.16–.18, suite 488 green, live-smoke-proven on :8081/b128,
both heid gates cleared. Current state / in-flight advanced to slice-6 (admin) next;
Recent-decisions index entry + detail file added; substrate at v0.21.18.

persistent-memory.md stays ~333 lines (over the ~300 soft cap): the length is
dominated by the non-archivable Current state / in-flight block plus <30-day July
entries (guarded), so archival can't reach the 250 target — left as-is per the
stop-where-the-guards-stop rule.
2026-07-19 11:34:10 -07:00
vh 4e20030229 fix(#20): heid-bug-hunt fixups — CLI open-world container-type hardening (slice-5)
Panel (Gróa + Hulda + Regin, source-verified by Heid): adapter/route-map/
ConnectFailed-at-call-sites sound against the declared invariants; 4 real
robustness findings, all in the CLI open-world presenter/probe paths — the
container-type layer BELOW the null/element holes the code-review already fixed.

Fixed (findings 1-3):
- `_format_whoami` (`cli.py`): a non-iterable `scopes`/`allowed_roles` scalar
  (`{"scopes": 123}`) made `x or []` yield `123` → `for s in 123` TypeError. New
  `_display_seq` helper degrades any non-list (scalar / bare string / null / absent)
  to empty; applied to both `scopes` and `allowed_roles`.
- `_characters_probe` (`cli.py`): same class on the model catalog `items` (`{"items":
  123}`) — now guards `models` is a Mapping and `items` is a list before iterating.
- `_characters_probe`: the top-level open-world reads `created` / `state` are now
  `isinstance(_, Mapping)`-guarded before any `.get` — a non-mapping SDK passthrough
  (`created=[...]`) aborts cleanly (exit 20) / renders `pad=None` instead of an
  AttributeError.

Accepted (finding 4, documented in contract § slice-5 notes): the `--characters`
probe leaks its transient character on a mid-lifecycle failure. PRE-EXISTING (the
retired probe had the identical linear no-`finally` structure — cutover did not
worsen it), TTL-bounded, one-shot diagnostic; a `try/finally` would swallow a
happy-path delete-failure (delete is both teardown and a tested step). Gróa + Heid
concur accept is defensible.

Dismissed (finding 5): Hulda flagged `sessions.py` dropping `get_me`/etc. as a
caller-contract break — it is the intended DEC-3 no-backwards-compat migration (all
in-repo callers rewired same-diff); Heid labels it intended-surface-change.

Added CLI tests for the three hardened paths (scalar scopes/roles; scalar items +
non-mapping state; non-mapping create abort). Suite 488 green; ruff clean; live
smoke re-run clean (identical happy-path output). Patch bump 0.21.17 → 0.21.18.
v0.21.18
2026-07-19 11:29:10 -07:00
vh d86d6df147 fix(#20): heid-code-review fixups — CLI presenter degrade-not-crash (slice-5)
Panel: Gróa + Regin returned zero (adapter/route-map/error-map faithful);
Hulda flagged two source-confirmed open-world-presenter crash holes — the same
class the slice-4 bug-hunt found in the agents presenters. Both fixed:

- `_format_whoami` scopes (`cli.py`): `', '.join(me.get('scopes', []))` crashes on
  a present-null `scopes` (`.get(k, [])` returns None, not the default) or a
  non-string element. Now `', '.join(str(s) for s in (me.get('scopes') or []))` —
  matching the `allowed_roles` hardening on the same function. The contract names
  `_format_whoami` as the degrade-not-crash exemplar (contract:144-146); the cited
  exemplar had an un-hardened line.
- `_characters_probe` model items (`cli.py`): the slice-5 `or []` guarded the
  list-level null but not each entry — `[None]` / `["x"]` / `[{"name":123}]` would
  raise. Now guards each item is a dict and str-coerces `name` (element-level
  completion of the list-level guard).

Hulda #3 (live-smoke not in the reviewed file set) → accept: the smoke WAS run and
is recorded in deab762 + coverage-map (artifact-only review couldn't see it).

Added CLI tests for both hardened paths (present-null/non-string scopes; malformed
model items). Suite 485 green; ruff clean; live smoke re-run clean (identical
happy-path output). Patch bump 0.21.16 → 0.21.17.
v0.21.17
2026-07-19 11:11:12 -07:00
vh deab7627eb feat(#20): characters + me/capabilities/models onto the wt adapter (slice-5)
Slice-5 of the worldtree-sdk cutover: migrate the remaining consumer READS +
transient-character CRUD off the hand-rolled httpx wrappers onto the
`ratatoskr.wt` adapter over the SDK, and delete the retired path.

Adapter (`wt.py`): add `get_me` / `get_capabilities` / `list_character_models`
/ `create_character` / `get_character_state` / `delete_character` over
`client.me` / `client.capabilities` / `client.models` / `client.characters.*`.
All six are open-world reads/acks returned verbatim; none carries a
discriminated SDK error, so each maps any `ApiError` → the `SessionApiFailed`
default (INV-CUT-2) — exact parity with the retired path. No new Error-map rows.

Decisions (contract § slice-5 notes): `create_character` omits `state` when None
(SDK-idiomatic inline literal, server-equivalent to the retired explicit null);
`delete_character` returns the SDK's open ACK verbatim (`-> Mapping|None`, not
normalized to None).

CLI rewire (`cli.py`): `--whoami` (me + capabilities) and `--characters`
(models → create → state → delete) build a `wt.build_client` over the injected
probe transport and catch `wt.SessionApiFailed` + `ConnectFailed`. Open-world
degrade-not-crash carried (cumulative cutover foot-gun): `_characters_probe`
reads `items` null-safe and extracts `character_id` defensively (clean abort,
no hard-index KeyError); `_format_whoami` widened to `Mapping`.

Deleted the six hand-rolled `sessions.py` wrappers (net -5 mypy no-any-return);
`endpoint_for_plane` + `get_session_bifrost` (slice-6) + the exception classes
stay. Retired the corresponding `test_sessions.py` classes; added the slice-5
adapter tests + a CLI malformed-create-abort test.

LIVE SMOKE (:8081, b128) — INV-CUT-5 / DEC-4 cleared: `--whoami` rendered real
identity + capabilities; `--characters` drove the full lifecycle end-to-end
(char-rp catalog → created char_8c00006e… → PAD read-back → deleted).

Suite 483 green; ruff clean; mypy at the 2 pre-existing baseline errors.
Patch bump 0.21.15 → 0.21.16 (the cutover MINOR is deferred to slice-7, DEC-6).
v0.21.16
2026-07-19 11:00:41 -07:00
vh 4f74645a00 memory: /snapshot — worldtree-sdk cutover slice-4 complete (c62b4ee→477d98f) 2026-07-19 10:24:35 -07:00
vh 477d98f52e fix(#20): heid-bug-hunt fixups — open-world presenter degrade-not-crash (slice-4)
Panel (Gróa+Hulda+Regin, 5/5/5, no false positives) confirmed two 3/3 crash
sites where open-world dict reads violate the declared "degrade, never crash the
presenter" invariant — the wt adapter tests + the live smoke used full server
dicts, so partial/drifted wire responses were never exercised:

- FIX (tier3.py _run_define/_run_patch): the CLI hard-indexed the open-world
  define/patch dicts (`info["agent_id"]` / `["role"]` / `["agent_name"]`), so a
  partial 2xx → KeyError escaping main()'s exit matrix as a raw traceback (exit 1);
  and `make_description(info.get("system_prompt", ""))` fed None to .splitlines()
  on a present-but-null field → AttributeError. Now reads via `_str_field` (absent/
  null/non-str → default), degrades role to '?', indexes only a well-formed identity,
  and maps a no-usable-agent_id 2xx to [api_failed] exit 20 (controlled, not a crash).
- FIX (web/server.py _agents_endpoint): the upstream dedup hard-indexed each item
  (`{a["agent_id"] for a in upstream}` + `_as_dict`), so a malformed item (`[{}]`,
  `["str"]`, `{"name":…}`, non-str agent_id) or a non-list envelope → 500 before the
  local fallback merged. Now filters to well-formed mappings first; a non-list
  upstream degrades to the local-only list.
- FIX (wt.py _error_field_from_body): type-check the parsed `field` is a str (the
  exception surface is `field: str | None`, the CLI prints it) — restores the retired
  hand-rolled `_extract_error_field` isinstance guard.

Held (triaged, no change): the 429→Tier3QuotaExceeded / bare-404→Tier3AgentNotFound
maps are ungated-by-error_code BY CONTRACT DESIGN (§ Error map route+status rows; the
SDK's ApiError floor drops Retry-After, so retry_after=0 is canonical) — the arms
flagged them spec-free; Heid's source-check confirmed intended. Dual-keying define's
429 for full row consistency is an available tightening (contract amendment), surfaced
not applied. The persona-endpoint SessionApiFailed gap the arms also caught was
already closed in the prior code-review fixup (aed9429).

Suite 475 green (+5).
v0.21.15
2026-07-19 10:18:36 -07:00
vh aed942972f fix(#20): heid-code-review fixups — persona-endpoint SessionApiFailed parity (slice-4)
Panel (Gróa+Hulda+Regin) returned zero adapter / error-map / model→role drift;
three actionable items triaged as genuine adds:

- FIX: `_persona_state_endpoint` now catches `wt.SessionApiFailed` and returns the
  `session_api_failed` envelope with the upstream status, for parity with
  `_agents_endpoint` / session-create / admin (2/3 arms flagged it; it was the lone
  sibling letting an unmatched upstream ApiError escape as a raw 500). Confirmed
  NOT a slice-4 regression — the pre-cutover persona endpoint had the same latent
  gap — but closed here since the endpoint's error surface is already being hardened
  (it gained the ConnectFailed catch this slice).
- TESTS: dual-key NEGATIVE rows — a wrong error_code at the same status defaults to
  SessionApiFailed for `define_agent` (403, 422) and `patch_agent` (422); plus the
  flat-`field` body-parse shape for `_error_field_from_body` (only the nested
  detail.field form was exercised). Closes the assertion-symmetry gap with the
  persona route's existing negative test.
- AMEND: contract slice-4 notes document the intentional client-side `":" in
  agent_id` PRE on patch/delete (a Tier-3 id is always <user>:<name>, ADR-0019).

Suite 470 green (+5).
v0.21.14
2026-07-19 10:13:11 -07:00
vh c62b4eecb3 feat(#20): agents/tier3 family onto the wt adapter + model→role fold (slice-4)
Cut ratatoskr's consumer agent-lifecycle routes over to worldtree-sdk
(issue #20 slice-4). Five routes now flow through `ratatoskr.wt` over the
SDK's `client.agents.*`, returning open-world dicts and mapping the SDK's
undiscriminated `ApiError` floor by route+(status,error_code) per INV-CUT-2:

- `list_agents`      → `agents.list`
- `get_persona_state`→ `agents.persona_state` (404 persona_not_configured /
                        404 agent_not_available / 403 auth_scope_denied)
- `define_agent`     → `agents.define` (429→Tier3QuotaExceeded(retry_after=0),
                        403→Tier3UserIdUnsupported, 422 layer_deferred→…)
- `patch_agent`      → `agents.patch`  (404→Tier3AgentNotFound, 422 field_not_mutable)
- `delete_agent`     → `agents.delete` (404→Tier3AgentNotFound; NOT hide-existence)

Rewired call-sites: the `python -m ratatoskr.tier3` CLI (define/patch/delete)
and the web `_agents_endpoint` / `_persona_state_endpoint`, both catching the
SDK's `ConnectFailed` transport-failure normalization. Deleted the hand-rolled
paths: `sessions.list_agents` / `get_persona_state` / `AgentInfo`, and
`tier3.define/patch/delete_agent` / `Tier3AgentInfo` / parse+extract helpers.

model→role fold (scope B): the define/patch response echoes `role` (spec 1.2 /
b128), read off the open-world dict; `LocalAgentEntry.model`→`.role`,
local-index schema v1→2 (old index discarded, no-backwards-compat).

The Tier-3 caller-semantic exceptions move to `sessions.py`: running the CLI
as `__main__` while `wt` imports `ratatoskr.tier3` bound two copies of each
exception class, so a raised `Tier3AgentNotFound` escaped the CLI's `except`
as an uncaught traceback. Homing them in `sessions` (never `__main__`) makes
the class identity single. The live smoke — not the unit tests, which call
`main()` in-process — caught this.

Error-map rows + slice-4 notes added to the cutover contract; coverage-map
re-anchored. LIVE-SMOKE on personal :8081 (b128): define(thoughtful-character)
→ patch → list(6 agents) → persona_state(→PersonaNotConfigured mapped) →
delete → index empty; non-existent-id patch via `-m` → [agent_not_found]
exit 20. Suite 465 green.
v0.21.13
2026-07-19 09:58:01 -07:00
vh 4f92a21cb9 memory: /snapshot — worldtree-sdk cutover slice-3 complete (ca9a339+fc256bb) 2026-07-19 09:20:18 -07:00
vh fc256bbaa4 fix(#20): heid-bug-hunt fixups — probe ConnectFailed + adapter finite-PAD (slice-3)
The slice-3 heid-bug-hunt panel (3/3) caught a real regression the cutover
introduced, plus a chokepoint-invariant gap:

- ConnectFailed escaped both rewired CLI probes. When --set-persona-pad and
  --seed-first-message moved off raw httpx onto the wt adapter, transport failures
  changed class: the SDK normalizes any pre-response transport error to
  worldtree_sdk.ConnectFailed (request.py), a WorldtreeError (not ApiError), so it
  passed the adapter unmapped AND the probes' httpx-only except tuples → an uncaught
  traceback instead of the graceful [network_error] exit 21. _amain (slice-2) already
  handled it; the probes lagged. Fix: add ConnectFailed to both probe except tuples
  (mirrors _amain). Live-verified at a refused host → [network_error] exit 21.

- Finite-PAD enforced only at the CLI, not the adapter chokepoint. wt.set_persona_state
  delegated finiteness to the caller (documented), so a direct/non-CLI caller passing
  nan/inf got a raw SDK ConfigurationError. Fix: assert finiteness in the adapter
  precondition (consistent with its other precondition asserts) so the invariant holds
  at the chokepoint in ratatoskr's own terms; the CLI pre-check stays for the friendly
  usage error.

Triaged-and-declined (all correct per the panel + Heid's source-check): the deleted
sessions.py exports (intended no-shim cutover, zero un-migrated importers), the
session["session_id"] index (accept-known-risk, matches --new), and Regin's "web
indefinite block" (refuted — the seed is asyncio.wait_for-bounded). The concurrent
heid-code-review panel returned zero drift, no code change.

TDD: 3 RED tests (both probes' ConnectFailed → exit 21; adapter nan/inf/-inf →
AssertionError, never reaches the SDK) → GREEN. Suite 469; ruff clean; mypy no new errors.
v0.21.12
2026-07-19 09:13:47 -07:00
vh ca9a339050 feat(#20): persona + authored-history + first-message onto the wt adapter (slice-3)
Slice-3 of the worldtree-sdk cutover: migrate the session persona-state write,
the #347 authored-history write, and get_session_messages onto ratatoskr.wt, and
route the first-message preset seed through the adapter. Retire the last
hand-rolled sessions.py paths the --seed-first-message probe kept alive
(create_session + SessionInfo, set_persona_state, write_authored_history,
get_session_messages, _bifrost_error_from).

- wt.set_persona_state (SDK PadState) — the CLI passes three finite PAD axes; the
  SDK owns the {"pad": {...}} wire (#317). No route-specific error row → the
  SessionApiFailed default.
- wt.write_authored_history (SDK write_history) — v1 author=assistant; 404 →
  AuthoredHistoryUnavailable (hide-existence; the route is the discriminator,
  never the body); every other ApiError → the default. Drops the unused
  author/effects/claimed_original_at params (no caller uses them).
- first_message.seed_preset_first_message now takes a WorldtreeClient and routes
  through wt.write_authored_history; the best-effort invariants (INV-001..004,
  never-raise/never-block/one-write/zero-worldtree-source-import) are unchanged.
  Tests drive a fake WorldtreeClient — the wire is the SDK's to prove.
- CLI --set-persona-pad / --seed-first-message + the _amain and web create-path
  first-message seeds rewired onto the adapter. --set-persona-pad pre-validates
  PAD finiteness (clean usage_error, never a crash on the SDK ConfigurationError).

LIVE-SMOKE on personal :8081 (b128, INV-CUT-5): --seed-first-message → 201
(seq=0, phase=seeded) → read-back verbatim; --set-persona-pad → 204; the --new
create-path preset seed observed routing through the adapter. All slice-3 route
families proven end-to-end through the ratatoskr surface.

docs/coverage-map.md + first_message.contract.md re-anchored onto the adapter;
the slice-2 create/stream/cancel rows re-anchored too (they still named the
deleted sse_client/sessions symbols).

Suite 466 green; mypy no new errors (baseline 22 → 20 in the touched modules);
ruff clean. INV-CUT-1..5 held. Bifrost provider planes untouched.
v0.21.11
2026-07-19 08:53:45 -07:00
vh 0b23334c68 memory: /snapshot — worldtree-sdk cutover slice-1+2 complete + pushed (aba1730)
Slice-2 (sessions/turn) done end-to-end through the House Code Discipline; both heid
gates triaged+fixed. Captures the KEY ADAPTER FACTS foot-guns for slices 3-7 (open-world
dicts, body-derived turn_id, ConnectFailed(0) transport normalization, consumer_key
bound-only, nested-detail error_code parsing) + the two-lens gate value proof. Slice-3
(persona/authored-history) next.
2026-07-19 08:23:23 -07:00
vh aba17304bd fix(#20): heid-bug-hunt fixups — cutover edge-path robustness (slice-2)
Triaged the heid-bug-hunt panel (Gróa 8 / Hulda 6 / Regin 6; Heid source-checked +
refuted 2 Regin FPs). The lens pulled real weight — confirmed bugs the conformance
review structurally could not see.

Confirmed bugs fixed:
- SessionRetired (410) stream-open maps to wt.SessionApiFailed, but neither cli
  _run_turn nor web gen() caught it → crash / dropped SSE stream. Both presenters now
  catch it (cli → exit 20; web → labeled `event: error`). (Gróa#2) + cli regression test.
- cli forwarded consumer_key unconditionally; an UNBOUND create with the env key set
  would auth as the Bifrost consumer, not the default bearer. Guarded in the adapter
  (consumer_key only when bifrost is set). (Gróa#4 + Regin#4) + test.
- cli _turn_id_from_sse_id crashed on a None/non-str sse_id (web guarded, cli didn't)
  → now tolerant. (Gróa#1 + Hulda#2) + test.
- _cancel_and_log broadened to `except Exception` — after the code-review's ApiError
  default, a cancel could raise SessionApiFailed it didn't catch, breaking INV-009
  (never-raise). (Gróa#3, Heid-endorsed over Regin's refuted mechanism).

Open-world degrade-not-crash (contract posture): render hardened — float duration_ms
(_format_duration_safe), non-mapping usage/snapshot guards, unknown event type
degrades instead of asserting (Gróa#5/#6 + Hulda#3); web _event_to_browser_payload
guards a non-mapping `raw` (Hulda#4); web _wt_client bearer extraction is now
case-insensitive + whitespace-robust (Hulda#5 + Regin#5). + render-degrade test.

Rejected (verified): Regin#1 (httpx IS caught), Regin#2 (wtsdk IS worldtree_sdk),
Regin#3 (sse_client.AgentNotAvailable IS caught by SseConnectFailed) — all FPs;
Hulda#1 (deleted funcs "break callers") — grep-verified zero callers pre-deletion.
Accepted-known-risk: lenient sse_id parse, CancelFailed status=0, async-gen aclose
(pre-existing pattern, not a cutover regression).

Suite 497 green; wt/cli/web ruff + wt mypy clean. Patch.
v0.21.10
2026-07-19 07:09:26 -07:00
vh 74d41eb559 fix(#20): heid-code-review fixups — INV-CUT-2 completeness on cancel/stream (slice-2)
Triaged the heid-code-review panel (Gróa + Hulda substantive, Regin zero=weak).

Adopted (genuine adds):
- cancel_turn + stream_turn gain a defensive `except ApiError -> SessionApiFailed`
  default after their discriminated branches. INV-CUT-2 ("every ApiError is mapped;
  default SessionApiFailed") now holds STRUCTURALLY on those routes, not by coupling
  to the SDK's internal guarantee that it maps them to discriminated types. + tests.
- get_session_tools error-path test (symmetric with messages).
- Contract § Error map amended: added the stream ProtocolError rows
  (Malformed*/TurnIdFlip -> ratatoskr same-named), clarified the cancel row (the SDK
  RAISES the typed races -> ratatoskr exceptions, only a 200/cancelled=False is a
  CancelResult; caller surface stays exception-based per DEC-2), and noted the
  ApiError default holds on stream+cancel too.

Rejected (category-5, wrong-grounding) — 2/3 arms flagged create's bound-502 as
"should gate on error_code like list's 422+cursor_invalid". Verified against the SDK
parser (not in the arms' file set): the bound-502 body is
{"error_code":"bifrost_handshake_failed","detail":{"bifrost_error":...}}, and the
SDK's envelope parser PREFERS the nested detail (which lacks error_code), so
ApiError.error_code resolves to "unknown" — gating would REGRESS handshake detection
(the cli/web integration tests caught it). INV-002 also makes the handshake the sole
bound-502 cause. Kept the any-bound-502 mapping; documented WHY in code + contract.

Accepted-as-is: create_session -> Mapping annotation (intentional open-world
passthrough, already documented in the route-map note; category 3).

Suite 493 green; wt.py mypy + ruff clean. Patch.
v0.21.9
2026-07-19 06:57:52 -07:00
vh 59602fe3ff refactor(#20): delete the orphaned hand-rolled turn-stream paths (slice-2, part 2b-iii)
DEC-4 live smoke PASSED first (personal :8081, b127/b128): create → streamed turn
that rendered (worker_phase/text/text_boundary/done with usage) → SIGINT cancel that
round-tripped to a cancelled terminal. With both CLI + web on the adapter, the
hand-rolled turn-stream family is fully orphaned — deleting it now.

- sse_client.py (714 → 224): removed stream_turn / reconnect_turn /
  stream_turn_resilient / cancel_turn + the Event dataclasses (Text/Done/…/Event
  union) + CancelResult + the SSE parse helpers (_iter_events / _envelope_for_type /
  _parse_sse_id / _eager_failure_fields / _INT_RE). KEPT: the caller-semantic
  exceptions (the adapter raises them, DEC-2), SseId, AdminEvent, stream_admin_events
  (slice-6 admin surface).
- sessions.py (677 → 608): removed list_sessions + get_session_tools (no surface
  users) + SessionPage. KEPT: create_session / get_session_messages (the
  --seed-first-message probe still uses them, slice-3) + all exceptions + SessionInfo.
- tests: test_sse_client pruned to TestStreamAdminEvents; test_sessions dropped the
  list_sessions + get_session_tools classes. The deleted turn-stream behavior is now
  covered by test_wt.py + the CLI/web integration tests + the live smoke.

Suite 490 green (570 − 80 deleted turn-stream tests); ruff clean on all touched
files; no new mypy errors. Patch (internal cleanup; behavior preserved).
v0.21.8
2026-07-19 06:35:04 -07:00
vh 5c595b862d feat(#20): rewire the web turn surface onto the wt adapter (slice-2, part 2b-ii)
The Starlette endpoints (create / stream / cancel / tools / messages) now go through
ratatoskr.wt over the worldtree-sdk; the browser contract is preserved. This is the
last consumer of the hand-rolled turn-stream family — after this, stream_turn* /
cancel_turn are orphaned and get deleted in part 2b-iii (with the live smoke).

- _wt_client wraps a client_factory transport as the adapter's WorldtreeClient
  (INV-CUT-1), reading base_url + bearer off the transport (a no-auth test transport
  falls back to a placeholder key). The hand-rolled endpoints (persona / agents /
  admin / bifrost) keep using the raw transport until their slices.
- _event_to_browser_payload derives the browser payload from the SDK's `raw` (the
  wire body) minus the redundant `type`, plus the composite `sse_id` string — the
  SAME shape the old dataclasses produced, so the presentation fixture + browser JS
  are unchanged; the browser event_type is the wire `type`, not the SDK class name.
- The stream endpoint captures the upstream cancel target from the composite sse_id
  (the SDK's top-level turn_id is body-derived, absent on text frames); create reads
  the SDK's open create dict; cancel reads CancelResult.cancelled and surfaces a
  generic 502 for CancelFailed (the SDK abstracts the upstream cancel HTTP status).
- test_web_presentation_contract builds SDK events via build_event; two cancel tests
  adopt the SDK's (status, error_code) race pairs + the 502.

Suite 570 green; web/server.py + presentation test ruff-clean, mypy unchanged
(same pre-existing errors). Patch (internal; browser contract preserved).
v0.21.7
2026-07-19 06:22:13 -07:00
vh e3a10ad80e feat(#20): rewire the CLI turn path onto the wt adapter (slice-2, part 2b-i)
The --send turn path (_amain create + _run_turn stream + _cancel_and_log) now goes
through ratatoskr.wt over the worldtree-sdk; external CLI behavior (output, exit
codes) is preserved. No hand-rolled path is deleted yet — web/server.py still uses
them (part 2b-ii), so the deletions + live smoke come after web is rewired.

- _amain builds one WorldtreeClient via wt.build_client over a ratatoskr-owned
  transport (INV-CUT-1); create → wt.create_session (reads the SDK's open create
  dict); the transport keeps the default bearer so the not-yet-migrated hand-rolled
  seed_preset_first_message (slice-3) still authenticates.
- _run_turn drives wt.stream_turn and consumes SDK TurnEvents; the mid-stream cancel
  target is parsed from the composite sse_id ("{turn}:{seq}") — the SDK's top-level
  turn_id is the body field and is absent on text/thinking frames.
- CliPresenterState.render consumes the SDK TurnEvent union with None-hardening on
  the now-optional fields (usage degrades to "(n/a)" rather than crashing).
- The SDK normalizes a pre-response transport failure to ConnectFailed(status=0);
  _amain (network → exit 21) and _cancel_and_log (swallow, INV-009) catch it.
- build_client gains max_reconnects (SDK default 5; tests pass 0 to surface drops
  immediately). test_cli: SDK-event factories keep the render-test bodies intact;
  client constructions wrap in build_client; cancel-race mocks carry the SDK's
  (status, error_code) pair.

Suite 570 green; cli.py + wt.py mypy + ruff clean (the pre-existing send_content
arg-type note is unchanged). Patch (internal; external CLI behavior preserved).
v0.21.6
2026-07-19 06:07:50 -07:00
vh b907a7b8a5 feat(#20): stream + cancel adapter routes complete the wt surface (slice-2, part 2a)
Completes the adapter's session/turn surface, still additive and non-breaking (no
surface rewired, no hand-rolled path deleted — the cli/web rewire + deletions +
live smoke are part 2b).

- stream_turn: drives the SDK's resilient stream (auto-resume absorbs the old
  reconnect_turn) and yields SDK TurnEvents, re-wrapping the stream's TERMINAL SDK
  errors into ratatoskr's caller-semantic exceptions per DEC-2 (SessionRetired →
  SessionApiFailed; AgentNotAvailable / TurnLaunchUnavailable / MalformedSse* /
  TurnIdFlip → ratatoskr's same-named types; ConnectionDropped → SseConnectionDropped;
  ConnectFailed / terminal ResumeError → SseConnectFailed). The presenter keeps
  catching ratatoskr types (part 2b aligns the except clauses).
- cancel_turn: returns the SDK CancelResult (a 200 cancelled=False is the benign
  late-cancel race, B-CAN-3), mapping the typed cancel races onto ratatoskr's
  CancelTurnNotFound / CancelAlreadyCompleted / CancelFailed.
- SseConnectionDropped.last_seen_sse_id widened to SseId | str | None: the SDK's
  resume cursor is a raw composite-id str (the cutover's target form); the
  hand-rolled path's SseId stays accepted until it is deleted. The one live reader
  (stream_turn_resilient) generalizes cleanly — a str cursor is already the id.

Suite 570 green (555 + 15); wt.py + sse_client.py mypy + ruff clean. Patch.
v0.21.5
2026-07-19 00:31:16 -07:00
vh bb158ae47d feat(#20): sessions read/create adapter routes — ratatoskr.wt (slice-2, part 1)
First slice-2 increment: the presenter-independent sessions routes, additive and
non-breaking (no surface rewired, no hand-rolled path deleted yet — the cli/web
rewire + deletions + live smoke land in part 2).

- create_session / list_sessions / get_session_messages / get_session_tools over
  WorldtreeClient.sessions.*, each building the request from ratatoskr's domain
  params and mapping the SDK's ApiError floor by ROUTE (INV-CUT-2): create 404 →
  AgentNotFound, bound 502 → BifrostHandshakeFailed, list 422 cursor_invalid →
  InvalidCursor, else the SessionApiFailed default.
- Open-world reads returned VERBATIM (parity-pass posture): the routes return the
  SDK's open dicts, not ratatoskr's typed SessionInfo/SessionPage — those typed
  result shapes retire when the presenters are rewired to read mappings (adopt the
  dep's canonical open-world way, reference-impl doctrine).
- Transitional: wt imports the caller-semantic exceptions + BifrostBinding from the
  retiring sessions module (one-way, no cycle); they relocate into the adapter as
  their call-sites are rewired.
- Cancel + the resilient turn STREAM are deferred to part 2, where they wire into
  the async presenter loop and are validated by the live smoke.

Suite 555 green (541 + 14); mypy strict + ruff clean. Patch (internal, additive).
v0.21.4
2026-07-19 00:19:33 -07:00
vh 12cd8642fa feat(#20): worldtree-sdk adapter foundation — ratatoskr.wt (slice-1)
Slice-1 of the SDK cutover (docs/contracts/worldtree_sdk_cutover.contract.md):
the adapter chokepoint onto worldtree-sdk 1.0.0, unit-tested but not yet wired
to any surface (that is slice-2).

- build_client(base_url, *, api_key, admin_key=None, transport) constructs the
  single WorldtreeClient over a ratatoskr-owned injected httpx.AsyncClient.
  INV-CUT-1: the SDK is given the transport (_owns_client=False) and never closes
  it — proven by a test asserting aclose() leaves ratatoskr's transport open.
- translate_error implements the § Error map DEFAULT: SDK ApiError → the adapter's
  SessionApiFailed (carrying the SDK's parsed status/error_code/body); every
  discriminated WorldtreeError subclass passes through by identity. Route-specific
  rows land at their call-sites in later slices (the route is the discriminator).
- SessionApiFailed gains error_code vs the retiring sessions.py copy (extends it
  per the contract error-map row); the two coexist transiently and reconcile in
  slice-2 (DEC-4 incremental cutover — nothing wires the adapter this slice, so
  they never meet at runtime).

Deletes no hand-rolled path, so DEC-4's live-smoke bar does not apply yet.
Suite 541 green (534 + 7 new); mypy + ruff clean. Patch (internal foundation;
the cutover's minor bump is DEC-6 at slice-7 ship).
v0.21.3
2026-07-18 23:59:33 -07:00
vh b7f4bc5e7c memory: /snapshot — slice-1 dep integrated + DEC-5 verified (29c4fda), adapter next
Re-snapshot for fresh context. Cutover slice-1 half-landed: worldtree-sdk==1.0.0
integrated + DEC-5 install-verified + committed (29c4fda), suite 534 green; the
ratatoskr.wt adapter (auth/transport split, error-map default) is the next step.
Substrate: unpushed cutover chain (e45640c contract, snapshot, 29c4fda dep) noted;
origin still at b1fbadd. Handoff aimed at the adapter. Index ~19 over soft cap;
archival deferred (guard-protected recents leave little to move — next run.)
2026-07-18 23:27:40 -07:00
vh 29c4fda12e build(deps): integrate worldtree-sdk==1.0.0 (DEC-5 verified) — #20 slice-1
Add the worldtree-sdk (Python) 1.0.0 consumer client to core deps + the gitea uv
source (reuses the existing bifrost gitea-index auth). DEC-5 precondition met: uv
resolves + installs it from the registry; WorldtreeClient constructs with an
injected httpx.AsyncClient and _owns_client=False (INV-CUT-1 transport ownership
confirmed live). httpx-sse retained until slice-7 teardown. Full suite 534 green —
the dep is inert until ratatoskr.wt wires it (next).

No version bump (dependency add, no runtime code consumes it yet). Refs #20.
2026-07-18 23:22:24 -07:00
vh 57fdc48e20 memory: /snapshot — worldtree-sdk cutover decided (#20), slice-1 next
Captures the SDK cutover as the active migration: contract done + heid-reviewed,
DEC-5 registry gate cleared, slice-1 foundation the next step. Plus ephemeral-Echo
(v0.21.2 #19) + reference-consumer parity arc in Recent decisions; tier3 model→role
deferred to slice-4 / worldtree-dev deploy flag. Handoff written for slice-1.
2026-07-18 23:19:21 -07:00
vh e45640c4da docs(contract): worldtree-sdk cutover — SDK-adapter contract (refs #20)
Consumer-layer cutover to worldtree-sdk (Python) 1.0.0: retire the hand-rolled
httpx wrappers (sessions/sse_client/tier3) behind a thin ratatoskr.wt adapter over
the SDK. Carries the 6 locked DECs (vor-cross'd with worldtree-codex), the route
map (21 wrappers -> SDK methods), the Error map table (heid-panel find:
route-as-discriminator, default -> SessionApiFailed), INV-CUT-1..5, and the 7-slice
plan. Adapter design: caller-injected transport (never closed), thin semantic error
adapter, Bifrost provider planes untouched.

heid-contract-review clean after fixups (3/3 error-map convergence + 6
clarifications folded in). No version bump (.contract.md, no code).

Refs #20.
2026-07-18 23:15:17 -07:00
vh b1fbadde3c docs(coverage): track v1 coverage against worldtree-sdk's 41-op ratified surface
worldtree-sdk v1.0.0 (wtsdk-dev, althing 01KXVF24WQD2T5ZCS49KKFCCMH) ratifies the
same 41-op surface from the identical OpenAPI 2.3.0 (sha 36148179601453a0) this
ledger already tracks — record it as the external parity authority. Ratatoskr is
the parallel Python/httpx reference-consumer (no TS adoption); the forthcoming
worldtree-sdk Python spine is the future consumable, noted as a repin candidate.

Fold in v0.21.2: POST /sessions row now notes ephemeral-Echo config passthrough
(role not model, W-4 cross-validated by the SDK); GET /capabilities row notes the
--whoami allowed_roles/default_role fix + spec v1.1. REST count unchanged (19/41 —
ephemeral is a depth enhancement to an already-covered route).

No version bump (docs-only coverage-ledger update).
2026-07-18 13:44:04 -07:00
vh c7016f23a6 feat(#19): ephemeral-template (Echo) session creation
create_session could only mint foundational sessions; an ephemeral template
(agent_id="echo") returned 422 ephemeral_requires_config because ratatoskr never
sent the required config block — Echo was uncreatable, surfacing as an opaque
session_api_failed at the CLI. Thread an opaque, role/model-agnostic config
passthrough through the create path so Echo sessions are creatable.

- sessions.py: create_session(config=...) verbatim passthrough (PRE-004 Mapping /
  PRE-005 config-xor-bifrost guards); SessionInfo gains kind + config, captured
  defensively (.get) on both create and list.
- cli.py: --system-prompt flag builds config={"system_prompt": ...} (validation:
  non-empty, requires --new+--agent, xor bifrost); _amain surfaces kind=; the
  --whoami renderer now reads allowed_roles/default_role (was reading the dead
  allowed_models/default_model) and tolerates a malformed capabilities shape.
- contract #2 amended (Amendment 2026-07-18); Heid-panel contract-reviewed +
  diff-scoped bug-hunted (one whoami null-join gap found + fixed).

Canonical grounding: config.role, never config.model (worldtree-dev althing
01KXT976NN91DRBZBPXNZ2BVZR; ADR-0012 role cutover). Verified end-to-end against
the live v0.16.2 target. TDD across create + CLI; full suite green (534).

Closes #19.
v0.21.2
2026-07-18 12:02:18 -07:00
vh 5d06a274bf chore(canonicals): re-sync conversation-api-spec to v1.1 + affect-egress
Pull Worldtree main's role/model-cutover doc correction (worldtree commit
b4a278c) into the pinned conversation-api-spec: the Echo ephemeral-template
section now documents allowed_roles/default_role, config.role (omitted ->
default_role "echo"), the repurposed model_not_allowed (any non-empty
config.model hard-rejects), and the new role_required error. Frozen OpenAPI
untouched. affect-egress-consumer-reference re-synced in the same pass;
re-pin hashes in .corviduo-canonicals.toml.

Resolves the role<->model drift ratatoskr-dev raised on althing (thread
01KXT976NN91DRBZBPXNZ2BVZR); worldtree-dev cleared the re-sync.

No version bump (vendored-canonical docs sync).
2026-07-18 12:01:48 -07:00
vh 80c8d58b83 chore(canonicals): sync soong-lab export + importer contracts
Refresh vendored soong-lab-bundle canonical copies against upstream and
re-pin hashes in .corviduo-canonicals.toml.

- export: open_question B resolved — the 4 role labels map 1:1 to WT
  model-role slugs by exact name (assistant/thoughtful-assistant under
  the `foundational` grant, character/thoughtful-character under
  `character`), so ship.native.role is directly define-valid. Also,
  motivational goals/fears are now structured objects (WT #187) with
  id/type/salience/description and validate_exportable gates
  (description >=20, type in GOAL_TYPES, salience in [0,1]).
- importer: adds _coerce_goal/_coerce_fear totality path (INV-I-6) with
  legacy bare-string back-compat and strict re-validate (no silent loss).

No ratatoskr code impact: the motivational Tier-3 layer is schema-deferred
(Phase 2.0 baseline-only) and no goals/fears string-consumers exist. No
version bump (vendored-canonical docs sync, skip-the-bump per SemVer).
2026-07-18 02:22:58 -07:00
vh 7bfd4ddc95 memory: SDK fixture-capture go given — no Rata-side #371 pending 2026-07-18 00:51:29 -07:00
vh 26482ba2e9 memory: apps leave Rata to a new repo — arbo-fork/SillyTavern resolved
Operator ruled: no arbo fork, no SillyTavern-on-Rata; both app products move to
a new repo (template-dev standing up). Rata does NOT fork and reverts to its core
(Worldtree debug surface + Bifrost reference impl + Conversation-API SDK #371
seed/future-consumer). Retires the 2026-07-17 fork-Rata-for-arbo NEXT-MAJOR plan;
resolves the arbo-vs-SDK open question the prior snapshot flagged.
2026-07-18 00:36:27 -07:00
vh 8f3e9dd896 memory: /snapshot — Worldtree SDK (#371) decision + issue-#1 drift fix
Capture this session's durable state: the "fork Rata for a SillyTavern-style
app?" question resolved to don't-fork — Worldtree owns an official Conversation
API SDK (WT #371), seeded from Rata's client spine; the app is a fresh TS
sibling. New Current-state thread + Recent-decisions entries + a full-arc detail
file; arbo-fork decision preserved with the arbo-vs-SDK priority flagged as an
open operator question. Auto-archived 35 settled 2026-06-14..06-18 entries
(19 Recent, 16 Tried) to archival-memory.md. Handoff refreshed.
2026-07-18 00:28:00 -07:00
vh 4bd9abdebc docs(contract): re-canonicalize #1 SSE event vocab against code
Add awaiting_llm_first_token (#201) and affect_update (#204) to issue #1's
Event union and TESTS via a dated amendment. Both events are parsed by
_envelope_for_type and covered in tests/test_sse_client.py, but issue #1's
Output union + full_event_vocab test were frozen at the v0.19.0 baseline's
8-event set — contract-vs-code drift surfaced during the Worldtree #371 SDK
parity-matrix pass. Documentation-only: no code change, no version bump.
2026-07-18 00:15:30 -07:00