Skip to content

fix(mcp): stop filing Sentry exceptions for missing _triggered_by#92

Merged
milstan merged 5 commits into
mainfrom
ArtyETH06/3718-triggered-by-no-sentry
Jun 10, 2026
Merged

fix(mcp): stop filing Sentry exceptions for missing _triggered_by#92
milstan merged 5 commits into
mainfrom
ArtyETH06/3718-triggered-by-no-sentry

Conversation

@ArtyETH06

@ArtyETH06 ArtyETH06 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

What

A composite tool called without _triggered_by is a recoverable agent mistake — the host LLM simply re-calls with the field set. It should not be reported to Sentry.

The guard previously throwed an {error:true, code:"LAST_PROMPT_REQUIRED"} envelope into the shared catch. There, isLeadbayBusinessError matched it (it has error:true + a string code) and called telemetry.captureException — filing a Sentry exception on every dropped field. Sentry's GitHub integration then auto-opened top-priority bugs.

The AsyncLocalStorage.run in the Sentry title is just the OTel/Sentry async-context wrapper frames; the real throw was server.ts line ~838.

Fix (targeted — "option A")

Return the isError envelope directly from the guard instead of throwing into the shared catch.

  • Behavior toward the LLM is unchanged — same envelope text, same isError: true, same recovery hint.
  • PostHog visibility preservedcaptureToolCall + captureCompositeCall still fire with ok:false / LAST_PROMPT_REQUIRED, so the rate of agents ignoring the mandate stays observable (the code comment explicitly wanted this).
  • Only captureException is dropped — this expected, recoverable condition no longer reaches Sentry, so it stops auto-filing bugs.

Test

New file packages/mcp/test/triggered-by-guard-no-sentry.test.ts (existing test files untouched):

  1. missing _triggered_by on a composite → isError envelope, captureException not called, both PostHog events fire with LAST_PROMPT_REQUIRED.
  2. _triggered_by present → guard does not short-circuit; dispatch proceeds past it.

Verification

  • packages/mcp: 368/368 tests pass (incl. triggered-by-mandate + audits), typecheck clean
  • packages/core: 317/317 tests pass, typecheck clean

closes https://github.com/leadbay/product/issues/3718

A composite tool called without `_triggered_by` is a recoverable agent
mistake — the LLM just re-calls with the field. The guard previously
`throw`ed an {error:true,code} envelope into the shared catch, where
isLeadbayBusinessError matched it and called captureException, filing a
Sentry exception on every dropped field. Sentry's GitHub integration
then auto-opened top-priority bugs (e.g. #3718).

Return the isError envelope directly from the guard instead. Behavior
toward the LLM is unchanged (same text, same isError). The PostHog
events (captureToolCall + captureCompositeCall, ok:false /
LAST_PROMPT_REQUIRED) still fire so the mandate-ignore rate stays
visible. Only captureException is dropped.

Closes #3718

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ArtyETH06 ArtyETH06 self-assigned this Jun 8, 2026
@ArtyETH06 ArtyETH06 marked this pull request as ready for review June 8, 2026 17:48
@ArtyETH06 ArtyETH06 requested a review from milstan June 8, 2026 17:48

@milstan milstan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm OK with looseing the error reporting on this, but I think we really do want to the agents using the MCPs to easily understand they need to pass this value - this error happens a lot, so clearly our prompts are not robust enough to enforce it.

I also suspect the user might have deselected telemetry option in the core settings of the MCP - there is a json file I believe (we should investigate this), so indeed in this case the agent might be operating with contradictory expecations (to not report telemetry and to always report the prompt).

I think in adition to silencing the sentry error, we also want:

  • coherence of behavior with settings
  • prompts/tooldescrptions that enforce the behavior we want.

…pt enforcement

Addresses Milan's review on PR #92. The _triggered_by field exists ONLY to
feed product analytics, yet the LAST_PROMPT_REQUIRED guard hard-blocked
composite calls regardless of the telemetry setting. With telemetry disabled
(LEADBAY_TELEMETRY_ENABLED=false) the agent faced contradictory expectations:
'do not report telemetry' + 'you must always pass this analytics-only field'.

Make the mandate telemetry-conditional. Thread a telemetryEnabled flag
(parseTelemetryEnv) from bin.ts + http-server.ts into buildServer, and use it
in all three places the mandate lives:
- schema injection: _triggered_by marked required only when telemetry on
- runtime guard: LAST_PROMPT_REQUIRED only fires when telemetry on
- server instructions: new triggered-by.md mandate paragraph emitted only
  when telemetry on

Also strengthens enforcement when telemetry IS on: the mandate previously
lived ONLY in the JSON-schema field description, with zero reinforcement in
the system prompt every agent reads at startup. Agents dropped the field a
lot. The new server-instruction paragraph (alongside the existing schema
description) gives the agent a far stronger nudge.

Defaults telemetryEnabled=true so tests/embeds keep historical behavior.

New test file triggered-by-telemetry-coherence.test.ts covers both modes
(guard fires/skips, schema required/optional, prompt paragraph present/absent).
Existing triggered-by-guard-no-sentry.test.ts still covers the telemetry-on
no-Sentry path.

Verified live: telemetry ON -> 32 composites mark _triggered_by required +
guard fires (code=LAST_PROMPT_REQUIRED (no-sentry)); telemetry OFF -> 0
required + guard skipped. mcp 373/373, core 317/317, typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -0,0 +1 @@
Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a `_triggered_by` argument — the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the literal string "<no user message>" so the call is auditable as agent-initiated. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) — replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This costs you nothing per call and is required on EVERY composite invocation, not just the first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't like the "no user message". Absent value should be unasigned or null, not a string. That is the antipatern.

I think in fact, either we make the parameter optional (which I fear would make the agents easily skip this and currently it's essential wthat we can improve this), or we ditch the telemetry opt-out parameter (I think a better soltuion) and keep mandatory, non empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — went with your option Y.

  1. Always mandatory now. Dropped the telemetry-conditional path entirely. _triggered_by stays mandatory + non-empty on every composite call regardless of LEADBAY_TELEMETRY_ENABLED. Reframed it as a protocol/audit requirement rather than analytics — that's what dissolves the original contradiction: when telemetry is off the value is still collected locally but never transmitted (capture is a no-op via NOOP_TELEMETRY), so the opt-out is honored without weakening enforcement.

  2. Sentinel gone. Removed "<no user message>". Agent-initiated calls (memory recall, scheduled run, retry) now pass the actual instruction being acted on — a real, auditable value, so the field is genuinely non-empty in every case.

Verified live: telemetry off + missing field now rejects with LAST_PROMPT_REQUIRED (still no Sentry exception). Tests green (mcp 372, core 317), typecheck clean. See efcab1d.

…onal + sentinel

Addresses Milan's second review on PR #92.

Two changes per his feedback:

1. Drop the telemetry-conditional behavior. Instead of making _triggered_by
   optional when telemetry is off, keep it ALWAYS mandatory + non-empty and
   reframe it as a protocol requirement (an auditable intent trace), not
   analytics. This dissolves the original 'opted out of telemetry but still
   required' contradiction differently: when telemetry is off the value is
   still collected locally but never transmitted (capture is a no-op via
   NOOP_TELEMETRY), so the opt-out is honored without weakening enforcement.
   Removes telemetryEnabled from the guard, schema injection,
   buildServerInstructions, BuildServerOptions, bin.ts and http-server.ts.

2. Remove the '<no user message>' magic-string sentinel (antipattern: an
   absent value shouldn't be a string). Agent-initiated calls (memory recall,
   scheduled run, retry) now pass the ACTUAL instruction being acted on — a
   real, auditable value — so the field is genuinely non-empty in every case.

The server-instruction mandate paragraph is now always emitted (no longer
telemetry-gated). Sentry is still never paged on a missing field (the #3718
fix is intact).

Verified live: telemetry off + missing field now rejects with
LAST_PROMPT_REQUIRED (no-sentry); sentinel absent from tool schemas.
mcp 372/372, core 317/317, typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ArtyETH06 ArtyETH06 requested a review from milstan June 9, 2026 20:23
milstan and others added 2 commits June 10, 2026 16:37
Land PR #92 (stop filing Sentry exceptions for missing _triggered_by;
make _triggered_by an always-mandatory auditable protocol field) on top
of current main (was 18 commits behind). Merge auto-resolved with no
conflicts; regenerated generated files (pnpm prompts:build → unchanged,
confirming the textual auto-merge produced consistent output).

Release metadata bumped in lockstep so auto-tag.yml fires the release:
- packages/mcp/package.json 0.19.1 → 0.19.2
- packages/mcp/server.json (top-level + packages[0].version) → 0.19.2
  (@0.19 npx pins unchanged — same minor line)
- CHANGELOG 0.19.2 entry

pnpm -r build / test / typecheck all green (core 380, mcp 382).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@milstan

milstan commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

[Claude]: Brought the branch up to date with main (it was 18 commits behind) and prepped release metadata, on Milan's request to land + release this.

  • Merged current main in — auto-resolved, no conflicts. server.ts was the only file both sides touched; the net diff vs main is exactly this PR's guard change, main's widget/notification work untouched.
  • Re-ran pnpm prompts:build against the merged tree — all generated files report unchanged, so the textual auto-merge produced consistent generated output.
  • Bumped @leadbay/mcp 0.19.1 → 0.19.2 (package.json + server.json both version fields, in lockstep; @0.19 npx pins stay valid). CHANGELOG entry added. The bump is what makes auto-tag.yml actually fire a release on merge.
  • pnpm -r build / test / typecheck green: core 380, mcp 382.

Squash-merging now; release.yml will publish @leadbay/mcp@0.19.2 to npm + the MCP registry.

@milstan milstan merged commit 9a30eb9 into main Jun 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants