phase: 95_read_truncation_cap
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:
@@ -80,6 +80,19 @@
|
||||
* optional `tools: [{name, argument}]` array next to `thinking` and
|
||||
* restore re-renders the lines (phase 14 convention).
|
||||
*
|
||||
* Truncated reads (phase 95, A15 extension — `tool_result` is the
|
||||
* seventh, optional SSE event type; existing frames untouched, unknown
|
||||
* types ignored): a `read` longer than BOR_READ_MAX_CHARS streams ONE
|
||||
* `tool_result` frame after its `tool` frame, and the handler appends
|
||||
* the " (truncated — showing N of M chars)" marker to that Reading
|
||||
* line (appendTruncatedNote — DOM append, never a re-render) AND stamps
|
||||
* the matching toolAcc entry (newest, same argument) with `truncated` /
|
||||
* `chars_shown` / `chars_total` — the save payload carries the record
|
||||
* with zero other change, and BOTH restore paths (the phase-14 local
|
||||
* renderStoredMessage and the shared page's addToolLines) re-render the
|
||||
* same marker from the stored record (pixel-identical, the phase-50
|
||||
* restore contract). A non-truncated read streams no frame at all.
|
||||
*
|
||||
* LLM retry status (phase 67, TODO.md L3): if the endpoint dies BEFORE
|
||||
* the first frame of an LLM request lands, the server restarts that
|
||||
* request (up to BOR_LLM_RETRIES retries, BOR_LLM_RETRY_DELAY seconds
|
||||
@@ -959,6 +972,34 @@ function appendToolLine(wrap, name, argument) {
|
||||
container.appendChild(line);
|
||||
}
|
||||
|
||||
/* Phase 95 (task 02): the truncation marker on a Reading line. The
|
||||
* `tool_result` frame's argument is the model's raw `source/path` — the
|
||||
* SAME string the matching `tool` frame put in the line's `<code>` child
|
||||
* (textContent carries data, never markup) — so the newest `.tool-call`
|
||||
* line whose code child holds that argument is the target (one line per
|
||||
* call, phase 37/48 — the marker APPENDS a span sibling, it never
|
||||
* rewrites the line's pinned template text). createElement + textContent
|
||||
* only — the house "this file never builds HTML" rule (no innerHTML).
|
||||
* A frame for a line that is no longer in the DOM (New Chat mid-turn)
|
||||
* is a silent no-op — the persisted record still carries the counts.
|
||||
* Pinned marker copy (unit + E2E assertion target): " (truncated —
|
||||
* showing N of M chars)" — plain integers, no thousands separators. */
|
||||
function appendTruncatedNote(wrap, argument, charsShown, charsTotal) {
|
||||
const calls = wrap?.querySelector?.(".tool-calls");
|
||||
if (!calls) return;
|
||||
const lines = calls.querySelectorAll(".tool-call");
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
const code = lines[i].querySelector("code");
|
||||
if (!code || code.textContent !== argument) continue;
|
||||
const note = document.createElement("span");
|
||||
note.className = "truncated-note";
|
||||
note.textContent =
|
||||
" (truncated — showing " + charsShown + " of " + charsTotal + " chars)";
|
||||
lines[i].appendChild(note);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- suggestions (shared chip component, phase 05) ----------
|
||||
*
|
||||
* One component, two homes: the onboarding row in the empty state and the
|
||||
@@ -1442,11 +1483,18 @@ function renderStoredMessage(m) {
|
||||
if (Array.isArray(m.tools)) {
|
||||
// Phase 37: restore the tool lines in saved order through the SAME
|
||||
// append helper as the live frames (no HTML from storage, ever).
|
||||
// Phase 95: a stored truncation record (truncated + the counts, the
|
||||
// live tool_result frame's stamp) re-renders the SAME marker next to
|
||||
// its Reading line — old records without the field render
|
||||
// unchanged (t.truncated falsy → no marker).
|
||||
for (const t of m.tools) {
|
||||
if (!t || typeof t.name !== "string") continue;
|
||||
const arg =
|
||||
typeof t.argument === "string" && t.argument ? t.argument : null;
|
||||
appendToolLine(wrap, t.name, arg);
|
||||
if (t.truncated && arg) {
|
||||
appendTruncatedNote(wrap, arg, Number(t.chars_shown) || 0, Number(t.chars_total) || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m.deflected) {
|
||||
@@ -2320,6 +2368,37 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
}, leavePartialIndex);
|
||||
lastBrainWrap = wrap; // this bubble is now the last brain answer
|
||||
markLastRetryable(); // phase 49: the Retry button is last-bubble-only
|
||||
} else if (ev.type === "tool_result") {
|
||||
// Phase 95 (A15 extension, task 02): the truncation the LLM is
|
||||
// told about is told to the USER. One frame per truncated read,
|
||||
// always after its `tool` frame and before the next round — the
|
||||
// Reading line is already on screen. Settle that line's
|
||||
// elapsed clock like every other frame (the marker is the
|
||||
// visible feedback now), then append the marker to the NEWEST
|
||||
// line carrying this argument (appendTruncatedNote — a DOM
|
||||
// append to the existing line: no new line, no re-render, the
|
||||
// phase-37/48 tool-line lifecycle is untouched) and stamp the
|
||||
// matching toolAcc entry so the save payload carries it (the
|
||||
// `done` save point below needs zero other change). A frame
|
||||
// whose line/toolAcc entry is gone (New Chat mid-turn) is a
|
||||
// silent no-op; a non-truncated read never sends one.
|
||||
settleToolLine();
|
||||
const argument =
|
||||
typeof ev.argument === "string" && ev.argument ? ev.argument : null;
|
||||
const shown = Number(ev.chars_shown) || 0;
|
||||
const total = Number(ev.chars_total) || 0;
|
||||
if (argument && ev.truncated) {
|
||||
appendTruncatedNote(wrap, argument, shown, total);
|
||||
for (let i = toolAcc.length - 1; i >= 0; i -= 1) {
|
||||
const t = toolAcc[i];
|
||||
if (t && t.argument === argument) {
|
||||
t.truncated = true;
|
||||
t.chars_shown = shown;
|
||||
t.chars_total = total;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ev.type === "error") {
|
||||
throw new Error(ev.detail || "Something went wrong on my side.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user