Skip to content
← Public packages

@kentcdodds/kody-issue-triage

Loop-safe triage for Kody run errors and fleet package-runtime error-rate elevations. Wakes Cole (Grok Bot) to decide; Cursor agents only when Cole escalates.

src/on-run-error.ts

130 lines · 3.4 KB · TypeScript
import { kody } from 'kody:runtime'
import {
	AGENT_EXECUTE_NOISE_NOTE,
	hourBucket,
	PLATFORM_WEATHER_NOTE,
	shouldIgnorePlatformWeather,
	skipReason,
} from './shared.ts'
import {
	incrementCounter,
	initSchema,
	updateFingerprint,
	upsertFingerprintFromRun,
} from './storage.ts'
import { scheduleEventSweep } from './schedule-event-sweep.ts'

export type RunErrorRecordedEvent = {
	event?: string
	run?: {
		id?: string
		surface?: string
		name?: string | null
		package_id?: string | null
		kody_id?: string | null
		job_id?: string | null
		error_name?: string | null
		error_message?: string | null
		started_at?: string | null
		/** Omitted on production `run.error.recorded`; present in run_list/run_get. */
		metadata?: Record<string, unknown> | null
	}
	activity_url?: string
	synthetic?: boolean
}

async function softTriageRun(
	runId: string | null | undefined,
	triage: 'ignored' | 'resolved',
	note: string,
) {
	if (!runId) return
	try {
		await kody.runUpdate({
			run_id: runId,
			triage,
			note,
		})
	} catch {
		// Best-effort Activity close; fingerprint status is the standing record.
	}
}

/**
 * Cheap `run.error.recorded` handler. Fingerprint + counter only, except a
 * cheap Activity `runUpdate` when the family is already standing noise
 * (platform isolate-reset weather or ignored / resolved_noise) or the run is
 * agent MCP execute-origin (caller already has the error). Never posts Discord or fetches run detail inline. When a fingerprint is
 * newly actionable it schedules a guarded `./sweep` workflow so Patch can wake
 * without waiting for the hourly job.
 */
export default async function onRunError(event: RunErrorRecordedEvent = {}) {
	const run = event.run
	if (!run?.id && !run?.error_message && !run?.surface) {
		return { ok: true, skipped: 'empty-event' }
	}

	const skip = skipReason(run)
	if (skip === 'agent-execute') {
		await softTriageRun(run.id, 'ignored', AGENT_EXECUTE_NOISE_NOTE)
		return { ok: true, skipped: skip, runId: run.id || null, ignoredExecute: true }
	}
	if (skip) return { ok: true, skipped: skip, runId: run.id || null }

	await initSchema()
	const bucket = hourBucket()
	const upserted = await upsertFingerprintFromRun({
		...run,
		activity_url: event.activity_url || null,
		metadata: run.metadata ?? null,
	})
	const eventsThisHour = await incrementCounter(`events:${bucket}`)
	if (upserted.created) {
		await incrementCounter(`fingerprints:${bucket}`)
	}

	const weather = shouldIgnorePlatformWeather({
		...run,
		metadata: run.metadata ?? null,
	})
	const standingNoise =
		weather ||
		upserted.nextStatus === 'ignored' ||
		upserted.nextStatus === 'resolved_noise'
	if (standingNoise) {
		if (weather) {
			await updateFingerprint(upserted.fingerprint, {
				status: 'resolved_noise',
				outcome: 'resolved_noise',
				classification: 'noise',
				completed_at: new Date().toISOString(),
				summary: PLATFORM_WEATHER_NOTE,
			})
		}
		await softTriageRun(
			run.id,
			'resolved',
			weather
				? PLATFORM_WEATHER_NOTE
				: 'Standing ignored/resolved_noise fingerprint; do not refill Activity.',
		)
	}

	const queued = !standingNoise
	const sweep = await scheduleEventSweep({
		fingerprint: upserted.fingerprint,
		queued,
	})

	return {
		ok: true,
		queued,
		fingerprint: upserted.fingerprint,
		created: upserted.created,
		count: upserted.count,
		eventsThisHour,
		resolvedNoise: standingNoise || undefined,
		synthetic: event.synthetic === true,
		eventSweep: sweep,
	}
}