fix(search): bound rolling usage query batches (#3456)

This commit is contained in:
Patrick Erichsen
2026-08-11 11:30:43 -07:00
committed by GitHub
parent e29b59c7eb
commit fb2515649a
4 changed files with 58 additions and 7 deletions
+1 -1
View File
@@ -5,5 +5,5 @@ export const CANONICAL_SKILL_SEARCH_BOUNDS = {
externalCandidateLimitPerIndex: 50,
externalIndexedReadCount: 6,
rollingAdoptionDays: 60,
rollingUsageBatchSize: 40,
rollingUsageBatchSize: 20,
} as const;
+52 -2
View File
@@ -1295,12 +1295,12 @@ describe("search helpers", () => {
getRollingSkillSearchUsageHandler(
{ db: { query: vi.fn() } },
{
skillIds: Array.from({ length: 41 }, (_, index) => `skills:${index}`),
skillIds: Array.from({ length: 21 }, (_, index) => `skills:${index}`),
startDay: 100,
endDay: 159,
},
),
).rejects.toThrow("skillIds exceeds 40");
).rejects.toThrow("skillIds exceeds 20");
});
it("returns one ordered native and external contract with canonical routes and install refs", async () => {
@@ -1370,6 +1370,56 @@ describe("search helpers", () => {
});
});
it("preserves rolling adoption ranking when usage reads require smaller transactions", async () => {
generateEmbeddingMock.mockRejectedValueOnce(new Error("embedding unavailable"));
const native = Array.from({ length: 100 }, (_, index) => ({
skill: makePublicSkill({
id: `skills:calendar-${index}`,
slug: `calendar-${index}`,
displayName: `Calendar ${index}`,
}),
version: null,
ownerHandle: "openclaw",
owner: null,
}));
const runQuery = vi.fn(
async (ref: Parameters<typeof getFunctionName>[0], args?: { skillIds?: string[] }) => {
switch (getFunctionName(ref)) {
case "search:getExactSkillSlugMatch":
case "search:lexicalFallbackSkills":
case "search:getExternalSkillSearchCandidates":
return [];
case "search:directPrefixSkillMatches":
return native;
case "search:getRollingSkillSearchUsage": {
const skillIds = args?.skillIds ?? [];
if (skillIds.length > 20) {
throw new Error("Function execution timed out (maximum duration: 1s)");
}
return skillIds.map((skillId) => ({
skillId,
installs: skillId === "skills:calendar-99" ? 10_000 : 1,
bookmarks: 0,
}));
}
default:
throw new Error(`Unexpected query ${getFunctionName(ref)}`);
}
},
);
const result = await canonicalSearchSkillsHandler(
{ runQuery, vectorSearch: vi.fn() },
{ query: "calendar", limit: 100 },
);
expect(result).toHaveLength(100);
expect(result[0]).toMatchObject({
id: "clawhub:skills:calendar-99",
metrics: { rolling60DayInstalls: 10_000 },
});
});
it("keeps exact mode deterministic across canonical sources", async () => {
const native = {
skill: makePublicSkill({
+2 -2
View File
@@ -664,8 +664,8 @@ type CanonicalSkillSearchResult = {
const CANONICAL_NATIVE_CANDIDATE_LIMIT = CANONICAL_SKILL_SEARCH_BOUNDS.nativeCandidateLimit;
const CANONICAL_RESULT_LIMIT_MAX = CANONICAL_SKILL_SEARCH_BOUNDS.resultLimit;
const ROLLING_ADOPTION_DAYS = CANONICAL_SKILL_SEARCH_BOUNDS.rollingAdoptionDays;
// Forty candidates can read at most 2,400 daily rows, leaving headroom below
// Convex's per-transaction document/byte limits for imported production-shaped data.
// Twenty candidates read at most 1,200 daily rows. Keep this well below the
// query CPU ceiling: production-shaped 40-skill batches had intermittent 1s timeouts.
const ROLLING_USAGE_QUERY_BATCH_SIZE = CANONICAL_SKILL_SEARCH_BOUNDS.rollingUsageBatchSize;
function chunkValues<T>(values: T[], size: number) {
+3 -2
View File
@@ -342,8 +342,9 @@ describe("clawhub e2e", () => {
const response = await fetchWithTimeout(url.toString(), {
headers: { Accept: "application/json" },
});
expect(response.ok).toBe(true);
const json = (await response.json()) as unknown;
const body = await response.text();
expect(response.ok, `search failed with ${response.status}: ${body}`).toBe(true);
const json = JSON.parse(body) as unknown;
const parsed = parseArk(ApiV1SearchResponseSchema, json, "API response");
expect(Array.isArray(parsed.results)).toBe(true);
});