fix(cron): normalize skill-filter snapshots and split isolated run helpers

This commit is contained in:
Peter Steinberger
2026-02-16 04:26:30 +01:00
parent 6754a926ee
commit aef1d55300
10 changed files with 360 additions and 191 deletions

View File

@@ -7,6 +7,7 @@ import {
parseAgentSessionKey,
} from "../routing/session-key.js";
import { resolveUserPath } from "../utils.js";
import { normalizeSkillFilter } from "./skills/filter.js";
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
@@ -128,12 +129,7 @@ export function resolveAgentSkillsFilter(
cfg: OpenClawConfig,
agentId: string,
): string[] | undefined {
const raw = resolveAgentConfig(cfg, agentId)?.skills;
if (!raw) {
return undefined;
}
const normalized = raw.map((entry) => String(entry).trim()).filter(Boolean);
return normalized.length > 0 ? normalized : [];
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
}
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import {
matchesSkillFilter,
normalizeSkillFilter,
normalizeSkillFilterForComparison,
} from "./filter.js";
describe("skills/filter", () => {
it("normalizes configured filters with trimming", () => {
expect(normalizeSkillFilter([" weather ", "", "meme-factory"])).toEqual([
"weather",
"meme-factory",
]);
});
it("preserves explicit empty list as []", () => {
expect(normalizeSkillFilter([])).toEqual([]);
expect(normalizeSkillFilter(undefined)).toBeUndefined();
});
it("normalizes for comparison with dedupe + ordering", () => {
expect(normalizeSkillFilterForComparison(["weather", "meme-factory", "weather"])).toEqual([
"meme-factory",
"weather",
]);
});
it("matches equivalent filters after normalization", () => {
expect(matchesSkillFilter(["weather", "meme-factory"], [" meme-factory ", "weather"])).toBe(
true,
);
expect(matchesSkillFilter(undefined, undefined)).toBe(true);
expect(matchesSkillFilter([], undefined)).toBe(false);
});
});

View File

@@ -0,0 +1,31 @@
export function normalizeSkillFilter(skillFilter?: ReadonlyArray<unknown>): string[] | undefined {
if (skillFilter === undefined) {
return undefined;
}
return skillFilter.map((entry) => String(entry).trim()).filter(Boolean);
}
export function normalizeSkillFilterForComparison(
skillFilter?: ReadonlyArray<unknown>,
): string[] | undefined {
const normalized = normalizeSkillFilter(skillFilter);
if (normalized === undefined) {
return undefined;
}
return Array.from(new Set(normalized)).toSorted();
}
export function matchesSkillFilter(
cached?: ReadonlyArray<unknown>,
next?: ReadonlyArray<unknown>,
): boolean {
const cachedNormalized = normalizeSkillFilterForComparison(cached);
const nextNormalized = normalizeSkillFilterForComparison(next);
if (cachedNormalized === undefined || nextNormalized === undefined) {
return cachedNormalized === nextNormalized;
}
if (cachedNormalized.length !== nextNormalized.length) {
return false;
}
return cachedNormalized.every((entry, index) => entry === nextNormalized[index]);
}

View File

@@ -82,6 +82,8 @@ export type SkillEligibilityContext = {
export type SkillSnapshot = {
prompt: string;
skills: Array<{ name: string; primaryEnv?: string }>;
/** Normalized agent-level filter used to build this snapshot; undefined means unrestricted. */
skillFilter?: string[];
resolvedSkills?: Skill[];
version?: number;
};

View File

@@ -19,6 +19,7 @@ import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
import { resolveSandboxPath } from "../sandbox-paths.js";
import { resolveBundledSkillsDir } from "./bundled-dir.js";
import { shouldIncludeSkill } from "./config.js";
import { normalizeSkillFilter } from "./filter.js";
import {
parseFrontmatter,
resolveOpenClawMetadata,
@@ -52,14 +53,16 @@ function filterSkillEntries(
let filtered = entries.filter((entry) => shouldIncludeSkill({ entry, config, eligibility }));
// If skillFilter is provided, only include skills in the filter list.
if (skillFilter !== undefined) {
const normalized = skillFilter.map((entry) => String(entry).trim()).filter(Boolean);
const normalized = normalizeSkillFilter(skillFilter) ?? [];
const label = normalized.length > 0 ? normalized.join(", ") : "(none)";
console.log(`[skills] Applying skill filter: ${label}`);
skillsLogger.debug(`Applying skill filter: ${label}`);
filtered =
normalized.length > 0
? filtered.filter((entry) => normalized.includes(entry.skill.name))
: [];
console.log(`[skills] After filter: ${filtered.map((entry) => entry.skill.name).join(", ")}`);
skillsLogger.debug(
`After skill filter: ${filtered.map((entry) => entry.skill.name).join(", ") || "(none)"}`,
);
}
return filtered;
}
@@ -232,12 +235,14 @@ export function buildWorkspaceSkillSnapshot(
const resolvedSkills = promptEntries.map((entry) => entry.skill);
const remoteNote = opts?.eligibility?.remote?.note?.trim();
const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
const skillFilter = normalizeSkillFilter(opts?.skillFilter);
return {
prompt,
skills: eligible.map((entry) => ({
name: entry.skill.name,
primaryEnv: entry.metadata?.primaryEnv,
})),
...(skillFilter === undefined ? {} : { skillFilter }),
resolvedSkills,
version: opts?.snapshotVersion,
};