Files
brain-of-reese/.agents/phases/complete/158_thinking_timeline/01_schema_segments.md
T
ducoterra 31bd903a93
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s
phase: 158_thinking_timeline
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.
2026-10-05 02:52:55 -04:00

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

  1. app/schemas.py:
    • ChatThinkingSegment (a BaseModel, beside ToolCall): kind: Literal["thinking"], text: str = Field(min_length=1, max_length=32_000) (HistoryTurn's cap, phase 83 — docstring cross-reference).
    • ChatToolSegment (a BaseModel): kind: Literal["tool"] + the EXACT ToolCall field set (name / argument / offset / limit / path / truncated / chars_shown / chars_total with their existing types and caps — mirror, do not weaken; docstring cross-references ToolCall as 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 the tools cap).
    • 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, never null (the phase-50 round-trip contract; the phase-122 SourceRef.image_url omission 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 ChatMessage docstring gains the phase-158 paragraph (the shape, the omission rule, the extra="forbid" boundary) in the established voice; HistoryTurn and everything in app/rag/ is UNTOUCHED (the history wire stays {who, text, thinking}).
  2. NO migration, NO router change (app/api/chats.py validates through ChatMessage — m.model_dump() + ChatMessage.model_validate(m) already carry the new field end to end, including the public shared read SharedChatOut.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 (one thinking entry + two tool entries carrying every additive field: offset/limit, path, truncated/chars_shown/chars_total) is accepted by POST /api/chats and round-trips through GET (the shape preserved, entry order preserved);
    • segments with 51 entries → 422; a thinking-segment text of 32 001 chars → 422; a segment with kind: "bogus" → 422; a record with a stray top-level key (e.g. "foo": 1) still → 422 (the extra="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 (never null — 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).
  • 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.py tests stay green unchanged
  • uv run pytest tests/integration -q green; uv run ruff check . && uv run pyright clean
  • no change in app/rag/, no migration, no router change (a git diff --stat scoped check)