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/sweep.ts

798 lines · 20.9 KB · TypeScript
import postMessage from 'kody:@kentcdodds/discord/post-message'
import editMessage from 'kody:@kentcdodds/discord/edit-message'
import { kody } from 'kody:runtime'
import { gatherPeerContext } from './peer-context.ts'
import {
	PATCH_BOT_ID,
	PATCH_BOT_NAME,
	buildPatchTriagePrompt,
	wakePatchForTriage,
} from './patch-handoff.ts'
import { gatherLoopGuardSnapshot } from './spawn-snapshot.ts'
import { emptyLoopGuardSnapshot, type LoopGuardSnapshot } from './loop-guard.ts'
import {
	emptyPeerContext,
	handledSiblingForRecord,
	inFlightSiblingForRecord,
	type PeerContext,
} from './peer-match.ts'
import {
	SELF_KODY_ID,
	SKIPPED_EXECUTE_STATUS,
	discordChannelId,
	isFleetRateFingerprint,
	eventAnomalyThreshold,
	fingerprintAnomalyThreshold,
	hourBucket,
	maxAgentsPerHour,
	retriageCooldownMs,
	AGENT_EXECUTE_NOISE_NOTE,
	isSelfPackageRun,
	shouldIgnoreAgentExecuteNoise,
	shouldIgnorePlatformWeather,
	skipReason,
	spawnSkipReasonForRecord,
	truncate,
	PLATFORM_WEATHER_NOTE,
	type FingerprintRecord,
	type RunSnippet,
} from './shared.ts'
import {
	getCounter,
	incrementCounter,
	initSchema,
	isEnabled,
	listFingerprintsByStatuses,
	listStaleSpawned,
	releaseLease,
	tryAcquireLease,
	updateFingerprint,
	upsertFingerprintFromRun,
} from './storage.ts'
import {
	applyTriageToMatchingOpenRuns,
	softTriageForOutcome,
} from './apply-matching-runs.ts'
import { formatKodyIssueReport } from './format-discord-report.ts'


async function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
	let timer: ReturnType<typeof setTimeout> | undefined
	try {
		return await Promise.race([
			promise,
			new Promise<T>((resolve) => {
				timer = setTimeout(() => resolve(fallback), ms)
			}),
		])
	} finally {
		if (timer) clearTimeout(timer)
	}
}

export type SweepInput = {
	dryRun?: boolean
	/** Process at most this many queued fingerprints (default 1). */
	limit?: number
	/** When true, also scan current open run errors into the queue. */
	reconcile?: boolean
}

function discordId(posted: unknown) {
	const value = posted as { id?: string; messageId?: string; message_id?: string }
	return value?.id || value?.messageId || value?.message_id || null
}


async function postOrEditQueuedCard(
	record: FingerprintRecord,
	content: string,
	flags: unknown,
) {
	if (record.discord_message_id) {
		try {
			await editMessage({
				channelId: discordChannelId,
				messageId: record.discord_message_id,
				content,
				flags,
			})
			return record.discord_message_id
		} catch {
			// Message may have been deleted; post a replacement.
		}
	}
	const posted = await postMessage({
		channelId: discordChannelId,
		content,
		flags,
	})
	return discordId(posted)
}

/** Edit an existing Queued card off pure "Waking Patch…" after a failed/interrupted spawn. */
async function editCardRetryable(
	record: FingerprintRecord,
	discordMessageId: string,
	reason: string,
) {
	const fleetRate = isFleetRateFingerprint(record.fingerprint)
	const report = formatKodyIssueReport({
		status: 'queued',
		title: fleetRate ? 'Fleet package error rate elevated' : undefined,
		record,
		summary: record.sample_message
			? truncate(record.sample_message, 240)
			: null,
		extras: [reason],
	})
	try {
		await editMessage({
			channelId: discordChannelId,
			messageId: discordMessageId,
			content: report.content,
			flags: report.flags,
		})
	} catch {
		// Best-effort; stored discord_message_id still lets the next sweep edit in place.
	}
}


function snippetFromListedRun(run: {
	id?: string
	surface?: string | null
	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
	metadata?: Record<string, unknown> | null
}): RunSnippet {
	return {
		id: run.id,
		surface: run.surface,
		name: run.name,
		package_id: run.package_id,
		kody_id: run.kody_id,
		job_id: run.job_id,
		error_name: run.error_name,
		error_message: run.error_message,
		started_at: run.started_at,
		metadata: run.metadata ?? null,
	}
}

const SELF_PACKAGE_NOTE =
	'Ignored: kody-issue-triage self-package run (UNIQUE SQLITE / KodyFetchGateway / own handler). Not a standing product failure.'

async function ignoreOpenRuns(runIds: string[], note: string, dryRun: boolean) {
	if (!runIds.length) return { ignored: 0, dryRun }
	if (dryRun) return { ignored: runIds.length, dryRun: true }
	let ignored = 0
	for (let i = 0; i < runIds.length; i += 100) {
		try {
			const result = await kody.runUpdateBulk({
				run_ids: runIds.slice(i, i + 100),
				triage: 'ignored',
				note,
			})
			ignored += result.updated_count || 0
		} catch {
			// Best-effort Activity drain.
		}
	}
	return { ignored, dryRun: false }
}

async function drainRecordedOutcomes(dryRun: boolean) {
	const records = await listFingerprintsByStatuses(
		['fixed', 'ignored', 'resolved_noise', 'loop_detected'],
		40,
	)
	let updated = 0
	for (const record of records) {
		const soft = softTriageForOutcome(record.outcome || record.status)
		if (!soft) continue
		if (dryRun) {
			updated += record.sample_run_ids.length
			continue
		}
		const applied = await applyTriageToMatchingOpenRuns(
			record,
			soft,
			`kody-issue-triage sweep drain ${record.outcome || record.status}`,
		)
		updated += applied.updated
	}
	return { fingerprints: records.length, updated, dryRun }
}

async function reconcileOpenErrors(dryRun: boolean) {
	const page = await kody.runList({
		status: 'error',
		error_triage: 'open',
		limit: 100,
	})
	const knownRuns = new Map<string, RunSnippet>()
	const executeIgnoreIds: string[] = []
	const selfIgnoreIds: string[] = []
	const weatherIgnoreIds: string[] = []
	const standingIgnoreIds: string[] = []
	let created = 0
	let queued = 0
	let skippedExecute = 0
	let skippedSelf = 0
	let skippedWeather = 0
	for (const run of page.runs || []) {
		const snippet = snippetFromListedRun(run)
		if (snippet.id) knownRuns.set(snippet.id, snippet)
		const skip = skipReason(snippet)
		if (skip === 'agent-execute') {
			skippedExecute++
			if (snippet.id && shouldIgnoreAgentExecuteNoise(snippet)) {
				executeIgnoreIds.push(snippet.id)
			}
			continue
		}
		if (skip === 'self-package' || isSelfPackageRun(snippet)) {
			skippedSelf++
			if (snippet.id) selfIgnoreIds.push(snippet.id)
			continue
		}
		if (skip) continue
		const result = await upsertFingerprintFromRun(snippet)
		const weather = shouldIgnorePlatformWeather(snippet)
		const standing =
			result.nextStatus === 'ignored' || result.nextStatus === 'resolved_noise'
		if (weather || standing) {
			skippedWeather++
			if (weather && !dryRun) {
				await updateFingerprint(result.fingerprint, {
					status: 'resolved_noise',
					outcome: 'resolved_noise',
					classification: 'noise',
					completed_at: new Date().toISOString(),
					summary: PLATFORM_WEATHER_NOTE,
				})
			}
			if (snippet.id) {
				if (weather) weatherIgnoreIds.push(snippet.id)
				else standingIgnoreIds.push(snippet.id)
			}
			continue
		}
		queued++
		if (result.created) created++
	}
	const ignoredExecute = await ignoreOpenRuns(
		executeIgnoreIds,
		AGENT_EXECUTE_NOISE_NOTE,
		dryRun,
	)
	const ignoredSelf = await ignoreOpenRuns(selfIgnoreIds, SELF_PACKAGE_NOTE, dryRun)
	const ignoredWeather = await ignoreOpenRuns(
		weatherIgnoreIds,
		PLATFORM_WEATHER_NOTE,
		dryRun,
	)
	const ignoredStanding = await ignoreOpenRuns(
		standingIgnoreIds,
		'Standing ignored/resolved_noise fingerprint; do not refill Activity.',
		dryRun,
	)
	return {
		scanned: (page.runs || []).length,
		queued,
		created,
		skippedExecute,
		skippedSelf,
		skippedWeather,
		ignoredExecuteNoise: ignoredExecute.ignored,
		ignoredSelfPackage: ignoredSelf.ignored,
		ignoredExecuteTimeouts: ignoredExecute.ignored,
		ignoredPlatformWeather: ignoredWeather.ignored,
		ignoredStandingNoise: ignoredStanding.ignored,
		knownRuns,
	}
}

async function sampleRunForRecord(
	record: FingerprintRecord,
	knownRuns: Map<string, RunSnippet>,
): Promise<RunSnippet | null> {
	const sampleId = record.sample_run_id
	if (!sampleId) return null
	const known = knownRuns.get(sampleId)
	if (known) return known
	try {
		const got = await kody.runGet({ run_id: sampleId })
		const run = got.run
		if (!run) return null
		return snippetFromListedRun(run)
	} catch {
		return null
	}
}

async function closeResolvedNoiseFingerprint(
	record: FingerprintRecord,
	summary: string,
	dryRun: boolean,
) {
	if (!dryRun) {
		await updateFingerprint(record.fingerprint, {
			status: 'resolved_noise',
			outcome: 'resolved_noise',
			classification: 'noise',
			completed_at: new Date().toISOString(),
			summary,
		})
	}
	return {
		ok: true,
		skipped: 'platform-weather' as const,
		fingerprint: record.fingerprint,
		dryRun,
	}
}

async function closeAgentExecuteFingerprint(
	record: FingerprintRecord,
	dryRun: boolean,
) {
	const summary =
		'Skipped spawn: sample run originated from an agent execute call; Activity soft-ignored.'
	if (!dryRun) {
		await updateFingerprint(record.fingerprint, {
			status: SKIPPED_EXECUTE_STATUS,
			summary,
		})
		await applyTriageToMatchingOpenRuns(
			record,
			'ignored',
			AGENT_EXECUTE_NOISE_NOTE,
		)
		const sampleIds = [
			...(record.sample_run_id ? [record.sample_run_id] : []),
			...(record.sample_run_ids || []),
		]
		await ignoreOpenRuns([...new Set(sampleIds)], AGENT_EXECUTE_NOISE_NOTE, false)
	}
	return {
		ok: true,
		skipped: 'agent-execute' as const,
		fingerprint: record.fingerprint,
		dryRun,
	}
}

function canRetriage(record: FingerprintRecord) {
	if (record.status === 'queued' || record.status === 'claimed') return true
	if (record.status === 'spawned') return false
	if (!record.completed_at) return true
	return Date.now() - Date.parse(record.completed_at) >= retriageCooldownMs
}

async function maybeAlert(kind: string, content: string, dryRun: boolean) {
	const key = `alerted:${kind}:${hourBucket()}`
	const already = await getCounter(key)
	if (already > 0) return { sent: false }
	if (dryRun) return { sent: false, dryRun: true }
	await incrementCounter(key)
	const alert = formatKodyIssueReport({
		status: 'queued',
		title: kind === 'cap' ? 'Hourly spawn cap reached' : 'Anomaly breaker paused spawning',
		record: {
			fingerprint: kind,
			surface: 'sweep',
			owner: 'kody-issue-triage',
			count: 0,
			activity_url: null,
			agent_url: null,
			kody_agent_url: null,
			classification: null,
			sample_message: content,
			error_family: kind,
		},
		summary: content,
	})
	await postMessage({
		channelId: discordChannelId,
		content: alert.content,
		flags: alert.flags,
	})
	return { sent: true }
}

async function spawnOne(record: FingerprintRecord, dryRun: boolean) {
	if (!dryRun) {
		const lease = await tryAcquireLease(record.fingerprint, SELF_KODY_ID)
		if (!lease.ok) {
			return { ok: true, queued: true, skipped: 'lease-held', heldBy: lease.heldBy }
		}
	}

	const bucket = hourBucket()
	const spawned = await getCounter(`spawned:${bucket}`)
	if (spawned >= maxAgentsPerHour) {
		await releaseLease(record.fingerprint)
		await maybeAlert(
			'cap',
			`Kody issue triage hourly cap (${maxAgentsPerHour}) reached. Fingerprint \`${record.fingerprint}\` stays queued.`,
			dryRun,
		)
		return { ok: true, skipped: 'hourly-cap', spawned }
	}

	let peerContext: PeerContext = emptyPeerContext()
	try {
		peerContext = await withTimeout(gatherPeerContext(record), 8_000, emptyPeerContext())
	} catch {
		peerContext = emptyPeerContext()
	}
	if (peerContext.highConfidenceLiveSibling) {
		await releaseLease(record.fingerprint)
		return {
			ok: true,
			queued: true,
			skipped: 'live-peer',
			fingerprint: record.fingerprint,
			peerAgentId: peerContext.highConfidenceLiveSibling.id,
			peerAgentUrl: peerContext.highConfidenceLiveSibling.url,
		}
	}

	const inFlightSibling = inFlightSiblingForRecord(peerContext.siblings, record)
	if (inFlightSibling) {
		await releaseLease(record.fingerprint)
		return {
			ok: true,
			queued: true,
			skipped: 'in-flight-sibling',
			fingerprint: record.fingerprint,
			sibling: inFlightSibling.fingerprint,
		}
	}

	const handledSibling = handledSiblingForRecord(peerContext.siblings, record)
	if (handledSibling) {
		const summary = `Same-owner sibling ${handledSibling.fingerprint} already ${handledSibling.outcome || handledSibling.status}; skipped spawn.`
		if (!dryRun) {
			await updateFingerprint(record.fingerprint, {
				status: 'resolved_noise',
				outcome: 'resolved_noise',
				classification: 'noise',
				completed_at: new Date().toISOString(),
				summary,
			})
		}
		await releaseLease(record.fingerprint)
		return {
			ok: true,
			skipped: 'handled-peer',
			fingerprint: record.fingerprint,
			sibling: handledSibling.fingerprint,
			siblingOutcome: handledSibling.outcome,
			dryRun,
		}
	}

	let loopGuard: LoopGuardSnapshot = emptyLoopGuardSnapshot()
	try {
		loopGuard = await withTimeout(
			gatherLoopGuardSnapshot(record),
			8_000,
			emptyLoopGuardSnapshot(),
		)
	} catch {
		loopGuard = emptyLoopGuardSnapshot()
	}

	const fleetRate = isFleetRateFingerprint(record.fingerprint)
	const report = formatKodyIssueReport({
		status: 'queued',
		title: fleetRate
			? 'Fleet package error rate elevated'
			: undefined,
		record,
		summary: record.sample_message
			? truncate(record.sample_message, 240)
			: null,
		extras: ['Waking Patch…'],
	})

	if (dryRun) {
		await releaseLease(record.fingerprint)
		return {
			ok: true,
			dryRun: true,
			fingerprint: record.fingerprint,
			wouldSpawn: true,
			promptChars: buildPatchTriagePrompt({
				record,
				discordMessageId: 'dry-run',
				peerContext,
				loopGuard,
				fleetRate,
			}).length,
			wouldWake: 'patch',
			peerAgents: peerContext.agents.length,
			peerPullRequests: peerContext.pullRequests.length,
			matchingOpenRuns: loopGuard.matchingOpenRunCount,
		}
	}

	// Post/edit Discord first, then persist discord_message_id BEFORE wake.
	// A DO reset between Discord write and storage was leaving orphan Queued
	// cards (no stored id → next hour posted a second card).
	let discordMessageId: string | null = null
	try {
		const postedPromise = postOrEditQueuedCard(
			record,
			report.content,
			report.flags,
		)
		discordMessageId = await withTimeout(postedPromise, 12_000, null)
		if (!discordMessageId) {
			// Timeout must not abandon an in-flight Discord write — await it so
			// a late success is still persisted (never orphan with no stored id).
			discordMessageId = await withTimeout(postedPromise, 20_000, null)
		}
		if (!discordMessageId) {
			await releaseLease(record.fingerprint)
			return {
				ok: false,
				fingerprint: record.fingerprint,
				error: 'discord-post-timeout',
			}
		}
		await updateFingerprint(record.fingerprint, {
			status: 'claimed',
			discord_message_id: discordMessageId,
		})
	} catch (error) {
		if (discordMessageId) {
			try {
				await updateFingerprint(record.fingerprint, {
					status: 'queued',
					discord_message_id: discordMessageId,
				})
			} catch {
				// Persist best-effort; Discord edit below still marks retryable.
			}
			await editCardRetryable(
				record,
				discordMessageId,
				'Spawn interrupted after Discord post — will retry next sweep.',
			)
		}
		await releaseLease(record.fingerprint)
		throw error
	}

	try {
		const prompt = buildPatchTriagePrompt({
			record,
			discordMessageId: discordMessageId || 'unknown',
			openSiblingCount: record.count,
			peerContext,
			loopGuard,
			fleetRate,
		})
		const { ok, timedOut, woke } = await wakePatchForTriage({
			prompt,
			discordChannelId,
			discordMessageId,
		})
		if (!ok) {
			// Keep discord_message_id so the next sweep edits in place.
			await updateFingerprint(record.fingerprint, { status: 'queued' })
			await editCardRetryable(
				record,
				discordMessageId!,
				'Patch wake failed — will retry next sweep.',
			)
			await releaseLease(record.fingerprint)
			return {
				ok: false,
				fingerprint: record.fingerprint,
				error: 'patch-wake-failed',
				wake: woke,
				discordMessageId,
			}
		}
		await incrementCounter(`spawned:${bucket}`)
		const agentUrl = `grok-bot:patch`
		await updateFingerprint(record.fingerprint, {
			status: 'spawned',
			agent_id: PATCH_BOT_ID,
			agent_url: agentUrl,
			spawned_at: new Date().toISOString(),
			discord_message_id: discordMessageId,
		})
		return {
			ok: true,
			fingerprint: record.fingerprint,
			agentId: PATCH_BOT_ID,
			agentUrl,
			wokePatch: true,
			timedOut,
			bot: PATCH_BOT_NAME,
			discordMessageId,
		}
	} catch (error) {
		// Keep discord_message_id; requeue for edit-in-place retry.
		await updateFingerprint(record.fingerprint, { status: 'queued' })
		await editCardRetryable(
			record,
			discordMessageId!,
			'Spawn interrupted — will retry next sweep.',
		)
		await releaseLease(record.fingerprint)
		throw error
	}
}

/**
 * Bounded spawn path. Default: wake Patch for at most one fingerprint. Safe to invoke manually.
 */
export default async function sweep(input: SweepInput = {}) {
	const dryRun = input.dryRun === true
	const limit = Math.min(Math.max(input.limit ?? 1, 1), 3)
	const reconcile = input.reconcile !== false

	await initSchema()
	if (!(await isEnabled())) {
		return { ok: true, skipped: 'paused' }
	}

	const recovered = []
	if (!dryRun) {
		for (const stale of await listStaleSpawned()) {
			const summary =
				'Sweeper marked failed: Patch never recorded an outcome within the stale-spawn window.'
			await updateFingerprint(stale.fingerprint, {
				status: 'failed',
				outcome: 'failed',
				completed_at: new Date().toISOString(),
				summary,
			})
			await releaseLease(stale.fingerprint)
			recovered.push(stale.fingerprint)
		}
	}

	// Wake Patch first. Reconcile/drain can burn the whole execute budget
	// soft-triaging Activity rows, which previously timed out before spawn.
	const bucket = hourBucket()
	const fingerprintsThisHour = await getCounter(`fingerprints:${bucket}`)
	const eventsThisHour = await getCounter(`events:${bucket}`)
	if (
		fingerprintsThisHour > fingerprintAnomalyThreshold ||
		eventsThisHour > eventAnomalyThreshold
	) {
		await maybeAlert(
			'anomaly',
			`Kody issue triage paused this hour (anomaly): ${fingerprintsThisHour} new fingerprints, ${eventsThisHour} events. Caps are ${fingerprintAnomalyThreshold} / ${eventAnomalyThreshold}.`,
			dryRun,
		)
		return {
			ok: true,
			skipped: 'anomaly-breaker',
			fingerprintsThisHour,
			eventsThisHour,
			recovered,
		}
	}

	// Include claimed: DO reset after persist-but-before-wake left fingerprints
	// claimed with a stored discord_message_id that queued-only sweeps skipped,
	// while a missing id would post a duplicate Queued card. Edit-existing-first
	// reuses the card when the id is present.
	const queued = (
		await listFingerprintsByStatuses(['queued', 'claimed'], 20)
	).sort((a, b) => String(a.first_seen).localeCompare(String(b.first_seen)))
	const actions: Array<Record<string, unknown>> = []
	let spawnedCount = 0
	for (const record of queued.filter(canRetriage)) {
		if (spawnedCount >= limit) break
		const sampleRun = await sampleRunForRecord(record, new Map())
		const executeSkip = spawnSkipReasonForRecord(record, sampleRun)
		if (executeSkip) {
			actions.push(await closeAgentExecuteFingerprint(record, dryRun))
			continue
		}
		if (sampleRun && shouldIgnorePlatformWeather(sampleRun)) {
			actions.push(
				await closeResolvedNoiseFingerprint(record, PLATFORM_WEATHER_NOTE, dryRun),
			)
			continue
		}
		try {
			const action = await spawnOne(record, dryRun)
			actions.push(action)
			if (action.skipped === 'hourly-cap') break
			if (action.agentId || action.wouldSpawn) spawnedCount++
		} catch (error) {
			actions.push({
				ok: false,
				fingerprint: record.fingerprint,
				error: String((error as Error)?.message || error),
			})
		}
	}

	let reconcileResult: {
		scanned?: number
		queued?: number
		created?: number
		skippedExecute?: number
		ignoredExecuteTimeouts?: number
		error?: string
		timedOut?: boolean
	} | null = null
	if (reconcile) {
		try {
			const reconciled = await withTimeout(
				reconcileOpenErrors(dryRun),
				20_000,
				{
					scanned: 0,
					queued: 0,
					created: 0,
					skippedExecute: 0,
					skippedSelf: 0,
					skippedWeather: 0,
					ignoredExecuteNoise: 0,
					ignoredSelfPackage: 0,
					ignoredExecuteTimeouts: 0,
					ignoredPlatformWeather: 0,
					ignoredStandingNoise: 0,
					knownRuns: new Map(),
				},
			)
			reconcileResult = {
				scanned: reconciled.scanned,
				queued: reconciled.queued,
				created: reconciled.created,
				skippedExecute: reconciled.skippedExecute,
				ignoredExecuteTimeouts: reconciled.ignoredExecuteTimeouts,
				timedOut: reconciled.scanned === 0 && reconciled.queued === 0,
			}
		} catch (error) {
			reconcileResult = { error: String((error as Error)?.message || error) }
		}
	}

	let drainResult: {
		fingerprints?: number
		updated?: number
		error?: string
		timedOut?: boolean
	} | null = null
	try {
		drainResult = await withTimeout(
			drainRecordedOutcomes(dryRun),
			20_000,
			{ fingerprints: 0, updated: 0, dryRun, timedOut: true },
		)
	} catch (error) {
		drainResult = { error: String((error as Error)?.message || error) }
	}

	return {
		ok: true,
		dryRun,
		spawned: spawnedCount,
		queued: queued.length,
		actions,
		reconcile: reconcileResult,
		drain: drainResult,
		recovered,
		fingerprintsThisHour,
		eventsThisHour,
	}
}