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
34 changes: 34 additions & 0 deletions layers/identity/server/utils/daily-health-check.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function summary(overrides: Partial<DailyHealthCheckSummary> = {}): DailyHealthC
discoveryCandidatesOverdue: 0,
discoveryClaimsStale: 0,
leaderboardApprovalsStuck: 0,
leaderboardApprovalDetails: [],
failedJobDetails: [],
},
cost: {
Expand Down Expand Up @@ -226,6 +227,21 @@ describe('evaluateDailyHealthStatus', () => {
})
})

it('names the invisible leaderboard repositories in the reason', () => {
const input = summary({
pipeline: {
...summary().pipeline,
leaderboardApprovalsStuck: 1,
leaderboardApprovalDetails: [{ owner: 'missing-owner', repo: 'missing-repo', reviewedAt: 1_774_473_000 }],
},
})

expect(evaluateDailyHealthStatus(input)).toEqual({
status: 'AMBER',
reasons: ['1 reviewed leaderboard repository remained invisible for over 15 minutes: missing-owner/missing-repo.'],
})
})

it('keeps unengaged source removal informational and flags user impact amber', () => {
const informational = summary({
pipeline: {
Expand Down Expand Up @@ -468,6 +484,9 @@ describe('buildDailyHealthCheck', () => {
expect(uncertain.activity.digestsFailed24h).toBe(1)
expect(uncertain.pipeline.discoveryCandidatesExhausted).toBe(1)
expect(uncertain.pipeline.leaderboardApprovalsStuck).toBe(1)
expect(uncertain.pipeline.leaderboardApprovalDetails).toEqual([
{ owner: 'missing-owner', repo: 'missing-repo', reviewedAt: nowSec - 901 },
])
expect(uncertain.status).toBe('RED')
sqlite.close()
})
Expand Down Expand Up @@ -518,6 +537,21 @@ describe('daily health rendering', () => {
expect(renderDailyHealthCheckHtml(input)).toContain('Pipeline &lt;degraded&gt;')
expect(renderDailyHealthCheckHtml(input)).not.toContain('<script>')
})

it('renders the invisible leaderboard repositories with their review verdict', () => {
const input = summary({
pipeline: {
...summary().pipeline,
leaderboardApprovalsStuck: 1,
leaderboardApprovalDetails: [{ owner: 'missing-owner', repo: 'missing-repo', reviewedAt: 1_774_473_000 }],
},
})

const text = renderDailyHealthCheckText(input)
const html = renderDailyHealthCheckHtml(input)
expect(text).toContain('missing-owner/missing-repo: eligible review 2026-03-25T21:10:00Z')
expect(html).toContain('missing-owner/missing-repo: eligible review 2026-03-25T21:10:00Z')
})
})

describe('sendDailyHealthCheck', () => {
Expand Down
95 changes: 75 additions & 20 deletions layers/identity/server/utils/daily-health-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ export interface DailyHealthCheckSummary {
discoveryCandidatesOverdue: number
discoveryClaimsStale: number
leaderboardApprovalsStuck: number
/** The invisible repositories behind the count, so the report names them. */
leaderboardApprovalDetails: Array<{
owner: string
repo: string
reviewedAt: number
}>
failedJobDetails: Array<{
queue: string
jobType: string
Expand Down Expand Up @@ -247,6 +253,12 @@ interface FailedJobRow {
count: number
}

interface LeaderboardApprovalRow {
owner: string
repo: string
reviewed_at: number
}

interface CostRow {
estimated_ai_usd_24h: number
estimated_ai_usd_month: number
Expand Down Expand Up @@ -611,7 +623,13 @@ export function evaluateDailyHealthStatus(
if (summary.pipeline.discoveryClaimsStale > 0)
amber.push(`${plural(summary.pipeline.discoveryClaimsStale, 'discovery claim')} remained active for over 1 hour.`)
if (summary.pipeline.leaderboardApprovalsStuck > 0) {
amber.push(`${summary.pipeline.leaderboardApprovalsStuck} reviewed leaderboard ${summary.pipeline.leaderboardApprovalsStuck === 1 ? 'repository' : 'repositories'} remained invisible for over 15 minutes.`)
// The count alone made two consecutive reports unactionable: the operator
// could not tell which repository to look at. The names come from the same
// stuck-row query, so they always match the count.
const named = summary.pipeline.leaderboardApprovalDetails
.map(approval => `${approval.owner}/${approval.repo}`)
.join(', ')
amber.push(`${summary.pipeline.leaderboardApprovalsStuck} reviewed leaderboard ${summary.pipeline.leaderboardApprovalsStuck === 1 ? 'repository' : 'repositories'} remained invisible for over 15 minutes${named ? `: ${named}.` : '.'}`)
}
if (summary.warnings.length > 0)
amber.push(`${plural(summary.warnings.length, 'report probe')} failed.`)
Expand Down Expand Up @@ -887,8 +905,38 @@ async function loadWeeklyRun(db: D1Database): Promise<DailyHealthCheckSummary['a
}
}

/**
* One reviewed leaderboard approval that has not become visible yet: the gate
* still says eligible, the owner is a user (the leaderboard scope), and no
* healthy skill row exists for the repository.
*
* Shared by the count and the named-rows query so the number and the names in
* the report cannot drift apart. `param` is the placeholder index for the
* visibility deadline, which each statement binds at its own position.
*/
function leaderboardStuckSql(param: number): string {
return `review.status = 'eligible'
AND review.reviewed_at < ?${param}
AND EXISTS (
SELECT 1
FROM owners AS owner
WHERE owner.owner = review.owner
AND owner.kind = 'user'
)
AND NOT EXISTS (
SELECT 1
FROM repos AS r
JOIN skills AS s
ON s.owner = r.owner
AND s.repo = r.repo
WHERE r.owner = review.owner
AND r.repo = review.repo
AND r.broken_since IS NULL
)`
}

async function loadPipeline(db: D1Database, nowSec: number, sinceSec: number): Promise<DailyHealthCheckSummary['pipeline']> {
const [row, jobRows, scheduledRunRows, failedJobRows] = await Promise.all([
const [row, jobRows, scheduledRunRows, failedJobRows, leaderboardApprovalRows] = await Promise.all([
first<PipelineRow>(db, `
SELECT
(SELECT COUNT(*) FROM repos WHERE broken_since >= ?1) AS newly_broken_repos_total_24h,
Expand Down Expand Up @@ -952,24 +1000,7 @@ async function loadPipeline(db: D1Database, nowSec: number, sinceSec: number): P
WHERE retry_state = 'claimed' AND claimed_at < ?6) AS discovery_claims_stale,
(SELECT COUNT(*)
FROM skill_repo_eligibility AS review
WHERE review.status = 'eligible'
AND review.reviewed_at < ?7
AND EXISTS (
SELECT 1
FROM owners AS owner
WHERE owner.owner = review.owner
AND owner.kind = 'user'
)
AND NOT EXISTS (
SELECT 1
FROM repos AS r
JOIN skills AS s
ON s.owner = r.owner
AND s.repo = r.repo
WHERE r.owner = review.owner
AND r.repo = review.repo
AND r.broken_since IS NULL
)) AS leaderboard_approvals_stuck
WHERE ${leaderboardStuckSql(7)}) AS leaderboard_approvals_stuck
`, [
sinceSec,
nowSec - DIRTY_STUCK_SECONDS,
Expand All @@ -995,6 +1026,15 @@ async function loadPipeline(db: D1Database, nowSec: number, sinceSec: number): P
ORDER BY count DESC
LIMIT 8
`, [sinceSec]),
// The repositories behind the count, oldest review first, so the nightly
// email names what to look at instead of only how many.
all<LeaderboardApprovalRow>(db, `
SELECT review.owner, review.repo, review.reviewed_at
FROM skill_repo_eligibility AS review
WHERE ${leaderboardStuckSql(1)}
ORDER BY review.reviewed_at
LIMIT 8
`, [nowSec - RESERVED_STUCK_SECONDS]),
])

const scheduledRun = (slot: ScheduledRunRow['slot']): Map<string, ScheduledRunRow> =>
Expand Down Expand Up @@ -1040,6 +1080,11 @@ async function loadPipeline(db: D1Database, nowSec: number, sinceSec: number): P
discoveryCandidatesOverdue: numberValue(row.discovery_candidates_overdue),
discoveryClaimsStale: numberValue(row.discovery_claims_stale),
leaderboardApprovalsStuck: numberValue(row.leaderboard_approvals_stuck),
leaderboardApprovalDetails: leaderboardApprovalRows.map(approval => ({
owner: approval.owner,
repo: approval.repo,
reviewedAt: numberValue(approval.reviewed_at),
})),
failedJobDetails: failedJobRows.map(failure => ({
queue: failure.queue,
jobType: failure.job_type,
Expand Down Expand Up @@ -1194,6 +1239,7 @@ export async function buildDailyHealthCheck(
discoveryCandidatesOverdue: 0,
discoveryClaimsStale: 0,
leaderboardApprovalsStuck: 0,
leaderboardApprovalDetails: [],
failedJobDetails: [],
}, () => loadPipeline(db, Math.floor(now.getTime() / 1000), sinceSec))
const cost = await capture(warnings, 'AI cost', {
Expand Down Expand Up @@ -1270,6 +1316,11 @@ function weeklyLine(summary: DailyHealthCheckSummary): string {
return `${weekly.sent} sent, ${weekly.skipped} skipped, ${weekly.failed} failed, ${weekly.uncertain} unresolved (${date})`
}

function leaderboardApprovalLine(approval: DailyHealthCheckSummary['pipeline']['leaderboardApprovalDetails'][number]): string {
const reviewedAt = new Date(approval.reviewedAt * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z')
return `${approval.owner}/${approval.repo}: eligible review ${reviewedAt}`
}

export function renderDailyHealthCheckText(summary: DailyHealthCheckSummary): string {
const failedJobs = summary.pipeline.failedJobDetails.map(item => `${item.queue}/${item.jobType}: ${item.count}, ${item.exception}`)
const unhealthyTasks = summary.pipeline.syncJobs
Expand Down Expand Up @@ -1315,6 +1366,9 @@ export function renderDailyHealthCheckText(summary: DailyHealthCheckSummary): st
`- jobs: ${summary.pipeline.failedJobs24h} failed in 24 hours, ${summary.pipeline.rejectedJobs24h} rejected, ${summary.pipeline.staleReservedJobs} stale reserved, ${summary.pipeline.openFailedBatches} open failed batches`,
`- discovery: ${summary.pipeline.discoveryCandidatesExhausted} exhausted, ${summary.pipeline.discoveryCandidatesOverdue} overdue retries, ${summary.pipeline.discoveryClaimsStale} stale claims`,
`- leaderboard: ${summary.pipeline.leaderboardApprovalsStuck} reviewed approvals invisible over 15 minutes`,
...(summary.pipeline.leaderboardApprovalDetails.length
? [plainList(summary.pipeline.leaderboardApprovalDetails.map(leaderboardApprovalLine))]
: []),
'',
'Scheduled task issues:',
plainList([...unhealthyTasks, ...unhealthyRuns]),
Expand Down Expand Up @@ -1423,6 +1477,7 @@ export function renderDailyHealthCheckHtml(summary: DailyHealthCheckSummary): st
<tr><td colspan="2" style="padding:8px"><table role="presentation" style="width:100%;background:#fff;border:1px solid #e4e7ec;border-radius:10px"><tr><td style="padding:14px 18px">
<div style="font-size:14px;font-weight:700;margin-bottom:6px">Scheduled task issues</div><ul style="margin:0 0 14px;padding-left:18px;font-size:13px;line-height:1.55">${htmlList([...unhealthyTasks, ...unhealthyRuns])}</ul>
<div style="font-size:14px;font-weight:700;margin-bottom:6px">Failed job fingerprints</div><ul style="margin:0 0 14px;padding-left:18px;font-size:13px;line-height:1.55">${htmlList(failedJobs)}</ul>
${summary.pipeline.leaderboardApprovalDetails.length ? `<div style="font-size:14px;font-weight:700;margin-bottom:6px">Leaderboard approvals invisible</div><ul style="margin:0 0 14px;padding-left:18px;font-size:13px;line-height:1.55">${htmlList(summary.pipeline.leaderboardApprovalDetails.map(leaderboardApprovalLine))}</ul>` : ''}
${summary.warnings.length ? `<div style="font-size:14px;font-weight:700;margin-bottom:6px">Report warnings</div><ul style="margin:0;padding-left:18px;font-size:13px;line-height:1.55">${htmlList(summary.warnings)}</ul>` : ''}
</td></tr></table></td></tr>
</table>
Expand Down
12 changes: 12 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ minimumReleaseAgeExclude:
- '@sentry/server-utils@10.73.0'
- '@sentry/vue@10.73.0'
- import-in-the-middle@3.4.0
# The published-cli-grammar check runs `pnpm dlx skilld@<fresh release>` on the
# main push, always inside the release-age window. Exempt the CLI and its
# platform binaries or the check can never pass after a CLI publish.
- skilld
- skilld-cli-darwin-arm64
- skilld-cli-darwin-x64
- skilld-cli-linux-arm64-gnu
- skilld-cli-linux-arm64-musl
- skilld-cli-linux-x64-gnu
- skilld-cli-linux-x64-musl
- skilld-cli-win32-arm64-msvc
- skilld-cli-win32-x64-msvc
shellEmulator: true
autoInstallPeers: false

Expand Down
1 change: 1 addition & 0 deletions test/unit/daily-health-check-x-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function healthy(
discoveryCandidatesOverdue: 0,
discoveryClaimsStale: 0,
leaderboardApprovalsStuck: 0,
leaderboardApprovalDetails: [],
failedJobDetails: [],
},
cost: { estimatedAiUsd24h: 0, estimatedAiUsdMonth: 0 },
Expand Down
Loading