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).
14 KiB
contract_version, target_module, scope, depends_on, used_by, language, complexity, estimated_loc, confidence, assumptions, open_questions, prd, dependencies
| contract_version | target_module | scope | depends_on | used_by | language | complexity | estimated_loc | confidence | assumptions | open_questions | prd | dependencies | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2.1 | ratatoskr.sessions | Add `end_user_id` support to the POST /sessions flow so per-end-user agents (lofn confirmed; presumably the Persona/Vili family) can be smoked. Small surface change distributed across three existing modules via in-place contract amendments: `ratatoskr.sessions.create_session` gains a keyword-only `end_user_id: str | None = None` parameter that is threaded into the POST body when non-None; `ratatoskr.cli` adds an `--end-user-id <id>` flag + corresponding `ParsedArgs.end_user_id: str | None` field + threading through `_amain`; `ratatoskr.tui.on_mount` threads `args.end_user_id` into its `create_session` call. No new modules, no new files (apart from this contract). Default-omitted preserves backwards compatibility: existing `mimir` smoke flows that don't pass `--end-user-id` continue to work unchanged. |
|
python | low | 40 | 0.9 |
|
|
|
|
end_user_id support — POST /sessions parameter for per-user agents
Context
Manual smoke against personal Worldtree on 2026-05-21 (ratatoskr --send "test" --new --agent lofn) returned a 422 from POST /sessions:
[session_api_failed] status=422 body=b'{"detail":{"error_code":"end_user_id_required","message":"end_user_id is required (non-empty string) for Lofn sessions"}}'
The lofn agent (and presumably others in the Persona/Vili family) requires an end_user_id field in the create-session body to scope state per end-user. Ratatoskr's current create_session(client, agent_id) only sends {"agent_id": agent_id}, so per-user agents are unreachable. Mimir, the agent used for prior smoke validation, doesn't require end_user_id and continues to work unchanged.
This issue threads end_user_id through the small chain: CLI flag → ParsedArgs → _amain / on_mount → create_session → POST body. The change is small (a keyword-only parameter widening + one new CLI flag + arg-passing in two places) but touches three existing contracts (#2, #3, #4) in-place. This issue's own contract is mostly a coordinating record + the new test additions.
Data flow
Input:
- Operator passes
--end-user-id <id>on the CLI when invoking against a per-user agent. - Worldtree's POST /sessions endpoint accepts
end_user_id: stras an optional field; rejects with 422end_user_id_requiredwhen omitted for an agent that requires it.
Output:
- POST /sessions body becomes
{"agent_id": <agent>, "end_user_id": <id>}when--end-user-idwas passed; remains{"agent_id": <agent>}when omitted. - Returned
SessionInfois unchanged shape (the server response doesn't change; only the request body widens).
Side effects: outbound HTTP only (no new state). No persistence — operator passes --end-user-id on each invocation; ratatoskr doesn't remember it.
Invariants
- INV-001 [hard]:
end_user_id, when passed via--end-user-id, MUST be a non-empty string. Empty-string--end-user-id ""raisesUsageErrorBEFORE any HTTP call (mirrors the existing--send ""empty-check in_parse_args). Server-side validation also rejects empty, so client-side rejection is friendlier. - INV-002 [hard]:
create_session(...)MUST omit theend_user_idfield from the POST body when the kwarg isNone. This preserves the existing 2-field body shape for agents that don't requireend_user_id(mimir today; other agents in the future). Sending an empty-stringend_user_idis NOT equivalent to omitting it (server rejects empty; INV-001 catches empty before HTTP). - INV-003 [hard]: Backwards compatibility: all existing
mimirsmoke flows that don't pass--end-user-idcontinue to work unchanged. The CLI's--end-user-idflag is OPTIONAL (no default required);_parse_argssucceeds without it;_amain/on_mountcallcreate_session(client, agent_id)(no end_user_id kwarg) when the flag wasn't passed, identical to today's behavior. - INV-004 [hard]: No
core.*/worldtree.*imports (existing boundary; this issue doesn't change it).
Out of scope
- 422
end_user_id_required→ user-friendly hint. The raw[session_api_failed] status=422 body=...label is honest; the message in the body ("end_user_id is required (non-empty string) for Lofn sessions") is reasonably clear. Hint translation deferred to a follow-up if the bare label proves empirically confusing. list_sessionsfilter byend_user_id. Spec allows it; no consumer needs it yet (the startup-session-picker issue is a separate ticket).- Environment-variable fallback for
end_user_id. Auto-defaulting (e.g.,$USER,$RATATOSKR_END_USER_ID) would paper over the per-user-isolation intent. Explicit flag only. - TUI startup error visibility — when
--end-user-idis missing for a per-user agent, the TUI's[session_api_failed]line still gets eaten by the alt-screen teardown. That's issue #6's scope, not this one. - Empty-data SSE crash — separate issue #7; mid-stream JSONDecodeError on empty
sse.datais independent ofend_user_id. - Other per-agent-required fields. If Worldtree later adds another required-by-some-agents field, file a sibling issue; don't generalize this one prematurely.
Constraints
- [compatibility] Spec pin unchanged. The
end_user_idfield is already in the v0.19.0 spec; we're just starting to use it. - [performance] No new round-trips; no new state. The change is one extra optional field in an existing POST body.
- [security]
end_user_idis logged as part of[create_session](alongsidesession_id,agent_id) — operator's choice of identifier may carry semantic meaning, but it's not auth-bearing. Don't redact. - [style] Keyword-only parameter for
end_user_id(matchespersist_partialoncancel_turn). Ruff line-length=100.
In-place amendments (the work)
This issue's contract is small because the real work is amendments to issues #2, #3, #4. The amendments are pinned here so reviewers see the whole change in one place; the actual contract files at docs/contracts/issues/2.contract.md, 3.contract.md, 4.contract.md are amended in-place as part of this issue's commit.
Issue #2 (ratatoskr.sessions) amendments
create_session signature widens:
FN create_session(client: httpx.AsyncClient, agent_id: str, *, end_user_id: str | None = None) -> SessionInfo
BRIEF: POST /sessions with {"agent_id": agent_id} and optionally {"end_user_id": end_user_id} when non-None. Returns SessionInfo populated from the 201 response.
PRE: [PRE-001 hard] client is not None
PRE: [PRE-002 hard] agent_id is a non-empty string
PRE: [PRE-003 hard] end_user_id is None OR a non-empty string -- assert end_user_id is None or (isinstance(end_user_id, str) and end_user_id)
POST: [POST-001 side_effect] exactly one POST to /sessions was issued; body is {"agent_id": agent_id} when end_user_id is None, OR {"agent_id": agent_id, "end_user_id": end_user_id} when non-None
... (other POST/ERROR_ROUTING unchanged) ...
STEPS:
...
2. [sequential, flexibility=prescriptive] Build body: body = {"agent_id": agent_id}; IF end_user_id is not None: body["end_user_id"] = end_user_id
3. [sequential, flexibility=prescriptive] CALL client.post("/sessions", json=body)
...
New TESTS entries:
happy_create_with_end_user_id [happy]: passend_user_id="alice"→ outbound JSON body =={"agent_id": "mimir", "end_user_id": "alice"}byte-for-byte; SessionInfo populated as today.default_omits_end_user_id [trace]: omitend_user_idkwarg → outbound JSON body =={"agent_id": "mimir"}(no end_user_id key); preserves the issue #2 baseline.empty_end_user_id [adversarial]:end_user_id=""→ AssertionError before HTTP (PRE-003).
Issue #3 (ratatoskr.cli) amendments
_parse_args STEPS gain a new flag + env-var fallback (amended 2026-05-23):
1. [setup] Construct argparse.ArgumentParser:
... (existing flags) ...
--end-user-id <id> (str, optional — non-empty if passed; validated in step 2b)
2a. [branch] IF ns.send is not None AND not ns.send: UsageError("--send content must be non-empty") (unchanged)
2b. [branch, NEW] IF ns.end_user_id is not None AND not ns.end_user_id: UsageError("--end-user-id must be non-empty when passed")
... (rest unchanged) ...
5b. [sequential, AMENDED 2026-05-23] Resolve end_user_id with env-var fallback:
end_user_id = ns.end_user_id or os.environ.get("RATATOSKR_END_USER_ID") or None
(Flag wins; env fallback active when flag omitted; None when neither set.)
6. [cleanup] RETURN ParsedArgs(
... existing fields ...,
end_user_id=end_user_id,
)
ParsedArgs gains end_user_id: str | None (default semantics handled by argparse default=None).
_amain threads through to create_session:
IF args.new:
TRY: info = await create_session(client, args.agent_id, end_user_id=args.end_user_id)
... existing error handling unchanged ...
New TESTS:
happy_new_with_end_user_id [happy]: argv includes--end-user-id alice→ ParsedArgs.end_user_id == "alice".end_user_id_default_none [trace]: argv omits--end-user-idAND env unset → ParsedArgs.end_user_id is None.empty_end_user_id [adversarial]: argv has--end-user-id ""→ UsageError.end_user_id_from_env [trace, amended 2026-05-23]: envRATATOSKR_END_USER_ID="ratatoskr-tui", flag omitted → ParsedArgs.end_user_id == "ratatoskr-tui".end_user_id_flag_beats_env [trace, amended 2026-05-23]: env set + flag passed → flag wins.- Update
_amain'shappy_new_session_then_streamtest: respx assertion on the POST /sessions body now confirmsend_user_idis OR isn't present per the test variant.
Issue #4 (ratatoskr.tui) amendments
Post-#6 adjustment: issue #6 (landed 2026-05-23, after this contract was drafted)
moved session resolution OUT of on_mount (alt-screen) into _resolve_then_run
(pre-App.run_async()). The equivalent end_user_id threading site is therefore
_resolve_then_run's create_session call, not on_mount's. on_mount
no longer calls create_session at all.
_resolve_then_run STEPS gain end_user_id threading (was on_mount pre-#6):
2. [branch] IF args.new:
TRY: info = await create_session(client, args.agent_id, end_user_id=args.end_user_id)
... existing error handling unchanged ...
New TEST (renamed _mount → _resolve per #6):
happy_new_with_end_user_id_resolve [happy]:_args_new(end_user_id="alice"); respx mocks POST /sessions; assert outbound body hasend_user_id: alice.
Acceptance
- All three amended contracts (#2, #3, #4) drift-check clean.
- All existing tests + new
end_user_idcoverage GREEN underuv run pytest tests/. uv run ruff check src/ tests/clean.- Boundary smoke
tests/test_no_worldtree_imports.pystill passes. - Manual smoke:
source env.sh && uv run ratatoskr --send "test" --new --agent lofn --end-user-id ratatoskr-devsucceeds against personal Worldtree (session creates, stream consumes to[done]). - The pre-existing
mimirsmoke flow without--end-user-idSTILL works (regression check).