Skip to content

mcp(telemetry): resolve the error envelope's own code instead of reporting `unknown_error #9659

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

@loopover/contract defines two halves of an error-envelope contract that nothing joins up:

  • toolErrorFields (packages/loopover-contract/src/shared.ts:38-45) — "Fields an error envelope
    carries when a tool fails in a way the caller can act on. code is drawn from a closed,
    developer-defined set rather than free text so telemetry can break failures down by cause
    (mcp(observability): PostHog error tracking, tracing, and usage telemetry at every dispatch chokepoint — redacted, gated, registry-driven #9525)
    ". It is exported from packages/loopover-contract/src/index.ts:28 and has zero
    consumers
    : grep -rn "toolErrorFields" finds the definition, the re-export, and one prose
    mention in a comment. No tool output schema spreads it.
  • resolveErrorCode (packages/loopover-contract/src/telemetry.ts:244-261) — "Map a thrown error
    or an error envelope onto the closed code set", whose first branch reads envelope.code and
    matches it against MCP_TELEMETRY_ERROR_CODES. No caller ever passes an envelope.

The result is that the code dimension is dead on the failure path that actually exists:

  • Remote (src/mcp/dispatch-telemetry.ts:99-112): on result.isError === true it emits
    errorCode: "unknown_error" as const — a literal, never resolveErrorCode.
  • Stdio (packages/loopover-mcp/lib/telemetry.ts:125-134): on the failure path it calls
    recordStdioDispatchTelemetry without an error field, so resolveErrorCode(undefined) at
    lib/telemetry.ts:175 returns unknown_error.
  • Miner (packages/loopover-miner/bin/loopover-miner-mcp.ts:127-131): it builds a typed
    envelope on line 128 — { error: { code: toolErrorCode(error), message } } — and then on line 129
    throws that away and passes the raw error to recordMinerDispatchTelemetry, which re-guesses a
    code from the message regexes. So the caller is told store_unavailable while telemetry records
    not_found (an ENOENT: no such file message matches the not found|no such regex at
    telemetry.ts:254) — two different classifications of one failure, produced by adjacent lines.

#9518's requirement 5 asked for exactly this to be closed: "Error envelopes unified... every tool on
every server returns the same schema-described error envelope."
The miner half landed
(withMinerToolErrorHandling); the schema-description and the telemetry half did not.

Requirements

  1. store_unavailable is added to MCP_TELEMETRY_ERROR_CODES
    (packages/loopover-contract/src/telemetry.ts:28-39). It is a developer-defined closed set and
    this is the code the miner already returns to callers; adding it is what lets one code serve both
    the envelope and the telemetry event.
  2. All eleven miner output schemas in packages/loopover-contract/src/tools/miner.ts spread
    toolErrorFields, so the advertised outputSchema describes the envelope
    withMinerToolErrorHandling really returns instead of only the success shape.
  3. withMinerToolErrorHandling passes the envelope it built to recordMinerDispatchTelemetry, so
    resolveErrorCode reads the declared code rather than re-deriving one from the message.
  4. instrumentToolDispatch (src/mcp/dispatch-telemetry.ts) replaces the hardcoded
    "unknown_error" with resolveErrorCode applied to the result's structuredContent.error, and
    wrapStdioToolHandler (packages/loopover-mcp/lib/telemetry.ts) passes that same value as its
    error field on the failure path. Both must keep returning unknown_error when no envelope is
    present.
  5. The pinned assertions that encode today's behaviour are updated deliberately, not deleted:
    test/unit/mcp-dispatch-telemetry.test.ts:229-238 ("treats an error envelope as a failed call")
    keeps its unknown_error case for an envelope-less isError result and gains a new case for an
    envelope carrying a declared code.
  6. Delete the empty import type { } from "@loopover/contract"; at
    src/mcp/dispatch-telemetry-sink.ts:18 while you are in the file.

⚠️ Required pattern: resolveErrorCode in packages/loopover-contract/src/telemetry.ts:244-261 is
the single classifier — its existing envelope.code branch is what gets wired up. Adding a second
per-server code-mapping function, widening MCP_TELEMETRY_ERROR_CODES to accept a caller-derived
string, or reading code out of the envelope without validating it against the closed set, do NOT
satisfy this issue.

Deliverables

  • store_unavailable in MCP_TELEMETRY_ERROR_CODES, with the closed-set test at
    test/unit/mcp-dispatch-telemetry.test.ts:200-203 still passing
  • All 11 miner output schemas in packages/loopover-contract/src/tools/miner.ts spread
    toolErrorFields; a test asserts an error-envelope structuredContent validates against each
    tool's advertised output schema
  • withMinerToolErrorHandling passes its own envelope to recordMinerDispatchTelemetry; a test
    asserts a store failure records error_code: "store_unavailable" and not not_found
  • instrumentToolDispatch and wrapStdioToolHandler resolve the code from the result's
    structuredContent.error, with tests for: envelope carrying a declared code, envelope carrying
    an undeclared code (falls back to unknown_error), and no envelope at all
  • The empty import type at src/mcp/dispatch-telemetry-sink.ts:18 is gone

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example wiring the three chokepoints to read the envelope while leaving toolErrorFields unused, so
no tool's advertised schema ever declares the field being read — does not resolve this issue.

Test Coverage Requirements

99%+ Codecov patch coverage, branch-counted, applies to everything this touches:
src/mcp/dispatch-telemetry.ts, src/mcp/dispatch-telemetry-sink.ts,
packages/loopover-contract/src/telemetry.ts, packages/loopover-contract/src/shared.ts,
packages/loopover-contract/src/tools/miner.ts, packages/loopover-mcp/lib/telemetry.ts and
packages/loopover-miner/bin/loopover-miner-mcp.ts are all in coverage.include
(vitest.config.ts:55-100). Every new conditional needs both arms (envelope present vs absent;
declared code vs undeclared code), plus a named regression test for the miner's
store_unavailable/not_found mismatch.

Expected Outcome

A PostHog breakdown of usage_event by error_code distinguishes real failure causes instead of
reporting every non-throwing failure as unknown_error; the miner's caller-visible error code and its
telemetry code are the same value; and toolErrorFields describes a shape that is actually
advertised.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions