fix: persist native trending activation pool (#3360)

This commit is contained in:
Patrick Erichsen
2026-07-31 15:02:23 -07:00
committed by GitHub
parent fb99952312
commit 6dcff11402
10 changed files with 1123 additions and 131 deletions
+2
View File
@@ -86,6 +86,7 @@ import type * as lib_githubIdentity from "../lib/githubIdentity.js";
import type * as lib_githubImport from "../lib/githubImport.js";
import type * as lib_githubOrgMemberships from "../lib/githubOrgMemberships.js";
import type * as lib_githubProfileSync from "../lib/githubProfileSync.js";
import type * as lib_githubRepositoryDispatch from "../lib/githubRepositoryDispatch.js";
import type * as lib_githubSkillScans from "../lib/githubSkillScans.js";
import type * as lib_githubSkillSync from "../lib/githubSkillSync.js";
import type * as lib_globalStats from "../lib/globalStats.js";
@@ -294,6 +295,7 @@ declare const fullApi: ApiFromModules<{
"lib/githubImport": typeof lib_githubImport;
"lib/githubOrgMemberships": typeof lib_githubOrgMemberships;
"lib/githubProfileSync": typeof lib_githubProfileSync;
"lib/githubRepositoryDispatch": typeof lib_githubRepositoryDispatch;
"lib/githubSkillScans": typeof lib_githubSkillScans;
"lib/githubSkillSync": typeof lib_githubSkillSync;
"lib/globalStats": typeof lib_globalStats;
+229 -3
View File
@@ -104,6 +104,47 @@ async function insertEligibleNativeSource(t: ReturnType<typeof convexTest>, slug
});
}
async function insertReadyNativePool(
t: ReturnType<typeof convexTest>,
input: {
poolId: string;
skillId: Awaited<ReturnType<typeof insertEligibleNativeSource>>["skillId"];
now: number;
},
) {
await t.mutation(internal.canonicalTrending.startNativePoolInternal, {
poolId: input.poolId,
generatedAt: input.now - 1_000,
expiresAt: input.now + 24 * 60 * 60 * 1_000,
windowStartHour: 100,
windowEndHour: 123,
sealedGeneration: 7,
});
await t.mutation(internal.canonicalTrending.writeNativePoolItemsInternal, {
poolId: input.poolId,
lane: "clawhub-trending",
items: [
{
identity: `clawhub:${input.poolId}`,
publisherKey: "user:patrick",
installs24h: 8,
bookmarks24h: 1,
createdAt: input.now - 10_000,
updatedAt: input.now - 1_000,
upstreamRank: null,
sourceRef: { kind: "clawhub", skillId: input.skillId },
card: nativeCard(`clawhub:${input.poolId}`, 8),
},
],
});
await t.mutation(internal.canonicalTrending.finalizeNativePoolInternal, {
poolId: input.poolId,
completedAt: input.now - 500,
sourceCounts: { clawhubTrending: 1, clawhubRising: 0 },
operations: { documentsRead: 10, documentsWritten: 4, functionCalls: 3 },
});
}
describe("canonical Trending snapshot storage", () => {
it("selects the newest completed Trending run even when no digest references it", async () => {
const t = convexTest(schema, modules);
@@ -557,12 +598,19 @@ describe("canonical Trending snapshot storage", () => {
const t = convexTest(schema, modules);
const now = Date.now();
const source = await insertEligibleNativeSource(t, "native-preflight-ready");
await insertReadyNativePool(t, {
poolId: "skills-native-preflight-ready",
skillId: source.skillId,
now,
});
await t.mutation(internal.canonicalTrending.startSnapshotInternal, {
snapshotId: "skills-native-preflight-ready",
generatedAt: now - 1_000,
expiresAt: now + 24 * 60 * 60 * 1_000,
windowStartDay: 40,
windowEndDay: 40,
windowStartHour: 100,
windowEndHour: 123,
});
await t.mutation(internal.canonicalTrending.writeItemsInternal, {
snapshotId: "skills-native-preflight-ready",
@@ -587,6 +635,7 @@ describe("canonical Trending snapshot storage", () => {
totalItems: 1,
sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 },
operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 },
nativePoolId: "skills-native-preflight-ready",
});
await expect(
@@ -600,10 +649,105 @@ describe("canonical Trending snapshot storage", () => {
totalItems: 1,
sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 },
operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 },
nativePool: {
poolId: "skills-native-preflight-ready",
sourceCounts: { clawhubTrending: 1, clawhubRising: 0 },
operations: { documentsRead: 10, documentsWritten: 4, functionCalls: 3 },
},
reused: true,
});
});
it("does not expose an orphan native pool as ready for mixed activation", async () => {
const t = convexTest(schema, modules);
const now = Date.now();
const source = await insertEligibleNativeSource(t, "orphan-native-pool");
await insertReadyNativePool(t, {
poolId: "skills-orphan-native-pool",
skillId: source.skillId,
now,
});
await expect(
t.query(internal.canonicalTrending.getReadyNativePoolInternal, { now }),
).resolves.toBeNull();
});
it("reuses an older verified native pool when a newer orphan exists", async () => {
const t = convexTest(schema, modules);
const now = Date.now();
const source = await insertEligibleNativeSource(t, "verified-before-orphan");
await insertReadyNativePool(t, {
poolId: "skills-verified-before-orphan",
skillId: source.skillId,
now,
});
await t.mutation(internal.canonicalTrending.startSnapshotInternal, {
snapshotId: "skills-verified-before-orphan",
generatedAt: now - 1_000,
expiresAt: now + 24 * 60 * 60 * 1_000,
windowStartDay: 40,
windowEndDay: 40,
windowStartHour: 100,
windowEndHour: 123,
});
await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, {
snapshotId: "skills-verified-before-orphan",
completedAt: now - 500,
totalItems: 0,
sourceCounts: { clawhubTrending: 1, clawhubRising: 0, skillsShTrending: 0 },
operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 },
nativePoolId: "skills-verified-before-orphan",
});
await insertReadyNativePool(t, {
poolId: "skills-newer-orphan",
skillId: source.skillId,
now: now + 500,
});
await expect(
t.query(internal.canonicalTrending.getReadyNativePoolInternal, { now }),
).resolves.toMatchObject({ poolId: "skills-verified-before-orphan" });
});
it("keeps a native snapshot ready but marks a mismatched pool unusable", async () => {
const t = convexTest(schema, modules);
const now = Date.now();
const source = await insertEligibleNativeSource(t, "mismatched-native-pool");
await insertReadyNativePool(t, {
poolId: "skills-mismatched-native-pool",
skillId: source.skillId,
now,
});
await t.mutation(internal.canonicalTrending.startSnapshotInternal, {
snapshotId: "skills-mismatched-native-pool",
generatedAt: now - 1_000,
expiresAt: now + 24 * 60 * 60 * 1_000,
windowStartDay: 40,
windowEndDay: 40,
windowStartHour: 100,
windowEndHour: 123,
});
await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, {
snapshotId: "skills-mismatched-native-pool",
completedAt: now - 500,
totalItems: 0,
sourceCounts: { clawhubTrending: 0, clawhubRising: 0, skillsShTrending: 0 },
operations: { documentsRead: 1, documentsWritten: 2, functionCalls: 2 },
nativePoolId: "skills-mismatched-native-pool",
});
await expect(
t.query(internal.canonicalTrending.getReadyNativeSnapshotInternal, { now }),
).resolves.toMatchObject({
snapshotId: "skills-mismatched-native-pool",
nativePool: null,
});
await expect(
t.query(internal.canonicalTrending.getReadyNativePoolInternal, { now }),
).resolves.toBeNull();
});
it("does not reuse a native-only snapshot from the pre-download ranking version", async () => {
const t = convexTest(schema, modules);
const now = Date.now();
@@ -701,20 +845,47 @@ describe("canonical Trending snapshot storage", () => {
},
],
});
await t.mutation(internal.canonicalTrending.startNativePoolInternal, {
poolId: "skills-expired-cleanup",
generatedAt: 1_000,
expiresAt: Date.now() - 1,
windowStartHour: 100,
windowEndHour: 123,
sealedGeneration: 1,
});
await t.mutation(internal.canonicalTrending.writeNativePoolItemsInternal, {
poolId: "skills-expired-cleanup",
lane: "clawhub-trending",
items: [
{
identity: "clawhub:old",
publisherKey: "user:patrick",
installs24h: 1,
bookmarks24h: 0,
createdAt: 1_000,
updatedAt: 1_000,
upstreamRank: null,
sourceRef: { kind: "clawhub", skillId: source.skillId },
card: nativeCard("clawhub:old", 1),
},
],
});
const result = await t.action(internal.canonicalTrending.pruneExpiredActionInternal, {});
const rows = await t.run(async (ctx) => ({
snapshots: await ctx.db.query("canonicalTrendingSnapshots").collect(),
items: await ctx.db.query("canonicalTrendingItems").collect(),
nativePools: await ctx.db.query("canonicalTrendingNativePools").collect(),
nativePoolItems: await ctx.db.query("canonicalTrendingNativePoolItems").collect(),
}));
expect(result).toEqual({
itemsDeleted: 1,
snapshotsDeleted: 1,
itemsDeleted: 2,
snapshotsDeleted: 2,
batches: 1,
continuationScheduled: false,
});
expect(rows).toEqual({ snapshots: [], items: [] });
expect(rows).toEqual({ snapshots: [], items: [], nativePools: [], nativePoolItems: [] });
});
it("materializes hourly native metrics with verified skills.sh rows under the activation lock", async () => {
@@ -910,6 +1081,16 @@ describe("canonical Trending snapshot storage", () => {
});
});
const nativePreflight = await t.action(internal.canonicalTrending.materializeInternal, {
activationLockToken: "activation-lock",
});
expect(nativePreflight).toMatchObject({
status: "ready",
totalItems: 1,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: { reused: false },
});
const result = await t.action(internal.canonicalTrending.materializeInternal, {
activationLockToken: "activation-lock",
});
@@ -917,6 +1098,7 @@ describe("canonical Trending snapshot storage", () => {
status: "ready",
totalItems: 2,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 1 },
nativePool: { poolId: nativePreflight.snapshotId, reused: true },
sample: [
{
rank: 1,
@@ -1033,6 +1215,11 @@ describe("canonical Trending snapshot storage", () => {
status: "ready",
totalItems: 1,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: {
reused: false,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1 },
operations: { documentsWritten: 6 },
},
});
await expect(
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
@@ -1040,5 +1227,44 @@ describe("canonical Trending snapshot storage", () => {
status: "ok",
page: { items: [{ source: "clawhub" }] },
});
await t.run(async (ctx) => {
const hourlyRows = await ctx.db.query("skillHourlyStats").collect();
for (const row of hourlyRows) await ctx.db.delete(row._id);
const mirrorControl = await ctx.db
.query("skillsShMirrorControls")
.withIndex("by_key", (q) => q.eq("key", "global"))
.unique();
if (!mirrorControl) throw new Error("mirror control missing");
await ctx.db.patch(mirrorControl._id, {
activationLockToken: "mixed-pool-lock",
activationLockedAt: Date.now(),
});
});
const mixedFromPool = await t.action(internal.canonicalTrending.materializeInternal, {
activationLockToken: "mixed-pool-lock",
});
expect(mixedFromPool).toMatchObject({
status: "ready",
totalItems: 2,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 1 },
nativePool: {
reused: true,
poolId: nativeOnly.snapshotId,
sourceCounts: { clawhubTrending: 1, clawhubRising: 1 },
},
sample: [
expect.objectContaining({
lane: "clawhub-trending",
trending24hDownloads: 18,
trending24hInstalls: 12,
}),
expect.objectContaining({
lane: "skills-sh-trending",
id: "skills-sh:patrick/repo/external",
}),
],
});
});
});
+547 -87
View File
@@ -37,10 +37,15 @@ const WRITE_BATCH_SIZE = 100;
const NATIVE_SOURCE_BATCH_SIZE = 100;
const SNAPSHOT_RETENTION_MS = 48 * 60 * 60 * 1_000;
const SNAPSHOT_MAX_SERVING_AGE_MS = 2 * 60 * 60 * 1_000;
const NATIVE_POOL_MAX_AGE_MS = SNAPSHOT_MAX_SERVING_AGE_MS;
const EXTERNAL_SOURCE_MAX_AGE_MS = 2 * 60 * 60 * 1_000;
const RISING_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
const PRUNE_BATCH_SIZE = 500;
const PRUNE_MAX_BATCHES = 20;
const NATIVE_POOL_REUSE_SCAN_LIMIT = 100;
const NATIVE_SNAPSHOT_REUSE_SCAN_LIMIT = 100;
const MAX_NATIVE_POOL_ITEMS_PER_LANE =
CANONICAL_TRENDING_LANE_LIMIT + CANONICAL_TRENDING_FIRST_PAGE_SIZE;
const internalRefs = internal as unknown as {
canonicalTrending: {
@@ -51,9 +56,15 @@ const internalRefs = internal as unknown as {
getMaterializationModeInternal: unknown;
getNativeSourceBatchInternal: unknown;
getLatestCompletedTrendingRunInternal: unknown;
getNativePoolPageInternal: unknown;
getReadyNativePoolInternal: unknown;
failNativePoolInternal: unknown;
finalizeNativePoolInternal: unknown;
pruneExpiredInternal: unknown;
pruneExpiredActionInternal: unknown;
startNativePoolInternal: unknown;
startSnapshotInternal: unknown;
writeNativePoolItemsInternal: unknown;
writeItemsInternal: unknown;
};
skillHourlyStats: {
@@ -86,6 +97,20 @@ const laneValidator = v.union(
v.literal("skills-sh-trending"),
);
const nativeLaneValidator = v.union(v.literal("clawhub-trending"), v.literal("clawhub-rising"));
const nativePoolItemValidator = v.object({
identity: v.string(),
publisherKey: v.string(),
installs24h: v.number(),
bookmarks24h: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
upstreamRank: v.union(v.number(), v.null()),
sourceRef: canonicalTrendingSourceRefValidator,
card: canonicalTrendingCardValidator,
});
const sourceCountsValidator = v.object({
clawhubTrending: v.number(),
clawhubRising: v.number(),
@@ -241,6 +266,220 @@ export const getLatestCompletedTrendingRunInternal = internalQuery({
},
});
export const getReadyNativePoolInternal = internalQuery({
args: { now: v.number() },
handler: async (ctx, args) => {
const pools = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_status_and_generated_at", (q) =>
q.eq("status", "ready").gte("generatedAt", args.now - NATIVE_POOL_MAX_AGE_MS),
)
.order("desc")
.take(NATIVE_POOL_REUSE_SCAN_LIMIT);
for (const pool of pools) {
if (
pool.expiresAt <= args.now ||
pool.rankingVersion !== CANONICAL_TRENDING_RANKING_VERSION ||
pool.completedAt === undefined ||
!pool.sourceCounts ||
!pool.operations ||
pool.sourceCounts.clawhubTrending !== pool.writtenTrendingItems ||
pool.sourceCounts.clawhubRising !== pool.writtenRisingItems ||
pool.writtenTrendingItems > MAX_NATIVE_POOL_ITEMS_PER_LANE ||
pool.writtenRisingItems > MAX_NATIVE_POOL_ITEMS_PER_LANE
) {
continue;
}
const snapshot = await ctx.db
.query("canonicalTrendingSnapshots")
.withIndex("by_snapshot_id", (q) => q.eq("snapshotId", pool.poolId))
.unique();
if (
!snapshot ||
snapshot.status !== "ready" ||
snapshot.nativePoolId !== pool.poolId ||
snapshot.rankingVersion !== pool.rankingVersion ||
snapshot.generatedAt !== pool.generatedAt ||
snapshot.windowStartHour !== pool.windowStartHour ||
snapshot.windowEndHour !== pool.windowEndHour ||
snapshot.sourceCounts?.skillsShTrending !== 0 ||
snapshot.sourceCounts.clawhubTrending !== pool.sourceCounts.clawhubTrending ||
snapshot.sourceCounts.clawhubRising !== pool.sourceCounts.clawhubRising
) {
continue;
}
return {
status: "ready" as const,
poolId: pool.poolId,
rankingVersion: pool.rankingVersion,
generatedAt: pool.generatedAt,
completedAt: pool.completedAt,
expiresAt: pool.expiresAt,
windowStartHour: pool.windowStartHour,
windowEndHour: pool.windowEndHour,
sealedGeneration: pool.sealedGeneration,
sourceCounts: pool.sourceCounts,
operations: pool.operations,
};
}
return null;
},
});
export const getNativePoolPageInternal = internalQuery({
args: {
poolId: v.string(),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
const pool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.poolId))
.unique();
if (
!pool ||
pool.status !== "ready" ||
!pool.sourceCounts ||
pool.sourceCounts.clawhubTrending !== pool.writtenTrendingItems ||
pool.sourceCounts.clawhubRising !== pool.writtenRisingItems
) {
throw new Error("native Trending candidate pool is not ready");
}
const page = await ctx.db
.query("canonicalTrendingNativePoolItems")
.withIndex("by_pool_id_and_lane_and_position", (q) => q.eq("poolId", args.poolId))
.paginate(args.paginationOpts);
return { ...page, documentsRead: page.page.length + 1 };
},
});
export const startNativePoolInternal = internalMutation({
args: {
poolId: v.string(),
generatedAt: v.number(),
expiresAt: v.number(),
windowStartHour: v.number(),
windowEndHour: v.number(),
sealedGeneration: v.number(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.poolId))
.unique();
if (existing) throw new Error("native Trending candidate pool already exists");
return await ctx.db.insert("canonicalTrendingNativePools", {
...args,
status: "building",
rankingVersion: CANONICAL_TRENDING_RANKING_VERSION,
writtenTrendingItems: 0,
writtenRisingItems: 0,
});
},
});
export const writeNativePoolItemsInternal = internalMutation({
args: {
poolId: v.string(),
lane: nativeLaneValidator,
items: v.array(nativePoolItemValidator),
},
handler: async (ctx, args) => {
if (args.items.length < 1 || args.items.length > WRITE_BATCH_SIZE) {
throw new Error("Invalid native Trending candidate-pool batch size");
}
const pool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.poolId))
.unique();
if (!pool || pool.status !== "building") {
throw new Error("native Trending candidate pool is not writable");
}
const written =
args.lane === "clawhub-trending" ? pool.writtenTrendingItems : pool.writtenRisingItems;
if (written + args.items.length > MAX_NATIVE_POOL_ITEMS_PER_LANE) {
throw new Error("native Trending candidate pool exceeded its lane bound");
}
for (const [batchIndex, item] of args.items.entries()) {
if (
item.sourceRef.kind !== "clawhub" ||
item.card.source !== "clawhub" ||
item.card.id !== item.identity
) {
throw new Error("native Trending candidate pool contains a non-native identity");
}
await ctx.db.insert("canonicalTrendingNativePoolItems", {
poolId: args.poolId,
lane: args.lane,
position: written + batchIndex,
...item,
expiresAt: pool.expiresAt,
});
}
const nextWritten = written + args.items.length;
await ctx.db.patch(
pool._id,
args.lane === "clawhub-trending"
? { writtenTrendingItems: nextWritten }
: { writtenRisingItems: nextWritten },
);
return { lane: args.lane, writtenItems: nextWritten };
},
});
export const finalizeNativePoolInternal = internalMutation({
args: {
poolId: v.string(),
completedAt: v.number(),
sourceCounts: v.object({
clawhubTrending: v.number(),
clawhubRising: v.number(),
}),
operations: operationsValidator,
},
handler: async (ctx, args) => {
const pool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.poolId))
.unique();
if (!pool || pool.status !== "building") {
throw new Error("native Trending candidate pool cannot be finalized");
}
if (
pool.writtenTrendingItems !== args.sourceCounts.clawhubTrending ||
pool.writtenRisingItems !== args.sourceCounts.clawhubRising ||
args.sourceCounts.clawhubTrending > MAX_NATIVE_POOL_ITEMS_PER_LANE ||
args.sourceCounts.clawhubRising > MAX_NATIVE_POOL_ITEMS_PER_LANE
) {
throw new Error("native Trending candidate-pool count mismatch");
}
await ctx.db.patch(pool._id, {
status: "ready",
completedAt: args.completedAt,
sourceCounts: args.sourceCounts,
operations: args.operations,
});
return { poolId: args.poolId, status: "ready" as const };
},
});
export const failNativePoolInternal = internalMutation({
args: { poolId: v.string(), completedAt: v.number(), error: v.string() },
handler: async (ctx, args) => {
const pool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.poolId))
.unique();
if (!pool || pool.status !== "building") return { changed: false };
await ctx.db.patch(pool._id, {
status: "failed",
completedAt: args.completedAt,
error: args.error.slice(0, 500),
});
return { changed: true };
},
});
export const startSnapshotInternal = internalMutation({
args: {
snapshotId: v.string(),
@@ -319,6 +558,7 @@ export const finalizeSnapshotInternal = internalMutation({
totalItems: v.number(),
sourceCounts: sourceCountsValidator,
operations: operationsValidator,
nativePoolId: v.optional(v.string()),
activationLockToken: v.optional(v.string()),
},
handler: async (ctx, args) => {
@@ -349,6 +589,7 @@ export const finalizeSnapshotInternal = internalMutation({
totalItems: args.totalItems,
sourceCounts: args.sourceCounts,
operations: args.operations,
nativePoolId: args.nativePoolId,
});
return { snapshotId: args.snapshotId, status: "ready" as const };
},
@@ -379,12 +620,21 @@ export const pruneExpiredInternal = internalMutation({
args: { now: v.number(), batchSize: v.number() },
handler: async (ctx, args) => {
const batchSize = Math.min(Math.max(Math.trunc(args.batchSize), 1), PRUNE_BATCH_SIZE);
const items = await ctx.db
const snapshotItems = await ctx.db
.query("canonicalTrendingItems")
.withIndex("by_expires_at", (q) => q.lte("expiresAt", args.now))
.take(batchSize);
for (const item of items) await ctx.db.delete(item._id);
const remaining = batchSize - items.length;
for (const item of snapshotItems) await ctx.db.delete(item._id);
let remaining = batchSize - snapshotItems.length;
const nativePoolItems =
remaining > 0
? await ctx.db
.query("canonicalTrendingNativePoolItems")
.withIndex("by_expires_at", (q) => q.lte("expiresAt", args.now))
.take(remaining)
: [];
for (const item of nativePoolItems) await ctx.db.delete(item._id);
remaining -= nativePoolItems.length;
const snapshots =
remaining > 0
? await ctx.db
@@ -393,10 +643,21 @@ export const pruneExpiredInternal = internalMutation({
.take(remaining)
: [];
for (const snapshot of snapshots) await ctx.db.delete(snapshot._id);
remaining -= snapshots.length;
const nativePools =
remaining > 0
? await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_expires_at", (q) => q.lte("expiresAt", args.now))
.take(remaining)
: [];
for (const pool of nativePools) await ctx.db.delete(pool._id);
const itemsDeleted = snapshotItems.length + nativePoolItems.length;
const snapshotsDeleted = snapshots.length + nativePools.length;
return {
itemsDeleted: items.length,
snapshotsDeleted: snapshots.length,
fullBatch: items.length + snapshots.length === batchSize,
itemsDeleted,
snapshotsDeleted,
fullBatch: itemsDeleted + snapshotsDeleted === batchSize,
};
},
});
@@ -452,91 +713,253 @@ export const materializeInternal = internalAction({
type HourlyWindow = {
startHour: number;
endHour: number;
startAt: number;
endAt: number;
lastAggregationCompletedAt: number;
sealedGeneration: number;
};
const proofWindow =
args.proofSnapshotId !== undefined
? {
...getCompletedRolling24HourWindow(startedAt),
lastAggregationCompletedAt: startedAt,
sealedGeneration: 0,
}
: null;
const hourlyWindow = proofWindow
? proofWindow
: ((await ctx.runMutation(
internalRefs.skillHourlyStats.sealForSnapshotInternal as never,
type ReadyNativePool = {
status: "ready";
poolId: string;
rankingVersion: string;
generatedAt: number;
completedAt: number;
expiresAt: number;
windowStartHour: number;
windowEndHour: number;
sealedGeneration: number;
sourceCounts: { clawhubTrending: number; clawhubRising: number };
operations: {
documentsRead: number;
documentsWritten: number;
functionCalls: number;
};
};
const canReuseNativePool =
args.activationLockToken !== undefined &&
args.skillsShMode !== "native-only" &&
args.proofSnapshotId === undefined;
const readyNativePool = canReuseNativePool
? ((await ctx.runQuery(
internalRefs.canonicalTrending.getReadyNativePoolInternal as never,
{ now: startedAt } as never,
)) as HourlyWindow | null);
if (!proofWindow) functionCalls += 1;
if (!hourlyWindow) {
return { status: "unavailable" as const, reason: "hourly-stats-not-ready" as const };
}
const usageBySkill: RollingHourlyStatTotals = new Map();
const hourlySource = await forEachCanonicalTrendingSourcePage(
ctx,
internalRefs.canonicalTrending.getHourlySourcePageInternal,
{
startHour: hourlyWindow.startHour,
endHour: hourlyWindow.endHour,
maxGeneration: hourlyWindow.sealedGeneration,
},
(page) => accumulateRollingHourlyStats(usageBySkill, page as Doc<"skillHourlyStats">[]),
);
finalizeRollingHourlyStats(usageBySkill);
)) as ReadyNativePool | null)
: null;
if (canReuseNativePool) functionCalls += 1;
const persistOnlyNativePreflight = canReuseNativePool && readyNativePool === null;
let hourlyWindow: HourlyWindow;
let nativeCandidates: CanonicalTrendingMaterializationCandidate[] = [];
let risingCandidates: CanonicalTrendingMaterializationCandidate[] = [];
const nativeSource = { documentsRead: 0, functionCalls: 0 };
const risingCutoff = startedAt - RISING_MAX_AGE_MS;
let pendingNativeSkillIds: Id<"skills">[] = [];
const flushNativeSourceBatch = async () => {
if (pendingNativeSkillIds.length === 0) return;
const skillIds = pendingNativeSkillIds;
pendingNativeSkillIds = [];
const sourceBatch = (await ctx.runQuery(
internalRefs.canonicalTrending.getNativeSourceBatchInternal as never,
{ skillIds } as never,
)) as { page: Doc<"skillSearchDigest">[]; documentsRead: number };
nativeSource.documentsRead += sourceBatch.documentsRead;
nativeSource.functionCalls += 1;
for (const digest of sourceBatch.page) {
const usage = usageBySkill.get(String(digest.skillId));
if (!usage || usage.downloads + usage.installs + usage.bookmarks <= 0) continue;
const candidate = buildNativeCanonicalTrendingCandidate(digest, usage);
if (!candidate) continue;
nativeCandidates.push(candidate);
if (candidate.createdAt >= risingCutoff) {
risingCandidates.push({ ...candidate, lane: "clawhub-rising" });
let nativePool: {
poolId: string;
reused: boolean;
sourceCounts: { clawhubTrending: number; clawhubRising: number };
operations: {
documentsRead: number;
documentsWritten: number;
functionCalls: number;
};
};
if (readyNativePool) {
hourlyWindow = {
startHour: readyNativePool.windowStartHour,
endHour: readyNativePool.windowEndHour,
sealedGeneration: readyNativePool.sealedGeneration,
};
const poolSource = await forEachCanonicalTrendingSourcePage(
ctx,
internalRefs.canonicalTrending.getNativePoolPageInternal,
{ poolId: readyNativePool.poolId },
(page) => {
for (const row of page as Doc<"canonicalTrendingNativePoolItems">[]) {
const candidate: CanonicalTrendingMaterializationCandidate = {
identity: row.identity,
lane: row.lane,
publisherKey: row.publisherKey,
installs24h: row.installs24h,
bookmarks24h: row.bookmarks24h,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
upstreamRank: row.upstreamRank,
sourceRef: row.sourceRef,
card: row.card,
};
if (row.lane === "clawhub-trending") nativeCandidates.push(candidate);
else risingCandidates.push(candidate);
}
},
);
documentsRead += poolSource.documentsRead;
functionCalls += poolSource.functionCalls;
if (
nativeCandidates.length !== readyNativePool.sourceCounts.clawhubTrending ||
risingCandidates.length !== readyNativePool.sourceCounts.clawhubRising
) {
throw new Error("native Trending candidate-pool read count mismatch");
}
nativePool = {
poolId: readyNativePool.poolId,
reused: true,
sourceCounts: readyNativePool.sourceCounts,
operations: readyNativePool.operations,
};
} else {
const proofWindow =
args.proofSnapshotId !== undefined
? { ...getCompletedRolling24HourWindow(startedAt), sealedGeneration: 0 }
: null;
const sealedWindow = proofWindow
? proofWindow
: ((await ctx.runMutation(
internalRefs.skillHourlyStats.sealForSnapshotInternal as never,
{ now: startedAt } as never,
)) as HourlyWindow | null);
if (!proofWindow) functionCalls += 1;
if (!sealedWindow) {
return { status: "unavailable" as const, reason: "hourly-stats-not-ready" as const };
}
hourlyWindow = sealedWindow;
const usageBySkill: RollingHourlyStatTotals = new Map();
const hourlySource = await forEachCanonicalTrendingSourcePage(
ctx,
internalRefs.canonicalTrending.getHourlySourcePageInternal,
{
startHour: hourlyWindow.startHour,
endHour: hourlyWindow.endHour,
maxGeneration: hourlyWindow.sealedGeneration,
},
(page) => accumulateRollingHourlyStats(usageBySkill, page as Doc<"skillHourlyStats">[]),
);
finalizeRollingHourlyStats(usageBySkill);
const nativeSource = { documentsRead: 0, functionCalls: 0 };
const risingCutoff = startedAt - RISING_MAX_AGE_MS;
let pendingNativeSkillIds: Id<"skills">[] = [];
const flushNativeSourceBatch = async () => {
if (pendingNativeSkillIds.length === 0) return;
const skillIds = pendingNativeSkillIds;
pendingNativeSkillIds = [];
const sourceBatch = (await ctx.runQuery(
internalRefs.canonicalTrending.getNativeSourceBatchInternal as never,
{ skillIds } as never,
)) as { page: Doc<"skillSearchDigest">[]; documentsRead: number };
nativeSource.documentsRead += sourceBatch.documentsRead;
nativeSource.functionCalls += 1;
for (const digest of sourceBatch.page) {
const usage = usageBySkill.get(String(digest.skillId));
if (!usage || usage.downloads + usage.installs + usage.bookmarks <= 0) continue;
const candidate = buildNativeCanonicalTrendingCandidate(digest, usage);
if (!candidate) continue;
nativeCandidates.push(candidate);
if (candidate.createdAt >= risingCutoff) {
risingCandidates.push({ ...candidate, lane: "clawhub-rising" });
}
}
// The fetched batch is capped at 100, so each lane stays within 100 rows of its limit.
nativeCandidates = retainTopCanonicalTrendingCandidates(
nativeCandidates,
"clawhub-trending",
CANONICAL_TRENDING_LANE_LIMIT,
LANE_DIVERSITY_RESERVE,
);
risingCandidates = retainTopCanonicalTrendingCandidates(
risingCandidates,
"clawhub-rising",
CANONICAL_TRENDING_LANE_LIMIT,
LANE_DIVERSITY_RESERVE,
);
for (const skillId of skillIds) usageBySkill.delete(String(skillId));
};
for (const skillId of usageBySkill.keys()) {
pendingNativeSkillIds.push(skillId as Id<"skills">);
if (pendingNativeSkillIds.length === NATIVE_SOURCE_BATCH_SIZE) {
await flushNativeSourceBatch();
}
}
// The fetched batch is capped at 100, so each lane stays within 100 rows of its limit.
nativeCandidates = retainTopCanonicalTrendingCandidates(
nativeCandidates,
"clawhub-trending",
CANONICAL_TRENDING_LANE_LIMIT,
LANE_DIVERSITY_RESERVE,
);
risingCandidates = retainTopCanonicalTrendingCandidates(
risingCandidates,
"clawhub-rising",
CANONICAL_TRENDING_LANE_LIMIT,
LANE_DIVERSITY_RESERVE,
);
// Each digest is unique by skill, so its rolling totals are no longer needed.
for (const skillId of skillIds) usageBySkill.delete(String(skillId));
};
for (const skillId of usageBySkill.keys()) {
pendingNativeSkillIds.push(skillId as Id<"skills">);
if (pendingNativeSkillIds.length === NATIVE_SOURCE_BATCH_SIZE) {
await flushNativeSourceBatch();
await flushNativeSourceBatch();
documentsRead += nativeSource.documentsRead + hourlySource.documentsRead;
functionCalls += nativeSource.functionCalls + hourlySource.functionCalls;
const poolId = snapshotId;
let poolStarted = false;
try {
await ctx.runMutation(
internalRefs.canonicalTrending.startNativePoolInternal as never,
{
poolId,
generatedAt: startedAt,
expiresAt: startedAt + SNAPSHOT_RETENTION_MS,
windowStartHour: hourlyWindow.startHour,
windowEndHour: hourlyWindow.endHour,
sealedGeneration: hourlyWindow.sealedGeneration,
} as never,
);
poolStarted = true;
functionCalls += 1;
documentsWritten += 1;
for (const [lane, candidates] of [
["clawhub-trending", nativeCandidates],
["clawhub-rising", risingCandidates],
] as const) {
for (let index = 0; index < candidates.length; index += WRITE_BATCH_SIZE) {
const batch = candidates.slice(index, index + WRITE_BATCH_SIZE);
await ctx.runMutation(
internalRefs.canonicalTrending.writeNativePoolItemsInternal as never,
{
poolId,
lane,
items: batch.map((candidate) => ({
identity: candidate.identity,
publisherKey: candidate.publisherKey,
installs24h: candidate.installs24h,
bookmarks24h: candidate.bookmarks24h,
createdAt: candidate.createdAt,
updatedAt: candidate.updatedAt,
upstreamRank: candidate.upstreamRank,
sourceRef: candidate.sourceRef,
card: candidate.card,
})),
} as never,
);
functionCalls += 1;
documentsWritten += batch.length + 1;
}
}
const sourceCounts = {
clawhubTrending: nativeCandidates.length,
clawhubRising: risingCandidates.length,
};
const poolWriteBatches =
Math.ceil(nativeCandidates.length / WRITE_BATCH_SIZE) +
Math.ceil(risingCandidates.length / WRITE_BATCH_SIZE);
const poolOperations = {
documentsRead: nativeSource.documentsRead + hourlySource.documentsRead,
documentsWritten:
nativeCandidates.length + risingCandidates.length + poolWriteBatches + 2,
functionCalls:
nativeSource.functionCalls + hourlySource.functionCalls + poolWriteBatches + 2,
};
await ctx.runMutation(
internalRefs.canonicalTrending.finalizeNativePoolInternal as never,
{ poolId, completedAt: Date.now(), sourceCounts, operations: poolOperations } as never,
);
poolStarted = false;
functionCalls += 1;
documentsWritten += 1;
nativePool = { poolId, reused: false, sourceCounts, operations: poolOperations };
} finally {
if (poolStarted) {
await ctx.runMutation(
internalRefs.canonicalTrending.failNativePoolInternal as never,
{
poolId,
completedAt: Date.now(),
error: "native Trending candidate-pool persistence failed",
} as never,
);
}
}
}
await flushNativeSourceBatch();
type TrendingRun = {
runId: Doc<"skillsShMirrorRuns">["_id"] | null;
completedAt: number | null;
@@ -549,6 +972,7 @@ export const materializeInternal = internalAction({
};
let externalCandidates: CanonicalTrendingMaterializationCandidate[] = [];
if (
!persistOnlyNativePreflight &&
args.skillsShMode !== "native-only" &&
getRuntimeRolloutCapabilities().skillsSh.runtimeEnabled
) {
@@ -583,10 +1007,8 @@ export const materializeInternal = internalAction({
);
}
}
documentsRead +=
nativeSource.documentsRead + hourlySource.documentsRead + externalSource.documentsRead;
functionCalls +=
nativeSource.functionCalls + hourlySource.functionCalls + externalSource.functionCalls;
documentsRead += externalSource.documentsRead;
functionCalls += externalSource.functionCalls;
if (latestTrendingRun) {
const confirmedTrendingRun = (await ctx.runQuery(
@@ -658,6 +1080,7 @@ export const materializeInternal = internalAction({
completedAt: Date.now(),
totalItems: blended.length,
sourceCounts,
nativePoolId: nativePool.poolId,
operations,
activationLockToken: args.activationLockToken,
} as never,
@@ -673,6 +1096,7 @@ export const materializeInternal = internalAction({
rankingVersion: CANONICAL_TRENDING_RANKING_VERSION,
totalItems: blended.length,
sourceCounts,
nativePool,
operations: {
documentsRead,
documentsWritten,
@@ -709,13 +1133,18 @@ export const materializeInternal = internalAction({
export const getReadyNativeSnapshotInternal = internalQuery({
args: { now: v.number() },
handler: async (ctx, args) => {
const snapshot = await ctx.db
const snapshots = await ctx.db
.query("canonicalTrendingSnapshots")
.withIndex("by_kind_and_status_and_expires_at", (q) =>
q.eq("kind", "skills").eq("status", "ready").gt("expiresAt", args.now),
)
.order("desc")
.first();
.take(NATIVE_SNAPSHOT_REUSE_SCAN_LIMIT);
const snapshot = snapshots.find(
(candidate) =>
candidate.sourceCounts?.skillsShTrending === 0 &&
candidate.generatedAt + SNAPSHOT_MAX_SERVING_AGE_MS > args.now,
);
if (
!snapshot ||
snapshot.generatedAt + SNAPSHOT_MAX_SERVING_AGE_MS <= args.now ||
@@ -727,6 +1156,36 @@ export const getReadyNativeSnapshotInternal = internalQuery({
) {
return null;
}
const poolId = snapshot.nativePoolId ?? snapshot.snapshotId;
const pool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", poolId))
.unique();
const nativePool =
pool &&
pool.status === "ready" &&
pool.expiresAt > args.now &&
pool.generatedAt + NATIVE_POOL_MAX_AGE_MS > args.now &&
pool.rankingVersion === snapshot.rankingVersion &&
pool.completedAt !== undefined &&
pool.sourceCounts !== undefined &&
pool.operations !== undefined &&
pool.poolId === snapshot.nativePoolId &&
pool.generatedAt === snapshot.generatedAt &&
pool.windowStartHour === snapshot.windowStartHour &&
pool.windowEndHour === snapshot.windowEndHour &&
pool.sourceCounts.clawhubTrending === snapshot.sourceCounts.clawhubTrending &&
pool.sourceCounts.clawhubRising === snapshot.sourceCounts.clawhubRising &&
pool.sourceCounts.clawhubTrending === pool.writtenTrendingItems &&
pool.sourceCounts.clawhubRising === pool.writtenRisingItems &&
pool.writtenTrendingItems <= MAX_NATIVE_POOL_ITEMS_PER_LANE &&
pool.writtenRisingItems <= MAX_NATIVE_POOL_ITEMS_PER_LANE
? {
poolId: pool.poolId,
sourceCounts: pool.sourceCounts,
operations: pool.operations,
}
: null;
return {
status: "ready" as const,
snapshotId: snapshot.snapshotId,
@@ -736,6 +1195,7 @@ export const getReadyNativeSnapshotInternal = internalQuery({
totalItems: snapshot.totalItems,
sourceCounts: snapshot.sourceCounts,
operations: snapshot.operations,
nativePool,
reused: true as const,
};
},
@@ -91,6 +91,11 @@ describe("skills.sh catalog Test HTTP API", () => {
status: "ready",
snapshotId: "skills-native-ready",
sourceCounts: { clawhubTrending: 10, clawhubRising: 5, skillsShTrending: 0 },
nativePool: {
poolId: "skills-native-ready",
sourceCounts: { clawhubTrending: 10, clawhubRising: 5 },
operations: { documentsRead: 100, documentsWritten: 20, functionCalls: 5 },
},
};
const runQuery = vi
.fn()
+18
View File
@@ -232,6 +232,24 @@ export const RETENTION_POLICIES = {
retention: "Forty-eight hours after snapshot generation.",
},
),
canonicalTrendingNativePools: ephemeral(
"Verified bounded native Trending candidate-pool headers expire with their source window.",
{
expirationField: "expiresAt",
expirationIndex: "by_expires_at",
prune: "canonicalTrending.pruneExpiredActionInternal",
retention: "Forty-eight hours after candidate-pool generation.",
},
),
canonicalTrendingNativePoolItems: ephemeral(
"Verified bounded native Trending candidates expire with their pool header.",
{
expirationField: "expiresAt",
expirationIndex: "by_expires_at",
prune: "canonicalTrending.pruneExpiredActionInternal",
retention: "Forty-eight hours after candidate-pool generation.",
},
),
skillStatEvents: ephemeral(
"Skill stat event log is retained only after both consumers pass it.",
{
+52
View File
@@ -2722,6 +2722,7 @@ const canonicalTrendingSnapshots = defineTable({
windowEndDay: v.number(),
windowStartHour: v.optional(v.number()),
windowEndHour: v.optional(v.number()),
nativePoolId: v.optional(v.string()),
writtenItems: v.number(),
totalItems: v.optional(v.number()),
sourceCounts: v.optional(
@@ -2759,6 +2760,55 @@ const canonicalTrendingItems = defineTable({
.index("by_snapshot_id_and_position", ["snapshotId", "position"])
.index("by_expires_at", ["expiresAt"]);
const canonicalTrendingNativePools = defineTable({
poolId: v.string(),
status: v.union(v.literal("building"), v.literal("ready"), v.literal("failed")),
rankingVersion: v.string(),
generatedAt: v.number(),
completedAt: v.optional(v.number()),
expiresAt: v.number(),
windowStartHour: v.number(),
windowEndHour: v.number(),
sealedGeneration: v.number(),
writtenTrendingItems: v.number(),
writtenRisingItems: v.number(),
sourceCounts: v.optional(
v.object({
clawhubTrending: v.number(),
clawhubRising: v.number(),
}),
),
operations: v.optional(
v.object({
documentsRead: v.number(),
documentsWritten: v.number(),
functionCalls: v.number(),
}),
),
error: v.optional(v.string()),
})
.index("by_pool_id", ["poolId"])
.index("by_status_and_generated_at", ["status", "generatedAt"])
.index("by_expires_at", ["expiresAt"]);
const canonicalTrendingNativePoolItems = defineTable({
poolId: v.string(),
lane: v.union(v.literal("clawhub-trending"), v.literal("clawhub-rising")),
position: v.number(),
identity: v.string(),
publisherKey: v.string(),
installs24h: v.number(),
bookmarks24h: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
upstreamRank: v.union(v.number(), v.null()),
sourceRef: canonicalTrendingSourceRefValidator,
card: canonicalTrendingCardValidator,
expiresAt: v.number(),
})
.index("by_pool_id_and_lane_and_position", ["poolId", "lane", "position"])
.index("by_expires_at", ["expiresAt"]);
const skillStatEvents = defineTable({
skillId: v.id("skills"),
kind: v.union(
@@ -4263,6 +4313,8 @@ export default defineSchema({
rankingMetricImports,
canonicalTrendingSnapshots,
canonicalTrendingItems,
canonicalTrendingNativePools,
canonicalTrendingNativePoolItems,
skillStatEvents,
skillStatUpdateCursors,
skillStatDocSyncLeases,
+146 -25
View File
@@ -182,7 +182,7 @@ describe("skills.sh mirror visibility operations", () => {
const t = convexTest(schema, modules);
const now = Date.now();
const window = getCompletedRolling24HourWindow(now);
await t.run(async (ctx) => {
const { nativeSkillId } = await t.run(async (ctx) => {
const leaderboardRunId = await ctx.db.insert("skillsShMirrorRuns", {
snapshotId: "skills-sh:leaderboard:verified",
sourceView: "leaderboard",
@@ -353,7 +353,7 @@ describe("skills.sh mirror visibility operations", () => {
createdAt: now - 10_000,
updatedAt: now,
});
const nativeSkillId = await ctx.db.insert("skills", {
const createdNativeSkillId = await ctx.db.insert("skills", {
slug: "native-ready",
displayName: "Native ready",
summary: "Native canonical Trending fixture",
@@ -365,7 +365,7 @@ describe("skills.sh mirror visibility operations", () => {
updatedAt: now,
});
const nativeVersionId = await ctx.db.insert("skillVersions", {
skillId: nativeSkillId,
skillId: createdNativeSkillId,
version: "1.0.0",
changelog: "Initial",
files: [],
@@ -374,7 +374,7 @@ describe("skills.sh mirror visibility operations", () => {
createdAt: now - 10_000,
});
await ctx.db.insert("skillSearchDigest", {
skillId: nativeSkillId,
skillId: createdNativeSkillId,
slug: "native-ready",
displayName: "Native ready",
summary: "Native canonical Trending fixture",
@@ -384,7 +384,7 @@ describe("skills.sh mirror visibility operations", () => {
ownerName: "Native owner",
ownerDisplayName: "Native owner",
latestVersionId: nativeVersionId,
latestVersionSkillId: nativeSkillId,
latestVersionSkillId: createdNativeSkillId,
publicVersion: { status: "available", versionId: nativeVersionId },
tags: {},
statsInstallsAllTime: 900,
@@ -392,6 +392,18 @@ describe("skills.sh mirror visibility operations", () => {
createdAt: now - 10_000,
updatedAt: now,
});
return { nativeSkillId: createdNativeSkillId };
});
await expect(
t.action(internal.skillsShMirrorVisibility.verifyAndActivateInternal, {
actor: "codex-test",
reason: "CLAW-603 unavailable native source",
confirm: "activate-skills-sh-public-test",
}),
).rejects.toThrow("skills.sh Trending activation snapshot failed source verification");
await t.run(async (ctx) => {
await ctx.db.insert("skillHourlyStatStates", {
key: "canonical_trending",
liveStartedAt: now - 3_600_000,
@@ -414,6 +426,20 @@ describe("skills.sh mirror visibility operations", () => {
});
});
await expect(
t.action(internal.skillsShMirrorVisibility.prepareNativeTrendingInternal, {
actor: "codex-test",
reason: "CLAW-603 persist native candidate pool",
confirm: "deactivate-skills-sh-public-test",
}),
).resolves.toMatchObject({
nativeTrending: {
status: "ready",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: { reused: false },
},
});
await expect(
t.action(internal.skillsShMirrorVisibility.verifyAndActivateInternal, {
actor: "codex-test",
@@ -439,6 +465,7 @@ describe("skills.sh mirror visibility operations", () => {
trendingSnapshot: {
status: "ready",
sourceCounts: { skillsShTrending: 1 },
nativePool: { reused: true },
},
scansPlanned: 0,
scansAdmitted: 0,
@@ -457,7 +484,7 @@ describe("skills.sh mirror visibility operations", () => {
runs: await ctx.db.query("skillsShMirrorRuns").withIndex("by_started_at").collect(),
}));
expect(activationState.mirrorControl?.activationLockToken).toBeUndefined();
expect(activationState.snapshots).toHaveLength(1);
expect(activationState.snapshots).toHaveLength(2);
const activatedLeaderboard = activationState.runs.find(
(run) => run._id === activationState.mirrorControl?.latestCompletedLeaderboardRunId,
);
@@ -465,7 +492,8 @@ describe("skills.sh mirror visibility operations", () => {
activationState.runs.find((run) => run.sourceView === "trending")?._id,
);
expect(activatedLeaderboard?.activationSnapshotId).toBe(
activationState.snapshots[0]?.snapshotId,
activationState.snapshots.find((snapshot) => snapshot.sourceCounts?.skillsShTrending === 1)
?.snapshotId,
);
expect(activatedLeaderboard?.activatedAt).toEqual(expect.any(Number));
await expect(
@@ -489,14 +517,70 @@ describe("skills.sh mirror visibility operations", () => {
reason: "CLAW-603 fail-closed rollback",
confirm: "deactivate-skills-sh-public-test",
}),
).rejects.toThrow("native-only canonical Trending did not become ready");
).resolves.toMatchObject({
enabled: false,
nativeTrending: {
status: "ready",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: { poolId: expect.any(String) },
reused: true,
},
});
await expect(
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
).resolves.toMatchObject({
status: "ok",
page: { items: [{ source: "clawhub" }] },
});
});
it("keeps the public gate closed when activation has to build the native pool", async () => {
const t = convexTest(schema, modules);
const now = Date.now();
const window = getCompletedRolling24HourWindow(now);
await t.run(async (ctx) => {
const leaderboardRunId = await ctx.db.insert(
"skillsShMirrorRuns",
mirrorRun({
sourceView: "leaderboard",
sourceTotal: 1,
counts: mirrorRunCounts({ observed: 1, inserted: 1, detailsInserted: 1 }),
startedAt: now - 4_000,
completedAt: now - 3_000,
}),
);
const trendingRunId = await ctx.db.insert(
"skillsShMirrorRuns",
mirrorRun({
sourceView: "trending",
sourceTotal: 1,
counts: mirrorRunCounts({ observed: 1, trendingJoined: 1 }),
startedAt: now - 2_000,
completedAt: now - 1_000,
}),
);
await ctx.db.insert(
"skillsShMirrorDigests",
digest({
lastObservedRunId: leaderboardRunId,
trendingObservedRunId: trendingRunId,
trendingRank: 1,
trendingLifetimeInstalls: 10,
trendingObservedAt: now - 1_000,
}),
);
await ctx.db.insert("skillsShMirrorControls", {
key: "global",
enabled: true,
paused: false,
maxRowsPerRun: 50_000,
maxRowsPerBatch: 50,
maxDetailBytes: 65_536,
latestCompletedLeaderboardRunId: leaderboardRunId,
updatedBy: "codex-test",
reason: "CLAW-603 activation fallback test",
updatedAt: now - 3_000,
});
await ctx.db.insert("skillHourlyStatStates", {
key: "canonical_trending",
liveStartedAt: now - 3_600_000,
@@ -510,26 +594,44 @@ describe("skills.sh mirror visibility operations", () => {
});
await expect(
t.action(internal.skillsShMirrorVisibility.deactivateAndMaterializeInternal, {
t.action(internal.skillsShMirrorVisibility.verifyAndActivateInternal, {
actor: "codex-test",
reason: "CLAW-603 native-only rollback",
confirm: "deactivate-skills-sh-public-test",
reason: "CLAW-603 must reuse native preflight",
confirm: "activate-skills-sh-public-test",
}),
).rejects.toThrow("skills.sh activation must reuse the verified native candidate pool");
await expect(
t.run(async (ctx) =>
ctx.db
.query("canonicalTrendingSnapshots")
.withIndex("by_kind_and_status_and_expires_at", (q) =>
q.eq("kind", "skills").eq("status", "ready").gt("expiresAt", now),
)
.order("desc")
.first(),
),
).resolves.toMatchObject({ sourceCounts: { skillsShTrending: 0 } });
await expect(
t.run(async (ctx) =>
ctx.db
.query("skillsShCatalogControls")
.withIndex("by_key", (q) => q.eq("key", "global"))
.unique(),
),
).resolves.toBeNull();
await expect(
t.action(internal.skillsShMirrorVisibility.verifyAndActivateInternal, {
actor: "codex-test",
reason: "CLAW-603 retry with verified native pool",
confirm: "activate-skills-sh-public-test",
}),
).resolves.toMatchObject({
ok: true,
enabled: false,
nativeTrending: {
status: "ready",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
activated: true,
trendingSnapshot: {
sourceCounts: { skillsShTrending: 1 },
nativePool: { reused: true },
},
scansPlanned: 0,
scansAdmitted: 0,
});
await expect(
t.query(internal.canonicalTrending.getPageInternal, { cursor: null, limit: 20 }),
).resolves.toMatchObject({
status: "ok",
page: { items: [{ source: "clawhub" }] },
});
});
@@ -636,13 +738,30 @@ describe("skills.sh mirror visibility operations", () => {
expiresAt: now + 24 * 60 * 60 * 1_000,
windowStartDay: 40,
windowEndDay: 40,
windowStartHour: 960,
windowEndHour: 983,
});
await t.mutation(internal.canonicalTrending.startNativePoolInternal, {
poolId: "skills-native-preflight-existing",
generatedAt: now - 1_000,
expiresAt: now + 24 * 60 * 60 * 1_000,
windowStartHour: 960,
windowEndHour: 983,
sealedGeneration: 1,
});
await t.mutation(internal.canonicalTrending.finalizeNativePoolInternal, {
poolId: "skills-native-preflight-existing",
completedAt: now - 500,
sourceCounts: { clawhubTrending: 0, clawhubRising: 0 },
operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 },
});
await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, {
snapshotId: "skills-native-preflight-existing",
completedAt: now - 500,
totalItems: 0,
sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 },
sourceCounts: { clawhubTrending: 0, clawhubRising: 0, skillsShTrending: 0 },
operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 },
nativePoolId: "skills-native-preflight-existing",
});
await expect(
@@ -657,6 +776,7 @@ describe("skills.sh mirror visibility operations", () => {
status: "ready",
snapshotId: "skills-native-preflight-existing",
sourceCounts: { skillsShTrending: 0 },
nativePool: { poolId: "skills-native-preflight-existing" },
reused: true,
},
});
@@ -766,6 +886,7 @@ describe("skills.sh mirror visibility operations", () => {
leaderboardRunId,
trendingRunId,
snapshotId: "stale-snapshot",
nativePoolId: "stale-native-pool",
expectedSkillsShTrending: 0,
}),
).rejects.toThrow("activation lock or source run changed before publication");
+44 -6
View File
@@ -510,11 +510,16 @@ async function materializeNativeTrending(
status: "ready";
snapshotId: string;
sourceCounts: { clawhubTrending: number; clawhubRising: number; skillsShTrending: 0 };
nativePool: {
poolId: string;
sourceCounts: { clawhubTrending: number; clawhubRising: number };
operations: { documentsRead: number; documentsWritten: number; functionCalls: number };
} | null;
reused: true;
} | null;
// Native-only data is independent of skills.sh run chronology. The activation path
// always materializes its mixed snapshot after verifying the exact imported runs.
if (reusable) return reusable;
if (reusable?.nativePool) return reusable;
const nativeTrending = (await ctx.runAction(
internalRefs.canonicalTrending.materializeInternal as never,
{ activationLockToken: lockToken, skillsShMode: "native-only" } as never,
@@ -709,6 +714,7 @@ export const finalizeActivationInternal = internalMutation({
leaderboardRunId: v.id("skillsShMirrorRuns"),
trendingRunId: v.id("skillsShMirrorRuns"),
snapshotId: v.string(),
nativePoolId: v.string(),
expectedSkillsShTrending: v.number(),
},
handler: async (ctx, args) => {
@@ -742,18 +748,38 @@ export const finalizeActivationInternal = internalMutation({
)
.order("desc")
.first();
const now = Date.now();
if (
!snapshot ||
snapshot.status !== "ready" ||
snapshot.snapshotId !== latestReady?.snapshotId ||
snapshot.nativePoolId !== args.nativePoolId ||
snapshot.sourceCounts?.skillsShTrending !== args.expectedSkillsShTrending
) {
throw new Error("verified skills.sh Trending snapshot is not the current ready snapshot");
}
const nativePool = await ctx.db
.query("canonicalTrendingNativePools")
.withIndex("by_pool_id", (q) => q.eq("poolId", args.nativePoolId))
.unique();
if (
!nativePool ||
nativePool.status !== "ready" ||
nativePool.completedAt === undefined ||
nativePool.expiresAt <= now ||
!nativePool.sourceCounts ||
!nativePool.operations ||
nativePool.rankingVersion !== snapshot.rankingVersion ||
nativePool.sourceCounts.clawhubTrending !== nativePool.writtenTrendingItems ||
nativePool.sourceCounts.clawhubRising !== nativePool.writtenRisingItems ||
nativePool.sourceCounts.clawhubTrending !== snapshot.sourceCounts?.clawhubTrending ||
nativePool.sourceCounts.clawhubRising !== snapshot.sourceCounts.clawhubRising
) {
throw new Error("verified skills.sh Trending native candidate pool is not ready");
}
const actor = args.actor.trim();
const reason = args.reason.trim();
if (!actor || !reason) throw new Error("skills.sh public gate actor and reason are required");
const now = Date.now();
await writePublicGate(ctx, { enabled: true, actor, reason, now });
await ctx.db.patch("skillsShMirrorRuns", args.leaderboardRunId, {
activatedTrendingRunId: args.trendingRunId,
@@ -967,14 +993,25 @@ export const verifyAndActivateInternal = internalAction({
internalRefs.canonicalTrending.materializeInternal as never,
{ activationLockToken: lockToken } as never,
)) as {
status: "ready";
snapshotId: string;
sourceCounts: { skillsShTrending: number };
status: "ready" | "unavailable";
snapshotId?: string;
sourceCounts?: { skillsShTrending: number };
nativePool?: { poolId: string; reused: boolean };
};
if (
trendingSnapshot.status !== "ready" ||
!trendingSnapshot.nativePool ||
!trendingSnapshot.sourceCounts ||
!trendingSnapshot.snapshotId
) {
throw new Error("skills.sh Trending activation snapshot failed source verification");
}
if (trendingSnapshot.nativePool.reused !== true) {
throw new Error("skills.sh activation must reuse the verified native candidate pool");
}
if (
trendingSnapshot.sourceCounts.skillsShTrending !==
corpusAudit.counts.activationRunTrendingEligible
corpusAudit.counts.activationRunTrendingEligible
) {
throw new Error("skills.sh Trending activation snapshot failed source verification");
}
@@ -986,6 +1023,7 @@ export const verifyAndActivateInternal = internalAction({
leaderboardRunId: leaderboardRun._id,
trendingRunId: trendingRun._id,
snapshotId: trendingSnapshot.snapshotId,
nativePoolId: trendingSnapshot.nativePool.poolId,
expectedSkillsShTrending: trendingSnapshot.sourceCounts.skillsShTrending,
} as never,
)) as {
+46
View File
@@ -52,6 +52,10 @@ function nativeTrendingPreparation() {
status: "ready",
snapshotId: "skills-native-before-import",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: {
poolId: "skills-native-before-import",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1 },
},
},
});
}
@@ -191,6 +195,10 @@ describe("skills.sh synchronization runner", () => {
status: "ready",
snapshotId: "skills-native-after-timeout",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: {
poolId: "skills-native-after-timeout",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1 },
},
},
runs: [],
invariants: { publicVisible: false },
@@ -244,6 +252,44 @@ describe("skills.sh synchronization runner", () => {
]);
});
it("fails closed when timed-out native readiness has a mismatched candidate pool", async () => {
const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
if (body.operation === "status") {
return fetchImpl.mock.calls.length === 1
? response({ runs: [], invariants: { publicVisible: false } })
: response({
control: {},
nativeTrending: {
status: "ready",
snapshotId: "skills-native-after-timeout",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1, skillsShTrending: 0 },
nativePool: {
poolId: "skills-different-native-pool",
sourceCounts: { clawhubTrending: 1, clawhubRising: 1 },
},
},
runs: [],
invariants: { publicVisible: false },
});
}
if (body.operation === "prepare-native-trending") {
throw new DOMException("The operation timed out.", "TimeoutError");
}
if (body.operation === "configure") return response({ ok: true });
throw new Error(`unexpected operation ${String(body.operation)}`);
});
await expect(
runSkillsShSync({
targetUrl: "https://clawhub.ai/ops/skills-sh/mirror",
authorization: "github-oidc",
reason: "scheduled proof",
fetchImpl,
}),
).rejects.toThrow("native-only Trending snapshot and candidate pool do not match");
});
it("fails closed when a timed-out native preflight releases without a ready snapshot", async () => {
const operations: string[] = [];
const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => {
+34 -10
View File
@@ -54,6 +54,32 @@ function optionalRecord(value: unknown) {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
}
function assertReadyNativeTrending(value: unknown, unavailableMessage: string) {
const nativeTrending = optionalRecord(value);
const sourceCounts = optionalRecord(nativeTrending?.sourceCounts);
const snapshotId = nativeTrending?.snapshotId;
if (
nativeTrending?.status !== "ready" ||
typeof snapshotId !== "string" ||
!snapshotId ||
sourceCounts?.skillsShTrending !== 0
) {
throw new Error(unavailableMessage);
}
const nativePool = optionalRecord(nativeTrending.nativePool);
if (nativePool?.poolId !== snapshotId) {
throw new Error("native-only Trending snapshot and candidate pool do not match");
}
const poolSourceCounts = optionalRecord(nativePool.sourceCounts);
if (
poolSourceCounts?.clawhubTrending !== sourceCounts.clawhubTrending ||
poolSourceCounts?.clawhubRising !== sourceCounts.clawhubRising
) {
throw new Error("native-only Trending snapshot and candidate pool counts do not match");
}
return nativeTrending;
}
function jwtExpiresAt(jwt: string) {
const payload = jwt.split(".")[1];
if (!payload) throw new Error("GitHub OIDC returned a malformed token");
@@ -329,11 +355,10 @@ export async function runSkillsShSync(options: {
}
break;
}
const nativeTrending = optionalRecord(status.nativeTrending);
const sourceCounts = optionalRecord(nativeTrending?.sourceCounts);
if (nativeTrending?.status !== "ready" || sourceCounts?.skillsShTrending !== 0) {
throw new Error("native-only Trending preflight finished without a ready snapshot");
}
const nativeTrending = assertReadyNativeTrending(
status.nativeTrending,
"native-only Trending preflight finished without a ready snapshot",
);
return {
ok: true,
nativeTrending,
@@ -368,11 +393,10 @@ export async function runSkillsShSync(options: {
}
}
if (nativeBefore) {
const nativeTrending = nativeBefore.nativeTrending as Record<string, unknown> | undefined;
const sourceCounts = nativeTrending?.sourceCounts as Record<string, unknown> | undefined;
if (nativeTrending?.status !== "ready" || sourceCounts?.skillsShTrending !== 0) {
throw new Error("native-only canonical Trending preflight did not become ready");
}
assertReadyNativeTrending(
nativeBefore.nativeTrending,
"native-only canonical Trending preflight did not become ready",
);
}
const recoveredSourceView = recoverable?.sourceView ?? "leaderboard";
const recovered = recoverable ? await completeRun(recoverable, recoveredSourceView) : null;