Skip to content

refactor(mcp): answer search through the owner - #2296

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
fleet/mcp-typed-search-workflow
Sep 27, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
fleet/mcp-typed-search-workflow

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

tracedecay_search is now an ApplicationSurfaceOperation that the project's graph-tool owner answers, on MCP and tracedecay tool alike. It follows #2284 (retrieve) and #2273.

  • Typed request and result. Both live in crates/tracedecay-contracts/src/retrieval/search_surface.rs. The result is either a complete page or a typed unavailable state, and it serializes to the JSON this tool already emitted. It carries:
  • Owner path. The stale-graph code_graph_freshness trailer now comes from the owner's served generation, and touched_files from the typed completion. Search does not emit a tracedecay_cost receipt today, so the completion's cost slot stays empty and the output stays identical.
  • Refusals. Previously an unknown key was silently ignored, and a mistyped limit ("5", 5.0) silently fell back to 10. Both are now refused with the field named. A missing query is now the typed refusal every owner-served tool gives.
  • The search handler module is split by tool. crates/tracedecay-mcp/src/handlers/graph/search.rs goes from 2,345 lines to 762 (491 production, the rest tests). It now holds three pieces: the typed request decode (compute_search → lexical_routing::routing_from_request), the owner computation (compute_search), and the one renderer every surface uses (render_search). The already owner-served reads move to their own modules, unchanged except for imports: context.rs, exact_symbol.rs, clones.rs (similar/redundancy) and rename_preview.rs. The kernel-receipt → wire anchor conversion is now one function (lexical_routing::anchor_outcome), shared by search and context. The in-client search path is deleted: the root dispatch_graph_tools, the portable graph::dispatch_tool table, McpToolDispatchGroup::Graph and its binding group row.
  • Schema. The search input schema is generated from the typed request. The lexical bounds (SEARCH_MAX_LEXICAL_*) move to the contracts crate, next to the request they bound.
  • The tool keeps its two-minute dispatch ceiling as its capability deadline (graph_report_spec).

Fail-before / pass-after (production MCP)

mcp_handler_test::search_behavior_test::search_returns_the_named_symbol_and_refuses_arguments_outside_its_typed_request now asserts typed refusals for three requests: a missing query, an unknown semantic_mode, and limit: "5". On unchanged origin/master (0c85014):

search_behavior_test.rs:54:5:
  left:  {"code": -32602, "data": {"detail": "missing required parameter: query", "reason_code": "missing_required_parameter", ...}, "message": "missing required parameter: query"}
  right: {"code": -32603, "data": {"cli_fallback": "...", "tool": "tracedecay_search"}, "message": "tool execution failed: config error: invalid arguments for tracedecay_search: missing field `query`"}

With this change, it passes.

On the built binaries, the same corpus shows master silently accepting what this branch refuses (tracedecay serve):

master: tracedecay_search {"query": "cancellation_probe_0039_017", "semantic_mode": "hybrid", "format": "json"}: isError=False 404ms   (41485-char page served)
master: tracedecay_search {"query": "cancellation_probe_0039_017", "limit": "1", "format": "json"}: isError=False 398ms   (10 results served)
branch: tracedecay_search {..., "semantic_mode": "hybrid"}: JSON-RPC error -32603
     tool execution failed: config error: invalid arguments for tracedecay_search: unknown field `semantic_mode`, expected one of `query`, `limit`, `cursor`, `lexical_anchors`, `prefer_symbol`, `lexical_aliases`, `lexical_phrases`, `lexical_proximities`, `lexical_field_filters`, `lazy_index_ignored_dependencies`
branch: tracedecay_search {..., "limit": "1"}: JSON-RPC error -32603
     tool execution failed: config error: invalid arguments for tracedecay_search: invalid type: string "1", expected u64

Identical output (80 responses)

The check runs the same 80 calls through tracedecay serve, first with a binary built from this branch's base (origin/master cdc1a2a), then with this branch. Both run against one isolated profile and one 768-file corpus (768 files × 128 one-line functions). The 80 calls are 20 symbols × {search, context} × {markdown, json}. Each run waits until the verified graph answers context before it starts. The full text blocks are compared, including the freshness line, the code_graph_freshness trailer when present, and the tracedecay_metrics footer. A truncated response is compared through its stored handle content.

byte-identical
search, markdown 20/20
context, markdown 20/20
context, json 20/20
search, json 0/20 raw, 20/20 after blanking the cursor's time fields

The 20 search JSON pages differ only in next_cursor.expiry, next_cursor.signature, and the query_fallback_digest that covers the cursor. These fields are minted per call. Two runs of the master binary differ in exactly the same fields: master-vs-master is 60/80 raw and 80/80 normalized. Branch-vs-master is 60/80 raw and 80/80 normalized, with the same normalized digest 1cabaaa2d7fa7419 on all three runs. The first comparison exposed one real drift before this was fixed: a partial lexical lane (candidate_sources…) lost its null generation. SearchCoverageV1 now keeps it.

Existing tests changed

  • Pinned hand-written messages now typed refusals: schema_test (missing query) and search_behavior_test, as above.
  • Moved to the owner path, same assertions: graph_search_dispatch_tests (9) and search_graph_independence_tests (4), plus the dispatch_tests stale-trailer probe. They now run through dispatch_on_graph_authority, the owner computation plus the shared renderer. cancel_candidate_journey mounts the in-process daemon invocation service beside the server (mcp_server_with_project_retained_owner_for_test), because the search it cancels is now an owner invocation.
  • lexical_routing unit tests decode a typed request and assert typed rows. The route and anchor JSON they pin is unchanged. search_schema_tests read the generated catalog schema and resolve its $defs. catalog_discovery's "legacy tools stay discoverable" probe uses tracedecay_dashboard, and search joins the catalog-bound tools filtered like context.

Runtime journey (debug tracedecay from this branch, isolated HOME/profile, one daemon under systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G)

tracedecay serve --path corpus768 (768 files × 128 fns):
tools/list: 230 tools
tracedecay_search {"query": "cancellation_probe_0039_017", "limit": 1}: isError=False 378ms
     freshness: fresh
     ## Search Results
     - **cancellation_probe_0039_017** (function, exact_message), rank 1 · utility 2000000 · via query, split:cancellation|probe|0039|017, ...
tracedecay_search {..., "semantic_mode": "hybrid"}: JSON-RPC error -32603 (unknown field, as above)

Checks (local)

On 4a015bf (rebased on cdc1a2a):

  • cargo test -p tracedecay --features test-transport,test-helpers --test mcp_suite (full): 576 passed, 5 failed. The 5 are the read-cost pins that are red on master since perf(graph-db): store each code edge as one relation row #2277/perf(graph-db): stream the sealed store build one column at a time #2282, filed as test(mcp): read-cost pins red after single-row code edges (#2277) #2291 with the master reproduction (relation_page_cost ×2, typed_evidence_trailers::typed_callees_carry_their_read_cost_on_the_envelope_and_the_trailer, typed_callers_carry_their_read_cost, test_map_reads_each_test_once_across_the_symbols_it_covers). This covers every search, retrieve, schema, protocol, dependency-hint and graph-query module.
  • cargo test -p tracedecay --features test-transport,test-helpers --lib -- mcp::: 182 passed. mcp::server::routing::tests::many_slow_initialize_roots_share_one_discovery_budget timed out once under host load at 3.007 s against its 3 s bound. It passed when rerun alone. The test is unrelated to this change.
  • tracedecay-mcp lib 390, tracedecay-query lib 264, contracts 420 + contracts_suite 277, mcp-catalog 29, daemon-protocol 65, tool-catalog 5 + 22, search_quality_suite 71. cargo test -p tracedecay-cli --bin tracedecay -- tool_command: 57 passed.
  • cargo clippy -p tracedecay -p tracedecay-mcp -p tracedecay-contracts -p tracedecay-daemon-protocol -p tracedecay-api -p tracedecay-mcp-catalog -p tracedecay-tool-catalog -p tracedecay-cli --all-targets -- -D warnings, with and without --features tracedecay/test-transport,tracedecay/test-helpers: clean. cargo fmt --all -- --check: clean. pnpm run contracts:generate, then contracts:check: up to date (SDK regenerated).

After the final rebase onto 450a55f (#2294, #2295; neither touches this path), focused reruns:

  • mcp_suite -- search retrieve schema_test protocol_test dependency_hint graph_query_test: 127 passed, 5 failed. Four of the failures are the test(mcp): read-cost pins red after single-row code edges (#2277) #2291 read-cost pins. The fifth, retrieve_truncation_test::diff_context_large_response_uses_retrievable_truncation_handle, hit a graph that was still warming under host load (load average 105). Rerun alone, it passes.
  • --lib -- mcp::: 179 passed, 3 failed. The three failures were host-load fixture timeouts: connection::cancellable_queue_tests ×2 ("production-composition code index did not publish … after 20000 ms") and host_admission_tests::owned_project_replay_worker_continues_past_one_bounded_batch. Rerun alone: 7 passed.
  • tracedecay-mcp lib 390 passed. contracts:check: up to date.

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 86272c6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T05:45:45.059125Z 4a015bf PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

tracedecay_search is an ApplicationSurfaceOperation the project's
graph-tool owner answers, on MCP and `tracedecay tool` alike. Its
arguments decode against a typed request in the contracts crate, so an
unknown key or a mistyped limit is refused instead of being silently
ignored or defaulted. Its result is a typed catalog result that
serializes to the JSON the tool already emitted: the freshness verdict,
per-lane coverage (a partial lane keeps its null generation and reason),
ranked rows with display and route evidence, the lexical route and anchor
receipts with admitted/dropped counts, the scope report, graph evidence,
the external import hint, and parked indexing detail on an unavailable
search. Every surface renders it through one renderer; the stale-graph
trailer rides the owner's served generation.

The 2,345-line graph search module is split by tool: search keeps the
request decode, the owner computation, and the renderer; context,
find_exact_symbol, the clone family, and rename_preview move to their own
modules. The root graph dispatch group, its portable dispatch table, and
the Graph binding group are deleted, and the search schema bounds move to
the contracts crate beside the request they describe.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a015bfbdc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +184 to +187
compute_search(
ctx,
open(read("code_symbol_search")?),
args,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve JSON format when computing search results

When a caller requests format: "json" and a ranked result has display metadata but a non-code-symbol: anchor (for example, a lexical chunk), this new owner path receives only the canonical request map because separate_application_tool_request removes format before invocation. Consequently compute_search sees render::wants_json(&args) as false and never calls node_id_for, whereas the previous in-client handler enriched these JSON rows. The response therefore silently loses node_id, preventing clients from following the result directly with tracedecay_source_body; carry the requested format to the owner or compute node IDs independently of presentation.

AGENTS.md reference: AGENTS.md:L213-L214

Useful? React with 👍 / 👎.

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