Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions layers/registry/server/utils/ai-generation-work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
`
Expand Down
6 changes: 3 additions & 3 deletions server/plugins/sentry.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions shared/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions test/unit/ai-generation-work.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions test/unit/sentry-cache-write-filter.nuxt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Loading