Skip to content
← Public packages

@kentcdodds/sentry-triage

Sentry triage wakes Cole (Grok Bot) per issue: one active wake per repo (lease + queue), loop-safe Discord. Cole may spawn Cursor for isolated repo work.

src/awaiting-seer.ts

56 lines · 1.9 KB · TypeScript
import { seerGraceMs, seerPrGraceMs } from './shared.ts'
import { listStorageEntries, recordFromListEntry } from './list-storage.ts'

export function awaitingSeerGraceMs(status) {
	if (status === 'awaiting-seer') return seerGraceMs
	if (status === 'awaiting-seer-pr') return seerPrGraceMs
	return null
}

export function awaitingSeerSinceMs(record) {
	return (
		Date.parse(
			record?.seerPrAwaitedAt ??
				record?.seerRequestedAt ??
				record?.seenAt ??
				'',
		) || 0
	)
}

/**
 * Whether a stored issue record is stuck awaiting Seer past its grace
 * window. Automation-owned Seer runs produce no later webhook when Sentry
 * declines an issue, so this is the routing signal for from-scratch rescue.
 */
export function isAwaitingSeerStale(record, now = Date.now()) {
	const graceMs = awaitingSeerGraceMs(record?.status)
	if (graceMs == null) return false
	if (!record?.issueId || !record.projectSlug) return false
	return now - awaitingSeerSinceMs(record) > graceMs
}

/**
 * Find issue records stuck awaiting Seer past their grace window, oldest
 * first. Automation-owned Seer runs produce no later webhook when Sentry
 * declines an issue, so rescue cannot wait on that issue's own delivery.
 * Drivers: the inbound-issue sweeper (opportunistic), `./sweep-stale-awaiting`
 * every 15m (quiet-period), and daily `./reconcile` (completeness).
 *
 * Uses paginated packageStorage.list — a single page silently misses newer
 * `issue:` keys once the store grows past the default page size.
 */
export async function findStaleAwaitingIssues(
	storage,
	limit = 2,
	now = Date.now(),
) {
	const rawEntries = await listStorageEntries(storage, { prefix: 'issue:' })
	const stale = []
	for (const entry of rawEntries) {
		const record = await recordFromListEntry(storage, entry)
		if (isAwaitingSeerStale(record, now)) stale.push(record)
	}
	stale.sort((left, right) => awaitingSeerSinceMs(left) - awaitingSeerSinceMs(right))
	return stale.slice(0, Math.max(0, limit))
}