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
+28
View File
@@ -152,6 +152,23 @@ class Settings(BaseSettings):
#: request with ``tools=None`` (the pre-phase-37 path — the kill
#: switch). Negative values are rejected at startup (validator).
agent_max_rounds: int = 10
#: Cap in characters on the agent ``read`` tool's result (phase 95,
#: ``BOR_READ_MAX_CHARS``): a document LONGER than this is cut at the
#: cap and the shared ``[…truncated…]`` marker plus the grep-pointer
#: notice (``app.rag.agent``) are appended; a document at or under the
#: cap is read whole, byte-identical to the pre-phase-95 result. Spec
#: rationale (pinned): 128 000 chars ≈ **32 000 tokens** at the
#: ~4-chars/token house estimate (``app.rag.llm``'s embed batching
#: notes ~3 chars/token for code-dense text, 4 for prose) — a quarter
#: of the 128k-token **minimum** context the owner's LLMs all have, so
#: a truncated read still leaves ~96k tokens for the system prompt, the
#: top-2 ``<documents>``, the tool rounds, and the 32 768-token answer
#: cap (``max_output_tokens``). Char-based (no tokenizer in the repo —
#: the ``BOR_SUMMARY_MAX_CHARS`` precedent) and env-tunable in both
#: directions. This is the ONLY truncated read path (owner permission
#: 2026-09-10, ``TODO.md`` L5): A7's never-truncated contract is for
#: the retrieval ``<documents>`` path, which stays whole.
read_max_chars: int = 128_000
# --- Hybrid retrieval (A7, revised 2026-08-21) ---
# cosine top-N ∪ Postgres FTS top-N, fused with Reciprocal Rank Fusion
@@ -274,6 +291,17 @@ class Settings(BaseSettings):
raise ValueError("agent_max_rounds must be >= 0 (0 = no tools)")
return v
@field_validator("read_max_chars")
@classmethod
def _read_max_chars_non_negative(cls, v: int) -> int:
"""A negative cap is a typo — it would slice from the END of the
content (negative indexing) instead of failing. Fail loud at
startup (the ``agent_max_rounds`` pattern). ``0`` is legal (every
non-empty read truncates to the marker + notice)."""
if v < 0:
raise ValueError("read_max_chars must be >= 0 (chars)")
return v
@field_validator("llm_retries")
@classmethod
def _llm_retries_non_negative(cls, v: int) -> int: