phase: 182_verbatim_show_text
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
This commit is contained in:
2026-10-15 08:33:12 -04:00
parent 814a400e6d
commit b34b8ca672
104 changed files with 10852 additions and 203 deletions
+402 -2
View File
@@ -662,7 +662,10 @@ Implements just enough of the aipi surface:
- user message containing ``show me the picture of``
(``SHOW_IMAGE_TRIGGER``, phase 124 task 07 — the image-chips-and-
answers suite ``tests/e2e/test_answer_images.py``) **and** the
system prompt contains ``show_image`` (the tool clause's presence
system prompt carries the show_image clause's marker
(:data:`_SHOW_IMAGE_CLAUSE_MARKER` — a sentence only the image
clause carries, so the phase-182 ``show_text`` clause's pointer to
``show_image`` never trips it; the tool clause's presence
— the ``BOR_ANSWER_IMAGES`` toggle's server-side marker: the
clause rides inside the HIGH prompt's ``<tools>`` section ONLY
when the toggle is on, so on a default-OFF app the trigger NEVER
@@ -691,6 +694,62 @@ Implements just enough of the aipi surface:
phrase — the phase-71/72/94/118 ordering convention); verified
2026-09-26: no existing E2E question or fixture file contains the
phrase, so every other suite is unaffected.
- user message containing ``show me the exact text of``
(``SHOW_TEXT_TRIGGER``, phase 182 task 08 — the verbatim-display suite
``tests/e2e/test_verbatim_show_text.py``) **and** the system prompt
carries the show_text clause's marker
(:data:`_SHOW_TEXT_CLAUSE_MARKER` — a sentence only the TEXT clause
carries, the exact mirror of ``_SHOW_IMAGE_CLAUSE_MARKER``, so a
``BOR_ANSWER_TEXT=0`` app never fires it) -> the deterministic
SHOW_TEXT flow, discriminated statelessly from the tool results
(streaming only). The question carries the scripted target — a
combined ``source/path`` with an optional ``12-40`` line range and an
optional ``describe it without a block`` marker
(``SHOW_TEXT_NO_FENCE_MARKER``):
* request 1 (``tools`` offered, no tool result yet): stream ONLY
``tool_calls`` deltas — ``show_text`` with ``{"path": <target>}``
plus ``offset``/``limit`` when the question named a range
(synthetic id ``call_show_text``); the target comes from the
QUESTION (not the seed), which is what lets one trigger drive a
text doc, an image doc (the LOCKED-A6 refusal) and an
out-of-range request;
* the follow-up request (any ``tool``-role result — a refusal means
"stop calling" too): the FINAL answer — prose, one
`` ```bor <locator> `` fence whose BODY is a DELIBERATELY
CORRUPTED copy of the slice the tool returned (one flipped
character + a trailing space via
:func:`corrupt_verbatim_line`, plus a line carrying
``SHOW_TEXT_MODEL_BODY_MARKER``), then a SECOND standalone
`` ```bor `` fence for the OTHER seeded text document when the show
SUCCEEDED (the answer-order half of the contract; a refused turn
stays a single-fence answer so the honest-state cases are clean), then
one ordinary
`` ```bash `` block (the model's own, A5's Copy control) — unless
the question carried ``SHOW_TEXT_NO_FENCE_MARKER``, which drops
every fence so the shown block lands UNLOCATED (the client's
append path, above ``.sources``).
Checked BEFORE the plain ``TOOLS_TRIGGER`` flow (disjoint trigger
phrase — the phase-71/72/94/118 ordering convention); no existing E2E
question or fixture file contains the phrase.
- user message containing ``put the block inline``
(``SHOW_TEXT_INLINE_TRIGGER``, phase 182 task 08, same suite) -> the
deterministic answer with ONE `` ```bor `` locator fence and ZERO tool
calls — the half of the contract the tool does not cover (a fence that
appears in the answer text on its own resolves through the same
server-side pass). Its body is the model's own claim (it never saw the
bytes), so it carries ``SHOW_TEXT_MODEL_BODY_MARKER`` and the suite
asserts none of it painted. NOT clause-gated on purpose: on the
``BOR_ANSWER_TEXT=0`` app the same text lands, which is how that suite
proves the client never paints the model's bytes even with the tool and
the clause gone. Checked BEFORE the ``DEFLECT_MODE`` branch (the marker
lives in the user message — the ``TABLE_TRIGGER`` convention).
- user message containing ``show me a block i cannot find``
(``SHOW_TEXT_MISSING_TRIGGER``, phase 182 task 08, same suite) -> the
deterministic answer whose one `` ```bor `` fence names
``SHOW_TEXT_MISSING_TARGET`` — a locator the server has NO document
for, so the client must land on its honest unresolved state (no badge,
no body, none of the model's bytes). Checked BEFORE the
``DEFLECT_MODE`` branch, same convention.
- user message containing ``which diagram is in your notes``
(``DIAGRAM_TRIGGER``, phase 124 task 07 — the BUG-FIX-ONLY
trigger, same suite) -> the deterministic ZERO-READ answer: the
@@ -2716,6 +2775,20 @@ CONTEXT_LINE_TRIGGER = "quote the image reference"
#: the house marker-flow convention).
_SHOW_CONFIRM_MARKER = "Image shown to the user"
#: The SHOW_IMAGE flow's clause gate (phase 124, task 07) — tightened
#: 2026-10-15 (phase 182, task 04). The gate's INTENT is "the
#: ``show_image`` clause is in the system prompt" (the
#: ``BOR_ANSWER_IMAGES`` toggle's server-side marker) and it was written
#: as the loose substring check ``"show_image" in system``. Phase 182's
#: locked ``SHOW_TEXT_CLAUSE`` ends with "For an image use `show_image`
#: instead.", so that approximation now also matches a text-clause-only
#: prompt and the image flow fired on the default-OFF app (the phase-124
#: toggle-off pin failed). One sentence only the IMAGE clause carries is
#: the exact marker: present iff ``show_image=True``, so the ON app fires
#: exactly as before and a ``BOR_ANSWER_IMAGES=0`` app stays silent even
#: when ``BOR_ANSWER_TEXT`` (default ON) is what mentions the tool.
_SHOW_IMAGE_CLAUSE_MARKER = "displays a standalone image document"
#: The image-doc extension family (``app.rag.summarizer.IMAGE_MIMES``
#: — phase 122's ext→mime map, imported so the mock can never drift
#: from the app's image family): a seeded ``<documents>`` block is
@@ -2800,7 +2873,7 @@ def _show_image_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
if SHOW_IMAGE_TRIGGER not in _user(body).lower():
return None
system = _system(body)
if "show_image" not in system:
if _SHOW_IMAGE_CLAUSE_MARKER not in system:
return None
doc = _seeded_image_doc(system)
if doc is None:
@@ -2812,6 +2885,295 @@ def _show_image_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
return ("call", f"{doc[0]}/{doc[1]}")
# ---------------------------------------------------------------------------
# Phase 182 (task 08): the SHOW_TEXT (verbatim display) flows
# ---------------------------------------------------------------------------
#: A user message containing this substring (case-insensitive) — combined
#: with the ``show_text`` clause's marker (:data:`_SHOW_TEXT_CLAUSE_MARKER`,
#: the ``BOR_ANSWER_TEXT`` toggle's server-side marker, exactly as
#: ``_SHOW_IMAGE_CLAUSE_MARKER`` is phase 124's) — drives the deterministic
#: SHOW_TEXT flow described in the module docstring: the scripted tool call
#: on the first request, the fence-carrying final answer once a tool result
#: exists. The TARGET lives in the question (a combined ``source/path`` +
#: an optional ``12-40`` range + an optional :data:`
#: SHOW_TEXT_NO_FENCE_MARKER`), which is what lets ONE trigger drive a text
#: doc, an image doc (LOCKED A6's refusal) and a wrong-range request.
#: Disjoint from every other trigger phrase — the phase-71/72/94/118
#: ordering convention; no existing E2E question or fixture file contains
#: the phrase, so every other suite is unaffected.
SHOW_TEXT_TRIGGER = "show me the exact text of"
#: In the show-me question: the final answer carries NO locator fence, so
#: the block the tool showed is UNLOCATED by the prose — the client's
#: append path (below the answer text, above ``.sources``, phase 124's DOM
#: order) is what renders it.
SHOW_TEXT_NO_FENCE_MARKER = "describe it without a block"
#: A user message containing this substring (case-insensitive) -> the ONE
#: locator fence with NO tool call at all (see the module docstring). Not
#: clause-gated: the ``answer_text=False`` app receives the same text, so
#: the suite can pin the client's own no-model-bytes rule on the kill-switch
#: build too.
SHOW_TEXT_INLINE_TRIGGER = "put the block inline"
#: A user message containing this substring (case-insensitive) -> the ONE
#: fence naming :data:`SHOW_TEXT_MISSING_TARGET` (a locator with no row).
SHOW_TEXT_MISSING_TRIGGER = "show me a block i cannot find"
#: The hallucinated locator (never seeded — the fixture's sources are
#: ``baselab`` and the suite seeds no file of this name).
SHOW_TEXT_MISSING_TARGET = "baselab/notes/the-file-that-is-not-there.md"
#: A token that exists ONLY in the model's re-typed fence BODY. The suite
#: asserts it appears nowhere in the rendered DOM (the body is dropped at
#: render, :mod:`app.rag.verbatim` supplies the bytes) while the answer's
#: ordinary `` ```bash `` block — legitimately the model's — stays.
SHOW_TEXT_MODEL_BODY_MARKER = "MODEL_RETYPED_THIS_LINE"
#: The ordinary (non-locator) fenced block every phase-182 answer carries —
#: LOCKED A5's control: `.code-block` + Copy, NO badge, flag-independent.
SHOW_TEXT_PLAIN_LANG = "bash"
SHOW_TEXT_PLAIN_BODY = "systemctl --user start gufo.container"
#: The SHOW_TEXT flow's clause gate (phase 182, task 08) — one sentence
#: :data:`app.rag.prompts.SHOW_TEXT_CLAUSE` carries and nothing else does,
#: present iff ``answer_text=True`` (the exact mirror of
#: :data:`_SHOW_IMAGE_CLAUSE_MARKER`, tightened for the same reason).
_SHOW_TEXT_CLAUSE_MARKER = "displays part of a document to the user EXACTLY as stored"
#: The agent's ``show_text`` SUCCESS result's confirming first line
#: (``app.rag.agent``'s ``show_text`` branch — the line ``read``'s result
#: never carries). Kept for the wire assertion; the flow itself stops on
#: ANY tool result, because a REFUSAL also means "do not call again" (the
#: wrong-range and image-doc variants depend on that: the model still
#: answers, and the fence it writes is what the client must judge honestly).
_SHOW_TEXT_CONFIRM_MARKER = "Text shown to the user"
#: The target inside the question: a combined ``source/path`` plus an
#: optional 1-based ``12-40`` / ``lines 12-40`` range — the model's CLAIM.
#: (The server's extent is whatever the resolver returned; the suite pins
#: the two apart.)
_SHOW_TEXT_TARGET_RE = re.compile(
r"(?P<path>[a-z0-9_.\-]+(?:/[a-z0-9_.\-]+)+)"
r"(?:\s+(?:lines?\s+)?(?P<start>\d+)\s*[-\u2013]\s*(?P<end>\d+))?"
)
def corrupt_verbatim_line(line: str) -> str:
"""The model's one-character drift on a real line (phase-182 E2E).
Flips the first ``a`` to ``4`` (or the last character when the line has
no ``a``) and appends a trailing space — exactly the silent drift a
retyping model produces, the incident the phase exists for. Exported so
the suite can derive the SAME string from the fixture's stored text and
assert none of it reached the DOM.
"""
flipped = line.replace("a", "4", 1)
if flipped == line:
flipped = line[:-1] + "4" if line else "4"
return flipped + " "
def _show_text_request(body: dict[str, Any]) -> tuple[str, int | None, int | None]:
"""The scripted target ``(path, claimed_start, claimed_end)``.
Markdown emphasis/backticks are stripped first: the questions quote the
path the way a person types it (``Show me the exact text of
`baselab/a/b.md` 2-999?``), and the closing backtick sits between the
path and its range.
"""
text = _user(body).lower().replace("`", "")
match = _SHOW_TEXT_TARGET_RE.search(text)
if match is None:
return "", None, None
start, end = match.group("start"), match.group("end")
return (
match.group("path"),
int(start) if start else None,
int(end) if end else None,
)
def _show_text_locator(path: str, start: int | None, end: int | None) -> str:
"""The locator the model writes: ``source/path`` or ``source/path#12-40``."""
return f"{path}#{start}-{end}" if start and end else path
def _show_text_slice(result: str) -> str:
"""The verbatim slice a ``show_text`` result carried (everything after its
``lines: a-b of N`` extent line), or ``""`` when the call was refused."""
lines = result.split("\n")
for index, line in enumerate(lines):
if line.startswith("lines: "):
return "\n".join(lines[index + 1 :])
return ""
def _show_text_fence_body(slice_text: str) -> str:
"""The deliberately corrupted copy of the slice + the sentinel line."""
lines = [line for line in slice_text.split("\n") if line.strip()]
if not lines:
lines = ["the model has nothing to retype after a refusal"]
return "\n".join(
[corrupt_verbatim_line(lines[0]), *lines[1:], SHOW_TEXT_MODEL_BODY_MARKER]
)
def _show_text_plain_block() -> str:
"""One ordinary fenced block — the model's own, badged nothing (A5)."""
return f"```{SHOW_TEXT_PLAIN_LANG}\n{SHOW_TEXT_PLAIN_BODY}\n```"
def _second_seeded_document(system: str, exclude: str) -> str:
"""The OTHER text document in the ``<documents>`` seed (combined path),
or ``""`` — the second fence's target (the answer-order half)."""
for block in _DOCUMENT_BLOCK_RE.finditer(system):
identity = f"{block.group('source')}/{block.group('path')}"
if identity == exclude or identity.lower().endswith(_IMAGE_PATH_SUFFIXES):
continue
return identity
return ""
def _show_text_call_arguments(path: str, start: str, end: str) -> dict[str, Any]:
"""The scripted ``show_text`` arguments: ``path`` always, ``offset`` +
``limit`` only when the question named a range (``limit`` derived from
the CLAIMED range — the server clamps it and the caption follows the
server)."""
arguments: dict[str, Any] = {"path": path}
if start:
first = int(start)
last = int(end) if end else first
arguments["offset"] = first
arguments["limit"] = max(last - first + 1, 1)
return arguments
def _show_text_answer(body: dict[str, Any], result: str) -> str:
"""The final answer of the SHOW_TEXT turn (prose + fences + a plain block).
With :data:`SHOW_TEXT_NO_FENCE_MARKER` in the question the answer carries
NO locator fence at all — the block the tool showed then has no home in
the prose, which is the client's APPEND path (below the answer text,
above ``.sources``). The SECOND (other-file) fence rides only a turn whose
show SUCCEEDED (:data:`_SHOW_TEXT_CONFIRM_MARKER`): a refused turn (the
image document, a range past the end) is the suite's honest-state case and
stays a single-fence answer.
"""
path, start, end = _show_text_request(body)
parts = ["Here is the exact text, straight from the file:"]
if SHOW_TEXT_NO_FENCE_MARKER not in _user(body).lower():
parts += [
"",
"```bor " + _show_text_locator(path, start, end),
_show_text_fence_body(_show_text_slice(result)),
"```",
]
second = (
_second_seeded_document(_system(body), path)
if _SHOW_TEXT_CONFIRM_MARKER in result
else ""
)
if second:
parts += [
"",
"And the note file next to it, in full:",
"",
"```bor " + second,
f"The model re-typed this file instead — {SHOW_TEXT_MODEL_BODY_MARKER}.",
"```",
]
else:
parts += [
"",
"It is on screen above — the panel holds it, so it is not repeated here.",
]
parts += [
"",
"To start it afterwards:",
"",
_show_text_plain_block(),
"",
"(Deterministic mock answer for E2E.)",
]
return "\n".join(parts)
def _show_text_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
"""Classify a phase-182 SHOW_TEXT request (see the module docstring).
Stateless over the messages, like the other marker flows:
* ``("call", path, start, end)`` — the trigger is in the question, the
system prompt carries the ``show_text`` clause and no ``tool``-role
result exists yet: stream ONLY the ``show_text`` tool-call delta built
by :func:`_show_text_call_arguments` from the three strings (``""`` =
no range).
* ``("answer", text)`` — a ``tool``-role result is in the messages
(success or refusal — both mean "stop calling"): the fence-carrying
final answer.
* ``None`` — the trigger is absent, the clause is absent (a
``BOR_ANSWER_TEXT=0`` app: the mock behaves exactly as pre-phase), or
the question names no ``source/path`` target.
"""
if SHOW_TEXT_TRIGGER not in _user(body).lower():
return None
if _SHOW_TEXT_CLAUSE_MARKER not in _system(body):
return None
path, start, end = _show_text_request(body)
if not path:
return None
results = _tool_results(body)
if results:
return ("answer", _show_text_answer(body, results[-1]))
return (
"call",
path,
str(start) if start is not None else "",
str(end) if end is not None else "",
)
def show_text_inline_answer(body: dict[str, Any]) -> str:
"""The ``put the block inline`` answer: ONE locator fence, zero tool
calls (see the module docstring)."""
path, start, end = _show_text_request(body)
return "\n".join(
[
"Sure — the block itself:",
"",
"```bor " + _show_text_locator(path, start, end),
f"Run `podman generate quadlet gufo` and paste the inline block "
f"for baselab — {SHOW_TEXT_MODEL_BODY_MARKER}.",
"```",
"",
"To apply it:",
"",
_show_text_plain_block(),
"",
"(Deterministic mock answer for E2E.)",
]
)
def show_text_missing_answer() -> str:
"""The ``show me a block i cannot find`` answer: a locator the server has
no document for, so the client must land on its honest state."""
return "\n".join(
[
"Here is the block you asked for:",
"",
"```bor " + SHOW_TEXT_MISSING_TARGET + "#3-4",
f"A file that does not exist, re-typed by the model "
f"{SHOW_TEXT_MODEL_BODY_MARKER}.",
"```",
"",
"(Deterministic mock answer for E2E.)",
]
)
def _summary_seed_read_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
"""Classify a phase-118 scripted summary-read request (see the
module docstring). The question carries the scripted call (``read
@@ -4165,6 +4527,22 @@ def compose_answer(body: dict[str, Any]) -> str:
# the user message, so a marker question always gets the inline
# answer whatever the gate says.
answer = INLINE_ANSWER
elif SHOW_TEXT_INLINE_TRIGGER in user.lower():
# Phase 182 (task 08, the verbatim-display suite): ONE locator fence
# and ZERO tool calls — the standalone-fence half of the contract
# (the server resolves the bytes from the row; the model's body is
# dropped at render). Checked BEFORE the DEFLECT_MODE branch, like
# TABLE_TRIGGER: the marker lives in the user message, so a marker
# question always gets the fenced answer whatever the gate says.
# NOT gated on the show_text clause (unlike the SHOW_TEXT_TRIGGER
# flow): on a BOR_ANSWER_TEXT=0 app the same text lands with no
# answer_text on the wire, which is the suite's witness that the
# client never paints the model's bytes.
answer = show_text_inline_answer(body)
elif SHOW_TEXT_MISSING_TRIGGER in user.lower():
# Phase 182 (task 08): the unresolvable locator — the client's honest
# unresolved state is the subject. Same branch convention as above.
answer = show_text_missing_answer()
elif HISTORY_TRIGGER in user.lower():
# Phase 74 (TODO L4, chat history): the deterministic history
# echo — proves on the wire that the client's prior turns (and
@@ -5310,6 +5688,28 @@ def chat_completions(body: dict[str, Any]) -> Any:
# TOOLS_TRIGGER flow (disjoint trigger phrases — the
# phase-71/72/94 ordering convention; the trigger needs the
# ``<tools>`` section, so deflected turns never hit it).
# Phase 182 (task 08): the deterministic SHOW_TEXT marker flow —
# checked BEFORE the plain TOOLS_TRIGGER flow (disjoint trigger
# phrase — the phase-71/72/94/118 ordering convention; the trigger
# needs the show_text clause, so deflected turns and a
# BOR_ANSWER_TEXT=0 app never hit it).
show_text = _show_text_flow(body)
if show_text is not None:
if show_text[0] == "call":
stream = _tool_call_stream(
"show_text",
_show_text_call_arguments(show_text[1], show_text[2], show_text[3]),
"call_show_text",
)
else: # "answer" — the fence-carrying final answer
stream = _sse_stream(
_apply_max_tokens(show_text[1], body.get("max_tokens")), 0.0
)
return StreamingResponse(
stream,
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
turn_progress = _turn_progress_flow(body)
if turn_progress is not None:
stream = _turn_progress_stream(turn_progress)