Skip to content

Give vinga a lookup over its pages: search_docs, gate, docs - #638

Merged
rafacm merged 24 commits into
mainfrom
feature/612-m5-lookup-tool
Oct 9, 2026
Merged

rafacm merged 24 commits into
mainfrom
feature/612-m5-lookup-tool

Conversation

@rafacm

@rafacm rafacm commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

M5 of #612 (plan, implementation doc section), the last milestone. vinga, the built-in agent, can look things up in the Use pages it ships with: a search_docs(query) builtin, offered to the built-in agent alone, on every stack, returning the top three passages. M5's own gate missed its bar on the local default model, and the lookup ships everywhere by Rafael's decision of 2026-10-09 (#612 comment), recorded in the plan's Gate section as a decision, not a pass.

The gate

Same 32 questions (frozen set, sha 1442b569...) and a rephrased set written and hashed before any retrieval work, same harness, prompt and tools (lookups answered through the shipped search_docs builtin since the review round), every answer hand-checked (overrides noted in the hand-check files):

Model, set Correct Hallucinated Declined (of 3) Searched when needed (of 15) Device commands (of 2) Median / p90 s per question (to first text) First byte after a search
gemma4:e4b, frozen 50% 0% 3 5 1 6.3 / 55.0 median 46.3 s, all past 30 s
gemma4:e4b, rephrased 41% 3% 3 2 1 7.4 / 48.2 median 41.2 s, all past 30 s
claude-sonnet-5, frozen 78% 0% 3 13 2 2.9 / 3.9 median 0.8 s
claude-sonnet-5, rephrased 81% 0% 3 13 2 3.1 / 5.6 median 0.9 s
  • Bar: at least 70% correct and at most 10% hallucinated. Gemma 4 e4b (on a Raspberry Pi 5, CPU only) misses on correctness, mostly by not calling the lookup and saying it does not know; Claude Sonnet 5 (the cloud preset's model) passes on both sets.
  • Retrieval, no model: 29 of the 39 recorded queries found their answer in the top three with the board known (target 32; the original gate's term overlap reached 22). About 300 variants plateaued at 28 to 30; reaching 32 would have meant a synonym list written from these exact ten misses, which was not done.
  • Why ship anyway: the confirmed fallback (no lookup locally, lookup on vendor models) needed a rule telling the two apart, two personas, two sets of docs and two local promises; a small local model mostly does not use the tool and then declines rather than guessing (0 to 3% hallucinated), so one behavior for everyone costs less than the split buys.
  • A correction to earlier numbers: the first Gemma runs listed search_docs twice (the harness re-added a tool BuiltinTools already offers; Ollama accepted it, Anthropic refused it). The harness now offers each tool once and checks it; both Gemma sets were re-run, and the table holds the clean runs (the duplicate runs scored 47% and 41%).
  • Sonnet ran without temperature 0: it was driven through vinga's own AnthropicLlm, unchanged, which sends none, and claude-sonnet-5 refuses the parameter. Tokens: about 268,000 input and 4,000 to 5,000 output per set, none cached (about 8,400 input tokens per question).

What changes

  • knowledge/lookup.py: search(query, guide) cuts the packaged Use pages at ##/### into passages of at most 1,000 characters, ranks them with BM25 over word stems with the session's board guide weighed up 1.5x, and returns the top three within a 3,300-character budget. knowledge.board_guide() maps a reported board type to a packaged path, never the reported string.
  • search_docs builtin (tools/builtin.py, tools/source.py, the names registry): offered to the built-in agent only; an operator agent's call, mangled spellings included, is answered "there is no tool called". Named search_docs, not the gate's bare search, because builtin names are reserved as MCP entry names.
  • Persona: vinga searches before declining. The wording the plan implied stopped every lookup in probes (0 of 4); the rewritten persona reaches 2 of 4 on Gemma.
  • Unknown board: VAGUE_BOARD_FACTS now sends device questions to the lookup over the common device guide and still never guesses the board.
  • Holding phrase: FillerRunner.hold() plays vinga's filler while a lookup runs (silent when vinga's filler is off). The round after a lookup is watched like any other round (the first-token watchdog, one retry); the lookup-round allowance M5 first added is removed by Rafael's decision, because the fixed 30 s transport read timeout caps it.
  • Gate fixtures under vinga-server/tests/local/lookup_gate/ (frozen and rephrased sets, the 39 recorded queries, amendments with two re-anchored key facts, the scorer with an undecided category never credited, the harness) and an opt-in local test.
  • Docs: docs/concepts.md Agent, the glossary's vinga entry, the device guide's "What vinga knows" section (packaged copy regenerated), docs/run/configuration.md, a new "The built-in agent's lookup" section in docs/run/llm.md after M7's paragraphs, the local-baseline item in docs/architecture/product-promises.md written from what Gemma 4 e4b measurably does, and the plan's Gate section.
  • Fragment: changelog.d/612-lookup-tool.md, with an Upgrade: line: an mcp_servers entry named search_docs is now refused.

Discoveries

  • vinga's real prompt is about 3,100 tokens against Ollama's default 4,096 context: nothing truncated in the logs, but little headroom.
  • With LLM-input export on, a lookup query appears in three fields of that opt-in export: the tool span's arguments (which the plan names) and the LLM spans' input and output messages (which it does not); a test pins exactly these three.

Verification

  • uv run ruff check .: all checks passed.
  • Unit lane, -n auto --dist loadfile: 8675 passed, 19 skipped in 1067.74s.
  • Integration lane, -n auto --dist loadfile: 365 passed in 225.48s.
  • Drift checks (domain, server, conversations schema, metrics views, events, OpenAPI, CLI reference): all current.
  • Doc links checked 337 files, 0 failures; Run/Use pages 0 findings; fragments 0 failures.
  • tests/census run last: 69 passed.
  • Mutations: 33 runs, every one killed, including the plan's target (lookup offered to every agent); the one first survivor ("budget not enforced") exposed a missing budget test, which was added.
  • The gate, four hand-checked runs (above); voided and superseded run groups listed in the implementation doc.
  • External review round (gpt-6.1-sol, high): 1 P1 and 3 P2, all adopted and fixed; an audit of the 55 recorded search_docs calls found none production would refuse, so the gate's numbers stand. Resolutions in the PR comments.
  • A lookup turn through a running server end to end: the 30 s give-up on the Pi is inferred from first-byte times, not observed in a session.
  • tests/local/test_lookup_gate.py as a pytest case: the harness it calls produced every number, run directly.
  • A real board, the image, the smoke and browser lanes: CI's for the image and lanes; no board attached.

Attribution: plan and PR text by anthropic/claude-opus-5-5, thinking high; commits by anthropic/claude-opus-5-5, thinking medium; review round 1 by openai/gpt-6.1-sol, thinking high.

🤖 Generated with Claude Code

rafacm added 19 commits October 9, 2026 23:52
The #612 gate's inputs were kept outside the repository; plan review
round 2, finding 6, asked for them here, so a later maintainer can
rerun and judge the gate without the session's working files.

- frozen.json: the 32 questions the gate froze before its first model
  call, byte for byte (sha256 1442b569...).
- amendments.json: the key facts of B4 and S5 follow the pages M1b
  changed (vinga info no longer prints a try link; vinga device invite
  prints an invite link). The questions' text does not change.
- rephrased.json: the same questions in other words, written and
  hashed before retrieval was touched, for M5's second model run.
- queries.json: the 39 search() calls the models made on lookup
  questions in the gate's shape-A runs.
- scoring.py: the gate's automatic rules, with an undecided category
  where the hand-check used to decide, reported and never credited.

A unit test holds the frozen set to its hash, both sets to the same
32 ids, and every key fact to the page it quotes, found by text in the
packaged copy; it failed when one phrase was edited out of a copy and
passed once restored.

Attribution: anthropic/claude-opus-5-5, thinking medium
knowledge.search(query, guide) is the lookup the #612 gate chose (shape
A): the packaged pages cut at ## and ### headings into passages of at
most 1,000 characters, scored by BM25 over word stems with the title
counted twice, the session's own board guide weighed up by 1.5, and
the best three answered within 3,300 characters, a passage past the
budget left out whole with a line saying so. A query that matches
nothing answers a sentence telling the model to say it does not know.
boards.board_guide maps a reported board type to the guide's packaged
path, so a caller may keep the path where it may not keep the string.

Measured with no model against the 39 queries the models sent in the
gate: 29 of 39 answers carry the fact the question needed with the
LCD-1.54 guide weighed up, 25 of 39 with no board, against 22 of 39 in
the gate. The plan's target was 32, and it is not met; the unit test
holds the measured floor rather than the target, and the implementation
doc records every variant measured.

Every test was watched failing: the board weight dropped, any path
weighed as a guide, stems not cut, the fence rule off, "On this page"
kept, the budget not enforced, the left-out line dropped, long
paragraphs left whole, the query echoed into an answer, and a level
three heading titled without its section each failed a test here
(.logs/m5-mutations.log). The budget mutation survived the first test
set, since three passages always fit; the budget test was added for it.

Attribution: anthropic/claude-opus-5-5, thinking medium
search_docs is a builtin (its name joins names.BUILTIN_TOOL_NAMES, so a
call is recorded with source builtin and TOOL_SOURCES does not move),
offered by BuiltinTools to the built-in agent alone, by the predicate
its device-scoped memory and threads already use, and answered from
knowledge.search with the session's board guide, a packaged path the
runtime maps once at construction and never the reported string.

An operator's agent that asks for it is answered as a name nobody
publishes, by both askers of the shared withheld rule: the builtin
source at the call, and the runtime before it answers anything about a
call at all, so a mangled call is not told its arguments were wrong,
which only a tool that exists would say. The rule gains the predicate
beside the memory policy; a ToolExecution built without it withholds
the lookup from everyone.

The name is search_docs rather than the gate's bare search: a builtin's
name is an entry name no mcp_servers entry may take, and search is one
an operator's own search server could plausibly have.

The persona says where the lookup fits: buttons and controls from the
board's facts, anything else about the device or vinga searched first,
and only what neither covers declined. A first gate run with M4's
"from those facts alone" wording had the model decline three of the
first six questions that needed the lookup without making it.

The session tests and their mutations are in the commit that tests
the lookup in a session.

Attribution: anthropic/claude-opus-5-5, thinking medium
server.llm_lookup_first_token_timeout_s (default 30) bounds the first
token of the one round that follows a search_docs call, in place of
llm_first_token_timeout_s and never shorter than it: that round sends
the passages found, and a small local model reads them before it says
a word. The original gate measured that wait at a median 44 s for
Gemma 4 e4b on a Raspberry Pi 5, against the 10 s watchdog.

ProviderWatch.reply_stream takes the round's own bound as an optional
argument and keeps its retry and give-up unchanged, so a round after a
lookup that stalls twice is given up as FirstTokenTimeout like any
other. The tool loop sets it for the round after a lookup and clears
it for every other round.

The LLM clients' own read timeout (providers/kit.py, 30 s without a
byte, not configuration) still applies, so against a runner that sends
nothing before its first token the allowance buys nothing past 30 s;
the key's description, config.example.yaml and docs/run/llm.md say so.

Attribution: anthropic/claude-opus-5-5, thinking medium
The round after vinga's lookup is the slow one on a small local model,
so the tool loop asks the filler runner to hold: the agent's next
filler phrase, played at once when a round's calls include
search_docs, before the tools run. It is the one exception to one
filler per turn: the turn's timer covers the wait for the first round,
and a lookup starts a second wait after it, so the hold plays whether
or not the timer already has. An unfired timer is stood down in its
favour, so a turn still holds one clip task and tail and settle see it
as they see the timer's; a clip still sounding is already holding, and
the mask still yields to a user speaking or a barge-in being confirmed.

The holding phrase is the agent's own filler, so a vinga whose filler
section is off waits in silence through the lookup round, as every
agent waits at the start of a turn.

Attribution: anthropic/claude-opus-5-5, thinking medium
Driven through real sessions with scripted models: vinga is offered
search_docs and an operator's agent is not; an operator's agent asking
for it anyway, or mangling the call, is told there is no such tool;
vinga's lookup answers from its board's guide, asks for a missing
query, and says when nothing matched. The round after a lookup waits
out its allowance where any other round is retried; past it, the
round is retried once and given up as FirstTokenTimeout; a shorter
allowance bounds nothing tighter; and it is the round after the lookup
alone. The lookup plays the agent's filler as a holding phrase, no
other tool does, and a turn whose timer already played still holds.
A credential-shaped query reaches no log line in either format and no
span with LLM input export off; with it on, it is in the tool span's
arguments and the LLM spans' messages, the export's own content
fields, and nowhere else. tests/support/llm_input.traced takes the
board and the agent, so a traced session can be vinga's.

Every behavior was broken once and its test watched failing, then
restored by copy and touch (.logs/m5-mutations.log): the lookup offered
to every agent (the plan's M5 target), the withheld rule ignoring it,
the runtime never told who is built in, the guide not handed over, the
allowance never passed, applied after any tool, used when shorter or
kept past its round, the hold never asked for, asked for after any
tool, skipped once the timer had played or held to the timer's
speaking-started rule, and the query logged by the tool. No survivor.

Attribution: anthropic/claude-opus-5-5, thinking medium
tests/local/lookup_gate/harness.py asks one question set of a real
model with what vinga is sent on the LCD-1.54 board: its persona and
summary, the board's facts, the tools BuiltinTools offers the built-in
agent on a device bound to it alone with search_docs among them, and
the board's volume tool, through the server's own OpenAI-compatible
provider at temperature 0 and seed 42 with thinking off, round after
round as the tool loop runs them. The lookup is the real one; the other
tools answer a fixed line. It reads the shared prefix once before the
first question, unmeasured, since a cold runner on a Pi spends minutes
on it, and resumes a stopped run where it stopped. Each answer is one
JSON line with every round's first byte and first spoken text.

tests/local/test_lookup_gate.py, opt-in with the rest of the lane,
runs both sets and holds each to the gate's bar: at least 70% correct
by the gate's own category, at most 10% hallucinated, and both device
commands made. The hand-check is replaced by the scorer's undecided
category, listed in the lane's summary and never credited.

Attribution: anthropic/claude-opus-5-5, thinking medium
docs/reference/server-config.md gains the row for
llm_lookup_first_token_timeout_s, written by its generator
(vinga-server config reference server), as the drift check requires.

Attribution: anthropic/claude-opus-5-5, thinking medium
The persona M5 first wrote still had Gemma 4 e4b decline every lookup
question probed without calling search_docs: with vinga's real prompt
and tools, D5, D9, B1 and S9 of the gate's frozen set got "I do not
have information ... check the device guide" and no call. Probes
(.logs/m5-probe-*.log) placed the cause in the hand-written persona:
the gate's own prompt with vinga's tools looked up three of the four,
and kept doing so with vinga's board facts or vinga's summary swapped
in, and stopped the moment vinga's persona replaced its opening. Two
smaller changes (only two tools offered, the gate's rule appended) did
not move it.

The persona now says where each answer comes from: buttons and
controls from the board's facts, what vinga is from the summary, and
anything else from search_docs first, searched before it ever says it
does not know and never by sending the person to a guide instead. With
it the same probe looked up two of the four.

The pin follows the wording, and dropping the search-first sentence
fails it (.logs/m5-mutations.log).

Attribution: anthropic/claude-opus-5-5, thinking medium
The rephrased set asks "What is the vinga info command for?", which the
command-spellings census read as the invocation `vinga info command`
and failed as a command the tree does not have. The two question sets
are a person's words frozen and hashed before any model answered them,
so they join _HISTORICAL_PATHS beside the other committed captures, and
the manifest is regenerated by its generator.

Attribution: anthropic/claude-opus-5-5, thinking medium
server.llm_lookup_first_token_timeout_s goes, by Rafael's decision on
2026-10-09: it could not do what its name says. The LLM clients give up
after 30 s without a byte (providers/kit.py, fixed rather than
configuration), and the gate measured Ollama sending nothing before the
first token: the round after a lookup reached its first byte in 32.9
to 51.3 s on a Raspberry Pi 5 with Gemma 4 e4b, every one of eleven
past 30 s, so no allowance above 30 could have bought those rounds
anything. The key, its config.example.yaml entry, the round's own bound
in ProviderWatch.reply_stream, the tool loop's bookkeeping and the
generated server reference row go. The holding phrase stays.

A correction to "Give the round after a lookup its own allowance": its
body said docs/run/llm.md explained that the read timeout caps the
allowance. It did not; llm.md says it now about the round after a
lookup, with the measurements, in this milestone's documentation.

The tests for the allowance are replaced by tests that the round after
a lookup is watched like any other (retried once, given up on a second
stall) and that a retried round still hears one holding phrase. Each
was watched failing: the watchdog's bound multiplied tenfold, the hold
never asked for, and the hold asked for at every round
(.logs/m5-mutations.log).

Attribution: anthropic/claude-opus-5-5, thinking medium
Plan D3 wanted the vague text, what vinga is told when the server does
not know the board, to send a device question to the common device
guide "a lookup away". M4 could not, with no lookup yet, and had vinga
decline and point to the guide. With search_docs offered to the
built-in agent on every stack (Rafael's decision, 2026-10-09), the text
now tells vinga to search for what the common guide says, never to
guess which board this is, and to say so when the common guide does
not cover the question. It also stops contradicting the persona, which
tells vinga never to send someone to a guide instead of searching.

The pin follows, and putting M4's decline-and-point wording back fails
it (.logs/m5-mutations.log).

Attribution: anthropic/claude-opus-5-5, thinking medium
The harness drives Anthropic's API through vinga's own AnthropicLlm
adapter when VINGA_LOCAL_LLM_PROVIDER=anthropic, its key read from
VINGA_DEV_ANTHROPIC_API_KEY and never printed, and records each
round's reported tokens. It sends exactly what a deployment's entry
sends: no temperature, since the adapter sets none and claude-sonnet-5
refuses one ("temperature is deprecated for this model", HTTP 400 on
the first attempt). A vendor's API has no cold prefix, so it is not
warmed.

The first Anthropic request also failed with "Tool names must be
unique": the harness appended search_docs to a list BuiltinTools
already put it in. Ollama accepted the duplicate silently, so every
Gemma run until then offered the lookup twice; the list is now each
tool once, asserted, and the Gemma runs were repeated.

Attribution: anthropic/claude-opus-5-5, thinking medium
M6 reworded the concepts page's wake rule: a fresh wake opens on the
first agent the device reaches. The frozen S2 fact quoted the old
sentence ("A fresh wake always gets the default agent"), and the
fixture test that holds every key fact to its page failed on the
rebased tree, which is what it is for. S2's fact now quotes the new
sentence and accepts "first agent" beside "default"; the question
does not change. The file is rewritten by json.dump, hence the
reindented lines.

Attribution: anthropic/claude-opus-5-5, thinking medium
docs/concepts.md (Agent), the glossary's vinga entry and the device
guides' common page say what M5 built: vinga alone is offered a search
over the concepts page, the glossary and the device guides packaged
with the build, is told to search before it says it does not know, and
never guesses which board an unknown device is. They say honestly how
often it searches, from the 2026-10-09 measurements: claude-sonnet-5
on 13 of the 15 questions that needed it, 78% correct; Gemma 4 e4b,
the local preset's model, on 5 of 15, 50% correct, so a small local
model answers the controls and the summary reliably and deeper
questions less often. The device page no longer says vinga points to
a guide for a board it does not know.

The packaged copy is regenerated with
`uv run python -m tests.census.test_packaged_pages`.

Attribution: anthropic/claude-opus-5-5, thinking medium
docs/run/llm.md gains "The built-in agent's lookup", beside M7's
paragraphs on the local model and the cold first turn rather than over
them: the same 32 questions, in two wordings, on claude-sonnet-5 and
on Gemma 4 e4b on a Raspberry Pi 5, with what each answered, invented,
searched, declined and commanded, how long a question took, and the
tokens the vendor model spent. It says plainly that on the Pi the
round after a search reached its first byte after 34 to 53 s, past the
30 s at which a request ends without a byte, so a turn that searches
is given up there today, and that raising the watchdog cannot help.
This is the explanation of the 30 s cap that "Give the round after a
lookup its own allowance" promised in its body
and did not deliver. docs/run/configuration.md's "Overriding vinga"
names the lookup and links the measurements.

Attribution: anthropic/claude-opus-5-5, thinking medium
The enumerated local baseline gains the built-in agent, as the plan's
"Local baseline: joins" line said it would in the change that ships
the lookup: vinga answers the device from its board's guide and vinga
from the concept summary on local providers, and its lookup is offered
there, used on a minority of the questions that need it by Gemma 4 e4b
(5 of 15, 50% correct against claude-sonnet-5's 13 of 15 and 78%), and
too slow on a Raspberry Pi 5 for the server to wait for. The promise
is written from what the local stack measurably does: the first half
works, the lookup is there, and how well it is used is the model's.
It cites the plan that joins it, under the enumerated-baseline record.

Attribution: anthropic/claude-opus-5-5, thinking medium
The implementation doc gains M5's section: the gate as measured
(retrieval 29 of 39 against 32; Gemma 4 e4b 50% and 41% correct
against 70%, with 0 and 1 hallucinated; claude-sonnet-5 78% and 81%),
recorded as missed, with Rafael's decision of 2026-10-09 to ship the
lookup on every stack and drop the allowance; why no synonym list was
fitted to reach 32; every voided and superseded run; the deviations,
resolutions and discoveries; and the mutations. The plan's Gate
section records the decision and both measurements, and M5 is ticked
with a link to its section. The changelog fragment carries the
lookup, the unknown board's new behavior, and an Upgrade: line for an
mcp_servers entry named search_docs.

Attribution: anthropic/claude-opus-5-5, thinking medium
The milestone was ticked "PR TBD" before the pull request existed; #638 is its number, so the checklist links it.

Attribution: anthropic/claude-opus-5-5, thinking high
@rafacm

rafacm commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

External review round

Automated external review of this PR's diff (origin/main...8026ed2). Reviewed 2026-10-09 by openai/gpt-6.1-sol, thinking high via codex CLI 0.162.0, read-only sandbox, runtime 3m51s, at commit 8026ed2. Posted verbatim by the review run itself; resolutions follow as replies.


  1. P1: The gate harness leaks rejected input and library tracebacks.
    Evidence: harness.py:192 constructs the client without a sanitized failure boundary; main() lets failures escape. Running the documented harness with VINGA_LOCAL_OLLAMA=http://localhost:REJECTED_INPUT_SENTINEL/v1 prints the sentinel, httpx’s exception words, and its traceback to stderr.
    Fix: contain construction and execution failures, report only failure_name(exc) outside the catch, and terminate without retaining or rendering the original exception chain. Add sentinel tests for malformed URLs and unsafe exception class names.

  2. P2: The gate bypasses production lookup argument validation.
    Evidence: harness.py:153 converts arbitrary query values with str() and calls knowledge.search directly. Production builtin.py:673 refuses missing, blank, or non-string queries. With {"query": 2.4}, the harness returns documentation passages while production requests a valid query. The gate therefore measures behavior the shipped tool cannot provide.
    Fix: dispatch lookup calls through builtin.search_docs, preserving production handling of malformed arguments. Test missing, blank, numeric, and malformed queries through the harness.

  3. P2: The lookup documentation states the wrong default timeout outcome.
    Evidence: llm.md:191 says these turns are given up as ProviderCallTimeout. The default watchdog is 10 seconds; provider_watch.py:255 cancels and retries before the 30-second transport timeout, then raises FirstTokenTimeout if the retry also stalls. The harness bypasses that watchdog, so its uninterrupted timings do not establish the retry’s outcome.
    Fix: describe the default watchdog and retry accurately, distinguish transport timeout from watchdog timeout, and label the running-server outcome as unverified. Correct the matching implementation-doc claim.

  4. P2: Published measurements claim board playback that the harness never measured.
    Evidence: llm.md:173 describes measurements “on an ESP32-S3-Touch-LCD-1.54”; line 185 calls 1.8 seconds time to the “first spoken word.” harness.py:120 measures the first nonblank text delta, with no TTS, device output, or board involved. The implementation doc explicitly records board and end-to-end verification as outstanding.
    Fix: describe a model harness using the LCD board’s facts, and label 1.8 seconds as time to first text. Apply the same correction to the implementation doc.

Verdict: mergeable after the listed fixes.

PR #638's review, finding 2: the harness turned any query into text
with str() and searched it, where the shipped search_docs refuses a
missing, blank or non-string query with its own sentence, so the gate
measured a friendlier tool than the one that ships. A lookup call in
the harness is now answered by the shipped builtin, and a call whose
arguments never parsed by the runtime's own sentence, which
tool_execution names UNPARSEABLE_ARGUMENTS so both read one constant.

A unit test drives harness.ask with a scripted model over a missing,
an empty, a blank, a numeric and a list query and a mangled call, and
each fails with the old str() line back (.logs/m5-mutations.log).

The audit of the four recorded runs (frozen and rephrased, Gemma 4 e4b
and claude-sonnet-5) read every search_docs call from their raw files
for a missing, blank or non-string query or unparseable arguments:
55 calls, 0 that production would have refused
(.logs/m5-audit-queries.log). The published numbers stand.

Attribution: anthropic/claude-opus-5-5, thinking medium
PR #638's review, finding 1: run as documented with
VINGA_LOCAL_OLLAMA naming a malformed address, the harness printed the
address back, httpx's own words about it and the traceback. main() now
contains every failure, building the provider included: the report is
built in the except arm from failure_name(exc) alone (the fixed
phrase for a class name it will not print) and said after the block,
so neither the exception, its message nor the chain behind it outlives
the arm. The harness exits 1 with one line, "the lookup gate stopped:
InvalidURL" for that address.

Two sentinel tests: the documented command run in a subprocess against
http://localhost:<sentinel>/v1, holding stdout and stderr to that one
line, sentinel-free and traceback-free; and an exception class whose
name carries the sentinel and a newline, raised from a cause carrying
it too, holding the output to the fixed phrase and leaving no
exception live after main returns. Each was watched failing with the
except arm removed, the message reported, the raw class name
reported, and a re-raise from the arm (.logs/m5-mutations.log).

Attribution: anthropic/claude-opus-5-5, thinking medium
PR #638's review, finding 4: docs/run/llm.md described the lookup
measurements as taken "on an ESP32-S3-Touch-LCD-1.54" and called 1.8 s
the time to the first spoken word. The harness sends vinga's prompt
with that board's facts in it and reads the model's text back, with no
board, no speech recognition or synthesis and no device output, and it
times the first non-blank text. Both pages now say so: llm.md
describes the harness and calls 1.8 s the time to first text, and the
implementation doc's figure is labelled the same way. An untruncated
`git grep` of docs/, changelog.d/ and README.md for "spoken word" and
"on an ESP32-S3-Touch-LCD-1.54" found no other M5 claim of the kind;
the plan's Risks line saying the gate "records time to first spoken
word" is the plan's intention from before M5 and is left as written.

Attribution: anthropic/claude-opus-5-5, thinking medium
PR #638's review, finding 3: docs/run/llm.md said a turn that searches
on the Pi "is given up as a ProviderCallTimeout". With the defaults the
10 s first-token watchdog fires long before the 30 s transport read
timeout: it cancels the round, sends it once more, and when the second
attempt stalls too gives the turn up with a FirstTokenTimeout and the
agent's fallback phrase, as M7's cold-turn paragraph above it says. A
ProviderCallTimeout is reached only with the watchdog raised past 30 s.
The harness waits for as long as the model takes, so its 34 to 53 s
first bytes do not observe the retry; the page now says the running
server's outcome is inferred from them, not observed. The same
correction is made in the implementation doc, and where M5's other
pages stated the outcome (the baseline item in product-promises.md,
the changelog fragment, the plan's Gate paragraph) they now say it was
measured in a model harness. The implementation doc's line about "the
one hallucination on each side" is also reworded: there was one, in
Gemma's rephrased run.

Attribution: anthropic/claude-opus-5-5, thinking medium
The implementation doc's M5 section gains the round: the four
findings, each adopted with its fix commit, and the audit of the four
recorded runs for lookup calls production would refuse (55 calls,
none), so the published numbers stand.

Attribution: anthropic/claude-opus-5-5, thinking medium
@rafacm

rafacm commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Resolutions for the external review round

All four findings adopted; fixes by the milestone's implementer (anthropic/claude-opus-5-5, thinking medium), recorded in the implementation doc's M5 "PR review round" subsection (7d52961).

  1. P1, the gate harness leaked rejected input and tracebacks: fixed in e28432f. main() now contains every failure, provider construction included, builds a one-line report from failure_name(exc) in the except arm, prints it after the block and exits 1; against the review's malformed URL it prints only the lookup gate stopped: InvalidURL. Two sentinel tests: the documented command in a subprocess against http://localhost:REJECTEDINPUTSENTINEL/v1 (one line on stdout and stderr, no sentinel, no traceback), and an exception class whose name carries the sentinel and a newline, raised from a cause that also carries it (the fixed phrase only, no exception left live). Both were broken four ways (no except arm, str(exc), the raw class name, a re-raise from the arm) and failed each time.
  2. P2, the gate bypassed production's query validation: fixed in bb59243, and the published numbers stand. The harness now answers a lookup through the shipped builtin.search_docs, and an unparseable call with the runtime's own sentence (now one constant, UNPARSEABLE_ARGUMENTS in runtime/tool_execution.py, read by both). New tests drive the harness's round loop with missing, empty, blank, numeric and list queries and a mangled call; they fail with the old str() line or without the mangled-call branch. Audit of the four recorded runs: 55 search_docs calls (Gemma 6 and 4, Sonnet 22 and 23), 0 with a missing, blank or non-string query or unparseable arguments, so no run measured behavior the shipped tool would not have provided, and nothing was rerun.
  3. P2, the wrong default timeout outcome: fixed in fe695d6. docs/run/llm.md now says that with the defaults the 10 s first-token watchdog cancels a slow round and retries once, and a second stall gives up with FirstTokenTimeout and the fallback phrase, consistent with M7's cold-turn paragraph; ProviderCallTimeout is reached only when the watchdog is raised past the 30 s transport read timeout; the running-server outcome of a lookup turn is labelled inferred, not observed. The same correction is made in the implementation doc, the baseline item in product-promises.md, the changelog fragment and the plan's Gate paragraph.
  4. P2, timings described as board playback: fixed in 4435df6. llm.md and the implementation doc now say the timings come from a model harness given the LCD-1.54 board's facts, with no board, speech or device output, and call 1.8 s the time to first text. An untruncated grep of docs/, changelog.d/ and README.md found no other such claim; the plan's own Risks line, which states the intention from before M5, is left as written.

Checks after the fixes: ruff clean; the knowledge, builtin-tool, harness and session-lookup unit tests 162 passed; drift checks current; doc links 0 failures; Run/Use pages 0 findings; fragments 0 failures; tests/census last, 69 passed; six new mutations for this round, all killed. The full lanes rerun in CI on the new head (runtime/tool_execution.py gained a named constant).

@rafacm
rafacm merged commit 6f3ab08 into main Oct 9, 2026
9 of 10 checks passed
rafacm added a commit that referenced this pull request Oct 9, 2026
The milestone was ticked "PR TBD" before the pull request existed; #638 is its number, so the checklist links it.

Attribution: anthropic/claude-opus-5-5, thinking high
rafacm added a commit that referenced this pull request Oct 9, 2026
PR #638's review, finding 2: the harness turned any query into text
with str() and searched it, where the shipped search_docs refuses a
missing, blank or non-string query with its own sentence, so the gate
measured a friendlier tool than the one that ships. A lookup call in
the harness is now answered by the shipped builtin, and a call whose
arguments never parsed by the runtime's own sentence, which
tool_execution names UNPARSEABLE_ARGUMENTS so both read one constant.

A unit test drives harness.ask with a scripted model over a missing,
an empty, a blank, a numeric and a list query and a mangled call, and
each fails with the old str() line back (.logs/m5-mutations.log).

The audit of the four recorded runs (frozen and rephrased, Gemma 4 e4b
and claude-sonnet-5) read every search_docs call from their raw files
for a missing, blank or non-string query or unparseable arguments:
55 calls, 0 that production would have refused
(.logs/m5-audit-queries.log). The published numbers stand.

Attribution: anthropic/claude-opus-5-5, thinking medium
rafacm added a commit that referenced this pull request Oct 9, 2026
PR #638's review, finding 1: run as documented with
VINGA_LOCAL_OLLAMA naming a malformed address, the harness printed the
address back, httpx's own words about it and the traceback. main() now
contains every failure, building the provider included: the report is
built in the except arm from failure_name(exc) alone (the fixed
phrase for a class name it will not print) and said after the block,
so neither the exception, its message nor the chain behind it outlives
the arm. The harness exits 1 with one line, "the lookup gate stopped:
InvalidURL" for that address.

Two sentinel tests: the documented command run in a subprocess against
http://localhost:<sentinel>/v1, holding stdout and stderr to that one
line, sentinel-free and traceback-free; and an exception class whose
name carries the sentinel and a newline, raised from a cause carrying
it too, holding the output to the fixed phrase and leaving no
exception live after main returns. Each was watched failing with the
except arm removed, the message reported, the raw class name
reported, and a re-raise from the arm (.logs/m5-mutations.log).

Attribution: anthropic/claude-opus-5-5, thinking medium
rafacm added a commit that referenced this pull request Oct 9, 2026
PR #638's review, finding 4: docs/run/llm.md described the lookup
measurements as taken "on an ESP32-S3-Touch-LCD-1.54" and called 1.8 s
the time to the first spoken word. The harness sends vinga's prompt
with that board's facts in it and reads the model's text back, with no
board, no speech recognition or synthesis and no device output, and it
times the first non-blank text. Both pages now say so: llm.md
describes the harness and calls 1.8 s the time to first text, and the
implementation doc's figure is labelled the same way. An untruncated
`git grep` of docs/, changelog.d/ and README.md for "spoken word" and
"on an ESP32-S3-Touch-LCD-1.54" found no other M5 claim of the kind;
the plan's Risks line saying the gate "records time to first spoken
word" is the plan's intention from before M5 and is left as written.

Attribution: anthropic/claude-opus-5-5, thinking medium
rafacm added a commit that referenced this pull request Oct 9, 2026
PR #638's review, finding 3: docs/run/llm.md said a turn that searches
on the Pi "is given up as a ProviderCallTimeout". With the defaults the
10 s first-token watchdog fires long before the 30 s transport read
timeout: it cancels the round, sends it once more, and when the second
attempt stalls too gives the turn up with a FirstTokenTimeout and the
agent's fallback phrase, as M7's cold-turn paragraph above it says. A
ProviderCallTimeout is reached only with the watchdog raised past 30 s.
The harness waits for as long as the model takes, so its 34 to 53 s
first bytes do not observe the retry; the page now says the running
server's outcome is inferred from them, not observed. The same
correction is made in the implementation doc, and where M5's other
pages stated the outcome (the baseline item in product-promises.md,
the changelog fragment, the plan's Gate paragraph) they now say it was
measured in a model harness. The implementation doc's line about "the
one hallucination on each side" is also reworded: there was one, in
Gemma's rephrased run.

Attribution: anthropic/claude-opus-5-5, thinking medium
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant