diff --git a/layers/registry/server/utils/ai-generation-work.ts b/layers/registry/server/utils/ai-generation-work.ts index ecd6edaf..824d3021 100644 --- a/layers/registry/server/utils/ai-generation-work.ts +++ b/layers/registry/server/utils/ai-generation-work.ts @@ -121,6 +121,8 @@ const ELIGIBLE_SKILL_SQL = ` WHERE r.broken_since IS NULL AND s.current_sha IS NOT NULL AND s.rendered_raw IS NOT NULL + -- Match String.trim(): empty content cannot produce an embedding or classification. + AND LENGTH(TRIM(s.rendered_raw, char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND s.rendered_status = 'ok' AND s.seo_indexable = 1 ` diff --git a/server/plugins/sentry.ts b/server/plugins/sentry.ts index 92a756f6..075fb5b8 100644 --- a/server/plugins/sentry.ts +++ b/server/plugins/sentry.ts @@ -1,10 +1,10 @@ import { createSentryInitOptions } from '@harlan-zw/nuxt-sentry/server' import { SDK_VERSION } from '@sentry/cloudflare' import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins' -import { isBestEffortCacheWriteError, isExpectedUpstreamOutageError, isMcpAcceptProbeError, scrubSentryBreadcrumb, scrubSentryEvent } from '../../shared/sentry' +import { isBestEffortCacheWriteError, isExpectedUpstreamOutageError, isMcpAcceptProbeError, isMcpProtocolProbeError, scrubSentryBreadcrumb, scrubSentryEvent } from '../../shared/sentry' function isDroppedSignature(error: unknown): boolean { - return isBestEffortCacheWriteError(error) || isExpectedUpstreamOutageError(error) || isMcpAcceptProbeError(error) + return isBestEffortCacheWriteError(error) || isExpectedUpstreamOutageError(error) || isMcpAcceptProbeError(error) || isMcpProtocolProbeError(error) } export default defineNitroPlugin((nitroApp) => { @@ -28,7 +28,7 @@ export default defineNitroPlugin((nitroApp) => { // Nitro's route cache catches its own KV write failures and forwards the // caught error here as unhandled (SKILLD-17). The registry handlers' // intended upstream-outage 503s land here the same way (SKILLD-11, - // SKILLD-1E), as does the MCP transport's intended probe 406 (SKILLD-35). + // SKILLD-1E), as do MCP probe rejections: 406 (SKILLD-35) and 400 (SKILLD-3H). // All are failure classes already answered or recorded, so drop the // duplicates and keep the wide events as the visibility path. beforeSend(event, hint) { diff --git a/shared/sentry.ts b/shared/sentry.ts index 06594d9b..f570b14c 100644 --- a/shared/sentry.ts +++ b/shared/sentry.ts @@ -123,6 +123,12 @@ export function isMcpAcceptProbeError(error: unknown): boolean { return typeof message === 'string' && message.startsWith('Not Acceptable: Client must accept') } +/** MCP answers unsupported protocol probes with 400 before running a tool (SKILLD-3H). */ +export function isMcpProtocolProbeError(error: unknown): boolean { + const message = sentryExceptionMessage(error) + return typeof message === 'string' && message.startsWith('Bad Request: Unsupported protocol version:') +} + /** * A URL without its query string or fragment. Query strings carry OAuth codes, * signed tokens, and search terms, so no URL reaches Sentry with one. diff --git a/test/unit/ai-generation-work.test.ts b/test/unit/ai-generation-work.test.ts index 98ce8fef..5fef15ce 100644 --- a/test/unit/ai-generation-work.test.ts +++ b/test/unit/ai-generation-work.test.ts @@ -73,6 +73,12 @@ describe('ai generation work', () => { afterEach(() => sqlite.close()) + it.each(['', ' \t\n\r', '\u00A0\u2003\uFEFF'])('keeps empty source out of runtime generation work: %j', async (source) => { + sqlite.prepare('UPDATE skills SET rendered_raw = ?').run(source) + expect(await selectMissingGeneratedSkills(db, 'abstractness', 50)).toEqual([]) + expect(await selectMissingGeneratedSkills(db, 'embedding', 50)).toEqual([]) + }) + it('does not select current runtime kinds because paused batch kinds are missing', async () => { const embeddings = await selectMissingGeneratedSkills(db, 'embedding', 50) const abstractness = await selectMissingGeneratedSkills(db, 'abstractness', 50) diff --git a/test/unit/sentry-cache-write-filter.nuxt.test.ts b/test/unit/sentry-cache-write-filter.nuxt.test.ts index 775343a4..448a2b5c 100644 --- a/test/unit/sentry-cache-write-filter.nuxt.test.ts +++ b/test/unit/sentry-cache-write-filter.nuxt.test.ts @@ -196,4 +196,15 @@ describe('sentry beforeSend cache-write filter', () => { } expect(beforeSend(page406Event, {})).toBe(page406Event) }) + + it.each([true, false])('drops rejected MCP protocol probes with original exception: %s', (original) => { + const message = 'Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18)' + const event: SentryEventFixture = { exception: { values: [{ type: 'Error', value: message }] } } + expect(beforeSend(event, original ? { originalException: new Error(message) } : {})).toBeNull() + }) + + it('keeps other protocol errors', () => { + const event: SentryEventFixture = { exception: { values: [{ type: 'Error', value: 'Unsupported protocol version in database connection' }] } } + expect(beforeSend(event, {})).toBe(event) + }) })