phase: 158_thinking_timeline
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s

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.
This commit is contained in:
2026-10-05 02:52:55 -04:00
parent 3f752fb77f
commit 31bd903a93
49 changed files with 4280 additions and 482 deletions
@@ -0,0 +1,69 @@
# 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)