Skip to content

fix(cli)!: one typed tool-result shape for every --json refusal - #2690

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

ScriptedAlchemy merged 1 commit into
masterfrom
fleet/one-tool-json-shape

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Fixes #2672

Cause

After #2651, tracedecay tool --json refusals still came out in three shapes, depending on which path produced them:

  • A compatibility tool (search) printed the MCP tool result. This was already correct.
  • A typed application tool (callers) went through print_cli_application_surface, whose --json branch printed json_line(&result.result): the application envelope with a top-level problem, and no isError.
  • Some refusals settle before any owner answers them: a missing daemon socket, a rejected argument, an unknown tool. For these, run() printed {"problem": project_route_problem(..)}, the JSON-RPC error-data shape (reason_code/retryable/detail), which is not a problem record. The daemon-down error had no typed detail at all; its socket and service-unit facts existed only inside a sentence.

Change

  • One refusing tool-result renderer, problem_tool_result (tracedecay-mcp): isError, with the typed record at structuredContent.problem. render_application_result uses it. A typed-path --json refusal now prints that tool result, marked isError just as on the compatibility path.
  • A refusal that settles before any owner answers goes through render_settled_route_refusal. The error is mapped to a problem by the owner's own mapping (graph_tool_error_problem). The envelope uses the tool's CLI result contract (settled_tool_refusal, which replaces the MCP-only mcp_project_open_reset_refusal); a tool name no binding owns uses the adapter problem contract (tracedecay_api::adapter_problem). The untyped {"problem": ...} printer is deleted, and project_route_problem is now private to the JSON-RPC error-data path.
  • New typed detail ApplicationProblemDetailV1::DaemonUnreachable, code daemon.unreachable, with fields socket, named_by, and service_unit (not_installed | unobservable{error} | installed{path, serves}). It maps to unavailable, retry: after_delay, legal_actions: [retry]. unreachable_daemon_detail replaces unavailable_daemon_socket_message. The rendered message and stderr keep the same sentence.
  • The contracts and SDK were regenerated (contracts:generate, no drift after the rebase), and @tracedecay/sdk goes 0.9.0 → 0.10.0.

Still open, and outside this issue's scope: a successful typed-path --json call still prints the application envelope, while a compatibility tool prints the MCP tool result.

Fails on master

core_cli_suite tool_cursor_test::every_json_refusal_prints_one_typed_problem_placement runs search with an invalid cursor, callers with an unknown node id, and status against a missing TRACEDECAY_DAEMON_SOCKET. It asserts that all three have the same placement and carry the literal typed records. On master (with only this test added) it fails with:

left: [{is_error: true, members: [content, isError, structuredContent], structured_members: [problem]},
       {is_error: null, members: [contract, problem, request_id], structured_members: null},
       {is_error: null, members: [problem], structured_members: null}]

With the change it passes. The records are (invalid_request, cursor.invalid, never), (not_found_or_not_authorized, not_found_or_not_authorized, never), and (unavailable, daemon.unreachable, after_delay), with detail {"kind":"daemon_unreachable","socket":"<path>","named_by":"TRACEDECAY_DAEMON_SOCKET","service_unit":{"state":"not_installed"}} and legal_actions: ["retry"], and exit code 69.

Runtime journey

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

search: keys=['content', 'isError', 'structuredContent'] isError=True top-level problem=None kind=invalid_request code=cursor.invalid retry=never legal=['correct_request', 'restart_without_cursor'] detail=null
callers: keys=['content', 'isError', 'structuredContent'] isError=True top-level problem=None kind=not_found_or_not_authorized code=not_found_or_not_authorized retry=never legal=[] detail=null
down: keys=['content', 'isError', 'structuredContent'] isError=True top-level problem=None kind=unavailable code=daemon.unreachable retry=after_delay legal=['retry'] detail={"kind": "daemon_unreachable", "named_by": "TRACEDECAY_DAEMON_SOCKET", "service_unit": {"state": "not_installed"}, "socket": "/fast/tmp/fleet-one-tool-json-shape/journey.3vLE/does-not-exist.sock"}
down exit=69; stderr: Daemon unreachable: TraceDecay daemon socket '/fast/tmp/fleet-one-tool-json-shape/journey.3vLE/does-not-exist.sock' named by TRACEDECAY_DAEMON_SOCKET is not available. No managed TraceDecay daemon service is installed. Run `tracedecay daemon install-service` only if you want a managed daemon.

(search --cursor not-a-cursor --json exit 1, callers --node-id no-such-node --json exit 1, TRACEDECAY_DAEMON_SOCKET=<missing> status --json exit 69.)

Local verification (after the rebase)

  • core_cli_suite: 170/170 passed (tracedecay-cli bins 350). A re-run of tool_cursor/tool_surface_transport/tool_daemon after the final rebase passed 64/64.
  • mcp_suite: 611/611 passed.
  • daemon_suite code_index_park 1, runtime_acceptance (reachability, advisory, tool_client_transport) 22, transport_acceptance typed_terminal_restart_acceptance 10: all passed.
  • Library tests: contracts 431, daemon-control 100, daemon-service 322, domain 224, tracedecay-mcp 388, tracedecay-api 53, focused tracedecay lib (application_surface, project_open_handshake, http_application, socket, core_client) 66, tracedecay-application 482 + application_suite 66 + pr_tracking 7. All passed.
  • Clippy -D warnings on every touched crate is clean. cargo fmt --check is clean. The Windows cross-check (cargo check --workspace --all-targets --target x86_64-pc-windows-gnu ...) exits 0.
  • Dashboard typecheck is clean. pnpm test: 2016/2017, with one codegen fixture test timing out at a load average near 100; it passed 15/15 when run alone. contracts:generate shows no drift. SDK typecheck and test (33) pass.

BREAKING CHANGE: tracedecay tool --json refusals from typed application tools and from pre-owner failures print the MCP tool result with the record at structuredContent.problem instead of an application envelope or an untyped {"problem": ...} object. The problem detail union gains daemon_unreachable, and @tracedecay/sdk is 0.10.0.

`tracedecay tool --json` printed a refusal in three shapes. A compatibility
tool printed the MCP tool result. A typed application tool printed its
application envelope with a top-level `problem`. A refusal settled before any
owner answered (a missing daemon socket, a rejected argument, an unknown
tool) printed `{"problem": {reason_code, retryable, ...}}`, which is not a
problem record at all.

Every path now prints the one refusing tool result, `isError` beside the
typed record at `structuredContent.problem`, through one renderer
(`problem_tool_result`). Pre-owner refusals map through the owner's
error-to-problem mapping under the tool's CLI result contract, or under the
adapter problem contract for a name no binding owns. The untyped
`{"problem": ...}` printer is deleted.

A missing daemon socket is now the typed `daemon.unreachable` detail
(`daemon_unreachable`): the socket, the variable that named it, and the
managed service unit as observed (`not_installed`, `unobservable`, or
`installed` with the socket it serves). Its message and stderr keep the
same sentence.

BREAKING CHANGE: `tracedecay tool --json` refusals from typed application
tools and from pre-owner failures print the MCP tool result with the
record at `structuredContent.problem` instead of an application envelope
or an untyped `{"problem": ...}` object. The problem detail union gains
`daemon_unreachable`, and `@tracedecay/sdk` is 0.10.0.
@changeset-bot

changeset-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c8f8f5

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

@ScriptedAlchemy ScriptedAlchemy changed the title fix(cli)!: print every tool --json refusal as the typed tool result fix(cli)!: one typed tool-result shape for every --json refusal Sep 29, 2026
@ScriptedAlchemy
ScriptedAlchemy merged commit dcf2fad into master Sep 29, 2026
6 of 7 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fleet/one-tool-json-shape branch September 29, 2026 23:34
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.

tool --json refusals print three shapes; callers keeps top-level problem

1 participant