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
4 changes: 2 additions & 2 deletions layers/artifact-delivery/server/tasks/sweep-skill-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { runSkillRunSweep } from '../utils/run-sweep'

const CRON = '7,22,37,52 * * * *'
/**
* At most 40 checks every 15 minutes. A Skill is checked again after a push to
* its repository, or after a backstop (`RECHECK_AFTER_SECONDS`), so most runs
* At most 40 checks every 15 minutes. A Skill is checked again after a file in
* its folder changes, or after a backstop (`RECHECK_AFTER_SECONDS`), so most runs
* start no checks. A settled check deletes the Resolution of the one it replaced.
* Each check still writes a Resolution with about 60 KB of attestations, even
* when it reuses a ready build. Checking every Skill hourly filled the 10 GB
Expand Down
14 changes: 7 additions & 7 deletions layers/artifact-delivery/server/utils/run-check-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ export function runCheckColumns(state: RunCheckState): RunCheckColumns {
}
}

/** How long a settled check stands when its repository gets no push. */
/** How long a settled check stands when its Skill folder does not change. */
export interface RecheckAfterSeconds {
ready: number
failing: number
}

/**
* Backstops for a repository that gets no push. A check of an unchanged
* repository still writes a Resolution with about 60 KB of attestations, and
* Backstops for a Skill folder that does not change. A check of an unchanged
* Skill still writes a Resolution with about 60 KB of attestations, and
* checking every Skill hourly filled the 10 GB database in November 2026. The
* backstop catches a change in the checks themselves.
*/
Expand All @@ -135,18 +135,18 @@ export const RECHECK_AFTER_SECONDS: RecheckAfterSeconds = {
export interface RecheckInput {
state: RunCheckState
settledAt: number | null
/** The repository revision the last check ran at. */
/** The Skill folder revision the last check ran at. */
checkedRevision: string | null
/** The repository revision the registry holds now. */
/** The Skill folder revision the registry holds now. */
revision: string | null
}

/**
* Whether the sweep should check a Skill again now.
*
* An unchecked Skill and a transient failure, such as a spent quota, are due
* at once: neither says anything about the Skill yet. Otherwise a push to the
* repository makes the Skill due, and the backstop covers the rest.
* at once: neither says anything about the Skill yet. Otherwise a change to the
* Skill folder makes the Skill due, and the backstop covers the rest.
*/
export function recheckDue(input: RecheckInput, now: number, after: RecheckAfterSeconds): boolean {
const { state, settledAt } = input
Expand Down
2 changes: 1 addition & 1 deletion layers/artifact-delivery/server/utils/run-sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface RunnableSkill {
owner: string
repository: string
name: string
/** Changes whenever the repository gets a push. Null or absent when unknown. */
/** Git tree SHA of the Skill folder. Null or absent when unknown. */
revision?: string | null
}

Expand Down
11 changes: 3 additions & 8 deletions layers/registry/server/presenters/runnable-skills.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
export interface RunnableSkillsDto {
/** `revision` changes whenever the repository gets a push. Null when sync has not read it. */
/** `revision` is the Skill folder's Git tree SHA. It changes only when a file in that folder changes. Null when sync has not read it. */
items: Array<{ owner: string, repository: string, name: string, revision: string | null }>
}

export function presentRunnableSkills(rows: Array<{ owner: string, repo: string, name: string, pushedAt: number | null }>): RunnableSkillsDto {
export function presentRunnableSkills(rows: Array<{ owner: string, repo: string, name: string, revision: string | null }>): RunnableSkillsDto {
return {
items: rows.map(row => ({
owner: row.owner,
repository: row.repo,
name: row.name,
revision: row.pushedAt === null ? null : String(row.pushedAt),
})),
items: rows.map(row => ({ owner: row.owner, repository: row.repo, name: row.name, revision: row.revision })),
}
}
11 changes: 7 additions & 4 deletions layers/registry/server/utils/skills-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,8 +742,8 @@ export interface SkillSitemapEntry {
repo: string
/** Resolved-Skill total for the repo; 1 means the hub URL is canonical. */
repoSkillCount: number
/** Unix seconds of the repository's last push, as the last sync read it. */
pushedAt: number | null
/** Git tree SHA of the Skill folder, as the last sync read it. Null until a sync records it. */
revision: string | null
}

interface SkillDuplicateRow extends DuplicateCandidate {
Expand All @@ -754,6 +754,8 @@ interface SkillDuplicateRow extends DuplicateCandidate {
// loadRepoSkillCounts in shared/server/trending-skills.ts). Drives the
// single-Skill repo hub routing so links never point at a URL that 301s.
repo_skill_count: number
/** Only the sitemap query selects it. */
current_sha?: string | null
}

export interface SkillDuplicateSibling {
Expand Down Expand Up @@ -901,7 +903,7 @@ function findDuplicateGroupInRows(rows: SkillDuplicateRow[], slug: string): Skil
export function listAllSkillsForSitemap(event: H3Event): Promise<SkillSitemapEntry[]> {
return cached({
storage: useStorage('cache'),
key: 'skills:sitemap-all:v6',
key: 'skills:sitemap-all:v7',
ttlSeconds: DUPLICATE_CANDIDATES_TTL,
staleSeconds: DUPLICATE_CANDIDATES_STALE_TTL,
compute: () => queryAllSkillsForSitemap(getDB(event)),
Expand All @@ -919,6 +921,7 @@ export async function queryAllSkillsForSitemap(db: D1Database): Promise<SkillSit
s.display_name,
s.description,
s.rendered_raw_sha256,
s.current_sha,
r.stars,
r.pushed_at,
supported_repos.support_tier,
Expand Down Expand Up @@ -948,7 +951,7 @@ export async function queryAllSkillsForSitemap(db: D1Database): Promise<SkillSit
const weakerSlugs = duplicateWeakerSlugSet(rows)
const entries = rows
.filter(row => row.repo_kind !== 'aggregator' && !weakerSlugs.has(skillSlug(row)))
.map(row => ({ name: row.name, owner: row.owner, repo: row.repo, repoSkillCount: row.repo_skill_count ?? 0, pushedAt: row.pushed_at ?? null }))
.map(row => ({ name: row.name, owner: row.owner, repo: row.repo, repoSkillCount: row.repo_skill_count ?? 0, revision: row.current_sha ?? null }))
return entries
}

Expand Down
12 changes: 12 additions & 0 deletions test/unit/trending-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ describe('sitemap equals the indexable set', () => {
})
})

describe('runnable Skill revision', () => {
it('reports the Skill folder tree SHA the last sync stored', async () => {
addSkill({ owner: 'o1', repo: 'r1', name: 'scored' })
db().raw.prepare(`UPDATE skills SET current_sha = 'tree-abc' WHERE name = 'scored'`).run()
db().raw.prepare('DELETE FROM skill_trending_admissions').run()

const [entry] = await queryAllSkillsForSitemap(db().db)

expect(entry).toMatchObject({ owner: 'o1', repo: 'r1', name: 'scored', revision: 'tree-abc' })
})
})

describe('empty admitted set', () => {
it('lists the sitemap by the old decision, and the page agrees', async () => {
addSkill({ owner: 'o1', repo: 'r1', name: 'scored' })
Expand Down
Loading