92aa05c68801342795e6d4d638aaef85d955041c
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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`). |
||
|
|
1526f0bc8e |
contract(issue#1): amend per Volva paraphrase — body, id range, INV-003
Volva's contract paraphrase round (thread 01KS4B3B0Y62) surfaced three
real contract-time ambiguities — addressing each here before applying
ready-for-agent.
1) reconnect_turn body was a punt. STEP 2 literally said "json={'content':
''} OR with no body (TBD per spec — confirm during implementation)".
The spec §Reconnect flow example shows POST with Content-Type:
application/json and a body shaped {"content": "..."} — the wire schema
requires content; the server identifies the resume target via the
Last-Event-ID header and does NOT re-process content (spec line 732:
"agent's tools and LLM call run exactly once regardless of disconnects/
reconnects"). reconnect_turn now takes content: str explicitly; STEP 2
sends json={"content": content}. Caller convention: pass the original
content sent to stream_turn. POST-002 added to assert byte-for-byte
body shape; new test body_threads_content covers it.
2) _parse_sse_id allowed turn_id and seq ≥ 0 — too loose. Spec §SSE id
format line 705 says seq starts at 1 (resets per turn); turn_id is
from SQLite turns.id (autoincrement, ≥1). Tightened POST-001 to require
both ≥1; STEP 5 raises ValueError on either < 1. Test happy_zero_seq
flipped to adversarial zero_seq; added zero_turn_id and negative_seq.
INV-002 tightened to reflect the same range.
3) INV-003 (TurnIdFlip) had a subtle wording gap between stream_turn
(first event ESTABLISHES turn_id; cannot be a flip) and reconnect_turn
(expected turn_id parsed FROM last_event_id BEFORE connect; first event
is already a flip-candidate). Volva noticed the reconnect test said
"first event was not yielded" while stream_turn semantics depend on
the first event being yielded unconditionally. Spelled out both entry
points in INV-003 as a numbered sub-list. reconnect_turn STEP 4 and
test turn_id_flip_on_first_event reworded to match.
Volva flags #3 (MalformedSseId-vs-ValueError split) and #5 (exactly-
one-terminal as server-assumed, not client-verified) reviewed and kept
as-is — both intentional. Drift check unchanged: amending the contract
does not touch the pinned issue body, so prd: hashes remain valid.
|
||
|
|
999b0b4765 |
contract(issue#1): pin sse_client to gitea issue + seed default labels
Convert the sse_client contract into an issue-scoped contract bound to the freshly-filed gitea issue #1. Frontmatter migrates from module-shape (module:/purpose:) to issue-shape (target_module:/scope:/prd:) per CONTRACT-FORMAT §2.1.I. The prd: block pins to issue #1's body SHA-256 (abcbc49467e86f1d at 2026-05-21T03:57:37+00:00); drift check verifies the pin matches the live issue body. scripts/contract_drift_check.py needs pyyaml; added to [dev] in pyproject.toml. Without it the drift check (and the contract parser) fail with ModuleNotFoundError — that's a scaffold hole I'd hit again on a fresh checkout. Also seed 17 default labels on gitea via tea so issue tracking has a working vocabulary out of the gate. Five buckets: Sleipnir gating (ready-for-agent, blocked-needs-contract, blocked-needs-dependency), triage (needs-triage, needs-architect-decision, needs-info), type (bug, enhancement, task, documentation), resolution (duplicate, wontfix, invalid), Ratatoskr-specific area (sse-client, tui, cli, observability). Labels are gitea-side state — not in this commit. Known: contract_parser.py --validate ERRORs on the issue-scoped frontmatter because the parser is v2.0-shape. CONTRACT-FORMAT §2.1.L H10 explicitly marks parser kind-aware validation as a Brokkr-side follow-up. Parser is a canonical-synced file so we do NOT patch it locally (would drift from corviduo-project-template). |