92aa05c68801342795e6d4d638aaef85d955041c
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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`). |
||
|
|
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. |
||
|
|
69fdfaa104 |
contract(issue#3): author ratatoskr.cli --send — scaffold + body + Volva amend
Issue #3: non-interactive `--send` stdout presenter (design-brief §8b). Composes sessions.create_session (when --new) with sse_client.stream_turn + cancel_turn. First contract in the repo with a code-level dependencies: block (issues #1 and #2). Six FN blocks (main, _parse_args, _amain, _render_event, _run_turn, _cancel_and_log). 10 hard invariants codifying: no textual/rich imports; Text deltas + post-Done newline are the only stdout writes; --session xor --new + agent/api-key resolution chains; SIGINT semantics (cancel-with- known-turn-id; early-exit code 3 without; swallow cancel failures during drain). 9-bucket exit code table. Volva paraphrase round flagged 5 ambiguities; all 5 amended: - INV-001 wording: "no in-repo modules other than X/Y" (was strict-only) - INV-002 + _render_event POST-002/003: Done's stdout newline is part of the contract, restructured to avoid the "no other event writes stdout" contradiction - Data flow stderr list: added the pre-event [cancelled] (before any event arrived) label - _run_turn STEPS 3 race-loop: gate sigint_task creation behind `if not cancelling` to avoid busy-wake once event is set; new no_busy_loop_after_cancel trace test - assumptions list: added CancelTurnNotFound + CancelAlreadyCompleted prd: block pinned to issue #3 body SHA 206ef51709d43b2c at 2026-05-21T05:13:29+00:00. Drift check clean. |