phase: 192_verbatim_inline_midturn
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 14s

Phase 192 — final verification pass complete.

- **Verified (all 4 tasks already complete):** additive `verbatim_block` SSE frame (`VerbatimBlockPiece` → `ChatVerbatimBlockEvent` → chat dispatch, after the round's `tool`/`tool_result`, before the next round); `hydrateVerbatimNow` matched-only mid-turn pass + app.js branch; `dropPlacedTwin` per-locator done/restore dedupe; the dedicated E2E suite.
- **Root cause of the rejected attempt:** my own concurrent background `pytest` run deadlocked the harness validation run on the shared Postgres (`psycopg.errors.DeadlockDetected`). No code defect — re-ran every gate **exclusively**.
- `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, `TOTAL 7181 16 99%` (>90%)
- `uv run pytest tests/e2e/test_verbatim_inline_midturn.py --no-cov` → **6 passed** (isolated)
- `uv run pytest tests/e2e/test_verbatim_show_text.py tests/e2e/test_chat_history.py --no-cov` → **18 passed** (`test_chat_history.py` byte-unedited; the phase-182 suite's "no eighth type" pin was tightened, not loosened, to seven + `verbatim_block` with frame-order asserts)
- `uv run ruff check .` → All checks passed (exit 0) · `uv run pyright` → **0 errors**
- **Completion criteria:** single frame per success + zero per refusal ✅ · `BOR_ANSWER_TEXT=0` wire byte-identical (integration spelled baseline + E2E App B) ✅ · fence hydrates in place / unfenced lands at the call moment, before `done` ✅ · append+fence → 1 panel, two ranges → 2 panels ✅ · restore 1 panel/locator, `done.answer_text` + record key + `verbatim_blocks=N` unchanged ✅ · suite green, coverage 99%, lint/types clean ✅ · no-show_text turn byte-identical ✅ · commit → left to the harness (no git index/history commands run).
- **Docs:** README reviewed line-by-line (2399 lines) + AGENTS.md in full; mechanically verified all 86 `BOR_*` settings, every `.env.example` var, table defaults, internal anchors (0 broken), ports 8123/8901, tree depth 1–8, pagination 25/1–100, 17 palette colors, `0029`/`0030` migrations, and every cited test file. Fixed 1 inconsistency: the verbatim "Parity" bullet claimed the live turn/reload/shared page share "the same three globals" — now names the settle-path trio and states that `hydrateVerbatimNow` is the ONE chat-only addition (a shared page has no stream).
- **Notable:** `run_agent`'s four repeated piece unions were collapsed into the ONE exported `AgentPiece` alias, so a future piece type is spelled in one place; `test_frontend_edit_message.py`'s `runTurn` byte-pin was regenerated, not weakened.
- **Next pending phase:** `193_sync_remaining_count`.
This commit is contained in:
2026-10-19 12:19:51 -04:00
parent 4f20c256d7
commit fb17d15fbe
49 changed files with 17313 additions and 102 deletions
+41 -9
View File
@@ -1327,6 +1327,27 @@ The model never retypes quoted text, so a displayed quote cannot drift.
each block is capped at `BOR_SHOW_TEXT_MAX_CHARS`, cut at a line boundary
with a truncation marker. Image documents are refused with a pointer to
`show_image` — the two display tools never convert into each other.
- **The panel lands while the answer streams.** A successful `show_text`
sends its block TWICE on purpose (phase 192): once as the additive
`verbatim_block` SSE frame — the eighth event type,
`{type, source, path, title, start_line, end_line, total_lines, text,
truncated, locator}`, i.e. exactly one `done.answer_text` item — emitted
AFTER that round's `tool` / `tool_result` frames and BEFORE the next
model round, so the panel appears where the model introduced the bytes
instead of after it finished talking; and once on the `done` frame below.
A refused call sends nothing, and `BOR_ANSWER_TEXT=0` offers no tool at
all, so such a turn's frame sequence is byte-identical to pre-phase
(pinned on the wire). Client-side, `hydrateVerbatimNow` settles the pending
fence that block names IN PLACE the moment the frame arrives and leaves every
other placeholder pending; when the answer has not fenced it (yet), the panel
lands at the `answer_image` position then — not at the end of the answer. If
the model fences that same locator in a later round, hydration drops the
appended twin, so **one canonical locator paints exactly one panel**, at the
fence (two honest ranges of one file stay two panels: the scan is per
locator, bounded by the message's `.msg` wrap — or by the root when there is
no wrap, so a missing wrap narrows the scan instead of switching the dedupe
off, and one answer never reaches into the next one's panels). There is no
client-side flag check — the server's gate is the only one.
- **The bytes ride the `done` frame.** A turn that displayed text ends
with one extra, additive SSE key: `done.answer_text`, the list of blocks
in **display order** — answer order first (one per ` ```bor ` fence, the
@@ -1338,10 +1359,13 @@ The model never retypes quoted text, so a displayed quote cannot drift.
1-based inclusive extent — the numbers a caption shows can never be the
model's claim. `locator` is the canonical matching key
(`source/path`, or `source/path#12-40` for a range). There is no new
bytes route and no eighth SSE event type: the bytes travel inline, and a
saved or shared chat renders its panels from its own record. The same
array persists on the saved brain record (`bor.chat.v1` `answer_text`),
so a reload and an anonymous shared page show the same blocks.
bytes route: the bytes travel inline (the only extra frame is the
phase-192 `verbatim_block` above, carrying that same item), and a saved
or shared chat renders its panels from its own record — the `done`
frame, the saved record and the restore path are UNCHANGED by phase 192.
The same array persists on the saved brain record (`bor.chat.v1`
`answer_text`), so a reload and an anonymous shared page show the same
blocks.
- **Deflected turns resolve nothing.** A ` ```bor ` fence written into a
deflected answer leaks no KB text — the disclaimer turn resolves no
block and its frame carries no `answer_text` key.
@@ -1379,8 +1403,10 @@ The model never retypes quoted text, so a displayed quote cannot drift.
- **Hydration paints the panel, and the panel is the file.** The client half
is one classic script, `frontend/assets/verbatim.js`, loaded after
`markdown.js` on the chat page and the shared page. At `done` (and on every
restore) it matches each placeholder's normalized `data-bor-locator`
`markdown.js` on the chat page and the shared page. Mid-turn it settles the
one block a `verbatim_block` frame carries (`hydrateVerbatimNow`); at `done`
(and on every restore) it matches each placeholder's normalized
`data-bor-locator`
against the frame's `answer_text[].locator` with a plain string compare —
`markdown.js::borLocator` stays the ONLY locator grammar in the client, so
the two sides cannot drift — and paints:
@@ -1426,8 +1452,14 @@ The model never retypes quoted text, so a displayed quote cannot drift.
exists once (the owner-locked share strings are unchanged; phase 189 added
the save-before-the-token upsert in front of the same success paths).
- **Parity, and what stays untouched.** The live turn, a reload and an
anonymous shared page call the same three globals in the same order, so one
answer renders one way. Only the two answer renderers (plus the shared
anonymous shared page call the same three **settle-path** globals in the same
order — `hydrateVerbatim` → `appendUnlocatedVerbatim` →
`enhanceAnswerCodeBlocks` — so one answer renders one way. The phase-192
mid-turn pass (`hydrateVerbatimNow`) is the ONE chat-only addition: a shared
page has no stream to land a panel mid-turn, so it hydrates from its record
through the same trio (which is why a reload and a shared page show the same
panel the live turn showed at the `show_text` call). Only the two answer
renderers (plus the shared
page's) opt in: a question, a thinking run and the document viewer stay
one-argument calls, and `document.html` does not even load the panel client.
@@ -1886,7 +1918,7 @@ startup (the DB rows shadow the env, `.env` is never rewritten).
|-----|---------|---------|
| `BOR_AGENT_MAX_ROUNDS` | `10` | Hard cap on agent tool rounds per grounded turn (0 = no tools); every tool result carries the model's remaining count (phase 160) |
| `BOR_READ_MAX_CHARS` | `128000` | Cap on a `read` tool result (chars); over-cap reads truncate + a notice that teaches the grep → `read(offset, limit)` workflow (phase 95, rewritten phase 131) |
| `BOR_ANSWER_TEXT` | `1` | Verbatim file display (phase 182): the **master switch, ON by default** — an answer's code block is typed by the server from the indexed bytes, never retyped by the model. `0` = the escape hatch: the `show_text` tool is not offered, its `<tools>` teaching clause is absent, ` ```bor ` locator fences are not resolved, and the `done` frame carries no `answer_text` key (the server surface is exactly pre-phase; a stray fence on the client still settles to the honest unresolved sentence rather than the model's copy) |
| `BOR_ANSWER_TEXT` | `1` | Verbatim file display (phase 182): the **master switch, ON by default** — an answer's code block is typed by the server from the indexed bytes, never retyped by the model. `0` = the escape hatch: the `show_text` tool is not offered, its `<tools>` teaching clause is absent, ` ```bor ` locator fences are not resolved, no `verbatim_block` frame is sent, and the `done` frame carries no `answer_text` key (the server surface — frame sequence included — is exactly pre-phase; a stray fence on the client still settles to the honest unresolved sentence rather than the model's copy) |
| `BOR_SHOW_TEXT_MAX_CHARS` | `12000` | Per-block character budget for ONE displayed verbatim slice (phase 182): the slice is cut at a **line boundary**, the shared `[…truncated…]` marker is appended and the caption's line range shrinks to what is actually shown. `0` = inert (nothing displays); a negative value refuses startup naming the field |
| `BOR_SHOW_TEXT_MAX_BLOCKS` | `5` | Per-answer cap on displayed verbatim blocks (phase 182), counted in answer order. `0` = inert (nothing displays); a negative value refuses startup naming the field |