Skip to content

fix(mcp)!: carry tool problems only as structured content - #2651

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
fleet/one-problem-shape
Sep 29, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
fleet/one-problem-shape

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Cause

A refusing tool result carried its typed ApplicationProblemRecord in two places. The canonical renderer (render_application_result) and the project-open reset refusal wrote a top-level problem member. The rmcp server and the daemon's projectless tools/call route then moved it into MCP structuredContent.problem through structure_tool_problem. The CLI's locally rendered tracedecay tool --json output and the project-open refusal kept the top-level copy. As a result, #2632 had to teach the CLI mounting retry to read both locations, tool_result_process_outcome read both, and the mcp_suite helpers read both.

Change

  • tracedecay-mcp::tool_errors now has one writer, problem_structured_content, and one reader, tool_result_problem. The only location is structuredContent.problem, the one member CallToolResult lets MCP clients see.
  • render_application_result and tool_call_open_refusal_response write only that location. structure_tool_problem and its calls in the rmcp call_tool and projectless tools/call routes are deleted.
  • The CLI exit-status check (tool_result_process_outcome) and the one-shot mounting retry (tool_result_retry_after_delay) no longer read two locations; both go through tool_result_problem.
  • Placement is not part of the generated dashboard contracts or the TypeScript SDK, so contracts:generate has nothing to regenerate. The problem record's own schema is unchanged.

Consumers migrated

The CLI (tool_command.rs, core_client.rs) and these tests: mcp_suite support.rs (the dual-read helpers), plus feedback_list, type_hierarchy, hook_runtime_request, info_health_request, unsealed_graph, source_edit_reconcile, ast_grep_rewrite, affected_tests_behavior, the daemon lib tests (replay, feedback_impact, socket, project_open_handshake, core_client), configuration_set_behavior, retained_timeout_dispatch, application_surface, edit, core_cli_suite tool_daemon and tool_first_touch, and runtime_acceptance tool_client_transport. The dashboard, the SDK, hooks (runtime_ports reads the text envelope) and the Python sweep (walks every object) read no tool-result placement.

Fails on master

daemon::tests::socket::projectless_and_project_route_refusals_place_the_problem_identically drives a real projectless refusal over the daemon socket and a real project-route refusal (tool_call_open_refusal_response), then compares where each carries its problem. On a301a2c1db, with only the test and the pub(super) visibility applied:

assertion `left == right` failed
  left:  ["/structuredContent/problem"]   (projectless)
  right: ["/problem"]                     (project route)

That run printed the placements as byte vectors: left: [91, 34, 47, 115, 116, ...] decodes to ["/structuredContent/problem"] and right: [91, 34, 47, 112, ...] decodes to ["/problem"]. The test now prints strings. With this change it passes: both routes emit ["/structuredContent/problem"].

daemon::core_client::tests::projectless_mounting_refusal_is_re_sent_after_its_delay renders the mounting refusal with the production renderer. It asserts structuredContent.problem carries diagnostic application.runtime.mounting, retry: after_delay and retry_after_millis: 250, with no top-level problem, and that project_open_retry_wait re-sends after 250ms. A completed authority-unavailable answer is not re-sent.

Runtime journey

Debug tracedecay built from this branch, isolated HOME, one daemon in a MemoryMax=6G scope:

$ HOME=$J/home tracedecay tool tracedecay_status --json    # unenrolled git repo (CLI tool path)
exit=1
top-level keys: ['content', 'isError', 'structuredContent']
top-level problem: None
structuredContent.problem: {'kind': 'invalid_request', 'code': 'project_required', 'retry': 'never'}

$ HOME=$J/home tracedecay serve   # projectless dir; tools/call tracedecay_project_list {"limt":5}
top-level keys: ['content', 'isError', 'structuredContent']
top-level problem: None
structuredContent.problem: {'kind': 'invalid_request', 'code': 'application.surface.invalid_request', 'message': 'invalid arguments for tracedecay_project_list: unknown field `limt`, expected `limit`'}

$ HOME=$J/home tracedecay serve --path $J/repo   # enrolled project (rmcp project route), same call
top-level keys: ['content', 'isError', 'structuredContent']
top-level problem: None
structuredContent.problem: {'kind': 'invalid_request', 'code': 'application.surface.invalid_request', 'message': 'invalid arguments for tracedecay_project_list: unknown field `limt`, expected `limit`'}

Local verification (after rebasing on 8d4fd5e)

  • cargo test -p tracedecay --features test-transport,test-helpers --test mcp_suite: 609 passed, 0 failed
  • cargo test -p tracedecay-cli --features test-transport --test core_cli_suite: 169 passed, 0 failed
  • cargo test -p tracedecay-application (before the rebase; no application changes since): lib 482, application_suite 66, pr_tracking 7
  • cargo test -p tracedecay-mcp: lib 388 plus integration targets, all passing
  • focused tracedecay lib (core_client, project_open_handshake, socket, replay, feedback_impact, retained_timeout_dispatch, application_surface, production_harness, projectless): 64 passed
  • runtime_acceptance_suite -- tool_client_transport: 8 passed
  • cargo clippy -p tracedecay-mcp -p tracedecay -p tracedecay-cli --all-targets --features ... -- -D warnings: clean. cargo fmt --all -- --check: clean.

Load flakes seen before the rebase and not caused by this change: session_search_test::scheduled_session_import_makes_the_final_codex_source_searchable and workflow_query_test::workflows_query_surface_end_to_end failed at load average around 150 and passed in isolation on this branch. daemon_sigterm_exits_while_authenticated_project_client_is_connected is tracked as #2430 and passed on rerun. The post-rebase full runs above are fully green.

BREAKING CHANGE: a refusing MCP tool result, including tracedecay tool --json output, carries its typed problem record only at structuredContent.problem; the top-level problem member is removed.

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 976af8b

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 29, 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-29T16:07:21.803958Z b6020df 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.

A refusing tool result carried its typed problem record in two places:
the canonical renderer wrote a top-level `problem` member, and the rmcp
server and the daemon's projectless route then moved it into MCP
`structuredContent.problem`. The project-open refusal and the CLI's
locally rendered `tracedecay tool --json` output kept the top-level
copy, so the CLI retry check and the test helpers had to read both.

The renderer and the project-open refusal now write the record only at
`structuredContent.problem` (the one member MCP clients can see), the
rmcp and projectless post-processing that moved it is deleted, and the
CLI exit status and mounting retry read it through one reader.

BREAKING CHANGE: a refusing MCP tool result, including `tracedecay tool
--json` output, carries its typed problem record only at
`structuredContent.problem`; the top-level `problem` member is removed.

@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: b6020dfa17

ℹ️ 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 +112 to +113
"structuredContent".to_string(),
problem_structured_content(&problem.problem)?,

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 Migrate the remaining raw CLI refusal readers

When an application tool such as diagnostics returns a semantic refusal under --json, this writer now exposes the record only at structuredContent.problem, but the Unix core_cli_suite still has SurfaceOutcome::problem_code reading payload.problem and the tests at tool_surface_transport_test.rs:584 and :642 doing the same. Those deterministic refusal journeys consequently receive null instead of the expected code/detail, leaving the advertised full core CLI suite red; update these consumers to read the new location as part of this cutover.

AGENTS.md reference: AGENTS.md:L178-L180

Useful? React with 👍 / 👎.

@ScriptedAlchemy
ScriptedAlchemy merged commit 35fe79e into master Sep 29, 2026
5 of 7 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fleet/one-problem-shape branch September 29, 2026 16:08
ScriptedAlchemy added a commit that referenced this pull request Sep 29, 2026
* test(daemon): wait on readiness in load-sensitive lib tests

Replace test-invented wall-clock bounds and races in daemon and MCP
server lib tests with the readiness signal each one is waiting for:
armed idle notifications, the production store shutdown before an
in-process restart, a fake daemon that stays live, explicit socket
shutdown, paused clocks, and the cancellation signal itself. Also read
the routed refusal from structuredContent after #2651.

* test(daemon): connect TLS egress peers before pausing time

Connect the 128 non-reading peers one at a time so none waits out the
5 s request-read deadlines behind the others' handshakes, join the
handler barrier on its own readiness, and pause time at the release so
only the test's sleeps age the write-idle deadlines.

* test(daemon): await the reindexed generation without a wall clock

* test(daemon): keep the saturated fake daemon live for the client
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