You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
mcp(telemetry): one envelope, one code, and no registration outside the chokepoint (#9659)
`@loopover/contract` defined both halves of an error-envelope contract and joined neither.
`toolErrorFields` -- "code is drawn from a closed, developer-defined set so telemetry can break
failures down by cause" -- had zero consumers and typed that code as `z.string()`.
`resolveErrorCode`'s first branch reads an envelope's declared code; nothing ever passed it one.
So the code dimension was dead on the failure path that does not throw:
- the REMOTE emitted a hardcoded `"unknown_error"` for every returned failure;
- the STDIO wrapper passed no error at all, so `resolveErrorCode(undefined)` said the same;
- the MINER built a typed envelope for the caller and then threw it away, passing the raw error --
whose ENOENT message matches `/not found|no such/`, so a store that would not open told the caller
`store_unavailable` while telemetry recorded `not_found`. Two classifications of one failure, from
adjacent lines.
Now: `store_unavailable` joins the closed set (the miner already returned it to callers),
`toolErrorFields.error.code` IS that set rather than free text, all 16 miner output schemas spread it
so the envelope appears in the artifacts a consumer reads, and all three chokepoints classify from
the result's own envelope via the one classifier. A code outside the set still falls back to
`unknown_error`, so a tool cannot widen the dimension by inventing one.
Two things this uncovered, fixed here rather than left:
- `ChatActionDispatchResult.status` was `string`, which is what made the miner's refusal mapping
unverifiable. It is a discriminated union of the five outcomes `dispatchChatAction` can return, and
the refusal-to-code map is a `Record` over it -- so adding a status without deciding what it means
to a caller fails the build. A test fixture was inventing a sixth status the dispatcher cannot
return; it now uses a real one.
- Three governor-gated outputs declared their own `error: z.string()`, colliding with the shared
envelope. The refusal detail moves inside the envelope under the code its status maps to; `blocked`
and `reason` still carry the refusal's own vocabulary.
Also closes#9658: `loopover_miner_ping` was the one registration that never reached
`withMinerToolErrorHandling`, whose doc calls the tool name REQUIRED precisely so "instrumented" is a
property of the wrapper rather than of each call site. The health check an operator's monitoring hits
on a loop reported zero calls forever. A structural rule now fails on any registration that bypasses
the wrapper, and a behavioural test calls every tool the registry projects for this server and asserts
each produced exactly one record.
The empty `import type { } from "@loopover/contract"` in dispatch-telemetry-sink.ts is gone.
0 commit comments