All verification complete. Every gate is green and the docs check out. Final report:
## Phase 158 final verification pass — COMPLETE
**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**
**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)
**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**
**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.
**Next pending phase:** none — `todo/` contains only 158.
3.8 KiB
3.8 KiB
Task 01 — the validated ChatMessage.segments field (server)
Phase: 158_thinking_timeline · Story: n/a
Objective
Saved-chat writes accept the new optional segments array (a discriminated
thinking/tool union with phase-83-style caps), omit it when absent so
pre-phase records round-trip byte-identically, and reject malformed values at
the extra="forbid" boundary.
Work
app/schemas.py:ChatThinkingSegment(aBaseModel, besideToolCall):kind: Literal["thinking"],text: str = Field(min_length=1, max_length=32_000)(HistoryTurn's cap, phase 83 — docstring cross-reference).ChatToolSegment(aBaseModel):kind: Literal["tool"]+ the EXACTToolCallfield set (name / argument / offset / limit / path / truncated / chars_shown / chars_total with their existing types and caps — mirror, do not weaken; docstring cross-referencesToolCallas the shape source).ChatSegment = Annotated[Union[ChatThinkingSegment, ChatToolSegment], Field(discriminator="kind")].ChatMessage.segments: list[ChatSegment] | None = Field(default=None, max_length=50)— with the phase-83 voice comment (the cap sizing: far past any realistic turn — the round cap is 10 → ≤ 20 entries; 50 matches thetoolscap).- The EXISTING wrap serializer (
_drop_image_when_absent) gains the omission rule: after the two image pops,if self.segments is None: data.pop("segments", None)— ABSENT, nevernull(the phase-50 round-trip contract; the phase-122SourceRef.image_urlomission precedent, the phase-113 lesson applied up front: the field EXISTS so the boundary does NOT 422 the done-time auto-save carrying it). - The
ChatMessagedocstring gains the phase-158 paragraph (the shape, the omission rule, theextra="forbid"boundary) in the established voice;HistoryTurnand everything inapp/rag/is UNTOUCHED (the history wire stays{who, text, thinking}).
- NO migration, NO router change (
app/api/chats.pyvalidates throughChatMessage—m.model_dump()+ChatMessage.model_validate(m)already carry the new field end to end, including the public shared readSharedChatOut.messages).
Testing & Quality
- Integration (extend
tests/integration/test_chats_api.py— the file already pins the brain-record contract, phase 83/113/120/123/124):- a record WITH
segments(onethinkingentry + twotoolentries carrying every additive field:offset/limit,path,truncated/chars_shown/chars_total) is accepted byPOST /api/chatsand round-trips throughGET(the shape preserved, entry order preserved); segmentswith 51 entries → 422; a thinking-segmenttextof 32 001 chars → 422; a segment withkind: "bogus"→ 422; a record with a stray top-level key (e.g."foo": 1) still → 422 (theextra="forbid"boundary intact);- a record WITHOUT
segments(a pre-phase brain record, the full phase-113 shape) round-trips with the key ABSENT from the served JSON (nevernull— the byte-identity pin, the phase-50 contract); - the shared read path (the public read endpoint's
messages) carries the segments of a saved record with them (the public shape, the phase-51 contract).
- a record WITH
- Coverage: >90% on this task's new/modified code (the schema code is exercised by the integration tests).
Completion Criteria
- the three models + the field + the serializer rule land exactly as designed (a source check)
- the new integration tests are green; the EXISTING
test_chats_api.pytests stay green unchanged uv run pytest tests/integration -qgreen;uv run ruff check . && uv run pyrightclean- no change in
app/rag/, no migration, no router change (agit diff --statscoped check)