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
+79 -73
View File
@@ -1,17 +1,21 @@
"""Unit: the phase-109 never-frozen-turn frontend contract (tasks 01-02).
"""Unit: the phase-109 never-frozen-turn frontend contract (tasks
01-02), updated by phase 158 (task 02).
No new Python app logic exists for this task — the behavior lives in
``frontend/assets/app.js`` and is E2E-gated by the story suite (task 03).
Like the other frontend-adjacent unit files (the house
read-the-assets-as-text pattern, cf. ``test_frontend_tool_states.py``),
this module pins the JS markers the phase depends on: the thinking block
becomes a TOGGLE (D15) — ``thinking`` frames open it (idempotent),
``delta`` frames close it — while the phase-14 restore path and the
phase-17 follow-the-tail pin logic stay byte-for-byte untouched. Task 02
pins the persistent in-turn loader (D16): the static ``#turn-loader``
markup, ``setUiState`` as its SOLE visibility owner (the structural
never-stale guarantee), and the CSS contract (reused typing-dot
animation + reduced-motion + provenance).
this module pins the JS markers the phase depends on: the phase-109
D15 re-open pin is REPLACED by the phase-158 per-run contract (owner
direction 2026-10-04 — ``thinking`` frames open a NEW block per
contiguous run, ``delta`` frames fold the active run, a later
thinking frame opens a NEW run, not a re-open) — while the
phase-14 restore path and the phase-17 follow-the-tail pin logic stay
byte-for-byte untouched (the capture moves verbatim, per block).
Task 02 (phase 109) pins the persistent in-turn loader (D16): the
static ``#turn-loader`` markup, ``setUiState`` as its SOLE visibility
owner (the structural never-stale guarantee), and the CSS contract
(reused typing-dot animation + reduced-motion + provenance).
"""
from __future__ import annotations
@@ -48,87 +52,88 @@ def _thinking_branch() -> str:
return js[thinking_idx:tool_idx]
def test_thinking_handler_reopens_the_collapsed_block() -> None:
"""Phase 109 (D15): the `thinking` handler re-opens the block —
``block.open = true`` sits in the handler, positioned AFTER the
``ensureThinkingBlock(wrap)`` line (the block must exist before it
can be opened). Idempotent: a no-op while already open (the
pre-delta live flow), a re-open after a `delta` closed it (the next
agent round — the reported freeze, TODO.md L3)."""
def test_thinking_handler_opens_a_new_block_per_run() -> None:
"""Phase 158 (D1 — the phase-109 D15 re-open pin, replaced by
owner direction 2026-10-04): the `thinking` handler opens a NEW
block per contiguous run — ``ensureThinkingRun(wrap)`` sits in the
handler (a frame of a new kind closes the previous run and opens
a new block), the RUN's text accumulates + re-renders into THAT
block, and the D15 re-open assignment (``block.open = true``) is
GONE from the branch — the open state is owned by the builder
(a new run opens; the active run stays open)."""
branch = _thinking_branch()
ensure_idx = branch.find("ensureThinkingBlock(wrap)")
assert ensure_idx != -1, "the handler must ensure the block first"
reopen_idx = branch.find("block.open = true")
assert reopen_idx != -1, (
"the `thinking` handler must open the block (D15: "
"open-while-thinking)"
ensure_idx = branch.find("ensureThinkingRun(wrap)")
assert ensure_idx != -1, (
"the handler must open/extend the run's block (D1: per-run — "
"a new kind opens a NEW block)"
)
assert ensure_idx < reopen_idx, (
"`block.open = true` must sit AFTER `ensureThinkingBlock(wrap)` "
"— the block must exist before it is opened"
assert "currentRun.run.text += ev.text || \"\"" in branch, (
"the RUN's text accumulates (the block renders the run, not "
"the whole-turn accumulator)"
)
assert "block.open = true" not in branch, (
"the D15 re-open assignment is GONE — ensureThinkingRun owns "
"the open state"
)
def test_delta_handler_still_closes_the_block() -> None:
"""Phase 109 (D15): the close side of the toggle survives — the
`delta` handler still calls ``closeThinkingBlock(wrap)`` (closed
while answering); nothing else about the block's lifecycle changes."""
def test_delta_handler_folds_the_active_run() -> None:
"""Phase 158 (D5 — the phase-109 D15 close-side pin, updated):
the close side of the per-run contract — the `delta` handler calls
``closeCurrentRun()`` (folds the active run, idempotent); a later
thinking frame opens a NEW run above the bubble (the D15 re-open
of the shared block is gone)."""
js = _js()
delta_idx = js.find('ev.type === "delta"')
done_idx = js.find('ev.type === "done"')
assert -1 < delta_idx < done_idx, "the turn handler must branch on delta"
branch = js[delta_idx:done_idx]
assert "closeThinkingBlock(wrap)" in branch, (
"the `delta` handler must keep closing the block (D15: "
"closed-while-answering)"
assert "closeCurrentRun()" in branch, (
"the `delta` handler must fold the active run (D5: "
"folded-while-answering)"
)
def test_close_thinking_block_docstring_says_toggle_not_one_way() -> None:
"""Phase 109 (D15): the narrative flips. The section comment
documenting ``closeThinkingBlock`` no longer claims "never reopens"
— nowhere in app.js does — and documents the toggle contract instead:
open-while-thinking / closed-while-answering, naming the `thinking`
handler's re-open and the `delta` handler's close. The function
body itself is unchanged (idempotent close, no-op without a
block)."""
def test_section_comment_says_per_run_not_reopen() -> None:
"""Phase 158 (D1 — the phase-109 D15 narrative, replaced): the
section comment documenting the run builders no longer claims the
D15 toggle contract (the "re-opens" narrative is gone — nowhere
in app.js does "never reopens" survive either) — it names the
per-run timeline instead: each CONTIGUOUS run its own block,
arrival order, D2 placement, and the D15 contract as replaced."""
js = _js()
fn = js.find("function closeThinkingBlock")
assert fn != -1, "closeThinkingBlock must exist"
body = js[fn : js.find("\n}\n", fn)]
assert "block.open = false" in body, "the close side is unchanged"
# The block comment that documents the helpers: walk back from the
# function to its section comment start.
comment_start = js.rfind("/*", 0, fn)
assert comment_start != -1
section = js[comment_start:fn]
assert "never reopens" not in section, (
"the one-way-door claim is gone (phase 109, D15)"
)
assert "never reopens" not in js, (
"the 'never reopens' narrative must not survive anywhere in app.js"
"the one-way-door narrative must not survive anywhere in app.js"
)
# The block comment that documents the builders: walk back from
# the first builder to its section comment start.
comment_start = js.rfind("/*", 0, js.find("function createThinkingBlock"))
assert comment_start != -1
section = js[comment_start : js.find("function createThinkingBlock")]
assert "re-opens" not in section, (
"the D15 re-open narrative is gone (phase 158, D1)"
)
assert "PER-RUN" in section or "per-run" in section, (
"the contract is named: per-run"
)
assert "CONTIGUOUS" in section, "a contiguous run per block is named"
assert "arrival order" in section, "D2: arrival order is named"
assert "REPLACED" in section or "superseded" in section.lower(), (
"the D15 contract is named as replaced"
)
# The toggle contract is documented — and it names both sides:
# the `thinking` handler's re-open and the `delta` handler's close.
assert "TOGGLE" in section, "the contract is named: a toggle"
assert "open-while-thinking" in section
assert "closed-while-answering" in section
assert "`thinking`" in section, "the contract names the thinking handler"
assert "re-opens" in section, "the contract names the thinking handler's re-open"
assert "`delta`" in section, "the contract names the delta handler's close"
def test_restore_path_still_collapses_stored_blocks() -> None:
"""Phase-14 contract regression: ``renderStoredMessage`` restores
stored thinking blocks COLLAPSED (``block.open = false``) — the
phase-109 toggle only changes the LIVE `thinking` handler, the
restore path is untouched."""
stored thinking blocks COLLAPSED (``block.open = false`` — the
shared ``createThinkingBlock`` builder, phase 158 task 02) — the
phase-158 per-run live path never opens a restored block."""
js = _js()
fn = js.find("function renderStoredMessage")
assert fn != -1, "renderStoredMessage must exist"
body = js[fn : js.find("\n}\n", fn)]
assert "if (m.thinking)" in body
assert "ensureThinkingBlock(wrap)" in body
assert "createThinkingBlock()" in body
assert "block.open = false" in body, "stored blocks restore collapsed"
# The live re-open must NOT have leaked into the restore path:
# only the collapse assignment may appear in the function body.
@@ -142,10 +147,11 @@ def test_follow_the_tail_pin_logic_is_untouched() -> None:
unchanged — ``THINKING_NEAR_BOTTOM_PX`` (32px band) +
``isThinkingNearBottom`` and the pre-render capture
``const pinned = block.open && isThinkingNearBottom(textEl);``
(measured BEFORE the re-render) still sit in the `thinking` handler.
The capture already keys off ``block.open``, so a re-opened block
resumes pinned tail-following exactly like the live pre-delta
block — no pin logic change needed for D15."""
(measured BEFORE the re-render) still sit in the `thinking`
handler — now operating per block (phase 158: the re-render is
the RUN's text, not the whole-turn accumulator). The capture
already keys off ``block.open``, so a live-open block resumes
pinned tail-following exactly like the phase-17 flow."""
js = _js()
assert "export const THINKING_NEAR_BOTTOM_PX = 32;" in js, (
"the 32px window band is unchanged"
@@ -158,12 +164,12 @@ def test_follow_the_tail_pin_logic_is_untouched() -> None:
branch = _thinking_branch()
capture = "const pinned = block.open && isThinkingNearBottom(textEl);"
assert capture in branch, (
"the pre-render `block.open &&` guard line is unchanged — D15 "
"keeps the pin logic untouched (it already reads block.open "
"before the re-render)"
"the pre-render `block.open &&` guard line is unchanged — the "
"pin logic moves verbatim into the per-run branch (phase 17 "
"regression)"
)
capture_idx = branch.find(capture)
render_idx = branch.find("textEl.innerHTML = renderMarkdown(thinkingAcc)")
render_idx = branch.find("textEl.innerHTML = renderMarkdown(currentRun.run.text)")
assert -1 < capture_idx < render_idx, (
"pin state is still measured BEFORE the re-render (phase-17 "
"regression — the 2-newline-gap fix)"