804c2df6eb
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).
375 lines
22 KiB
Markdown
375 lines
22 KiB
Markdown
---
|
|
contract_version: "2.1"
|
|
target_module: "ratatoskr.tui"
|
|
scope: "Restructure `ratatoskr.tui`'s `run_tui` lifecycle so startup errors (`AgentNotFound`, `SessionApiFailed`, network-error-during-create_session) print to real stderr instead of getting eaten by the alt-screen teardown. Move session resolution OUT of `on_mount` (which runs inside the alt-screen) and INTO `run_tui` (sync wrapper, BEFORE `App.run()` opens the alt-screen). `httpx.AsyncClient` ownership moves with it: opened by `run_tui` via async-with; the `RatatoskrApp` instance becomes a consumer of an externally-owned client. `on_mount` shrinks to identity-widget population from pre-resolved state. Mid-session errors during streaming (issue #4 INV-008) continue to render in the alt-screen; only PRE-`App.run()` failures use stderr. No new modules; in-place amendment to issue #4's contract. No semantic change to issue #1/#2/#3 surfaces."
|
|
depends_on:
|
|
- "httpx"
|
|
- "textual"
|
|
- "ratatoskr.sessions"
|
|
- "ratatoskr.cli"
|
|
used_by: []
|
|
language: "python"
|
|
complexity: "medium"
|
|
estimated_loc: 60
|
|
confidence: 0.85
|
|
assumptions:
|
|
- "Textual's `App.run()` enters an alt-screen lifecycle that tears down on `self.exit()` or normal termination. Any content rendered to widgets inside the alt-screen (e.g., RichLog.write) is invisible to the operator after teardown — it lived in the alt-screen buffer, not the operator's scrollback. Real stderr writes survive teardown."
|
|
- "`httpx.AsyncClient` is safely usable across an `App.run()` boundary: opened in an `async with` block in `run_tui`, accessed by the App via `self.client`, closed by the same `async with` after `App.run()` returns. The client doesn't care about Textual's lifecycle; it's a plain httpx object the App holds a reference to."
|
|
- "Synchronous `run_tui` is the operator-facing entry point per issue #4's contract (called from `ratatoskr.cli.main` via lazy import). `run_tui` calls `asyncio.run(_resolve_then_run(args))` which is a single async function that handles BOTH the pre-flight HTTP (opening the client, resolving the session) AND the App lifecycle via `await app.run_async()`. Everything runs in one event loop managed by that single `asyncio.run(...)` call — NOT two sequential event loops. `app.run_async()` is Textual's async entry point (as opposed to the sync `app.run()`) and is the correct choice because we're already inside an async context with an active `httpx.AsyncClient`. Mixing a nested `asyncio.run()` inside an existing event loop would be incorrect."
|
|
- "Tests for the moved error paths use stderr capture (pytest's `capsys`) at the run_tui level, NOT the App.run_test() pilot. Pilot tests cover the in-alt-screen paths (#4's existing TestStreamTurnWorker / TestActionInterrupt / TestActionQuit). Pre-flight errors are App-free; standard stderr-capture works."
|
|
open_questions:
|
|
- "Should the pre-flight error label format match the cli's `[session_api_failed] status=... body=...` format exactly (already in cli's `_amain` STEP 2), so operators see the same label whether they hit the error via `--send` or via TUI startup? Draft: yes — same label, same shape, same exit code. Consistency beats per-presenter variance."
|
|
- "Should `run_tui` emit an `[connecting...]` status line to stderr before the pre-flight HTTP, so a slow connection isn't silently waited on? Draft: no for v1. The pre-flight is fast on a working network (< 1s for POST /sessions); if a slow path turns up, surface in a follow-up."
|
|
prd:
|
|
issue: 6
|
|
issue_url: "https://gitea.phasefinal.com/vh/ratatoskr/issues/6"
|
|
body_sha256_16: "6378989fbd465ba8"
|
|
lock_in_comment_id: null
|
|
lock_in_sha256_16: null
|
|
lock_in_at: null
|
|
pinned_at: "2026-05-23T02:11:18+00:00"
|
|
dependencies:
|
|
- issue: 4
|
|
path: "src/ratatoskr/tui.py"
|
|
reason: "In-place contract amendment: `run_tui` STEPS expanded with pre-App.run() session resolution + error routing to stderr; `RatatoskrApp.__init__` signature widens to accept pre-resolved `session_id` / `agent_id` / `client`; `on_mount` STEPS narrowed (no more session-create); `on_unmount` STEPS narrowed (client closed by run_tui's async-with, not on_unmount). Several issue #4 TESTS get restructured: error-on-mount tests become error-on-resolve tests at the run_tui layer."
|
|
|
|
---
|
|
|
|
# TUI startup error visibility — surface pre-flight errors to real stderr
|
|
|
|
## Context
|
|
|
|
Issue #4's `RatatoskrApp.on_mount` runs INSIDE the Textual alt-screen and
|
|
calls `create_session` (when `--new`) to mint a session before the app
|
|
becomes interactive. When `create_session` raises (`AgentNotFound`,
|
|
`SessionApiFailed`, network errors), my code today writes a labeled line
|
|
to the `RichLog` widget and calls `self.exit(<code>)`. The exit code is
|
|
right, but the labeled line is invisible: the alt-screen tears down
|
|
roughly 200ms after `self.exit()`, and the RichLog buffer goes with it.
|
|
The operator sees a blank terminal and an exit code — no diagnostic.
|
|
|
|
Surfaced 2026-05-21 (`ratatoskr --new --agent lofn` blanked silently;
|
|
turned out to be the issue #5 `end_user_id_required` 422; operator had
|
|
to re-run under `--send` to see the actual error). `--send` mode handles
|
|
this class of error correctly because stderr labels go to the operator's
|
|
real terminal, not the alt-screen.
|
|
|
|
This issue restructures the lifecycle so startup-phase errors use the
|
|
same stderr path that `--send` uses. Session resolution moves OUT of
|
|
`on_mount` (alt-screen) and INTO `run_tui` (sync wrapper, real terminal).
|
|
Mid-session errors during streaming continue to render in the alt-screen
|
|
per issue #4 INV-008 — that path is fine; the user is interactively
|
|
present and the transcript is visible.
|
|
|
|
## Data flow
|
|
|
|
**Input change:** none at the operator-facing level. `ratatoskr --new
|
|
--agent <id>` and `ratatoskr --session <id>` both still launch the TUI.
|
|
|
|
**Output change:**
|
|
- When session-create fails (in `--new` mode) OR when the network won't
|
|
reach the server, the operator now sees a labeled line on the **real
|
|
terminal stderr**, not the alt-screen:
|
|
- `[agent_not_found] agent_id={exc.agent_id}` (exit 12)
|
|
- `[session_api_failed] status={exc.status} body={exc.body!r}` (exit 20) — `exc.body` is already truncated to 1024 bytes at `SessionApiFailed.__init__` per issue #2 INV-004; `!r` is the repr of that already-truncated bytes value
|
|
- `[network_error] {type(exc).__name__}: {exc}` (exit 21)
|
|
- Format matches `ratatoskr.cli._amain`'s existing error labels exactly
|
|
(same shape, same exit codes) so operators see one consistent
|
|
vocabulary across `--send` and TUI modes.
|
|
- When session resolution succeeds, the alt-screen opens and behavior
|
|
is identical to today's: identity widgets populated, chat pane ready
|
|
for input.
|
|
- Mid-session errors during streaming (issue #4 INV-008 set:
|
|
`SseConnectionDropped`, `SseConnectFailed`, `MalformedSseId`,
|
|
`MalformedSseData`, `TurnIdFlip`) STILL render in the alt-screen
|
|
and return the app to idle. **Unchanged.**
|
|
|
|
**Side effects:**
|
|
- `httpx.AsyncClient` lifetime widens: now spans the pre-flight HTTP
|
|
AND the App's lifetime, owned by `run_tui` via `async with`.
|
|
|
|
**On disk:** none (unchanged).
|
|
|
|
## Invariants
|
|
|
|
- **INV-001 [hard]**: Session resolution (mint when `--new`; attach when
|
|
`--session`) MUST complete BEFORE `App.run()` enters the alt-screen.
|
|
Errors at this phase MUST print to `sys.stderr` (the real terminal,
|
|
not a RichLog widget) and MUST cause `run_tui` to return the
|
|
appropriate exit code WITHOUT calling `App.run()`. The alt-screen MUST
|
|
NOT open when session resolution fails — operators get a clean stderr
|
|
diagnostic on their normal terminal, with no flash-and-disappear
|
|
artifact.
|
|
- **INV-002 [hard]**: `httpx.AsyncClient` is owned by `run_tui` via
|
|
`async with`. The client is opened BEFORE the pre-flight session
|
|
resolution, passed by reference to `RatatoskrApp.__init__`, accessed
|
|
by the App via `self.client` during streaming, and closed by the same
|
|
`async with` AFTER `App.run()` returns. The App is a consumer of an
|
|
externally-owned client; it MUST NOT call `self.client.aclose()` (the
|
|
`async with` does that). Issue #4's `on_unmount` STEPS narrow
|
|
accordingly.
|
|
- **INV-003 [hard]**: `RatatoskrApp.__init__` signature widens to
|
|
`(args, *, session_id: str, agent_id: str | None, client:
|
|
httpx.AsyncClient)`. All three are pre-resolved by `run_tui` and
|
|
REQUIRED at construction. The app no longer mints anything; it
|
|
consumes pre-resolved state.
|
|
- **INV-004 [hard]**: `on_mount` STEPS narrow: open the identity Static
|
|
widget, set `self.state = "idle"`, set `self.hint = HINT_IDLE`.
|
|
No more session-create branch; no more client-open. The `<unknown>`
|
|
carve-out for agent_id (issue #4 INV-002) is preserved — when
|
|
`--session <id>` is used without `--agent`, `agent_id` is None and
|
|
the identity widget renders `<unknown> · …<tail>` as today.
|
|
- **INV-005 [hard]**: Mid-session errors during streaming (issue #4
|
|
INV-008 set) are UNCHANGED. They render to the RichLog transcript via
|
|
`_render_event_to_log` / explicit `log.write` and return the app to
|
|
`idle` state. Only PRE-`App.run()` errors get the new stderr-label
|
|
treatment. The split is: pre-alt-screen failures → real stderr;
|
|
in-alt-screen failures → RichLog. This is the load-bearing
|
|
observability invariant.
|
|
- **INV-006 [hard]**: Exit codes (12, 20, 21, 0, 3) and label formats
|
|
MUST match `ratatoskr.cli._amain`'s `[agent_not_found]` /
|
|
`[session_api_failed]` / `[network_error]` shape verbatim. Operators
|
|
see one vocabulary regardless of which presenter they're using.
|
|
- **INV-007 [hard]**: No `core.*` / `worldtree.*` imports (existing
|
|
boundary; unchanged).
|
|
|
|
## Out of scope
|
|
|
|
- **General TUI logging infrastructure** (e.g., a structured
|
|
DiagnosticsLog surface, log levels, log filtering). Each side pane is
|
|
its own issue.
|
|
- **Persistent error log file** at `~/.cache/ratatoskr/last-error.log`.
|
|
Rejected per design-brief §8d ("no cross-process resume, no config
|
|
dir"). The fix is "make startup errors visible on stderr", not "log
|
|
everything to disk".
|
|
- **In-alt-screen restructuring** (e.g., a status bar that surfaces
|
|
errors at the bottom of the screen during streaming). Issue #4 INV-008
|
|
already handles in-alt-screen errors correctly via RichLog; this issue
|
|
only addresses pre-alt-screen.
|
|
- **422 → user-friendly hint translation.** When `--new --agent lofn`
|
|
hits 422 `end_user_id_required`, this issue surfaces the raw label
|
|
to stderr; the user still has to read the body to understand. Hint
|
|
translation is issue #5's optional follow-up (deferred there).
|
|
- **Pre-flight status line** (`[connecting...]` before the HTTP).
|
|
Deferred; pre-flight is fast on a healthy network.
|
|
- **Re-entering the picker on failure.** If session-create fails, the
|
|
TUI exits cleanly; the operator re-launches with corrected args. No
|
|
retry loop in v1.
|
|
|
|
## Constraints
|
|
|
|
- **[compatibility]** Spec pin unchanged. The wire surface is
|
|
unchanged; only the client's invocation timing moves earlier.
|
|
- **[performance]** No new HTTP round-trips. The same single
|
|
`POST /sessions` happens once per `--new` launch; just sequenced
|
|
before `App.run()` instead of inside `on_mount`.
|
|
- **[security]** Same as today — `Authorization` header on the client,
|
|
no logged credentials.
|
|
- **[style]** Async-native at the resolve layer. `run_tui` becomes a
|
|
thin sync wrapper around `asyncio.run(_resolve_then_run(args))` to
|
|
keep one entry point. Ruff line-length=100.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
ratatoskr <args> [shell entry, console-script]
|
|
│
|
|
└─ ratatoskr.cli.main(argv) [sync]
|
|
│
|
|
└─ when args.send_content is None ──► from ratatoskr.tui import run_tui
|
|
return run_tui(args)
|
|
│
|
|
└─ run_tui(args) [sync]
|
|
│
|
|
├─ assert PRE-001..PRE-002
|
|
└─ asyncio.run(_resolve_then_run(args))
|
|
│
|
|
├─ async with httpx.AsyncClient(...) as client:
|
|
│ │
|
|
│ ├─ try: resolve session
|
|
│ │ IF args.new: info = await create_session(client, args.agent_id)
|
|
│ │ session_id = info.session_id; agent_id = info.agent_id
|
|
│ │ ELSE: session_id = args.session_id; agent_id = args.agent_id
|
|
│ │ except AgentNotFound: stderr label; return 12 ◄── PRE-alt-screen
|
|
│ │ except SessionApiFailed: stderr label; return 20 ◄── PRE-alt-screen
|
|
│ │ except (httpx.ConnectError|ReadTimeout|TransportError): stderr; return 21
|
|
│ │
|
|
│ ├─ # Session resolved; enter alt-screen
|
|
│ ├─ app = RatatoskrApp(args, session_id, agent_id, client)
|
|
│ ├─ return await app.run_async() or 0
|
|
│ │ │
|
|
│ │ ├─ on_mount: populate identity widget; state=idle
|
|
│ │ ├─ on_input_submitted: spawn _stream_turn_worker
|
|
│ │ ├─ _stream_turn_worker: stream; mid-session errors → RichLog per INV-008 ◄── IN-alt-screen
|
|
│ │ └─ on_unmount: nothing (client closed by async with below)
|
|
│ │
|
|
│ └─ # App returned; async with closes client
|
|
└─ # exit code propagated to cli.main
|
|
```
|
|
|
|
---
|
|
|
|
## In-place amendments to issue #4 (the work)
|
|
|
|
This issue's contract is small because the real work is amending issue
|
|
#4's contract in place. The amendments are pinned here so reviewers see
|
|
the whole change in one place; the actual contract file at
|
|
`docs/contracts/issues/4.contract.md` is amended in-place as part of
|
|
this issue's commit.
|
|
|
|
### Issue #4 (`ratatoskr.tui`) amendments
|
|
|
|
**`run_tui` STEPS expanded:**
|
|
|
|
```
|
|
FN run_tui(args: ParsedArgs) -> int
|
|
STEPS:
|
|
1. [setup, prescriptive] Validate PRE-001 (isinstance(args, ParsedArgs) and args.send_content is None)
|
|
2. [setup, prescriptive] Validate PRE-002 (bool(args.session_id) != bool(args.new))
|
|
3. [sequential, prescriptive] RETURN asyncio.run(_resolve_then_run(args))
|
|
|
|
|
|
FN _resolve_then_run(args: ParsedArgs) -> int # NEW helper
|
|
ASYNC: yes
|
|
STEPS:
|
|
1. [setup, prescriptive] OPEN httpx.AsyncClient(base_url=args.server_url,
|
|
headers={"Authorization": f"Bearer {args.api_key}"},
|
|
timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)) via async-with
|
|
2. [branch, prescriptive] IF args.new:
|
|
TRY: info = await create_session(client, args.agent_id)
|
|
ON AgentNotFound as exc:
|
|
sys.stderr.write(f"[agent_not_found] agent_id={exc.agent_id}\n")
|
|
RETURN 12
|
|
ON SessionApiFailed as exc:
|
|
sys.stderr.write(f"[session_api_failed] status={exc.status} body={exc.body!r}\n")
|
|
RETURN 20
|
|
ON (httpx.ConnectError | httpx.ReadTimeout | httpx.TransportError) as exc:
|
|
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
|
|
RETURN 21
|
|
SET session_id = info.session_id; agent_id = info.agent_id
|
|
ELSE:
|
|
SET session_id = args.session_id; agent_id = args.agent_id # agent_id may be None — INV-002 carve-out preserved
|
|
3. [sequential, prescriptive] Construct app = RatatoskrApp(args, session_id=session_id, agent_id=agent_id, client=client)
|
|
4. [sequential, prescriptive] exit_code = await app.run_async() # Textual's async-runner; lets the same event loop handle the alt-screen
|
|
5. [cleanup, prescriptive] RETURN exit_code or 0
|
|
```
|
|
|
|
**Note**: `app.run_async()` (Textual's async-runner) is used instead of
|
|
`app.run()` (sync) because we're already in an async context inside the
|
|
`async with httpx.AsyncClient(...)`. Mixing `asyncio.run(...)` inside an
|
|
existing event loop would be incorrect; the async variant lets one loop
|
|
handle both the pre-flight HTTP AND the App lifecycle.
|
|
|
|
**`RatatoskrApp.__init__` signature widens:**
|
|
|
|
```
|
|
def __init__(self, args: ParsedArgs, *, session_id: str, agent_id: str | None, client: httpx.AsyncClient) -> None
|
|
```
|
|
|
|
All three new kwargs are REQUIRED. Stored on self as
|
|
`self.session_id`, `self.agent_id`, `self.client`. The state machine
|
|
attributes (`self.state`, `self.active_turn_id`, `self.stream_worker`,
|
|
`self.hint`) are unchanged.
|
|
|
|
**`on_mount` STEPS narrow:**
|
|
|
|
```
|
|
async def on_mount(self) -> None:
|
|
STEPS:
|
|
1. [setup, prescriptive] assert self.client is not None and self.session_id is not None
|
|
2. [sequential, prescriptive] Compute identity:
|
|
agent_slot = self.agent_id or "<unknown>"
|
|
identity = f"{agent_slot} · …{self.session_id[-8:]}"
|
|
3. [sequential, prescriptive] Populate widgets:
|
|
self.sub_title = identity (Header mirror)
|
|
self.query_one("#identity", Static).update(identity)
|
|
4. [sequential, prescriptive] SET self.state = "idle"; self._set_hint(self.HINT_IDLE)
|
|
```
|
|
|
|
No more session-create branch; no more client-open. ERROR_ROUTING for
|
|
`AgentNotFound` / `SessionApiFailed` / network errors is **removed from
|
|
on_mount** — those routes now live in `_resolve_then_run`. Issue #4's
|
|
POST-001 (client-open-after-mount) and POST-002 (session_id non-empty)
|
|
are still satisfied but by `_resolve_then_run` setting up state, not
|
|
by `on_mount`'s create call.
|
|
|
|
**`on_unmount` STEPS narrow (or removed):**
|
|
|
|
```
|
|
async def on_unmount(self) -> None:
|
|
STEPS:
|
|
(none — client lifetime managed by run_tui's async-with, NOT this hook)
|
|
```
|
|
|
|
Issue #4's existing `on_unmount` test (`unmount_closes_client`) is
|
|
restructured: client closing now happens via `run_tui`'s async-with
|
|
exit, which fires after `app.run_async()` returns. The test moves
|
|
from "ctrl+d → on_unmount → client closed" to "ctrl+d → run_tui
|
|
returns → client closed".
|
|
|
|
**TESTS amendments (issue #4 in-place):**
|
|
|
|
Removed (or restructured to the run_tui layer):
|
|
- `agent_not_found_on_mount` → becomes `agent_not_found_on_resolve` at
|
|
the `_resolve_then_run` layer. Assertion shape: `capsys.readouterr().err`
|
|
contains `[agent_not_found]`; `run_tui` returns 12; no app instance
|
|
ever entered alt-screen.
|
|
- `session_api_failed_on_mount` → `session_api_failed_on_resolve`.
|
|
- `network_error_on_mount` → `network_error_on_resolve`.
|
|
- `client_open_after_mount` → `client_open_after_resolve` (client opened
|
|
by run_tui, accessible via `self.client` once the app is mounted).
|
|
- `unmount_closes_client` → `run_tui_closes_client_on_app_exit` (asserts
|
|
the async-with closed the client after `app.run_async()` returned).
|
|
|
|
New TESTS (in the `_resolve_then_run` block at the run_tui layer):
|
|
- `alt_screen_never_opens_on_resolve_error [trace]`: monkeypatch
|
|
`RatatoskrApp.run_async` to a sentinel that fails the test if called;
|
|
set up respx to return 404 from POST /sessions; assert `run_tui`
|
|
returns 12; assert the sentinel was NEVER invoked. Directly probes
|
|
INV-001 (alt-screen MUST NOT open).
|
|
- `client_lifetime_owned_by_run_tui [trace]`: spy on `httpx.AsyncClient.aclose`;
|
|
successful run; assert exactly one `aclose` call AFTER `app.run_async`
|
|
returned, NOT during on_unmount. Probes INV-002.
|
|
- `stderr_label_format_matches_cli [trace]`: assert the stderr label
|
|
shape (e.g., `[agent_not_found] agent_id=missing`) matches the format
|
|
emitted by `cli._amain`'s existing handler verbatim. Probes INV-006.
|
|
|
|
Issue #4's `happy_new_session_mount` test stays (now exercises the
|
|
identity widget population via the pre-resolved state); the assertion
|
|
on POST /sessions call count moves to the new `_resolve_then_run`
|
|
test layer.
|
|
|
|
---
|
|
|
|
## Acceptance
|
|
|
|
- Issue #4 contract amended in-place; drift-check clean.
|
|
- Issue #6 contract drift-check clean.
|
|
- All existing tests + new `_resolve_then_run` coverage GREEN under
|
|
`uv run pytest tests/`.
|
|
- `uv run ruff check src/ tests/` clean.
|
|
- Boundary smoke `tests/test_no_worldtree_imports.py` still passes.
|
|
- Manual smoke (the original failure mode from 2026-05-21):
|
|
`ratatoskr --new --agent <nonexistent>` produces a VISIBLE
|
|
`[agent_not_found]` line on stderr; no screen-blanking artifact; exit
|
|
code 12. (This is the smoke that surfaced the bug; verify it's now
|
|
the success case.)
|
|
- Regression smoke: `ratatoskr --new --agent mimir` against personal
|
|
Worldtree still works end-to-end (alt-screen opens, chat pane works,
|
|
Ctrl-D exits clean). Mid-session errors during a streaming turn STILL
|
|
render in the alt-screen per INV-008 — verify by hitting one (e.g.,
|
|
send a turn, then kill the server side, see `[connection_dropped]`
|
|
in the transcript, state returns to idle).
|
|
|
|
## Dependencies
|
|
|
|
- Issue #4 (`ratatoskr.tui` shell) — landed on main; this issue amends
|
|
its contract.
|
|
- Issue #5 (`--end-user-id` for per-user agents) — independent; both
|
|
can land in either order, but #5 + #6 compose naturally (#6 will
|
|
surface #5's 422 as a visible stderr label instead of a black
|
|
alt-screen).
|
|
- Issue #7 (mid-stream robustness, `MalformedSseData`) — landed; #6's
|
|
pre/in-alt-screen split is orthogonal to #7's empty-data/malformed
|
|
distinction (different error layers entirely).
|