main
85 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5c3d0ad010 |
feat(tts): config-driven voices + two-voice dialogue/narration split (DEC-11)
Voice assignment moves from the hardcoded server map to ~/.config/ratatoskr/ voices.json (per-agent voice + optional narration_voice). An agent with a narration_voice gets a two-voice split: quoted speech in `voice`, narration in `narration_voice`, synthesized per-span and stitched under one WAV header. - new src/ratatoskr/voices.py: load_voice_config (degrade-not-crash), segment_dialogue (quote-based, straight + curly), resolve_voice_spans - tts.py: tts_stream_stitched replaces tts_stream — serial per-span synth, span 0 verbatim, spans 1..N header-stripped -> one gapless 48kHz stream; a single-span list is a byte-identical passthrough (no single-voice regression) - server.py: _tts_endpoint resolves spans from app.state.voice_config; the hardcoded _TTS_VOICE_MAP is retired; create_app gains a voice_config param - entrypoint.py: loads voices.json at startup - contract DEC-11 + INV-TTS-5/6/7; initial config donut->donut, sindra->miranda (dialogue) / emmie (narration) Live-verified on :8765: Sindra mixed turn -> 2 dots calls (emmie+miranda) stitched into one 48kHz WAV with a single RIFF header; Donut single-voice unchanged. 545 tests green (incl. new test_voices.py). |
||
|
|
3e69bc9c01 |
feat(tts): map ratatoskr:sindra -> miranda voice
Sindra now voices with the dots "miranda" voice (operator-directed); donut stays on "donut", other agents fall to the "glados" default. One entry in _TTS_VOICE_MAP + a test; contract DEC-8 updated. Live-verified on :8765 (/api/tts with agent_id=ratatoskr:sindra -> 200 audio/wav @ 48kHz). |
||
|
|
38b78d8a4a |
feat(tts): migrate RP-surface TTS chatterbox-fast → dots-tts
Swap the voice synthesis backend from chatterbox-fast (:8197 bespoke /tts)
to dots-tts (rednote-hilab dots.tts-soar, :8198 OpenAI-shaped
/v1/audio/speech), operator-directed after an A/B win. tts.py stays the
single swap seam.
- gateway body OpenAI-shaped: {input, voice, response_format, stream}
(was chatterbox {text, voice, format, stream})
- sample rate 24000 -> 48000 Hz (browser Web Audio SR)
- default voice glados_25s -> glados; donut voice carries over
- serialized single-consumer (satisfied by the existing DEC-5 lock)
- affect stays dropped (dots has no emotion knob, same as chatterbox)
DOTS_TTS_URL replaces CHATTERBOX_TTS_URL; RATATOSKR_TTS_URL override
unchanged. chatterbox-fast :8197 kept up as rollback. Contract amended
(donut_voiced_interview.contract.md). Live-verified end-to-end on :8765
(RIFF/WAVE 48kHz mono s16le through /api/tts). 520 tests green.
|
||
|
|
7fdaf3bd23 |
fix(tts): revert sampling knobs — real cause was Turbo AR-tail over-run, fixed server-side
The long-turn "swaps to German" garble was NOT a language leak (infra-ops's
initial framing) and NOT the sampling entropy my interim curb targeted. The real
cause, signal-measured by infra-ops: the Chatterbox Turbo model over-runs its
generation TAIL — a long single generation degrades into garble/dead-air in its
final ~2-3s (voiced-tail zero-crossing rate 1.58x the middle). The gateway's
unbounded chunk-size ratchet built 300-600 char mega-chunks that landed in that
zone, and streaming concatenated each bad tail.
My interim curb (top_k 1000->80, top_p 0.95->0.85, temp 0.8->0.5) made it WORSE:
tight sampling pulls the degradation onset to a shorter length (~200 chars vs
~300 at defaults), so it fights the server-side fix rather than helping.
Fixed server-side (infra-ops, chatterbox-fast image :v2): a max_chunk_chars=250
cap bounds each generation below the ~300-char onset -> clean prosodic chunks
(verified ZCR 1.58x -> 0.64x; operator ear-confirmed clean audio + clean joins).
Consumer side, this commit:
- Revert the sampling knobs: gateway_body back to {text, voice, format, stream},
send full text with the gateway's default sampling. The server chunks at 250.
- Keep the /api/tts empty-200 -> 503 guard as hygiene (DEC-9a; the shared-3090
OOM that produced empty 200s is also resolved — Zonos moved off the card).
Contract DEC-9 rewritten with the resolved root cause + the two wrong hypotheses;
DEC-9a marked kept-as-hygiene. 520 green.
|
||
|
|
19b499ab50 |
feat(tts): migrate off Zonos to chatterbox-fast; drop affect, hold English
Repoint the TTS client from the Zonos gateway (:8890 /v1/audio/speech) to
chatterbox-fast (:8197 /tts — bespoke non-OpenAI {text,voice,format,stream}
schema, no auth, 24kHz, infra-ops-verified). tts.py stays the single swap seam.
Dropped, no backward-compat (pre-v1):
- Affect (DEC-7): the Turbo checkpoint has no emotion knob, so PadState,
EmotionDials, pad_to_dials, the /api/tts p/a fields, and the browser pad
argument are deleted. Voice is now flat.
- Client-side chunking (DEC-10): chatterbox has no per-synth cap and chunks
internally, so chunk_text/tts_stream_long/_pcm_after_header are deleted; a
single tts_stream call voices a whole turn, the mid-stream yielded_any degrade
folded into it.
- Language pin (DEC-9): no language field; re-purposed to sampling curbs (below).
Fixed / added:
- Browser Web Audio sample rate 44100 -> 24000 (the chatterbox rate).
- Default voice Cora -> glados_25s; donut registered lowercase at /refs/donut.wav.
- English-drift curb: Turbo is multilingual-leaky and wanders off English on a
long generation (the gateway scheduler ratchets chunk size unbounded). Tighten
sampling in gateway_body: top_k 1000->80, top_p 0.95->0.85, temperature
0.8->0.5. These reduce drift probability; the guaranteed fix is a server-side
max-chunk cap (infra-ops, greenlit).
- OOM guard (DEC-9a): a long generation can OOM the shared 3090, returning 200
with a 0-byte body; /api/tts surfaces an empty 200 as 503 rather than
committing silent audio.
Contract donut_voiced_interview.contract.md amended: migration banner, DEC-1/3/8
amended, DEC-7/9/10 retired with historical notes, DEC-9a added.
Tests rewritten to the new wire; 520 green. Live-smoked against the gateway
(24kHz synth + endpoint proxy + web console). persistent-memory.md committed
alongside (commit-along).
|
||
|
|
d59f907962 |
feat(tts): pin English, stream long turns via chunking, dialogue-only Donut
TTS fixes + hardening for the Donut voiced interview. Feature: - gibberish -> pin `language: "en-us"` on every gateway call (DEC-9); the multilingual model drifted into other-language phonemes without it. - truncation -> the Zonos model hard-caps one synthesis at 6144 tokens / 71.2s (infra-ops). Chunk client-side (paragraph-first, greedy to ~75% of cap for prosody; sentence/clause fallback) and concatenate the int16 PCM behind ONE WAV header (DEC-10). /api/tts becomes POST so a long turn rides the body, not a length-capped URL (DEC-10a). - persona -> dialogue-only rewrite (no asterisk RP beats -- they were being voiced as gibberish) + always consult the native `reference_knowledge` tool before answering (retires the stale kb_bridge references). Pushed live to ratatoskr:donut. Heid code-review + bug-hunt hardening (4-arm panels, triaged): - untrusted /api/tts body fields degrade, never 500: huge-int PAD (OverflowError), non-str agent_id (unhashable .get), lone surrogates (utf-8 encode), whitespace-only text. - serialize lock + client released on every peek escape (cancel / InvalidURL) -- previously a permanent deadlock. - a mid-stream drop after a committed 200 degrades (keeps what played), never raises into the response; a non-WAV 200 body is rejected (RIFF sniff + bounded header scan) instead of decoded as garbage. 546 tests green; long-form live-verified (106.6s, one header). Contract brought canonical (DEC-9/10, FN chunk_text/tts_stream_long, POST endpoint, INV-TTS-4 logging scope, FN pad_to_dials domain). reference_knowledge empty-recall root-caused to a Worldtree wing-misfile (escalated to worldtree-dev; not ratatoskr code). |
||
|
|
9041f1f402 |
fix: Web Audio streaming playback — fixes Safari NotSupportedError
Operator confirmed the "TTS blocked" was NotSupportedError on Safari — WebKit refuses a streaming 0xFFFFFFFF-length WAV via <audio src> (can't compute duration/seek), exactly as infra-ops warned. Replaced the <audio src> playback with a Web Audio path that works in all engines: - speakOnDone: fetch the chunked /api/tts stream, skip the WAV header to the data chunk, decode int16 LE PCM -> Float32, and schedule the samples GAPLESSLY into an AudioContext as they arrive (BufferSource per chunk, playAt += buf.duration). Progressive, TTFA ~0.5s. Decoding the raw PCM ourselves sidesteps every WAV-container quirk. - unlock: an AudioContext starts suspended; Safari + Chrome need resume() from a user gesture. _unlockTtsAudio() now resumes the ctx on the first interaction anywhere + toggle-on + submit, so it's running before the ~15s-delayed speak-on-done. - cancelTts: aborts the fetch + stops all scheduled BufferSource nodes. Validated in Chromium (Playwright, strict autoplay): 43 nodes scheduled, 5.1s of PCM decoded, ctx "running" 6.5s post-gesture, zero errors. Headless WebKit can't launch here (missing system libs — an infra-ops install), so the operator's live Safari is the final check; the code is standard Web Audio Safari has supported for years. Contract FN client:speakOnDone updated (Web Audio; the Safari NotSupportedError reason). |
||
|
|
7856ec5438 |
feat: stream Donut TTS play-as-it-arrives + autoplay unlock (supersedes buffered)
Operator: play-as-it-arrives, don't wait for the whole clip. infra-ops confirmed the
Zonos gateway ALREADY streams (chunked int16 WAV, TTFB ~0.44s vs ~7s total; placeholder
0xFFFFFFFF sizes are DESIGNED for progressive <audio src>). The buffering was entirely
in our proxy, and the _finalize_wav_header rewrite (
|
||
|
|
09e425787b |
refactor: retire the KB-recall bridge — WT #383 native reference_knowledge (b167)
Worldtree #383 shipped native Tier-3 reference_knowledge (v1.0.0b167, live on :8081 + demo): every Tier-3 agent context now carries the tool automatically, with evidence packets (note_id + path provenance, confidence bucket) and a server-side grounding rule. That supersedes the interim consumer-side memory_context pinning bridge (slice 3), so it is deleted per its INV-KB-1 retire seam. Removed: - src/ratatoskr/kb_bridge.py + tests/test_kb_bridge.py (the whole module). - server.py: the pin_kb_context import + the single turn-path call-site (reverted to the pre-bridge wt.stream_turn call), the SSE keepalive that only covered the consult delay, and the bridge-only agent_id plumbing (TurnHandle.agent_id + the submit read). - index.html: agent_id dropped from the turn POST body. - test_web_server.py: TestKbBridgeWiring (tested the removed call-site). Kept: - wt.stream_turn's memory_context param (inert SDK-parity passthrough; worldtree-dev concurred it stays) + its forwarding tests. - the non-str content 400 guard (general input hygiene, not bridge-specific). Retirement LIVE-VERIFIED before deletion: a Donut session on :8081/b167 carries builtin_tools=['reference_knowledge']; she called it and grounded in the DCC Collapse content fully in-voice, degrading gracefully on absent content. 524 green. Contract marks slice-3 RETIRED (historical record retained). #383 closed. |
||
|
|
eb0767e96d |
fix: heid-code-review fixups — donut voiced-interview slices 2+3
Triaged the heid-code-review panel (3 arms; reconciled against
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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). |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
be2c577884 |
feat(provider): mark_superseded verb — Worldtree #364 contradiction retirement + bifrost 1.1.4
Implement `mark_superseded(ids, *, superseded_by=None, reason=None)` — the SOLE supersession verb Worldtree #364's promotion-hygiene reconciliation calls to retire contradicted facts (wire shape confirmed by worldtree-dev, bifrost_memory_store.py:293). Live re-verify (2026-07-16) proved our provider 500-crashed on this call (unimplemented) → #364's retirement couldn't land + a retry-storm bloated the store; the readout only passed via transient recency-eviction. - `mark_superseded` mirrors the reference `_mark_lifecycle`: sets top-level `superseded=True` (+ `superseded_by`/`superseded_reason` when non-None), increments revision, NON-destructive (get still returns; recoverable). Unknown ids skipped. - `_is_live` (INV-011) now short-circuits on `superseded is True`, so a retired chunk is excluded from `scan` (person-prime) — durable retirement, not just recency-eviction. search is unfiltered (matches reference; WT re-checks liveness client-side). - Contract: un-defer mark_superseded (+ FN spec, INV-011); TDD 5/5 (retires-from-scan tracer, non-destructive-get, unknown-id no-op, non-None-fields-only, parity #195). - bifrost 1.1.1→1.1.4: hasattr-gate backstop for the maintenance verbs (unimplemented verb → unsupported_capability 400, never AttributeError/500/retry-storm — the gap we surfaced) + the 1.1.3 scan/cursor conformance harness. Full suite 644 green. |
||
|
|
f46ccbae1c |
fix(provider): sortable_chunk_fields needs required type — handshake was broken
DEPLOY-BREAKER caught by driving the live bind (unit tests + worldtree-dev's
name-only parser + heid-bug-hunt all missed it). bifrost handshake_response
`SortableChunkField` requires BOTH `name` and `type` (additionalProperties:false).
We advertised `[{"name":"updated_at"}]` (no `type`), so the handshake_response
failed wire-schema validation → `bifrost.schema_validation_failed` → the ENTIRE
Bifrost bind (memory + affect) broke, not just the sort. Advertise
`{"name":"updated_at","type":"timestamp"}` (matches the reference; `type` is
advisory-only). Regression guard added to the caps test (asserts required name+type,
no extra keys). Full suite 639 green.
|
||
|
|
8199774405 |
docs(contract): mark scan cursor v1-provisional (offset, not snapshot); route conformance gap to bifrost-dev
Operator accepted offset-cursor for v1 (person-prime single-page is conformant). INV-010 now documents the KNOWN DEVIATION: multi-page continuation diverges from bifrost's protocol snapshot-cursor contract (dispatch drops sort on continuation, ScanCursorExpired normative) — our offset cursor doesn't snapshot (dup/drop under concurrent write) and never expires. Durable fix routed to bifrost-dev as a conformance-coverage gap (scan/cursor is untested); ratatoskr will adopt reference snapshot-cursors if bifrost rules them normative. |
||
|
|
8fc757aa61 |
feat(provider): person-prime scan verb + sortable_chunk_fields cap (WT #349)
Implement the memory-store `scan` verb — a query-LESS, LIVE-only, globally
ordered top-N-by-recency read — and advertise `sortable_chunk_fields=
[{updated_at}]` at the Bifrost handshake. Advertising the cap is what lights
up Worldtree's #349 person-prime turn-1 durable-fact injection (Branch-A
`"updated_at" in caps.sort_fields_supported`); the fix is ZERO Worldtree
change — the running provider announcing the cap is the trigger.
scan is:
- LIVE-only server-side (INV-009): superseded/tombstoned excluded — a dead
fact can never inject; person-prime's `lifecycle_state=live` does not ride
the scan wire, so server-side is authoritative.
- Globally ordered before pagination (INV-010): the full scope-filtered live
set is ordered by (sort.field, direction) globally; missing value LAST,
chunk_id tiebreak. Backed by an expression index on
json_extract(record_json,'$.updated_at') to stay in the 500ms budget.
- Cursor = offset into the global order; emits a next cursor only when a
further match exists (no empty trailing page — matches the reference).
Sort is dispatch-gated: an unadvertised sort.field raises InvalidArguments,
never a silent unsorted fallback.
Contract amended: un-defers scan, adds the FN spec + INV-009/INV-010 +
sortable_chunk_fields to INV-006. TDD 7/7 green (scan_recency tracer,
live_only, scope_isolation, unadvertised_sort, person_prime_record_shape,
cursor pagination, parity_vs_reference vs InMemoryMemoryStore #195). Full
suite 638 green.
|
||
|
|
0441e319f6 |
feat(web): auto-scale PAD gauges for R32-1B unbounded-z (v0.20.9)
Sindra "full and unbounded": relax the debug affect console's PAD display from a hard [-1,1] clamp to auto-scaling on the session's own max |PAD| (padScale floor 1.0 → padFillFrac faders + _padNorm orbit). An unbounded-z PAD (Worldtree R32-1B, ~±10) now renders at full range and never pegs or escapes the frame; today's [-1,1] values are unchanged (scale==1); the exact value is always shown numerically (unclamped). Purely a debug-surface change — verified (grep, whole codebase) the only PAD clamps lived in the web display layer: the affect store is conduit- opaque, the read route + proxy pass verbatim, and --set-persona-pad writes unclamped. Ratatoskr is a downstream observer, so this has zero consequence to any agent's real affect or behavior (Worldtree-computed server-side). Playwright-verified: z=±6.2 → faders ≤ half-bar, orbit in-box, +6.20 readout, zero regression at scale 1. Proactive R32-1B prep. |
||
|
|
e5ec63967e |
feat(web): memory viewer + design iteration-3 + markdown pass-2 (v0.20.7)
Web-UI iteration-3 — three queued items landed together.
(A) Design iteration-3 into static/index.html:
- sparkline grid background (<pattern id=sparkGrid> + bg rect behind each
relation-row spark polyline);
- PAD strips → per-turn Δ bars: replace the vertical polyline strip
(stripPoints/proj3 removed) with padDeltas→deltaStrip, a 12-cell HTML
column of diverging bars (newest at bottom, magnitude→width, age→opacity);
- mood orbit → dimetric open box (viewBox 124×140, az35/el25,
D-right/A-left-back/P-up): ghost A×P wall + D×A floor +
orbitProj/orbitShadowY/orbitWallPt/orbitAxisPt, JS-driven animated replay
(orbitFrame per rAF via a singleton startOrbitAnim reading live ORBIT_HIST;
reduced-motion → static final-state).
(B) Memory viewer — a non-bifrost debug read on OUR own store, mirroring the
#18-D2 affect read:
- provider: RatatoskrMemoryStore.list_chunks + count_chunks + shared
add_memory_read_route (GET /memory/chunks?agent_id=&end_user_id=), wired
into build_memory_provider_app + the combined :8392 provider. end_user
strict, agent_id lenient (an {end_user}-only chunk stays visible);
{chunks,count,total}, empty match = 200 (not 404);
- web: _memory_chunks_endpoint (GET /api/memory/chunks) supplies end_user_id
server-side, forwards the browser agent_id, proxies to memory_read_url;
create_app gains memory_read_url, entrypoint reads RATATOSKR_MEMORY_READ_URL;
- pane: loadMemory/renderMemory/setMemHead — a live-polling MEMORY console
pane (content·scope·origin·revision per chunk; count/total distinguish
empty-store from scope-mismatch), polled on open + post-turn.
(C) Markdown pass-2 in markdownSafe: GFM pipe tables (mdTable, alignment
colons), indentation-nested lists (child list inside the open <li>),
ordered-list start=N numbering, streaming robustness (partial fence →
code block; header-without-delimiter → paragraph until it streams in).
esc-first → INV-004 held.
Contract web_debug_surface.contract.md amended in-commit (create_app
memory_read_url; dimetric-orbit + Δ-bar renderConsole POST-002; memory-viewer
+ markdown-pass-2 function contracts). 631 tests green; Playwright-verified
all render paths (dark+light).
|
||
|
|
459e7fa602 |
feat(web): SVG sparklines + 3D isometric mood cube (v0.20.4)
Import the updated Claude Design prototype's SVG sparkline system + 3D graph, replacing the unicode-char sparklines: - per-PAD-fader vertical SVG strips (stripPoints, 26x132 beside each bar — time down Y newest-at-bottom, value on X, gradient-faded, dot at newest); also answers the earlier "next to each meter" ask - relation-row horizontal SVG sparklines (sparkPointsH, 56x13, auto-scaled, gradient + end dot) — fixed-width, so the old unicode overflow onto the n column can't recur - mood-orbit reworked from a 2D P×A scatter into a 3D ISOMETRIC P×A×D cube (proj3: P right-down / A left-down / D up, 2:1 iso, scale 26, reverse-derived from the design's now-point + verified) with the trajectory, a pulsing now-marker, and a drop line + floor-shadow ellipse for depth - gradients in one hidden <defs> svg; removed the orphaned sparkline()/_SPARK Contract amended. Verified: pytest tests/test_web_* (84) + node Playwright (injected 24-sample history — 3 PAD strips + 4 relation sparklines + the 3D cube trajectory/drop/floor all render; gradients resolve). |
||
|
|
0b1d9e2b15 |
feat(web): context-injection panel — reconstruct the full hidden affect block (v0.20.2)
The affect console now reconstructs + displays the complete affect-context block
Worldtree assembles into the agent's system prompt — never on any wire, hidden from
regular consumers, surfaced here as the reference-impl's privileged dev view.
- extend build_persona_canon.py to emit mood_directive {occ_directives (15),
pad_band_fallback, salience, pad_band_cutoff, full_only} into the browser canon
(strings were already in the pinned d2-mood-render-canon; regen via Worldtree loader)
- canonPadFallback(pad) + canonEmotionDirective(type): byte-exact mirrors of Worldtree
core/persona/renderer._pad_band_fallback + derive_directive
- renderDirective -> a "CONTEXT INJECTION · reconstructed · hidden from consumers" panel:
mood descriptor [exact] + mood directive [candidate] + relationship directive [exact]
- honest-partial (affect-egress-reference sec 3): affect.emit is type-only (no
intensity), so the salience gate can't be evaluated -> show BOTH the OCC emotion
directive AND the PAD-band fallback with the "injected if intensity >= 0.2" caveat,
never asserting which fires; fallback alone is exact when no dominant_emotion
- vendor + pin affect-egress-consumer-reference.md (tolerate_drift; worldtree-dev
co-signs + pings on change). drift 6/6 green
- contract amended for the new reconstruction fns + honest-partial provenance
Verified: pytest tests/test_web_* (84) + node Playwright (sindra dominant_emotion=joy
-> joy OCC directive candidate + PAD-band fallback both render with exact/candidate tags).
|
||
|
|
1fcb17730e |
feat(web): Claude Design console — 3-column wire monitor (v0.20.0)
Adapt the Claude Design "Ratatoskr Console" prototype into the web SPA:
translate out of the .dc.html dialect (x-dc / sc-if / sc-for / {{}} /
DCLogic / external _ds CSS) into single-file / no-CDN / vanilla, and wire
all real /api/* fetch + SSE into its DOM. New 3-column command-console
replaces the tabbed telemetry layout; endpoint set + SSE vocab unchanged.
- left engine-ticker rail: DEBUG + ADMIN + tool/turn-lifecycle merged into
one timeline (tickerAdd); tools-armed chips; full-detail Bifrost rail pane
(endpoint / connected / consumer / caps / tools)
- center conversation: per-turn INLINE chain-of-thought
- right resizable affect console: dominant / canonical-mood centerpiece;
bipolar PAD faders EACH with a turn-to-turn delta + sparkline; P×A mood
orbit; relations metric rows; canonical directive
- light / dark theme toggle (dark default; full token override —
surfaces + fg + borders + accent-as-text)
- inlined data-URI favicon (downscaled 1024->64px), kills /favicon.ico 404
- ticker spine re-anchored to a content-height wrapper (was scrolling out of
view on auto-scroll)
- honest-shape (INV-001): dominant-emotion shows a real OCC emotion (Tier-1)
or the canonical mood word (Tier-3), never a fabricated one; affect-derived
grid drops non-emitted metrics (intensity / decay-tau)
All server routes unchanged. web_debug_surface.contract.md amended for the
presenter renames (renderBifrostState->renderBifrost, renderAffectPane->
renderConsole, setPersonaStrip removed).
Verified: pytest tests/test_web_* (84 passed) + node Playwright end-to-end
against personal :8081 (session open, Sindra seeded greeting, live turn SSE,
affect console + relations + bifrost detail, theme toggle, PAD deltas,
no favicon 404).
|
||
|
|
263ec2917b |
fix: render the seeded first-message on the web UI (v0.19.9)
The #347 auto-seed worked (the greeting was in the session ledger at seq-0), but the web UI never showed it: there was no GET /api/sessions/{id}/messages route and startSession() went straight from create to persona/tools/admin hydration, so the transcript only filled from the live turn stream + user echoes — a seeded turn-0 was invisible. - server: new proxy route GET /api/sessions/{id}/messages -> get_session_messages (mirrors the tools/bifrost proxies; status-preserving envelope). - SPA: loadTranscript(sessionId) fetches it on open and renders existing turns (assistant -> .response .md-body via markdownSafe escape-first; user -> .prompt-echo via textContent), called after the workspace opens. Best-effort. web_debug_surface contract amended (endpoint + loadTranscript). 2 web route tests, suite 617 green. Playwright DOM check proved the render end-to-end (drive the real UI -> Sindra's greeting bubble appears). |
||
|
|
be171304f5 |
feat: authored first-message presets — auto-seed on session-create (v0.19.8)
Codifies 'give an agent a first message' (Worldtree #347): new module ratatoskr.first_message (FIRST_MESSAGE_PRESETS + seed_preset_first_message) seeds a preset agent's opening as a #347 authored turn-0 on every new session, wired into all three create paths — cli._amain (--send --new), tui._resolve_then_run (bare --new), web._create_session_endpoint (POST /api/sessions). seed_preset_first_message is strictly best-effort (INV-001): it soft-guards its inputs (return None, never assert), bounds the write with asyncio.wait_for so a stalled /history can't block create (the CLI/TUI clients disable read timeout for SSE), and swallows every exception except asyncio.CancelledError (which propagates) — so it can NEVER raise into or block the session-create path it is wired into. Per-content idempotency key → idempotent replay, no dup. Seeded with ratatoskr:sindra, whose opening greeting moved out of her card: her live system_prompt was PATCHed (non-destructive) to drop the Startup workaround the #347 first-message now replaces. Quality gate (both cross-frontier panels): heid-code-review returned zero implementation drift (2 test-only fixups applied); heid-bug-hunt caught the gap the conformance lens can't see — code matched the contract's narrow ERROR_ROUTING but INV-001's 'never raises' is broader — driving the broad-except + soft-guard + wait_for hardening above. Contract docs/contracts/first_message.contract.md (module-scoped, validated). TDD: 12 unit + 1 web wire-in; the 3 existing sindra bind tests gained a history-endpoint mock (creating a preset agent now auto-seeds). Suite 615 green, ruff+mypy clean. Auto-seed live-proven generation-free against personal :8081. |
||
|
|
e643d38f58 |
fix: persona_state SET body → canonical {pad:{pleasure,arousal,dominance}} + re-vendor Tier-3 prose (v0.19.7)
worldtree-dev landed the Tier-3 persona/memory/persona_state prose docs
(c9e59ec) — shapes that serialize as freeform Any in the OpenAPI, so the
prose markdown is their source of truth. Re-vendored docs/conversation-api-spec.md
(tolerate_drift markdown pin; worldtree-spec-rev 879cefe→c9e59ec).
Consumer alignment: --set-persona-pad / _set_persona_probe was building
{pad:[list]}, but the canonical POST /sessions/{id}/persona_state body (#317)
is {pad:{pleasure,arousal,dominance}} (named dict). Aligned the probe to the
named dict + a len!=3 guard; updated contract #2's note, the set_persona_state
docstring, and the tests. The set_persona_state wrapper was already correct
(freeform pass-through) — only the CLI probe's body construction drifted.
Suite 602 green. (Also this session: heid-code-review on the #347 slice
returned unanimous zero drift across all three panel arms.)
|
||
|
|
6bf2a84ccd |
feat: authored-history-write consumer side (Worldtree #347) — v0.19.6
Consumer side of Worldtree's #347 authored-history-write (the SillyTavern first-message primitive), shipped via direct in-session TDD: - write_authored_history (POST /sessions/{id}/history): v1 author=assistant, effects=none, per-session idempotency; body server-pinned (AuthoredWriteRequest extra=forbid) so null effects/claimed_original_at are omitted; 200 replay / 201 fresh both return the AuthoredTurnResponse dict. - AuthoredHistoryUnavailable: the hide-existence 404 (feature-absent / ungranted / session-absent, indistinguishable by design — INV-347-1) raised DISTINCT from SessionApiFailed so callers branch feature-absent and never capability-probe. - get_session_messages (GET /sessions/{id}/messages): un-deferred as the seed read-back — confirms a seed renders as a normal role=assistant turn (model-invisible provenance). - --seed-first-message probe: create session -> seed -> read-back; a 404 reports a benign feature-absent result (exit 0), never a capability-probe. Contract #2 amended (2 FNs, validated OK). 19 new tests (12 wrapper + 7 cli), suite 601 green. Coverage-map re-converged: REST 19/41 (the #347 route + the messages read-back close the one gap the 2.3.0 re-vendor opened). Live-proof pending the session.history.write grant (requested infra-ops). |
||
|
|
a99f2473b6 |
feat(web): persona pane shows the CANONICAL affect->NL Worldtree injects (v0.19.5)
The pane now renders the LITERAL mood word + relationship directive Worldtree context-injects into the agent — adopted from Worldtree's canon, not invented: - canonMood(pad) mirrors Worldtree describe_pad (valence×arousal grid, ±0.3 bands); for sindra's PAD the canonical render is "neutral" — an invented octant vocab would have said "faintly excited" and MISLED. Adopting canonical is the point. - canonDirective(rel) mirrors render_d2_canonical byte-exact: "...warmth is clear warm regard; ability trust is strong; ...; speak with direct warmth; ..." — the exact stance instruction the agent receives (which makes the WAD "stranger" relation_context read even more incoherent, as flagged to worldtree-dev). - Both VERIFIED byte-exact against Worldtree's OWN renderer on the live snapshot. - Canon vendored (docs/vendor/worldtree-persona-canon/) + drift-pinned in .corviduo-canonicals.toml (canonical_drift green); flat browser form (static/persona_render_canon.json) regenerated by scripts/build_persona_canon.py via Worldtree's authoritative loader. Reference-impl posture: adopt canonical. - Fail-open (canon absent -> lines omit); INV-004 esc() preserved. JS syntax clean. Refresh + drive turns to see the canonical NL under mood + each relation. |
||
|
|
ca46a93171 |
feat(web): persona pane renders the relation_edge/1 affect model + per-value trend (v0.19.4)
The persona/affect pane read snap.valence (the pre-#265 shape) while Worldtree now emits snap.relations (relation_edge/1) — so the whole trust/warmth model rendered as an empty "valence (0)". Now renders the real signal, self-labelled: - MOOD (PAD, transient): pleasure/arousal/dominance with a one-word descriptor each. - RELATION → <target> (stage: <relation_context>): trust·ability / benevolence / integrity + warmth, each as value + evidence_count (n=) — the durable social model. - Per-value TREND: Δ-vs-previous (▲/▼) + a unicode sparkline auto-scaled to the value's own observed range (flat when sub-0.01 stable, so noise isn't amplified). History accumulates client-side, one sample/turn (deduped by emitted_at), capped at 24. - Falls back to the legacy snap.valence for an older emitter; INV-001 (no fabricated Tier-1 fields) + INV-004 (every cell escaped) preserved. Supersedes the #18-D2 valence assumption + retires the stale "regard dead axis" note. Verified: render logic asserted in node against the REAL affect.db snapshot + a perturbed 2nd sample (relations rendered, no "valence (0)", Δ ▲ shown, 2-char sparkline builds, INV-004 holds). JS syntax clean. No server change (static served per-request) — refresh + drive turns to watch the trends build. |
||
|
|
75dec016eb |
fix(web): heid-review findings — SSE lifecycle teardown + test-shape gaps (v0.19.3)
Cross-frontier panel (Gróa/Hulda/Regin) on the v0.19.2 web surface, triaged: - FIX (Gróa #1, drift): the turn EventSource `onerror` (raw transport drop) now calls hideThinkingNote() — a drop mid-reasoning no longer leaves the "<Agent> is pondering…" line + its setInterval running (INV-LIFECYCLE). - FIX (Gróa #4 + Hulda #1, convergent drift): openAdminEvents now closes the EventSource + clears state.adminES on `stream_error` (server signalled end) and on a PERMANENT onerror (readyState CLOSED) — native EventSource no longer auto-reconnects into a retry loop; transient CONNECTING drops still reconnect. - TEST (Gróa #2 + Hulda #3): test_routes_registered asserts the 3 new routes; test_state_attached asserts app.state.admin_key (create_app POST-001/002). - TEST (Gróa #3 + Regin #3): AdminEvents stream_error-on-connect-failure test — upstream non-200 -> exactly one `stream_error` frame, then ends (POST-003). - CONTRACT (Hulda #2 + Regin #2, accepted): clarified the Tools inventory renders NAMES only by design (descriptions live in the BifrostState pane); code unchanged. Also lands the web_debug_surface contract as the trail. Accepted-no-op: 403-bifrost / non-404-tools tests (identical code path to the tested 404). Panel found ZERO functional server-side drift; INV-004 escaping confirmed clean across the new panes. 60 web tests pass; JS + ruff clean. |
||
|
|
af07a2329a |
feat(#2): Tier-2 — transient characters + persona-state write; audit converges
v1 coverage-audit: the last in-scope client I/O points. The audit now CONVERGES — REST 17/40 covered with zero in-scope gaps (23 excluded-by- design), SSE 11/11, Bifrost planes 8/8. - sessions.py: list_character_models / create_character / get_character_state / delete_character (#161, character.read/write) + set_persona_state (POST /sessions/{id}/persona_state — freeform body, unpinned in the frozen surface). 200/201 -> dict (or None on 204), off-status -> SessionApiFailed. - cli.py: two one-shot probes (mirror --whoami): --characters (CRUD lifecycle report) + --set-persona-pad "p,a,d" (requires --session). New ParsedArgs.characters/set_persona_pad + probe mutual-exclusion. - Contract #2 amended (5 FNs) + validated. TDD: 7 wrapper + 5 cli tests. Suite 573 green; touched code ruff-clean. - Char read side live-proven (GET /models/available-for-characters -> 200). Coverage-map: convergence frontier CLOSED — scope-A "done" (every frozen I/O point classified) is met; ratatoskr cuts v1 when Worldtree tags 1.0. |
||
|
|
9ce83d5fdc |
feat(#2): BifrostState pane — GET /admin/sessions/{id}/bifrost (admin-key)
v1 coverage-audit: the last unbuilt design-brief §5 debug widget. First
admin-key consumer in ratatoskr.
- sessions.py: get_session_bifrost(client, session_id, *, admin_key) —
admin-scoped (admin.sessions.read); the request overrides Authorization
with admin_key (distinct from the consumer bearer). 200 -> dict, non-200
-> SessionApiFailed (403 scope-denied, 404 not-bound).
- cli.py: --admin-key flag + RATATOSKR_ADMIN_API_KEY env -> ParsedArgs.admin_key.
- tui.py: new "Bifrost" TabPane + _format_bifrost_state + _hydrate_bifrost_state
best-effort worker (unconditional on_mount). Writes {endpoint, connected,
caps, tools} + audits; self-labels "not configured" / "not bound" / graceful
on 403+error, never crashes.
- Contract #2 amended (FN, incl. the bearer-override POST) + validated. TDD:
4 wrapper tests + 1 format unit + 3 hydrate integration. Suite 552 green.
- LIVE-AUTH-PROVEN on :8081 (admin key reached resource-layer 404, not 401/403).
Ledger correction: #11 (AdminEvents) is NO LONGER BLOCKED — the admin key
was verified to carry admin.events.read; only the pane is unbuilt. Coverage:
REST 11/40.
|
||
|
|
e62208d8e3 |
feat(#2): consume GET /sessions/{id}/tools — Tools-pane inventory hydrate
v1 coverage-audit Tier-2 quick win. The owner-scoped tool-inventory endpoint (#183) had no caller; wire it into the TUI Tools pane. - sessions.py: get_session_tools (GET /sessions/{id}/tools) — owner- scoped (consumer key, no admin scope), 200 -> parsed dict verbatim, non-200 -> SessionApiFailed. Mirrors get_persona_state / get_me. - tui.py: _format_tool_inventory helper + _hydrate_session_tools best-effort worker (mirrors _hydrate_persona), wired unconditionally in on_mount. Writes the merged {agent_id, builtin_tools, bifrost_tools} inventory the LLM saw at turn-fire into the Tools pane + audits; never crashes on failure. - Covers the design-brief 5 "Tools widget" via the reachable owner endpoint (the admin variant stays a gap only for cross-user debug). - Contract #2 amended (FN) + validated. TDD: 3 wrapper tests + 1 format-helper unit + 2 hydrate integration tests. Coverage: REST 10/40. Suite 544 green; touched code ruff-clean. |
||
|
|
387ac4ab2c |
feat(#2): consume GET /me + GET /capabilities via --whoami one-shot
v1 coverage-audit slice (capabilities+me). Both endpoints had no caller; add them as cheap boot-time debug primitives. - sessions.py: get_me (GET /me — identity/whoami) + get_capabilities (GET /capabilities — Echo ephemeral-template discovery). Mirror get_persona_state: 200 -> parsed dict verbatim, non-200 -> SessionApiFailed. Freeform dicts (frozen OpenAPI types both as objects). - cli.py: new --whoami one-shot mode (mirrors --send). Fetches both, prints an identity + capabilities report, exits. Standalone probe: mutually exclusive with --send/--session/--new/--agent; opens no session. New ParsedArgs.whoami field + main() dispatch. - Contract #2 amended (2 FNs) + validated. TDD: 5 wrapper tests + 5 cli tests (validation + mode + error). Coverage map: REST 9/40. Suite 538 green; touched code ruff-clean. Audit note: /capabilities is the Echo ephemeral-template discovery endpoint, not a generic server-caps endpoint (coverage-map framing corrected). TUI-surfacing of /me + /capabilities deferred. |
||
|
|
5c1b9816d4 |
feat(#6): startup session picker for bare TUI mode
v1 coverage-audit slice b2. The audit found list_sessions had no caller — the startup session picker (design-brief §4) was never built; bare TUI mode was a hard usage error. Add SessionPickerApp (mirrors AgentPickerApp) and resolve bare mode in _resolve_then_run. - Bare TUI mode (no --session/--new) now valid → session picker. Resolution: 0 sessions -> [no_sessions] exit 14 (resume-only per §4 "no in-app creation, --new only"); exactly 1 -> auto-resume (§4 "picker only when >1"); >=2 -> SessionPickerApp -> resume pick (Esc/Ctrl-D -> exit 0). - cli._parse: bare TUI valid; --send still requires one flag; --agent forbidden in bare mode. run_tui PRE-002 xor -> mutually-exclusive. - Contract #6 amended (SessionPickerApp + bare-mode resolution) + validated. TDD: 3 picker pilot tests + 5 resolution tests + 3 cli validation tests. Suite 528 green; touched code ruff-clean. Design note: bare + 0 sessions errors (honors §4's no-in-app-creation clause); the friendlier auto-fall-through-to-new is deferred pending operator preference. |
||
|
|
0c7660791f |
feat(#1): shared SSE resume orchestration; wire cli --send
v1 coverage-audit slice b1. The audit found reconnect_turn had no caller — every presenter dropped the stream on disconnect instead of resuming, leaving the "reference SSE-resume implementation" (design- brief §3/§8d) unreachable. Add stream_turn_resilient as the single shared resume surface (design-brief §8b "share the consumer, branch the presenter") and route cli --send through it. - stream_turn_resilient wraps stream_turn + reconnect_turn: on SseConnectionDropped (mid-stream drop or clean EOF before terminal), resume from the last-seen sse_id via reconnect_turn (Last-Event-ID), up to max_reconnects (default 5). last_seen persists across attempts. - Non-drop reconnect failures (412/410/400/TurnIdFlip/SseConnectFailed) propagate unchanged, per contract #1's "surface, not recover". - cli.py: --send consumer now drives stream_turn_resilient (transparent reconnect). tui/web still consume bare stream_turn (follow-up). - Contract #1 amended (FN stream_turn_resilient) + validated; 8 TDD cases (happy, resume-after-1/2-drops, clean-EOF resume, unresumable zero-event, max-reconnects-exhausted, zero-budget, buffer-expired- propagates). Suite 518 green; ruff + mypy clean on touched code. |
||
|
|
b2e4901264 |
feat: map Worldtree b1 eager turn-launch statuses (409/503) in stream_turn
Worldtree v1.0.0b1 (#331) decoupled turn execution from the SSE connection, so turn-launch failures now arrive EAGERLY as an HTTP status before any stream: 409 agent_not_available (pre-b1 was a 200 + in-stream error event) and 503 (retryable turn-launch / infra failure). stream_turn previously funneled both into a generic SseConnectFailed. Map them to typed SseConnectFailed subclasses — AgentNotAvailable (409) and TurnLaunchUnavailable (503, retryable=True) — carrying the parsed error_code/message from the {detail:{error_code,message}} envelope. Subclassing keeps existing `except SseConnectFailed` handlers working with zero changes (POST-003 preserved — no synthetic event yielded; raise mirrors reconnect_turn's 400/410/412 pattern). worldtree-dev confirmed 409/503 are real runtime statuses; the OpenAPI 2.1.0 gap (not enumerating them) is theirs to fix (doc-completeness, not a wire break). The 503 error_code is being re-pinned upstream (today internal_error -> likely not_ready); our handling keys on STATUS so it's robust to the final code — tighten the 503 default once they confirm. Body shape live-confirmed against demo b1's 404/401 responses. Suite 509 green. Contract docs/contracts/issues/1.contract.md updated. |
||
|
|
719e4d605b |
feat: web SPA bind — add 'combined' (:8392) both-plane option as default
The bind dropdown offered only memory/affect single-plane binds; #18's composite endpoint (:8392, both planes in one session) was never reachable from the SPA. Add 'combined' as the default-selected option, keeping memory-only / affect-only for single-plane isolation diagnostics. - endpoint_for_plane: combined -> :8392 (sessions.py) - web server: accept bifrost_plane="combined" (server.py) - dropdown: combined (:8392) default-selected, single-plane retained (index.html) - #17 contract: endpoint_for_plane FN + plane-selector spec updated to combined - tests: endpoint_for_plane combined, server combined bind -> :8392, dropdown default Suite 506 green. Live-verified on :8765 (current code). |
||
|
|
7f4ceaab2b |
feat(#18): composite Bifrost endpoint — build_combined_app (Deliverable 1)
One ASGI app fronting BOTH the memory.* and affect.* planes (:8392), so a single bound Worldtree session both remembers AND shows live PAD. Closes #18 end-to-end (D2 PAD read-endpoint shipped v0.17.14; D1 was bifrost-blocked, now unparked by bifrost 0.10.0's public build_combined_app + FR-1 resolved — zero Worldtree change). - provider/combined.py: build_combined_provider_app wraps bifrost.consumer.build_combined_app over both stores + mounts the shared affect read route. Advertises both caps by store presence; per-route call-time isolation is bifrost's (INV-013). - affect_store.py: extract add_affect_read_route shared helper (the D2 INV-007 promise — composite + standalone mount the SAME read route over the same affect.db, INV-011). - opfeed.py: plane='combined' derives the OpEvent plane per request path (memory-call->memory, affect-call->affect, handshake->combined; INV-012). - serve_combined.py + ratatoskr-combined-provider console script on :8392 (additive — standalone :8390/:8391 untouched, INV-014). - contract: 18.contract.md § Deliverable 1 (INV-009..INV-014); D1 un-deferred. Latent bug fixed (exposed by the contract-mandated memory `search` dispatch test running through TestClient = a worker thread): open_memory_store lacked check_same_thread=False — the SAME sqlite thread-safety bug already fixed in the affect store (D2). The composite serves the memory plane over HTTP, so a memory-call on uvicorn's threadpool would trip it. Fix: check_same_thread=False + PRAGMA busy_timeout=5000 (memory contract Concurrency note). heid-code-review panel (Groa/Hulda/Regin): ZERO drift findings; the implementation matches INV-009..INV-014 at function-block level. Folded the genuine test-fidelity fix (memory leg describe_store -> search per the contract TEST) + added the PRE-001/PRE-002 guard tests. Suite 486 -> 502 green. |
||
|
|
ca6af6bdaa |
feat(#18): affect.fetch — adopt bifrost 0.10.0 mandatory fetch (D1 prerequisite)
bifrost 0.10.0's _supports_affect_plane (bifrost/affect.py:75-80) now requires a
callable fetch for the affect capability to advertise/dispatch at all (INV-012
strong-or-absent), so an emit-only store 400s on EVERY affect op — repinning past
the affect.fetch release (#12/#13) breaks our shipped affect plane until fetch
exists. Implement affect.fetch as a thin async wrapper over the existing get()
read seam, conformed verbatim to the reference InMemoryAffectStore.fetch:
{"found": False} or {"found": True, "snapshot": <verbatim>}, AffectInvalidArguments
on empty ids, opaque (INV-001 — never reads pad/valence).
This is the forced prerequisite for the #18 D1 composite (build_combined_app),
and a new Worldtree I/O point consumed (affect read-back over bifrost).
- Repin bifrost>=0.8.0 -> >=0.10.0 (uv lock: 0.8.0 -> 0.10.0)
- affect_store.py: add async fetch() over get()
- contract bifrost_affect_provider v1.2: fetch FN block + INV-010 (cap = supported+emit+fetch)
- tests: 3 fetch unit + parity_vs_reference_fetch through dispatch_affect_call
- suite 482 -> 486 green
|
||
|
|
39eebd1a55 |
feat(#18): PAD read-endpoint — web pane renders live PAD/valence from our affect store (Deliverable 2)
The web persona pane now renders live PAD/valence for Tier-3 agents from our
:8390 affect store, closing the persona-telemetry gap (Worldtree persona_state
404s for Tier-3 per ADR-0009; Tier-3 emits no affect_update SSE).
- provider: non-bifrost GET /affect/state/{agent_id} on the affect-store-owning
app (add_route — keeps /bifrost/* top-level + op-feed-skipped); explicit
no_affect_snapshot 404 (never a zeroed PAD); busy_timeout + check_same_thread
on the connection.
- web: GET /api/affect/{agent_id} proxy — end_user_id server-supplied (never the
browser), colon-id round-trip, configured RATATOSKR_AFFECT_READ_URL.
- pane: honest affect render (pad + valence + emitted_at, labelled "affect", no
fabricated Tier-1 fields); explicit empty-state; polls 2s post-turn.
Contract-first (docs/contracts/issues/18.contract.md, Deliverable-2-scoped;
Deliverable 1 / composite endpoint deferred — bifrost-blocked on a public
build_combined_app, WT dispatch confirmed single-endpoint caps-routed).
Heid-code-review panel: 1 INV-001 drift (strip fabricated "neutral") + 4
test-gaps fixed. Live-smoke PROVEN: web->provider->affect.db chain returns real
sindra/vuong PAD; Playwright DOM check confirms the pane render + the fix.
Suite 482 green.
|