Repository navigation
Say why /research web search setup fails, and log the cause - #786
Merged
Merged
Conversation
A research request whose web search could not be set up failed with the same opaque `web-search-unavailable` whatever the cause. In stage(), the `!web.ok` branch threw a fresh generic error and dropped the underlying cause. webSearchTool() never logged it. A missing `web_search` tool is returned from a path outside both catch blocks, so a console.error added to those blocks, as the reporter did, cannot show it. The failure line also carries no failure time. The only `elapsedMs` in the logs belongs to the `connecting-web` progress snapshot, which is when that phase began, so the failure read as instant. A local fake MCP server run through the real @ai-sdk/mcp HTTP transport shows this. HTTP 401, HTTP 403, HTTP 500, a JSON-RPC error while listing tools, and a tool list without `web_search` all produced the same `web-search-unavailable` line and card. Only the missing-tool case bypasses both catch blocks. With a 400 ms endpoint the attempt failed after 813 ms while the only `elapsedMs` logged was 0. Against the live endpoint, a fake `ghu_`-shaped token produced the same line the reporter posted (HTTP 401 on connect). The live endpoint answers HTTP 401 for fake `ghu_`, `gho_`, `ghp_` and `github_pat_` tokens alike, so these probes cannot show how it treats a real App user access token. webSearchTool() now classifies a setup failure as it happens and logs it once: the step (connect or tools), a token-safe classification with any HTTP status and JSON-RPC code, how long setup ran, the credential type by prefix (`ghu_` or `gho_`, never the token), and the client's error name and message collapsed to one line, with token-shaped text, `Bearer` values and the exact credential masked and clipped. The research stage records the classification and duration in the job diagnostic. An HTTP 401 or 403 now fails as `web-search-credential-rejected`, a connection without `web_search` as `web-search-not-offered`, and every other failure stays `web-search-unavailable`. The research card says which one happened and points at the server log. Self-hosting, background-jobs, authentication, conversation-research and the README now say what the endpoint is called with, how to read the log line, what an operator can check, and that whether it accepts a custom GitHub App's user access tokens for `web_search` is unconfirmed. This does not make web search work on such an instance. Whether GitHub's Copilot MCP endpoint refuses or withholds `web_search` for GitHub App user access tokens could not be established without one, so the cause behind the report stays unproven. Refs #699 Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ca8f48c1-dfc2-4573-a32d-5037607e08ed) Assistant-verification: bun test apps/server/src/{harness,jobs,research} passed: 547 pass, 0 fail across 43 files, including web-search, research-workspace, request-view and runner tests Assistant-verification: bun run types passed: every workspace and e2e type-check exited 0 Assistant-verification: bun run ci passed: dprint, oxlint (0 errors, 72 existing warnings), token, design contract, design record and design checks Assistant-verification: red-green and mutation checks passed: the new credential-rejection test failed on the unfixed code, and removing the 403 case, exact-token masking, token-shape masking, clipping, or the not-offered reason failed a test each Assistant-verification: local fake-MCP reproduction through the real @ai-sdk/mcp transport passed: before, every setup failure logged an identical web-search-unavailable line; after, 401 and 403 are credential-rejected, a missing tool is not-offered, and the others stay unavailable with a logged duration Assistant-verification: live endpoint probe with fake tokens passed: HTTP 401 for every token shape, so it cannot show how a real ghu_ token is treated Assistant-verification: real ghu_ token check unavailable: no real GitHub App user access token was held, so the cause behind the report is unproven Assistant-verification: relative link and anchor check of the changed docs passed: every link resolves Assistant-duration: unmeasured Co-authored-by: Alex Lavaee <lavaman131@github.com>
…every setup failure at it A transport that rejected with a string, such as a TLS handshake failure, logged only "a value that is not an Error was thrown", so the reason the setup failed was lost. The string is now logged, masked and clipped like an error's message. Any other non-Error value names its type. Only the credential-rejected and not-offered cards said the server log has the details. The unavailable card, which still covers an HTTP 500 or a 401 or 403 delivered as a JSON-RPC error, now says so too. The credential-rejected card names the token Chopin signs in with, a GitHub App user access token. Refs #699 Assistant-model: Claude Opus 5.5 Assistant-workflow: inline Assistant-verification: bun test passed: apps/server src/harness src/jobs src/research, 547 pass, 0 fail Assistant-verification: bun test failed without the logging change as intended: web-search.test.ts, the thrown-string test fails (21 pass, 1 fail) Assistant-verification: bun run types and bun run ci passed Co-authored-by: Alex Lavaee <lavaman131@github.com>
lavaman131
force-pushed
the
fix-699-research-web-search-custom-app
branch
from
November 5, 2026 00:49
6270a2e to
188ee17
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/researchpublic web search failed with an opaqueweb-search-unavailablewhenever its connection to GitHub's Copilot MCP endpoint could not be set up, and the underlying error was never logged. This change classifies and logs each setup failure, gives the research card an accurate reason, and documents what an operator can check. It does not make web search work on a self-hosted instance whose GitHub App token the endpoint refuses. Whether it refuses such tokens could not be established, so this isRefs #699, notFixes #699.Root cause
What is proven, with evidence:
stage()(apps/server/src/jobs/research-workspace.ts) the!web.okbranch threw a freshError("MCP web_search is unavailable")and droppedweb.error.cause.webSearchTool()(apps/server/src/harness/web-search.ts) returned{ kind: "Unavailable", cause }from both catch blocks without logging it. Every failure collapsed into one reason, one card text and one log line.web_search,requireTools()returnsMissingToolandwebSearchTool()returns it directly. I ran a fake MCP server through the real@ai-sdk/mcpHTTP transport withconsole.errortemporarily added to both catch blocks. HTTP 401, HTTP 403, HTTP 500 and a JSON-RPC error while listing tools each printed from a catch block. Only the missing-tool case printed nothing, while producing the sameweb-search-unavailableatconnecting-webthat the report shows. The report says neither of its ownconsole.errorcalls printed, which a 401/403 on connect would have triggered. That is consistent with the tool list lackingweb_search, but it does not prove it: the reporter's instrumentation may not have been loaded.elapsedMsin the logs is on theconnecting-webprogress snapshot, taken when that phase begins. With a fake endpoint that takes 400 ms per request, the attempt failed after 813 ms while the onlyelapsedMslogged was 0. The "1 to 12 ms" in the report matches that snapshot, not the time to failure. Against the live endpoint with a fake token, setup took about 650 ms before failing.ghu_-shaped token sent tohttps://api.githubcopilot.com/mcp/(nothing else was sent anywhere) produced HTTP 401 on connect and exactly the reportedweb-search-unavailableline withphase: connecting-web.What is not proven:
invalid_token, bodyunauthorized: token authentication failed) for fakeghu_,gho_,ghp_andgithub_pat_tokens, so these probes say nothing about real tokens of either type. No real App user token was held.web_searchnot offered" the reporter actually hit. The log output added here will name it on the next failure.web_search. GitHub's Copilot SDK documentation listsghu_tokens as supported for Copilot requests. Theweb_searchtool is not in GitHub's open-source MCP server (no occurrence ingithub/github-mcp-serverat99d6d65), and GitHub documents no token requirement for it. The reported contrast, a classic OAuth token accepted by the endpoint'sinitializerequest while the App token's search fails, never testedtools/listorweb_searchwith the OAuth token.Known limit: a server that answers HTTP 401/403 with a JSON-RPC error body reaches the client as an RPC error without an HTTP status, because
@ai-sdk/mcpdelivers it through the JSON-RPC response path. It is classifiedweb-search-unavailablewith itsrpc:code (reproduced with a fake server). The live endpoint's observed 401 bodies are plain text, so this does not affect them.Changes
apps/server/src/harness/web-search.ts:webSearchTool()classifies each setup failure at the step where it happens (connectortools) and returns it aserror.failurewith a reason, a token-safe classification (for examplelocal:connect:mcp-client:http:401,local:tools:missing-tool) and the duration. It logs one line with the step, classification, duration, credential type by prefix (ghu_orgho_, never the token) and the client's error name and message collapsed to one line, with token-shaped text,Bearervalues and the exact credential masked and clipped to 160 characters. Setup has sent no query, so unlike a call failure it can carry that text. The existing per-call classification now shares the HTTP and RPC code helper.apps/server/src/jobs/research-workspace.ts: the!web.okbranch records the classification and duration in the existingwebFailureandwebFailureDurationMsdiagnostic fields and fails with the classified reason. The diagnostic stays within the 20-key, token-like string format thatexecutionDiagnosticvalidates.apps/server/src/research/request-view.ts: card text for the two new reasons.web-search-credential-rejected(HTTP 401 or 403 while connecting or listing tools): "GitHub refused the credential from this sign-in for public web search. The server log has the details."web-search-not-offered(connected, noweb_searchtool): "GitHub did not offer public web search to this sign-in. The server log has the details."web-search-unavailablewith its existing text. Retry behavior is unchanged.Tests
web-search.test.ts: a refused credential at connect (reason, classification, credential type, redacted log); a refusal while listing tools; a connection withoutweb_search; other failures stayingweb-search-unavailableand being valid diagnostic values; masking of tokens,Bearervalues, the exact credential, control characters and token-shaped error names, plus clipping.research-workspace.test.ts: the evidence stage fails with each of the three reasons, records the classification and duration in a diagnostic that carries no upstream text, and never opens a worker session.request-view.test.ts: card text for both new reasons, including throughattempts-exhausted:.Docs
docs/self-hosting.md: a prerequisite, a smoke-test step, and a Troubleshooting entry covering the three reasons, how to read the log line (including thatelapsedMson theconnecting-webline is when the phase began), what is and is not known about the endpoint, what to check, andWEB_RESEARCH=offas the fallback.docs/background-jobs.md: setup classification,webFailureDurationMs, and what the setup log line may carry.docs/authentication.md,docs/conversation-research.md,README.md: one short note each that the token is also sent to the Copilot MCP endpoint, that acceptance forweb_searchis unconfirmed, and where to look.Verification
Assistant-model: Claude Sonnet 5.5
Assistant-workflow: goal (run ca8f48c1-dfc2-4573-a32d-5037607e08ed)
Assistant-verification: bun test apps/server/src/{harness,jobs,research} passed: 547 pass, 0 fail across 43 files, including web-search, research-workspace, request-view and runner tests
Assistant-verification: bun run types passed: every workspace and e2e type-check exited 0
Assistant-verification: bun run ci passed: dprint, oxlint (0 errors, 72 existing warnings), token, design contract, design record and design checks
Assistant-verification: red-green and mutation checks passed: the new credential-rejection test failed on the unfixed code, and removing the 403 case, exact-token masking, token-shape masking, clipping, or the not-offered reason failed a test each
Assistant-verification: local fake-MCP reproduction through the real @ai-sdk/mcp transport passed: before, every setup failure logged an identical web-search-unavailable line; after, 401 and 403 are credential-rejected, a missing tool is not-offered, and the others stay unavailable with a logged duration
Assistant-verification: live endpoint probe with fake tokens passed: HTTP 401 for every token shape, so it cannot show how a real ghu_ token is treated
Assistant-verification: real ghu_ token check unavailable: no real GitHub App user access token was held, so the cause behind the report is unproven
Assistant-verification: relative link and anchor check of the changed docs passed: every link resolves
Assistant-duration: unmeasured
Not run:
bun run e2eand the PostgreSQL contract (no storage, protocol or browser behavior changed), and a full-repobun test(the three affected server directories passed).Refs #699
Co-authored-by: Alex Lavaee lavaman131@github.com