a9c521a48a869794d6a86e690c2765dbffdc20a7
59 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
96d61a4bb1 |
feat(provider): split memory search scope_filter → scope_all + scope_any (bifrost 0.8.0/wire v0.6)
Repin bifrost 0.7.0→0.8.0 and reimplement the memory store's search scope filter to the v0.6 split (#11): scope_all (AND/intersection) + scope_any (OR/union over a list of conjunctive scopes), at parity with the v0.6 reference _matches_scope / _validate_scope. No-compat: scope_filter removed. scope_any is the union-visibility primitive that resolves the #295/#297 silent-zero AND foot-gun — a subset-scoped chunk now recalls via an OR member. End-to-end cold recall now gated only on Worldtree emitting scope_any on its recall path (#297, upstream). - store: search(scope_all, scope_any); _scope_subset + _matches_scope + _validate_scope - contract v1.2: search FN sig, INV-005 recomposed, PRE-003 both fields, scope_any_union test - tests: scope_any union, scope_all∧scope_any compose, both-empty match-all; parity vs real 0.8.0 dispatch (433 green) - #17 contract: sync stale scope_filter/_scope_matches-AND refs to scope_all/scope_any - runbook + persistent-memory updated; provider bounced onto 0.8.0 (fresh empty db) v0.17.6 |
||
|
|
aac4353933 |
feat(provider): validate scope_filter against the v0.5 4-axis lattice
bifrost 0.7.0 (wire v0.5) makes agent_self canonical: the scope lattice is
now {end_user, group, tenant, agent_self}. Our store was MORE permissive than
bifrost's reference (no _validate_scope_filter), which silently 0-zeroed the
#295 cold recall instead of a loud 400. Now matched: search rejects an
out-of-lattice axis with InvalidFilter (-> memory.invalid_filter 400), agent_self
admitted. Purely additive — everything that validated before still validates.
Closes the parity gap our own foot-gun flag opened (bifrost-dev shipped the
lattice add #10 off it). Pin bumped bifrost>=0.6.1 -> >=0.7.0. Contract
search PRE-003 + lattice_axes test; 2 new store tests; full suite 429 green.
|
||
|
|
ca02c70b7c |
docs(#17): self-drive+observe contract, bifrost self-test runbook + snapshot
- docs/contracts/issues/17.contract.md — issue-scoped v2.1 contract for #17 (Bifrost-binding the chat client). v1 scope = single-plane bind + dispatch-layer op-feed (composite endpoint + turn-pane UI parked). Design consulted via /heid, paraphrase-gated via /heid-contract-review panel; two internal inconsistencies fixed (OpEvent turn_id reservation made literal; session_id-for-all-verbs correction). Validates OK, prd drift-clean. - docs/bifrost-self-test.md — reusable runbook for driving + observing the full Bifrost round-trip against our own provider (the manual form of #17; pins the consumer-key-as-bearer tripwire). - persistent-memory.md — snapshot: observe brick shipped, self-drive proven, #295 root-caused (upstream, scope-axis asymmetry) -> #296/#297, agent_self -> canonical decided. |
||
|
|
335c835892 |
docs(contracts): complete + refresh #12 presenter STEPS (#3, #4)
Clears the two ✗ FAIL (missing STEPS) the v2.1 parser surfaced. #3: faithful STEPS for CliPresenterState.render, _format_duration_ms, _format_usage (the two formatters also gain PRE/POST from their real asserts). render STEPS enumerate AffectUpdate + AwaitingLlmFirstToken as demoted telemetry (Worldtree #204/#201), extending POST-005 beyond the issue #12 set. #4: refresh the TUI presenter contract from the abandoned single-RichLog double-display model to the shipped four-pane live-Markdown model (v0.5.0-v0.14.0 + Worldtree #201/#204). Rewrites TuiPresenterState.render and _stream_turn_worker (signature, POSTs, STEPS, TESTS), INV-005, the [performance] constraint, the COMPOSE sketch, the CLASS block (BRIEF/PROPERTIES/INV-WIRE-002), the resolved open_question, and the _cancel_via_sse call site. Verified against src/ratatoskr/tui.py and the real test names in tests/test_tui.py. Both contracts: 0 validation errors (pre-existing multi-tracer warnings on _run_turn / action_interrupt left untouched). |
||
|
|
d85ab43d58 |
chore(canonicals): sync contract-parser to v2.1 (e10a4460)
Pulls the upstream contract_parser.py canonical: v2.1 + issue-scoped contract validation (target_module/scope/prd frontmatter, four new test categories scenario/trace/adversarial/property, issue-aware human/list output). Re-pins pinned_sha256_16 + pinned_at in the manifest. canonical_drift.py reports 7/7 OK; no sync-induced contract regressions. |
||
|
|
1f94e5f251 |
docs(provider): memory contract v1.1 — heid-contract-review fixup
Panel review (Gróa/Hulda/Regin) → 11 spec-tightening fixes, no design change:
- INV-001 byte-equal → semantic round-trip (the slip that rode the affect copy-paste)
- search returns top_k IN-SCOPE results (filter-then-limit) — Regin's correctness catch
- idempotency_id reconciled to the reference's ("default", verb, actor, key)
- inline the reference's field keys (id/embedding/scope/distillate) + recalled_view + scope_filter shape
- drop scan from INV-005; clarify metadata_filter-v1 reject, transaction-term, delete atomicity, get_many, revision-on-replay
- revisions: marker records the v1.1 delta
|
||
|
|
eebab46812 |
docs(provider): memory-plane v1 contract (basic plane) + ignore provider runtime DBs
- docs/contracts/bifrost_memory_provider.contract.md: v1 memory consumer spec — SQLite+sqlite-vec basic plane (describe_store / search / get / upsert / delete), honest capability advertisement (edges/atomic/transaction off, optimistic-lock on), affect-reused replay-or-conflict idempotency, #195 parity gate. Memory is a STRUCTURAL INDEX (reads vector/scope/id/origin), not a blind conduit (INV-001). - .gitignore: *.db (+ wal/shm) — provider stores hold persisted agent affect/memory state; never track them. |
||
|
|
d90a58dc48 |
feat(provider): SQLite affect store + ASGI wiring — #195 parity green
The first slice of ratatoskr-as-Tier-3-Bifrost-consumer: a SQLite-backed, conduit-opaque affect store Worldtree emits Tier-3 persona/affect snapshots into, plus the thin build_affect_provider_app wiring. The bifrost library owns the wire; this owns only the store + glue. - ratatoskr.provider.affect_store: open_affect_store / emit / get / build_affect_provider_app. Two-table SQLite schema (snapshots + idempotency). - Conduit-opaque (INV-001): reads only the two addressing keys; whole-blob hash only. LWW-by-arrival across distinct keys; replay-or-conflict idempotency (actor-scoped from ctx), raising bifrost's typed exceptions. - Conformance: #195 parity vs InMemoryAffectStore through the real dispatch_affect_call engine. 17 provider tests; 395 full suite. - Contract v1.1: idempotency model corrected to bifrost's actual semantics (caught by real-lib TDD; the artifact-only review structurally could not). Heid-panel reviewed (contract + code); idempotency-cache TTL pruning, memory.* plane, and the combined two-plane server deferred (see Out of scope). |
||
|
|
1a73d777c9 |
chore(provider): pin bifrost>=0.6.1 + affect-provider contract
Onboard ratatoskr as the Tier-3 Bifrost consumer (durable memory + persona/affect persistence Worldtree writes into). Lands the dependency and the reviewed affect-plane spec; no production code yet — the patch bump fires when the store lands at TDD-green. - pyproject: bifrost>=0.6.1 in a `provider` optional-extra (+ starlette, jsonschema); gitea PyPI index wired, bifrost sourced from it. - uv.lock: bifrost 0.6.1 + jsonschema resolved from the gitea registry. - docs/contracts/bifrost_affect_provider.contract.md: SQLite-backed, conduit-opaque affect store (emit + ASGI wiring). Heid-panel-reviewed (Groa/Hulda/Regin), amended for 8 text-ambiguity findings. |
||
|
|
369857d3f1 |
feat(web): address Heid code-review findings — issue #16 (v0.16.0)
Heid panel review (Gróa + Hulda, thread 01KSP5P6CSJH) on v0.15.0/
v0.15.1 surfaced one load-bearing bug + several precision items. This
pass closes them.
Load-bearing fix — cancel paths targeted the wrong turn_id:
- `_TURN_COUNTER` allocates browser-local ids (1, 2, 3…); the real
upstream Worldtree turn_id (e.g. 799) only arrives in the first SSE
event. The v0.15.x cancel/disconnect/shutdown paths posted to
/sessions/{sid}/turns/{LOCAL_ID}/cancel — wrong URL upstream.
- TurnHandle.upstream_response (dead field) → upstream_turn_id: int|None.
Captured from the first event's sse_id.turn_id in the stream
generator. All cancel paths now target it. Cancel before the upstream
stream starts (upstream_turn_id None) is a no-op
({"cancelled": false, "reason": "not_started"}).
- The old cancel tests mocked the local-id URL, so they encoded the bug;
rewritten to assert the UPSTREAM id is targeted.
Behavior change (minor-bump driver) — server-side end_user_id:
- create_app gains end_user_id kwarg; entrypoint reads
RATATOSKR_END_USER_ID and threads it in. POST /api/sessions uses
app.state.end_user_id, IGNORING any browser-supplied value (a client
can't impersonate an arbitrary end-user partition). JS no longer
sends end_user_id.
Precision fixes:
- Entrypoint missing-extras ImportError catch scoped to starlette/
uvicorn ONLY; baseline-dep / first-party import failures now
propagate as real tracebacks instead of masking as exit-12.
- Lifespan shutdown logs per-pending session_id + upstream_turn_id
(was a single aggregate count).
Tests (+18; 376 total):
- disconnect_triggers_upstream_cancel (INV-005 load-bearing — drives
the stream generator directly + cancels the consuming task; would
have caught the turn_id bug)
- cancel_targets_upstream_turn_id, cancel_before_started_is_noop,
cancel_failed_500
- server-side end_user_id: uses / ignores-body / omits-when-unset
- create_app: routes_registered / state_attached / factory_stored
- entrypoint: default_host / port_zero / happy_argv / open / no-open
- real_import_bug_propagates (precision guard)
- full_event_vocab at the stream-endpoint layer
Contract #16 amended: v0.16.0 amendment banner + INV-005/006 reworded
for upstream_turn_id + FN sketches corrected (server-side end_user_id,
upstream_response→upstream_turn_id, manual client lifecycle vs the
non-executable async-with sketch, not-started cancel branch).
|
||
|
|
1228c37e6f |
feat(web): in-browser debug companion — issue #16 (v0.15.0)
Browser-based debug companion to the Ratatoskr TUI, reusing the
existing wire-layer modules unchanged. Same five surfaces (transcript,
thinking, tools, debug, persona) over the same Worldtree Conversation
API SSE wire, viewable from any device on the operator's LAN.
Per docs/contracts/issues/16.contract.md (full v2.1 module contract
with 11 FN blocks + 9 invariants + Heid panel review pass merged).
Architecture:
- New module `ratatoskr.web` with `server.py` (Starlette app, ~250 LOC),
`entrypoint.py` (lazy-import gate, ~100 LOC), `static/index.html`
(single-page vanilla JS UI, ~360 LOC)
- Optional-deps group `[web]` = starlette + uvicorn[standard]; dev
pulls these in transitively
- New console script `ratatoskr-web`
- Streaming via browser-native `EventSource` GET; prompt-submit is a
separate POST (load-bearing Hulda finding from R13 panel — EventSource
is GET-only)
- Small in-memory turn registry maps (session_id, turn_id) → upstream
request handle for cancel + browser-disconnect cleanup
Endpoint surface (9 routes):
- `GET /` → static index.html
- `GET /static/*` → static assets
- `GET /version` → {"ratatoskr": "<version>"}
- `GET /api/agents` → upstream /agents + local Tier 3 merge
- `POST /api/sessions` → upstream POST /sessions
- `GET /api/agents/{id}/persona_state` → upstream persona-state
- `POST /api/turns/{sid}` → allocate turn_id, register in turn registry
- `GET /api/turns/{sid}/stream?turn_id=N` → proxy upstream SSE to browser
- `POST /api/turns/{sid}/cancel?turn_id=N` → upstream cancel
Trust model: internal LAN debug surface. Binds 0.0.0.0:8765 default;
no auth, no CORS guard (operator direction). What stays disciplined
regardless of network trust:
- Transcript HTML-escapes assistant content (INV-004 — model output
is untrusted text; adversarial HTML must not execute in browser)
- Upstream API key never reaches browser DOM (INV-003 — proxy-only)
Lifecycle:
- Browser disconnect mid-stream → upstream cancel (INV-005;
asyncio.CancelledError caught in stream handler)
- Server Ctrl-C → lifespan shutdown drains turn registry within 5s
budget (INV-006; structured-log line on timeout)
Tests (37 new, 356 total; previous 319 baseline preserved):
- tests/test_web_server.py (23 cases): endpoint contract via Starlette
TestClient + respx mocks; covers each endpoint, browser-disconnect →
upstream cancel, lifespan shutdown draining the registry
- tests/test_web_presentation_contract.py (11 cases): proxy
serialization matches tests/fixtures/presentation_contract.json
for one of each Event type — drift detection between server-side
serializer and the JS presenter without forcing a shared abstraction
- tests/test_web_packaging.py (4 cases): static asset packaging via
importlib.resources; AST-checked lazy-import discipline (no top-
level starlette/uvicorn import in entrypoint.py); missing-API-key
exit-11 path; missing-extras exit-12 path
Provenance:
- Scope v1 → Heid panel review (Gróa + Hulda, R13) → 8 load-bearing
corrections (POST→GET split, Starlette > FastAPI, lazy-import
discipline, browser-disconnect → upstream cancel, presentation-
contract fixture, error event contract, static-asset packaging,
escaped plain-text Markdown deferred) merged into scope v2
- Operator direction: internal-LAN debug surface; auth + CORS
deliberately omitted
Not yet (deferred to v0.16.x+):
- Cross-reload session resume via Last-Event-ID
- Tier 3 lifecycle UI (define/patch/delete in browser)
- Markdown rendering with vendored safe-subset renderer
- TLS + real auth (only if a non-LAN use case ever surfaces)
|
||
|
|
9918c10acf |
fix(tui): coalesce thinking deltas on \n (v0.7.1)
Operator: "thinking tokens seem to be split by token — each on a
newline, is that correct? We don't want that."
Root cause: v0.6.5 wrote each Thinking SSE delta as its own
`thinking_log.write(event.content)` call. Worldtree emits Thinking
events at token granularity (per-token or per-few-tokens), so EACH
token became its own RichLog line — visually choppy, one short
fragment per visual row. Wrong UX.
## Fix: coalesce-on-newline
Thinking deltas accumulate in `TuiPresenterState.thinking_chunk_buffer`
(new str field). On each Thinking event:
1. Append delta content to buffer.
2. Flush every COMPLETE line (chars before each `\n`) as one
thinking_log.write(line) call.
3. Leave the post-final-`\n` tail in the buffer for the next delta.
On any non-thinking event (run close):
1. Flush remaining buffer tail (if any) as one final line.
2. Write Rule(end).
Empty lines (blank paragraph separators in the model's `\n\n` flow)
are skipped — they'd render as no-content RichLog entries which
just add vertical noise. Natural paragraph breaks become single
visible lines; multi-paragraph thinking renders top-to-bottom.
## Verified live (tier-3 smoke against personal Worldtree)
Defined a `thinky-smoke` agent via `python -m ratatoskr.tier3 define`,
asked "What is 12 times 13?". Thinking pane rendered with natural
paragraph chunks:
── turn N · thinking #1 start ──
Thinking Process:
1. **Analyze the Request:** The user wants to know the result of $12 \times 13$.
2. **Calculate:**
* Method 1: Standard multiplication.
$$12 \times 10 = 120$$
$$12 \times 3 = 36$$
$$120 + 36 = 156$$
* Method 2: $(10 + 2)(10 + 3) = 100 + 30 + 20 + 6 = 156$.
── turn N · thinking #1 end ──
Each line = one natural paragraph or list item. No per-token fragments.
## Edge cases noted
- Long-running thinking with NO `\n` at all stays buffered until run
close → operator sees nothing until close. Possible follow-up: add
a length-threshold flush (e.g., > 500 chars → flush at the last
space). For now this is acceptable; thinking content typically has
`\n` breaks every few sentences.
- Empty deltas (`""`) are ignored implicitly — no buffer growth, no
flush.
- `\n` at the very start of a delta flushes whatever was buffered
before, then leaves the empty post-`\n` tail (empty string) in the
buffer, which doesn't show up as an empty line because of the
`if line:` guard.
## Contract amendment
docs/contracts/issues/13.contract.md INV-022 amended for v0.7.1
coalesce semantics. Drift-check clean.
## Tests
265/265 GREEN; ruff clean. Two updated tests:
- `test_thinking_streams_into_thinking_log` → renamed
`test_thinking_coalesces_until_newline`: 3 token-shaped deltas
with no `\n` → only Rule(start) writes, buffer holds accumulated.
- NEW `test_thinking_flushes_on_newline`: delta carrying `\n` →
Rule(start) + accumulated line + clear buffer.
- `test_thinking_closes_to_thinking_log`: 2 deltas "a", "b" +
close → Rule(start) + tail-flush "ab" + Rule(end) = 3 writes
(was 4 with per-delta).
Patch bump (v0.7.0 → v0.7.1) — internal presenter routing change;
no public-API or layout change.
|
||
|
|
c086ae2b32 |
feat(tier3): ratatoskr.tier3 module + CLI (v0.7.0)
Issue #15. Worldtree Phase 2.0 ships Tier 3 (consumer-defined) agents at `<user_id>:<agent_name>`; ratatoskr now exposes their lifecycle via a dedicated module + CLI tool. The picker handles the colon-containing agent_id generically (per issue #8 out-of- scope clause); session creation works unchanged. What was missing was a way to DEFINE / PATCH / DELETE these agents from ratatoskr itself — operators previously had to curl the API directly. ## Public surface (ratatoskr.tier3) Tier3AgentInfo (frozen dataclass) define_agent (client, *, agent_name, system_prompt, model) → Info patch_agent (client, agent_id, *, system_prompt?, model?) → Info delete_agent (client, agent_id) → None Tier3QuotaExceeded — 429 agent_quota_exceeded (50-agent cap) Tier3UserIdUnsupported — 403 tier3_user_id_unsupported Tier3FieldNotMutable — 422 field_not_mutable (PATCH) Tier3LayerDeferred — 422 layer_deferred (define, defense-only) Tier3AgentNotFound — 404 SessionApiFailed (reused) — all other non-2xx Caller-owned httpx.AsyncClient posture (same as ratatoskr.sessions). Module is standalone — does NOT import sessions/sse_client/tui/cli beyond reusing the USER_AGENT constant from cli. ## CLI (python -m ratatoskr.tier3 <subcommand>) define --name <slug> --system-prompt <str> --model <id> patch <agent_id> [--system-prompt <str>] [--model <id>] delete <agent_id> Auth resolution mirrors ratatoskr.cli verbatim — --api-key flag > $WORLDTREE_API_KEY > exit 11. Server URL via --server > $WORLDTREE_API_URL > http://localhost:8000. Exit codes follow the cli.py matrix: 0 / 10 (usage) / 11 (auth) / 20 (api-failure) / 21 (network). ## Real-world finding from live smoke Tier-3 agents do NOT appear in `GET /agents` — the public list filters them out. The picker won't surface tier-3 agents; operators bypass it via `ratatoskr --send "..." --new --agent ratatoskr:<n>` directly. This contradicts the contract's acceptance assumption ("the new tier-3 agent should appear in the list") — caught at smoke time. The picker integration was hopeful; the real shape is "you know your tier-3 agent_id because you defined it." Adding a ratatoskr-side `tier3 list` subcommand would need a Worldtree endpoint that doesn't exist today; surfacing to worldtree-dev as a followup. ## Live lifecycle smoke (personal Worldtree v0.16.2) $ python -m ratatoskr.tier3 define --name smoke-tier3 \ --system-prompt "..." --model qwen3.6-35-a3b → defined ratatoskr:smoke-tier3 (qwen3.6-35-a3b) $ ratatoskr --send "hello via tier-3" --new --agent ratatoskr:smoke-tier3 → [done] turn_id=286 model=qwen3.6-35-a3b duration=14.2s usage 44 in → 390 out (434 total, 0 cached) $ python -m ratatoskr.tier3 delete ratatoskr:smoke-tier3 → deleted ratatoskr:smoke-tier3 $ python -m ratatoskr.tier3 delete ratatoskr:smoke-tier3 → [agent_not_found] ratatoskr:smoke-tier3 (exit 20) The colon-containing agent_id flowed transparently through ratatoskr.sessions.create_session, the SSE stream's text + worker_phase + done events all rendered correctly, and the ratatoskr.sessions module needed zero changes. ## Contract docs/contracts/issues/15.contract.md — new module spec; drift-check clean. Acceptance criterion about "appears in GET /agents" should be amended in a follow-up to reflect the empirical finding. ## Tests +26 tests (264 total GREEN, was 238). Covers all error paths via respx mocking — quota, user_id, layer_deferred, field_not_mutable, 404, 5xx — plus CLI happy + error paths. ruff clean. Minor bump (v0.6.5 → v0.7.0) per SemVer etiquette: new public module + CLI surface; new caller-visible behavior. |
||
|
|
d3569904bc |
refactor(tui): thinking streams into whole pane (v0.6.5)
Operator: "Why does the thinking scroll a little section at the
bottom of the thinking pane instead of scrolling the whole pane?"
Root cause: v0.6.1's thinking-current Static was docked to the
bottom of the Thinking pane and rendered the last 200 chars of
streaming content. As deltas arrived, the displayed 200-char tail
shifted — old text fell off the left, new text appeared on the
right — visually reading as "a little section scrolling at the
bottom" while the larger thinking-log RichLog above showed only
the previous run's closed content (or nothing on first turn).
## Fix: stream directly into thinking-log
The Static is gone. Thinking deltas now write straight to the
`thinking-log` RichLog (one delta = one line in the scrollable
log). The whole pane scrolls naturally as content arrives —
operator can switch to Ctrl+3 and see streaming content fill
the pane top-to-bottom.
Routing pattern:
First Thinking delta of run:
→ write Rule(title="turn N · thinking #K start") to thinking_log
→ write delta content as a line
→ set thinking_open = True
Subsequent Thinking deltas:
→ write delta content as a line
Non-thinking event (closes the run):
→ write Rule(title="turn N · thinking #K end") to thinking_log
→ reset thinking_open
The Rule(start) at the top of an in-progress run is now the
"thinking is happening" indicator. No more separate live-preview
widget required.
## Trade-off: no markdown re-render
Pre-v0.6.5 closed runs got a Markdown(full_content) render between
the start/end Rules. v0.6.5 drops that — the streamed deltas ARE
the content; re-rendering as Markdown would either need to wait
for run-end (no streaming) OR re-render incrementally per delta
(bad UX). Streaming wins for "live observability" framing.
The downside: if model thinking has Markdown structure (lists,
code), it renders as raw text. Acceptable per operator's "stream
in line" framing.
## Removed widgets
- `Static#thinking-current` (right column / Thinking pane bottom)
- `TuiPresenterState.render` no longer takes a `thinking_widget` param
- `TuiPresenterState.thinking_buffer` field dropped (no accumulation)
- `_stream_turn_worker` no longer queries `#thinking-current`
- `on_mount` no longer hides `#thinking-current`
- DEFAULT_CSS `#thinking-current` block removed
## Contract amendment
INV-022 amended: thinking now streams as raw delta lines, not
Markdown-rendered on close. INV-024 amended: thinking-current
Static removed entirely (was relocated v0.6.1, removed v0.6.5).
Drift-check clean.
## Tests
238/238 GREEN (was 241 — 3 obsolete widget tests deleted:
test_thinking_widget_truncation, test_thinking_widget_visibility_lifecycle,
test_terminal_events_belt_and_braces_widget_cleanup). 5 routing tests
rewritten for the new streaming shape (test_thinking_streams_into_thinking_log,
test_thinking_closes_to_thinking_log, test_multiple_thinking_runs_...,
test_render_exception_fallback, test_cancelled_mid_thinking_closes,
test_left_column_content_only).
ruff clean. Manual injection test confirms routing: Rule(start) +
delta lines write to thinking_log; transcript untouched.
Patch bump (v0.6.4 → v0.6.5) — internal restructure within Thinking
pane; presenter signature narrowed; no caller-visible public API
change (RatatoskrApp + AgentPickerApp surfaces identical).
|
||
|
|
8463eb22ff | docs(contract): amend issue #13 INV-024 for v0.6.1 thinking-current relocation | ||
|
|
cfee89ac1c |
refactor(tui): streaming + turn headers + Thinking pane + picker fix (v0.6.0)
Operator-driven big-batch polish + restructure:
## 1. Streaming text — no more per-token RichLog spam
Pre-v0.6.0, every Text SSE delta wrote its own RichLog line, so
"Let me read the..." became 4+ separate lines (a Worldtree-style
sentence-by-sentence reveal that read as broken). v0.6.0 adds a
`#current-text` Static docked above the prompt; TuiPresenterState
buffers Text deltas in `text_buffer` and updates the Static in
place. On terminal event the Static clears and the transcript
gets:
- raw=False: post-Done Markdown body + Rule separator
- raw=True: accumulated plain text
The Static collapses to height=0 when empty so the prompt sits at
the column bottom unchanged.
## 2. Turn-ID headers across every pane
`_stream_turn_worker` writes a `Rule(title="turn N")` to all four
log panes (transcript, tools, debug, thinking) on the first event
of each new turn. Operators can now visually correlate "what
happened in Tools during turn 42" by section markers in matching
positions across panes.
## 3. New Thinking TabPane (Ctrl+3)
Closed thinking runs now route to `#thinking-log` (a dedicated
TabPane) instead of `#debug-log`. Each closed run writes three
entries:
- Rule(title="turn N · thinking #K start")
- Markdown(thinking_content)
- Rule(title="turn N · thinking #K end")
Model reasoning often has lists/code/structure — rendering as
Markdown (instead of the previous "· thinking: ..." prefix line)
makes it scannable. The `thinking_run_index` counter scopes per
turn so multi-thinking-run turns get distinct markers.
`thinking-current` Static (live per-delta preview) stays in the
right column above TabbedContent (unchanged from v0.5.0) — live
visibility persists across tab switches.
## 4. Agent picker — multi-line items, full description visible
Pre-v0.6.0 the picker rendered each agent as a single Label with
"{id} · {name} — {description}", which truncated descriptions
visually. v0.6.0 uses two Static children per ListItem:
- bold Aurora bright-blue line: "{agent_id} · {name}"
- wrapped Sea dark-60 line(s): full description
ListItems are auto-height so long descriptions wrap as needed.
Highlighted (--highlight) row uses Sea dark-30 background instead
of Aurora blue (which the operator flagged as ugly).
## 5. Kill residual blue chrome
The user's "background is still blue" report traced to the prompt
Input's focused border, which I'd set to $primary (Aurora blue).
Switched to $au-bright-cyan (#42dcd1) — focus highlight is now
cyan, consistent with the operator's-voice accent throughout the
TUI. Also added explicit overrides for ContentTabs strip
background + active-tab underline color → Australis cyan.
## 6. Surfaced emotion-appraisal request to worldtree-dev
User asked for emotion-appraisal telemetry, but no SSE event for
this exists in the spec — persona/Vili affect lives in persona.log
(file-tail, blocked on remote-Worldtree topology) and per-character
state (poll endpoint, not per-turn). Posted an althing thread
proposing two shapes (worker_phase payload extension OR new
affect_update event type) and routing the decision to their team.
A 4th `Emotion` TabPane plugs in trivially when a wire event lands.
Low-priority / quality-of-life framing — not blocking ship.
## Contract amendment
docs/contracts/issues/13.contract.md amended in-place: INV-019
extended to 3 TabPanes; new INV-021 (Text → current_text Static),
INV-022 (thinking closed runs → thinking_log with Markdown +
start/end Rules), INV-023 (turn-ID headers across all panes),
INV-024 (thinking-current Static stays in right column with
"thinking… " prefix per v0.5.1 polish). INV-020 (render-exception
fallback routing) updated for Thinking → thinking_log. Drift-check
clean.
## Tests
241 GREEN (down from 244 in test count — 5 routing tests rewritten
for the new shape, replacing the v0.5.0 thinking-in-debug-log
assertions with the v0.6.0 thinking-log-as-Markdown shape; net
test coverage equivalent). ruff clean.
Live smoke against personal Worldtree's mimir confirmed:
- transcript: 27 lines (turn header + user echo + done +
markdown body, NO per-token spam)
- thinking_log: 19 lines (turn header + 2x thinking start/end
Rule sections with Markdown bodies)
- current_text cleared post-Done
Minor bump (v0.5.1 → v0.6.0) per SemVer etiquette: visible routing
+ new pane = operator-observable surface change.
|
||
|
|
ffd22fb587 |
refactor(tui): content-only main pane + Debug tab + dark chrome (v0.5.0)
Two operator-driven changes off v0.4.1: 1. **Main pane is content-only.** Pre-v0.5.0 the transcript mixed assistant text with telemetry (Thinking closed runs, WorkerPhase, TextBoundary) — only tool events were factored out per #13. The transcript now receives ONLY: user-prompt echo, assistant Text deltas, [done]/[error]/[cancelled] terminal labels, and the post-Done Markdown render. All telemetry routes to a new Debug tab in the right column. 2. **Chrome no longer blue.** Textual's default Header / Footer / active-tab styling tints with `$primary` (Aurora blue under Australis), which read as garish on dark terminals. Header, Footer, and the TabbedContent tab strip get explicit `background: $surface` (Sea bright-black #373b46) so the chrome sits cool and unobtrusive against the Ice black background. ## Layout reshape ``` LEFT COLUMN (content only): RIGHT COLUMN (telemetry): transcript (RichLog, 1fr) thinking-current (Static, dock top) prompt (Input, dock bottom) TabbedContent: Tools (tool_start, tool_result) Debug (thinking, worker_phase, text_boundary) ``` The thinking-current live-preview Static moves from left → right column so the left column is genuinely content-only. Live thinking visibility now persists across tab switches (it docks above the TabbedContent, not inside any tab). ## Presenter routing (TuiPresenterState.render) Signature widens with `debug_log: RichLog`. Routing matrix: Text → log (transcript) Done / Error / Cancelled → log (transcript) [terminal labels] ToolStart / ToolResult → tools_log (Tools tab) Thinking (closed run) → debug_log (Debug tab) WorkerPhase → debug_log (Debug tab) TextBoundary → debug_log (Debug tab) Thinking (per-delta) → thinking_widget (live preview) INV-009 render-exception fallback preserves routing per event class (new INV-020) — ToolStart/Result falls back to tools_log; Thinking/WorkerPhase/TextBoundary to debug_log; everything else to log. ## Keybindings - Ctrl+1 → Tools tab (existing, unchanged) - Ctrl+2 → Debug tab (NEW) `pane-name` footer widget updates dynamically as the operator switches tabs ("Tools" ↔ "Debug"). This was previously deferred to "the multi-tab issue" per the Volva contract-review amendment; multi-tab now exists, so the dynamic update lands here. ## Contract amendments docs/contracts/issues/13.contract.md amended in-place: - INV-015 amended: transcript is content-only; telemetry routes to debug_log. Old routing (telemetry in transcript) retired under the no-backwards-compat rule. - INV-017 amended: thinking-current docks to right column (was left). - INV-019 new: two TabPanes (Tools + Debug), Ctrl+1/Ctrl+2 bindings, dynamic pane-name update. - INV-020 new: render-exception fallback preserves per-event-class routing. - Layout-spec snapshot ASCII diagram updated. Drift-check clean. ## Tests 239/239 GREEN (+3 new: debug_tab_exists, ctrl_2_activates_debug_tab, pane_name_updates_on_tab_switch). 6 existing tests adjusted for the new routing (test_thinking_closes_one_debuglog_entry, test_multiple_thinking_runs_each_get_debuglog_entry, test_render_exception_fallback, test_cancelled_mid_thinking_closes, test_worker_phase_demoted_to_debug_log, test_left_column_content_only). ruff clean. Live smoke against personal Worldtree: mimir KB-search turn populated tools_log with 11 lines of tool events (search_library + read_note); debug_log with 20 lines of worker_phase + thinking content; transcript stayed content-only with `❯ user-prompt` (Aurora bright-cyan) + assistant text deltas. Routing matrix holds end-to-end. (Diagnostic note: RichLog.lines is the rendered- output buffer; inactive TabPane content shows lines=0 until the tab activates and renders. Internal write store is correct — this is a Textual rendering quirk, not a routing bug.) Minor bump (v0.4.1 → v0.5.0) per SemVer etiquette: visible routing surface change for operators; transcript and Debug tab contents look different from yesterday's v0.4.1. |
||
|
|
24e4371ec7 |
feat(tui): issue #13 — §5 layout reshape + Tools pane (v0.4.0)
Reshape the TUI from vertical-stack single-pane to Horizontal two-column with TabbedContent on the right; v1 has a single Tools tab that consumes ToolStart/ToolResult SSE events previously rendered inline in the transcript. Foundation for the rest of design-brief §5; subsequent panes (Persona/AdminEvents/BifrostState/ ServerLog) plug in as sibling TabPanes when their substrate blockers resolve. Three coupled pieces, all in-place amendments to issues #4 + #12: - **Layout**: compose() yields Horizontal#main-row containing Vertical#left-column (transcript + thinking-current + prompt) and Vertical#right-column (TabbedContent#side-panes with TabPane#tools-tab → RichLog#tools-log). Width split 2fr:1fr. CSS dock rules narrow to per-container scope so thinking-current toggling doesn't reflow the right column. - **Tools pane**: TuiPresenterState.render() signature widens with tools_log: RichLog. ToolStart/ToolResult route there per INV-014; every other event keeps its issue-#12 routing. Plain-label fallback under render-exception preserves routing (INV-009). - **Ctrl+1 binding + pane-name widget**: BINDINGS gains Binding("ctrl+1", "focus_tools") which programmatically sets TabbedContent.active; Textual's default preserves Input focus per INV-016 (test asserts; regression path documented). Static#pane-name in the footer renders "Tools" v1 (static — no tab-switch handler wiring lands in #13 per amendment-2 from Volva paraphrase review). CLI mode (--send) is unaffected by design per INV-018 — non- interactive, no tabs concept; CLI keeps inline tool-event rendering. Contract: docs/contracts/issues/13.contract.md (drift-check clean, two amendments applied from Volva contract-paraphrase pass). Tests: +9 net (TestLayoutShape × 7 + TestTuiPresenterState routing × 3, minus 1 deprecated test_tool_start_demoted superseded by test_tool_start_routes_to_tools_log). 236 total GREEN; ruff clean. Live smoke against personal Worldtree's mimir: tool-using turn (KB search) populated tools_log with tool_start + tool_result for search_library + read_note; transcript stayed chat-only with worker_phase + thinking. Routing-not-duplication confirmed end-to-end. |
||
|
|
d30be12deb |
feat(sessions,cli,tui): issue #8 — startup agent picker (v0.3.0)
Adds GET /agents fetch + ListView picker for bare `--new` (TUI mode without --agent). Three in-place amendments: - ratatoskr.sessions: new `list_agents()` + `AgentInfo` frozen dataclass with omit-when-null/empty defaults mirroring SessionInfo's INV-001/INV-002 origin-conditional pattern. Non-200 responses raise the existing SessionApiFailed (no new exception). - ratatoskr.cli: `_parse_args` softens `--agent` from absolute to mode-conditional — required for `--send --new`, optional for bare `--new`, forbidden with `--session` (unchanged INV-004). - ratatoskr.tui: new `AgentPickerApp(App[str | None])` — separate Textual App (not Screen-within-RatatoskrApp) so list_agents errors land on real stderr before any alt-screen opens (preserves issue #6's INV-001). `_resolve_then_run` gains a pre-create branch: fetch agents → empty list → exit 13; non-200 → exit 20; network error → exit 21; picker dismissed → exit 0; otherwise thread chosen agent_id into create_session. Contract: docs/contracts/issues/8.contract.md (drift-check clean). Tests: +18 (227 total, was 209). Live smoke against personal Worldtree (:8081) returned 12 agents; programmatic picker drive auto-picked lofn and created a real session with `end_user_id="ratatoskr-tui"`. |
||
|
|
3b9c610587 |
feat(cli,tui): issue #12 — presenter contract semantics amendment (v0.2.0)
Replaces the stateless _render_event / _render_event_to_log helpers with stateful per-turn presenters (CliPresenterState / TuiPresenterState). Coalesces thinking-event deltas into a single growing display per run; demotes telemetry events with editorial hierarchy; formats duration + usage for human reading. Headline behavior change: a 50-token thinking phase now renders as ONE coalesced growing line in CLI (or one closed RichLog entry + per-delta live Static widget in TUI), not 50 lines of [thinking] spam. Editorial promotion line (issue #12 INV-002): - Load-bearing (no demotion prefix): Text, Done, Error, Cancelled - Demoted telemetry (`. ` ASCII prefix in CLI; dim `· ` in TUI): WorkerPhase, Thinking, TextBoundary, ToolStart, ToolResult Stateful coalescing: - Thinking deltas accumulate into thinking_buffer; first non-thinking event closes the run with a single \n boundary in CLI / one closed dim RichLog entry in TUI. - TUI adds a dedicated Static(id="thinking-current") widget that shows the last ~200 chars of the active run, mirroring per-delta updates. Two-views-of-thinking decoupling per INV-004: chronological RichLog + always-visible widget. - CLI INV-005: when stdout text was streamed mid-line, text_written_since_newline triggers a stdout flush + \n before the next stderr terminal label — guarantees [done] / [error] / [cancelled] land on their own line in a TTY without breaking pipe-to-file scripted consumers. Formatting helpers (issue #12 INV-006 / INV-007): - _format_duration_ms — autoscale `347ms` / `5.5s` / `1.2m` - _format_usage — natural-language `6756 in -> 126 out (6882 total, 0 cached)` with arrow="->" CLI / "→" TUI Cross-frontier design pass (eitri-smithy-dev, althing 01KSBE52YZR5E3SPTKA672JE43) returned 16-of-16 confirmed decisions + 4 material divergences applied: - ASCII `. ` prefix in CLI (`·` is U+00B7, not ASCII) - RichLog one-closed-entry-per-run + Static per-delta updates (not inline-mirror as initially proposed) - presenter-state object instead of pure-function rendering - Framed as "contract semantics amendment", not "polish" Volva paraphrase round (5 prose-precision fixes applied to 12.contract.md): INV-001 "growing display" semantics; single hide mechanism for the Static widget (Textual reactive `display: bool`); [render_error] security clause (type-only, no exception message); text_written_since_newline `\n`-terminated text corner case; [create_session] integration path (bypasses state.render — not an SSE Event variant). Volva code-review round (5 findings applied): - F1 drift: render-exception fallback now writes BOTH a plain-label fallback line for the original event AND the `[render_error] <type>` line (was missing the fallback half). - F2 drift: dim Rich style applied to all demoted-telemetry RichLog writes via `rich.text.Text(..., style="dim")` (was plain str). - F3 drift: belt-and-braces widget clear+hide on EVERY terminal event (Done/Error/Cancelled), even when thinking_open was False. - F4 precision: _format_usage gains PRE-001 assertion on the four expected usage keys. - F5 precision: _run_turn signature amended in issue #3 contract to document the new `state: CliPresenterState | None = None` test- injection kwarg. [create_session] lifecycle line demoted to `. create_session:` (written directly by _amain; bypasses state.render since it's not a wire-level SSE Event variant). Pre-amendment _render_event / _render_event_to_log and their test classes removed under the no-backwards-compat rule. Issues #3 and #4 contracts amended in-place: #3 (CliPresenterState CLASS + FN block + helper FN blocks + _run_turn signature + _amain create_session demotion); #4 (TuiPresenterState CLASS + FN block + compose Static widget + _stream_turn_worker state construction). 209 tests GREEN; ruff clean. Bumps v0.1.0 → v0.2.0 (minor — output shape change breaks pre-amendment grep patterns like `[thinking] '`; no public API surface change beyond the rendering contract). Persistent-memory commit-along: captures the issue #12 decision, forward direction (require end_user_id for every access — declined worldtree-dev's requires_end_user_id offer because we'll send it universally), and the Heimdall scope-model foot-gun note (the "per-Tier-1-agent scope add" diagnosis was a phantom ask resolved by worldtree-dev's correction; agent.call:* baseline covers all Tier 1). |
||
|
|
804c2df6eb |
feat(sessions,cli,tui): issues #5 + #6 + worldtree-dev consumer-API follow-up
Issue #6 (TUI startup error visibility): restructure run_tui lifecycle so pre-App.run() failures land on real stderr instead of getting eaten by the alt-screen teardown. New _resolve_then_run async helper opens the AsyncClient via async-with, does pre-flight session resolution, routes AgentNotFound / SessionApiFailed / network errors to sys.stderr (verbatim same labels + exit codes as cli._amain), then constructs RatatoskrApp with pre-resolved state and awaits app.run_async(). RatatoskrApp.__init__ signature widens to (args, *, session_id, agent_id, client) — all three required. on_mount narrows to identity-widget population; on_unmount becomes a no-op (client lifetime owned by run_tui's async-with). Issue #5 (--end-user-id for per-end-user agents): sessions.create_session gains keyword-only end_user_id kwarg with PRE-003 non-empty assertion; ParsedArgs.end_user_id field added (default None); --end-user-id flag with non-empty validation; _amain + _resolve_then_run thread it to their create_session calls. RATATOSKR_END_USER_ID env-var fallback (flag > env > None) per the post-2026-05-23 amendment; env.sh (gitignored) ships "ratatoskr-tui" as project-stable partition default. Worldtree-dev consumer-API follow-up (althing 01KSBARG2B8M): User-Agent header added (ratatoskr/<version> (vh@phasefinal.com), version pulled via importlib.metadata) to both AsyncClient constructions so server logs can distinguish ratatoskr traffic from other consumers. Volva code-review (2 rounds on #6) found 8 test-precision gaps + 1 PRE assertion drift, all Category 1 fixed: missing PRE-001 at _resolve_then_run entry; Rule separator assertions on markdown render; RichLog-write spy on empty submit; input-cleared + no-new-worker on cancelling busy; worker.cancel observation on three force-exit paths; on_unmount-no-close focused test (the prior client-lifetime test patched run_async so on_unmount was never exercised); happy --new resolve test verifying POST count + identity propagation. Issues #2/#3/#4/#5 contracts amended in-place to reflect: - create_session widened (PRE-003, body construction step, body shape POST) - ParsedArgs description + _parse_args STEPS + _amain create_session call + new TESTS for end_user_id + env-var fallback - _resolve_then_run STEPS + new TEST entries; on_mount narrowed; INV-007 amended for new client ownership - Post-#6 adjustment note on issue #5 (_resolve_then_run replaces on_mount as the threading site since #6 moved session resolution out of the alt-screen) 188 tests GREEN; ruff clean. Bumps to v0.1.0 — first minor release, the load-bearing reason is RatatoskrApp.__init__'s breaking signature change (additive end_user_id alone wouldn't have triggered a minor pre-v1.x). Files Gitea issues #9 (spec-pin refresh v0.19.0 → v0.22.1), #10 (track Worldtree #196 subject:{type,id} migration), #11 (AdminEvents pane auth prerequisite admin.events.read). Infra-ops pinged via althing for agents.call:lofn scope add (broker pattern; they forwarded to worldtree-dev because personal Worldtree exposes no public scope-mutation endpoint). |
||
|
|
7028c5bc11 |
contract(issue#7): author + amend #1/#3/#4 for empty-skip + MalformedSseData
Issue #7: mid-stream robustness fix discovered via 2026-05-22 crash. Long mimir TUI conversation (turn 93, 1077 events consumed) crashed on event 1078 with JSONDecodeError("Expecting value: line 1 column 1 (char 0)") from json.loads('') on an empty-data SSE frame. _iter_events unconditionally called json.loads on every dispatched event; when httpx_sse surfaces a frame with id: present but data: empty/missing (a known library-vs-spec divergence), parsing fails and propagates. Two-rule fix in _iter_events: - Empty sse.data (exact `== ''`): SKIP silently per SSE spec (keepalive semantics). Don't yield, don't advance last_sse_id, don't set terminal_seen. ORDERING: skip fires BEFORE _parse_sse_id, so a keepalive with a malformed id is still a keepalive (intentional). - Non-empty sse.data that fails json.loads: raise new MalformedSseData (sibling to MalformedSseId, mirrors raw[:200] truncation pattern). Wire-level protocol error; presenters route to [malformed_sse_data] + exit 22 in cli, transcript label + state→idle in tui (INV-008). Volva paraphrase round: 4 ambiguities, all amended: 1. INV-001 prose tightened — exact `sse.data == ''` rule made prominent; "keepalive" framing demoted to intent-not-rule; whitespace-only data explicitly listed as malformed (not skipped); specific state names (last_sse_id, terminal_seen) instead of vague "any counter". 2. STEPS pseudocode spells out the ordering — empty-skip happens BEFORE _parse_sse_id; empty-data with bad id is silently swallowed. 3. empty_data_skipped test description fixed (had off-by-one count + wrong wording around last_sse_id intermediate state). 4. (paired with #1 above). Volva code-review post-implementation: 3 findings, all addressed: F1 (test-gap): empty_data_skipped proves yielded events but not internal last_sse_id non-advancement. New empty_data_skip_preserves_last_seen_sse_id test probes via SseConnectionDropped.last_seen_sse_id after a drop following the skipped frame — if the skip had transiently advanced last_sse_id, the exception payload would carry the wrong value. F2 (precision, contract amend): MalformedSseData ERROR_ROUTING said "log truncated raw" but stream_turn doesn't log — sse_client is a library, presenters own observability. Amended to "propagate to caller (no logging at sse_client layer); presenters log exc.raw." F3 (test-gap): cli malformed_sse_data test asserted label but not `raw='X'` shape and not truncation. Tightened existing test + added malformed_sse_data_truncation with 5000-char payload — verifies MalformedSseData.raw truncation carries through the presenter's repr() rendering. **Calibration milestone**: issue #7 is the first issue with ZERO drift findings from Volva code-review. TDD caught all runtime behavior cleanly. The 3 findings were assertion-precision and architectural-correctness-of-wording, not behavioral. Hypothesis: tighter contract spec + smaller code surface shifts Volva's role from "catch behavioral drift" to "tighten observability + wording". Cumulative calibration table: #1 (4 findings, 3 drift + 1 test-gap), #2 (3, 1+1+1 precision), #3 (5, 3+1+1), #4 (8, 5+2+1), #7 (3, 0 drift + 2 test-gap + 1 precision). Contracts touched (all drift-check clean): - docs/contracts/issues/7.contract.md (new): the coordinating record. - docs/contracts/issues/1.contract.md: _iter_events STEP 3.0 empty-skip + ordering note; STEP 3.c JSONDecodeError → MalformedSseData; new MalformedSseData ERROR_ROUTING (propagate-to-caller wording per F2); 4 new TESTS entries including F1's last-seen probe. - docs/contracts/issues/3.contract.md: _run_turn ERROR_ROUTING + malformed_sse_data tests (incl. F3 truncation). - docs/contracts/issues/4.contract.md: INV-008 mentions MalformedSseData; _stream_turn_worker ERROR_ROUTING + new TEST. |
||
|
|
61c3941ec3 |
fix(client): disable SSE read timeout — caught by personal Worldtree smoke
First manual smoke against personal Worldtree (10.250.50.152:8081) produced httpx.ReadTimeout mid-stream after the worker_phase BuildingPrompt event. Root cause: httpx's default 5s read timeout killed the connection during mimir's thinking phase (LLM streaming has multi-second idle gaps between SSE events). Fix at the caller layer (where the AsyncClient is owned): - cli._amain and tui.on_mount now construct AsyncClient with timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0). read=None disables the SSE-killing timeout; connect/write/pool keep modest timeouts so true network failures still surface promptly. Defense in depth in sse_client.stream_turn: - ERROR_ROUTING now also catches httpx.ReadTimeout (was just ReadError | RemoteProtocolError) and surfaces it as SseConnectionDropped, so if a caller misconfigures their client the failure is at least a named exception the presenters handle. Contract amendments (in-place): - Issue #1: new [compatibility] constraint documents the read=None recommendation; ERROR_ROUTING for stream_turn lists ReadTimeout alongside ReadError/RemoteProtocolError. - Issues #3 + #4: AsyncClient construction step now spells out the timeout shape explicitly. Smoke after fix: SSE stream consumed cleanly, agent responded, [done] turn_id=88 model=qwen3.6-35-a3b duration_ms=2351. Stdout-only (2>/dev/null) returned clean agent text + exit 0 — INV-002 stdout/stderr split holds end-to-end against real wire. Wire-compat envelope (personal v0.16.2 vs ratatoskr's v0.19.0 pin) confirmed. 164/164 tests GREEN; ruff clean; all three drift checks clean. Note: TUI mode not smoke-tested from this CC session (needs a TTY; operator-side check via `source env.sh && uv run ratatoskr --new --agent mimir`). |
||
|
|
942e33898c |
fix(tui): address Volva code-vs-contract drift (issue #4)
Volva code-review surfaced 8 findings against the TDD-passing
TUI shell. All 8 addressed.
Drift fixes (code):
- Primary: INV-002 + INV-003 require visible Footer-area rendering
of session-identity + Ctrl-C state hint. Implementation stored
the strings in `self.sub_title` (which lands in the Header, not
Footer) and `self.hint` (a plain attribute, never rendered). Fixed
by adding two `Static` widgets (id="identity" and id="hint") in
compose; the `_set_hint()` helper mirrors state into the widget on
every state transition. Same-model TDD missed this because tests
asserted internal state, not visible widget content.
- Reverted `_stream_turn_worker(content, log)` to single-param
`(content)` per the contract FN signature. The widened signature
was a TDD-time workaround for a NoMatches-during-worker
execution; root cause was test timing (added `await pilot.pause()`
before the polling loop in `_submit_and_wait`).
- Restored `exclusive=True` on `self.run_worker(...)` per the
contract STEP 6 spec.
- Added missing `isinstance(args, ParsedArgs)` PRE assertion to
`run_tui`. Required hoisting `from ratatoskr.cli import
ParsedArgs` out of TYPE_CHECKING — runtime import is fine (no
circular dependency: cli lazy-imports tui inside main; tui
imports cli unconditionally at module load).
- Added missing union-type PRE assertion to `_render_event_to_log`.
Contract amendments (precision):
- COMPOSE shape: RichLog `markup=False, highlight=False` (was True,
True). Explanatory comment in-line: bracketed labels like
[cancel_failed] would otherwise be interpreted+stripped as Rich
style spans; the post-Done Markdown rendering still works via
Markdown() Renderable.
- INV-002 reworded: identity rendered via dedicated
Static(id="identity") widget composed adjacent to Footer (Textual's
built-in Footer renders BINDINGS descriptions; a sibling Static
carries custom content in the same visual region).
- on_mount POST-003 amended to allow `agent_id is None` when
--session is used without --agent (matches INV-002 carve-out;
GET /sessions/{id} agent lookup is out of scope for this shell).
- run_tui happy_returns_zero_on_quit test description clarified:
App.run() is sync and can't be driven by Pilot, so run_tui's
wrapping behavior is tested via monkeypatch; the piloted Ctrl-D
exit path is covered separately by TestActionQuit.
Test fixes:
- footer_identity_visible_first_frame, footer_hint_flips_to_cancel,
streaming_first_ctrl_c_cancels: now query the Static(#identity) /
Static(#hint) widgets via `widget.render()` instead of asserting
on `app.sub_title` / `app.hint` internal state. The internal
state still exists (mirror), but the load-bearing assertion is
on visible widget content.
Meta-note from Volva: "TDD pass caught most stream/session/error
mechanics, but tested internal state where the contract required
visible Footer behavior, so same-model TDD would plausibly miss the
primary drift." Calibration shape continues across all four issues:
the post-TDD cross-model review consistently catches assert-boundary
+ observability-shape gaps the test-author's hypotheses don't cover
(#1: 4 findings, #2: 3, #3: 5, #4: 8).
164/164 tests GREEN; ruff clean; both contract drift checks clean.
|
||
|
|
9d469d5c67 |
contract(issue#4): author ratatoskr.tui shell + amend issue #3 cli
Issue #4: Textual TUI shell — the interactive primary presenter (design-brief §1, §5). Single chat-pane App[int] subclass + sync run_tui(args) entry. Composes existing sessions + sse_client modules (no forked API-consumption code, per design-brief §8b). Six FN blocks: run_tui, RatatoskrApp class + on_mount + on_unmount, on_input_submitted, _stream_turn_worker, _render_event_to_log, action_interrupt, action_quit, _cancel_via_sse. Nine hard invariants codifying: - INV-001: lazy-import boundary so cli.py STILL doesn't import textual at module scope (issue #3's INV-001 carried forward) - INV-002: session-identity-always-visible footer (`<agent> · …<tail8>`) with explicit `<unknown>` carve-out for --session without --agent - INV-003: two-stage Ctrl-C state machine (idle/streaming/cancelling) per design-brief §8c - INV-005: markdown default-on with --raw opt-out; deliberately produces streaming-deltas + post-Done markdown re-render (accepted v1 trade-off, Static-then-commit refactor deferred) - INV-007: one AsyncClient per app lifetime - INV-008: mid-session errors → idle (don't exit); only initial session-create errors exit Concurrent in-place amendment of issue #3's contract: - --send becomes optional; when omitted, send_content=None is the TUI-mode marker - --raw flag added to ParsedArgs - main dispatches via lazy `from ratatoskr.tui import run_tui` when send_content is None - _parse_args + main TESTS sections updated (no_send_marks_tui_mode replaces usage_no_send; new raw_flag_default_false / raw_flag_set / no_send_dispatches_to_tui) Volva paraphrase round on issue #4: 5 findings, all amended. (1) INV-002 `<unknown>` carve-out wording. (2) Idle "Ctrl-C twice to exit" hint kept per design-brief §8c's conservative-by-design rationale; INV-003 spells out the intentional one-press-from-idle discrepancy. (3) Markdown double-render trade-off made explicit in INV-005. (4) Submit-during-streaming now writes `[busy] turn in flight; input ignored` (visible notice, not silent swallow). (5) `{!r:.200}` format spec kept with explanatory inline comment. Both contracts drift-check clean. prd: pinned to issue #4 body SHA b1e73e7d2e3dd453 at 2026-05-21T06:21:37+00:00. |
||
|
|
9717fb80e2 |
fix(cli): address Volva code-vs-contract drift (issue #3)
Volva code-review surfaced 5 findings against the TDD-passing implementation; all 5 addressed. Drift fixes (code): - Add `assert argv is None or all(isinstance(a, str) for a in argv)` at both `main` and `_parse_args` entry points (PRE-001 was unenforced). - `main` now catches `SystemExit` and returns `exc.code` verbatim — argparse's --help (SystemExit(0)) was escaping through main as an unhandled exception. Contract amended in-place to spell out the SystemExit-from-argparse-clean-exits passthrough in both `main` and `_parse_args` ERROR_ROUTING. New `help_exits_cleanly` test added per the contract amendment. - Add the PRE-001 union-type assert at `_render_event` entry — unmatched Event variants would have silently no-op'd. - `_run_turn` now awaits `cancel_task` in the `finally` block before returning. Under fast-stream + slow-cancel scenarios the `[cancel_failed]` line could miss being written before _run_turn returns, AND _amain could close the AsyncClient while the cancel POST was still in flight. `_cancel_and_log` swallows all errors per INV-009 so the await is safe. Test gap fix: - New `_FlushCountingIO` subclass counts flush() calls; `test_text_to_stdout_only` and `test_done_writes_newline_and_label` now assert `flush_count == 1` to verify INV-010 (per-chunk flush). Previously the tests would have passed even with flush removed. Meta-note carried in persistent-memory: TDD caught central behavior (stdout/stderr routing, exit-code mapping, create-session ordering, SIGINT idempotence); the cross-model code review consistently catches assert-boundary + observability-shape gaps across all three issues (#1: 4 findings, #2: 3 findings, #3: 5 findings). 118/118 tests GREEN; ruff clean; drift check clean. |