phase: 95_read_truncation_cap
Build and Push Containers / build-and-push-app (push) Successful in 1m38s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All gates are green. Final verification is complete — no fixes were needed; the phase is fully implemented and passing.

**Phase 95 final verification report**

- Verified all 3 tasks already implemented (task files in `complete/`): read cap + notice + `ToolResultPiece` (task 01), `tool_result` SSE + live/saved/shared marker (task 02), E2E suite (task 03).
- Code checks passed: `BOR_READ_MAX_CHARS` (default 128 000, validator), pinned `READ_TRUNCATION_NOTICE` + `TRUNCATION_MARKER`, byte-identical at/under-cap read, holder `read_truncations` (no `tool_calls` impact), piece order (after `tool`, before next round), `ChatToolResultEvent`, `ToolCall` back-compat fields, `app.js` live/restore handlers, `shared.js`, `.truncated-note` CSS, `.env.example` entry, mock-LLM echo script.
- **Tests:** `uv run pytest` → 1966 passed; `uv run pytest --cov=app --cov-report=term-missing` → all green, TOTAL **99%** (>90% gate).
- **E2E:** `uv run pytest tests/e2e/test_read_truncation_cap.py -v --no-cov` → **3 passed** (frame order + live marker + LLM notice via echo; save→shared fidelity; short-read control).
- **Regression (isolated):** `test_agent_document_tools` 4 ✓, `test_chat_history` 5 ✓, `test_share_chat` 4 ✓, `test_big_read_progress` 4 ✓, `test_stop_generation` 3 ✓.
- **Lint/types:** `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.

**Completion criteria:** ① over-cap read → first-cap-chars + marker + pinned notice — ✓ (unit-pinned: at-cap/cap+1/notice tests); ② user marker live/saved/shared — ✓ (E2E + frontend tests); ③ at/under cap byte-identical, no frame — ✓ (unit + control E2E); ④ top-2 `<documents>` retrieval untouched — ✓ (`app/rag/retriever.py` unmodified vs HEAD); ⑤ suite green, >90% coverage, ruff+pyright clean — ✓; ⑥ no completed-phase behavior change — ✓ (all gates green; commit left to harness per pass rules).

- No defects found; no changes made this pass. Next pending phase: none in `todo/` (96 is the next free number).
This commit is contained in:
2026-09-11 03:42:51 -04:00
parent d4943b4822
commit bcaef800c5
36 changed files with 2836 additions and 43 deletions
+40
View File
@@ -153,6 +153,35 @@ class ChatToolEvent(BaseModel):
argument: str | None = None # the single string argument passed, or null
class ChatToolResultEvent(BaseModel):
"""SSE frame for one executed tool call whose result was truncated
(phase 95, ``TODO.md`` L5).
A15 extension (owner permission 2026-09-10 — recorded in the phase 95
overview ``00_phase.md``; PLAN.md is being redone by the owner): the
SSE event-type list grows from six to SEVEN — ``thinking``,
``tool``, ``retry``, ``delta``, ``done``, ``error`` and this
optional ``tool_result``. The frame is strictly ADDITIVE: existing
frames and clients are untouched (a client that does not know the
type simply ignores it), and it is emitted ONLY for a truncated
``read`` — one frame per truncated read, carrying the counts the UI
renders as "(truncated — showing N of M chars)". It always follows
the matching :class:`ChatToolEvent` frame for the same call (the
line is already on screen; the marker lands a beat later — the
phase-37/48 tool-line lifecycle is untouched). ``argument`` is the
combined ``source/path`` the model passed (identical to the matching
``tool`` frame's argument, so the client can match the two); a
non-truncated read streams NO frame of this type.
"""
type: str = "tool_result"
name: str # the tool that was executed (always "read" today)
argument: str | None = None # the model's argument (combined source/path)
truncated: bool = True # always True on a sent frame (the emission trigger)
chars_shown: int = Field(ge=0) # the cap kept (settings.read_max_chars)
chars_total: int = Field(ge=0) # the document's true length
class ChatDoneEvent(BaseModel):
"""Final SSE event of a chat turn: metadata for the finished answer."""
@@ -456,10 +485,21 @@ class ToolCall(BaseModel):
``AGENT_TOOLS`` names are short) and ``argument`` ≤ 2000 (the
combined ``source/path`` identity is ≤ 120 + 1 + 1000; 2 000 is 2×
headroom for a grep pattern).
Phase 95 (task 02): the truncation marker the UI renders next to the
Reading line rides the SAME record — ``truncated`` (default False:
the pre-phase-95 shape) + the two non-negative counts. Small
additive fields with defaults, no migration (``ChatMessage.tools``
is JSON) — a saved chat written before phase 95 (no fields) validates
UNCHANGED (the phase-50 backward-compat rule) and renders without
the marker.
"""
name: str = Field(max_length=100)
argument: str | None = Field(default=None, max_length=2000)
truncated: bool = False
chars_shown: int | None = Field(default=None, ge=0)
chars_total: int | None = Field(default=None, ge=0)
#: One suggestion chip (phase 83, A1): a short deterministic string —