Skip to content

Let live-routed answers reach the search page - #250

Merged
adamjohnwright merged 1 commit into
mainfrom
fix/live-answers-never-stream
Sep 18, 2026
Merged

adamjohnwright merged 1 commit into
mainfrom
fix/live-answers-never-stream

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

Reported by Adam: the search page could not answer "what species are in reactome". Reproduced with MCP configured — 0 tokens, state: nothing_found — while the chat answers the same question correctly.

Cause

Those questions route to the live MCP lookup, which answers from tools and uses no retriever anywhere on the path. astream_answer opens its token boundary on on_retriever_end:

if kind != "on_chat_model_stream" or not retrieval_done:
    continue

So on the live path the boundary never opened, every token was discarded, and answered stayed false → nothing_found. The answer existed the whole time: 54 stream events, 224 characters, at the answer node.

Why nothing caught it

Three independent reasons, which is why it reached production:

  • It only happens where MCP is configured — beta, not a developer's machine. Locally these questions fall back to the vector store and retrieve normally, so they answer. That is also why my own latency runs showed them answering.
  • The chat UI uses ainvoke, which takes the final answer and never reads this stream.
  • The answer sweep also uses ainvoke — so it passed 15/15 on beta while the endpoint was broken for the very questions it was testing.

Fix

Open the boundary when preprocess ends with active_sources containing live.

That is safe on this path for a specific reason rather than by luck: the query expander lives inside the retriever, so where there is no retrieval there is nothing else streaming at the answer node to tell the answer apart from. The non-live path still waits for retrieval, and a test pins that it does.

Verified

tokens citations state
with MCP, "what species are in reactome" 36 0 answered
with MCP, "Which release of Reactome is this?" 10 0 answered
without MCP (fallback path, unchanged) 1534 12 answered
vector-store control 1056 12 answered

The regression test replays event sequences captured from a real run against beta's MCP sibling, so it needs no MCP to run — and it fails against the unfixed code.

Live answers carry no citations, because no retriever ran. That is honest — they come from live services rather than indexed pathways — and callers already handle an empty citation list, which userguide answers made real.

CI-equivalent locally: ruff, format, mypy, full suite with no API keys set.

🤖 Generated with Claude Code

Reported: the search page could not answer "what species are in reactome".
Reproduced, with MCP configured: 0 tokens, state `nothing_found`. The chat
answers the same question correctly.

Those questions route to the live MCP lookup, which answers from tools and uses
no retriever anywhere on the path. `astream_answer` opens its token boundary on
`on_retriever_end`, so the boundary never opened, every token was discarded, and
the caller was told nothing was found.

Nothing could have caught it. It only happens where MCP is configured -- beta,
not a developer's machine, where the same questions fall back to the vector store
and retrieve normally. That is also why my own latency runs showed these questions
answering: they were falling back. And the chat UI and the answer sweep both use
`ainvoke`, which takes the final answer and never reads this stream, so the sweep
passed 15/15 on beta while the endpoint was broken.

The fix opens the boundary when preprocess ends with `active_sources` containing
`live`. That is safe only on this path, and for a specific reason: the query
expander lives inside the retriever, so with no retrieval there is nothing else
streaming at the answer node to tell the answer apart from. The non-live path
keeps waiting for retrieval, which a test pins.

Verified both ways. With MCP: 36 and 10 tokens, `answered`. Without MCP: the
fallback is unchanged, 1534 tokens with 12 citations.

Live answers carry no citations, because no retriever ran. That is honest -- they
come from live services rather than indexed pathways -- and callers already handle
an empty citation list, which userguide answers made real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adamjohnwright added a commit that referenced this pull request Sep 18, 2026
Measured against beta: a well-formed but unknown token returns 404 as the
OpenAPI says, but a malformed one returns 500, which the spec does not mention.

FR-009 already calls a malformed token a normal negative outcome. Without this
the implementation would have handled the two documented codes and let a
malformed token become a failed state, or a retry loop against a service that
will answer the same way every time.

Found while adversarially reviewing PR #250, by probing the endpoint the contract
depends on rather than trusting its documentation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit 68ddd3f into main Sep 18, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the fix/live-answers-never-stream branch September 18, 2026 16:21
adamjohnwright added a commit that referenced this pull request Sep 18, 2026
Measured against beta: a well-formed but unknown token returns 404 as the
OpenAPI says, but a malformed one returns 500, which the spec does not mention.

FR-009 already calls a malformed token a normal negative outcome. Without this
the implementation would have handled the two documented codes and let a
malformed token become a failed state, or a retry loop against a service that
will answer the same way every time.

Found while adversarially reviewing PR #250, by probing the endpoint the contract
depends on rather than trusting its documentation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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