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/peer-match.ts

290 lines · 8.1 KB · TypeScript
import {
	IN_FLIGHT_STATUSES,
	SKIP_SPAWN_OUTCOMES,
	coarseErrorFamily,
	matchFamiliesOverlap,
	sanitizeForPrompt,
	truncate,
	type FingerprintRecord,
} from './shared.ts'

export type PeerAgent = {
	id: string
	name: string
	status: string
	url: string
	createdAt: string | null
	why: string
}

export type PeerPullRequest = {
	repo: string
	number: number | null
	title: string
	url: string
	draft: boolean
	updatedAt: string | null
	agentId: string | null
	why: string
}

export type PeerPublish = {
	kodyId: string | null
	packageId: string | null
	name: string
	status: string
	startedAt: string | null
	url: string | null
	why: string
}

export type PeerSibling = {
	fingerprint: string
	owner: string
	error_family: string
	sample_message?: string | null
	status: string
	outcome: string | null
	completedAt: string | null
	agentUrl: string | null
	summary: string | null
	why: string
}

export type PeerContext = {
	agents: PeerAgent[]
	pullRequests: PeerPullRequest[]
	publishes: PeerPublish[]
	siblings: PeerSibling[]
	highConfidenceLiveSibling: PeerAgent | null
}

export const PEER_AGENT_LIMIT = 5
export const PEER_PR_LIMIT = 5
export const PEER_PUBLISH_LIMIT = 3
export const PEER_SIBLING_LIMIT = 5
/** Cursor listAgents often reports finished agents as ACTIVE. */
export const PEER_LIVE_CREATED_WITHIN_MS = 2 * 60 * 60 * 1000

const LIVE_STATUSES = new Set([
	'RUNNING',
	'ACTIVE',
	'CREATING',
	'WAITING',
	'WAITING_FOR_BACKGROUND_WORK',
	'NOT_YET_STARTED',
])

const RECENT_STATUSES = new Set([...LIVE_STATUSES, 'IDLE', 'FINISHED', 'COMPLETED'])

const ALLOWED_PR_REPOS = new Set(['kentcdodds/kody', 'kentcdodds/use-kody'])

export function recordMatchNeedles(
	record: Pick<FingerprintRecord, 'owner' | 'kody_id' | 'error_family' | 'package_id'>,
) {
	const coarse = coarseErrorFamily(record.error_family)
	return [
		coarse,
		record.error_family,
		record.owner,
		record.kody_id,
		record.package_id,
	]
		.filter((value): value is string => Boolean(value && String(value).length >= 4))
		.map((value) => value.toLowerCase())
}

export function textMatchesRecord(
	text: string,
	record: Pick<FingerprintRecord, 'owner' | 'kody_id' | 'error_family' | 'package_id'>,
) {
	const hay = String(text || '').toLowerCase()
	if (!hay) return false
	return recordMatchNeedles(record).some((needle) => hay.includes(needle))
}

export function isTriageAgentName(name: string) {
	const value = String(name || '').toLowerCase()
	return value.startsWith('kody-issue') || value.startsWith('kody-platform')
}

export function normalizeAgentStatus(status: string) {
	return String(status || '')
		.toUpperCase()
		.replace(/[\s-]+/g, '_')
}

export function isLiveAgentStatus(status: string) {
	return LIVE_STATUSES.has(normalizeAgentStatus(status))
}

export function agentCreatedRecently(
	createdAt: string | null | undefined,
	now = Date.now(),
) {
	if (!createdAt) return false
	const created = Date.parse(createdAt)
	if (Number.isNaN(created)) return false
	return now - created <= PEER_LIVE_CREATED_WITHIN_MS
}

export function isPeerCandidateStatus(status: string) {
	return RECENT_STATUSES.has(normalizeAgentStatus(status))
}

export function agentUrlFrom(agent: { id?: string; url?: string } | null) {
	if (!agent?.id && !agent?.url) return ''
	return agent.url || (agent.id ? `https://cursor.com/agents/${agent.id}` : '')
}

export function agentMatchesRecord(
	agent: { name?: string; status?: string },
	record: Pick<FingerprintRecord, 'owner' | 'kody_id' | 'error_family' | 'package_id'>,
) {
	const name = String(agent.name || '')
	if (!isTriageAgentName(name)) return false
	if (!isPeerCandidateStatus(String(agent.status || ''))) return false
	return textMatchesRecord(name, record)
}

export function liveSiblingForRecord(
	agents: Array<PeerAgent>,
	record: Pick<FingerprintRecord, 'owner' | 'kody_id' | 'error_family' | 'package_id'>,
	now = Date.now(),
) {
	return (
		agents.find(
			(agent) =>
				agentCreatedRecently(agent.createdAt, now) &&
				isTriageAgentName(agent.name) &&
				textMatchesRecord(agent.name, record),
		) ?? null
	)
}

export function repoSlugFromUrl(url: string) {
	const match = String(url || '').match(/github\.com\/([^/]+\/[^/#?]+)/i)
	return match ? match[1].replace(/\.git$/, '') : ''
}

export function prMatchesRecord(
	pr: { title?: string; repo?: string; url?: string; headRef?: string },
	record: Pick<FingerprintRecord, 'owner' | 'kody_id' | 'error_family' | 'package_id'>,
	options: { linkedToMatchedAgent?: boolean } = {},
) {
	const repo = (pr.repo || repoSlugFromUrl(String(pr.url || ''))).toLowerCase()
	const hay = [pr.title, pr.headRef, repo, pr.url].filter(Boolean).join(' ')
	if (options.linkedToMatchedAgent && repo) return true
	if (ALLOWED_PR_REPOS.has(repo) && textMatchesRecord(hay, record)) return true
	return textMatchesRecord(hay, record) && Boolean(repo)
}

export function siblingMatchesRecord(
	sibling: Pick<
		FingerprintRecord,
		'fingerprint' | 'owner' | 'error_family' | 'kody_id' | 'sample_message'
	>,
	record: Pick<
		FingerprintRecord,
		'fingerprint' | 'owner' | 'error_family' | 'kody_id' | 'sample_message'
	>,
) {
	if (sibling.fingerprint === record.fingerprint) return false
	return (
		sibling.owner === record.owner ||
		sibling.error_family === record.error_family ||
		matchFamiliesOverlap(sibling, record) ||
		Boolean(record.kody_id && sibling.kody_id === record.kody_id)
	)
}

export function siblingWhy(
	sibling: Pick<FingerprintRecord, 'owner' | 'error_family'>,
	record: Pick<FingerprintRecord, 'owner' | 'error_family'>,
) {
	if (
		coarseErrorFamily(sibling.error_family) ===
		coarseErrorFamily(record.error_family)
	) {
		return sibling.error_family === record.error_family
			? 'same error family'
			: 'same coarse family'
	}
	return 'same owner'
}

export function handledSiblingForRecord(
	siblings: PeerSibling[],
	record: Pick<FingerprintRecord, 'owner' | 'error_family' | 'sample_message'>,
) {
	return (
		siblings.find((sibling) => {
			const outcome = sibling.outcome || sibling.status
			if (!SKIP_SPAWN_OUTCOMES.has(outcome)) return false
			if (sibling.owner !== record.owner) return false
			return matchFamiliesOverlap(sibling, record)
		}) ?? null
	)
}

export function inFlightSiblingForRecord(
	siblings: PeerSibling[],
	record: Pick<
		FingerprintRecord,
		'owner' | 'error_family' | 'fingerprint' | 'sample_message'
	>,
) {
	return (
		siblings.find((sibling) => {
			if (sibling.fingerprint === record.fingerprint) return false
			if (!IN_FLIGHT_STATUSES.has(sibling.status)) return false
			if (sibling.owner !== record.owner) return false
			return matchFamiliesOverlap(sibling, record)
		}) ?? null
	)
}

export function emptyPeerContext(): PeerContext {
	return {
		agents: [],
		pullRequests: [],
		publishes: [],
		siblings: [],
		highConfidenceLiveSibling: null,
	}
}

function line(label: string, items: Array<string>) {
	if (!items.length) return `- ${label}: none found`
	return [`- ${label}:`, ...items.map((item) => `  - ${item}`)].join('\n')
}

export function formatPeerContextForPrompt(context: PeerContext) {
	const agents = context.agents.slice(0, PEER_AGENT_LIMIT).map((agent) =>
		sanitizeForPrompt(
			`${agent.status} ${truncate(agent.name, 80)} · ${agent.id} · ${agent.url} · ${agent.why}`,
		),
	)
	const prs = context.pullRequests.slice(0, PEER_PR_LIMIT).map((pr) =>
		sanitizeForPrompt(
			`${pr.repo}${pr.number != null ? `#${pr.number}` : ''} ${truncate(pr.title, 80)} · ${pr.url}${pr.draft ? ' · draft' : ''}${pr.agentId ? ` · agent ${pr.agentId}` : ''} · ${pr.why}`,
		),
	)
	const publishes = context.publishes.slice(0, PEER_PUBLISH_LIMIT).map((item) =>
		sanitizeForPrompt(
			`${item.status} ${truncate(item.name, 80)} · ${item.kodyId || item.packageId || 'unknown'} · ${item.startedAt || 'unknown time'} · ${item.why}`,
		),
	)
	const siblings = context.siblings.slice(0, PEER_SIBLING_LIMIT).map((item) =>
		sanitizeForPrompt(
			`${item.fingerprint} · ${item.owner} · ${item.status}${item.outcome ? `/${item.outcome}` : ''} · ${item.agentUrl || 'no agent'} · ${truncate(item.summary || item.why, 120)}`,
		),
	)

	return [
		line('Peer agents', agents),
		line('Related pull requests', prs),
		line('Owner package activity', publishes),
		line('Stored sibling fingerprints', siblings),
	].join('\n')
}