Skip to content

Commit 0ff24c7

Browse files
fix(skills): list omitted skill names when prompt is truncated
1 parent af42a70 commit 0ff24c7

2 files changed

Lines changed: 49 additions & 13 deletions

File tree

src/skills/loading/compact-format.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function makeSkillStatusEntry(entry: SkillEntry): SkillStatusEntry {
5858
blockedByAllowlist: false,
5959
blockedByAgentFilter: false,
6060
eligible: true,
61+
platformIncompatible: false,
6162
modelVisible: true,
6263
userInvocable: true,
6364
commandVisible: true,
@@ -261,6 +262,32 @@ describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => {
261262
}
262263
});
263264

265+
it("keeps omitted skill lookup keys when the audit suffix does not fit", () => {
266+
const maxSkillsPromptChars = 250;
267+
const entries = Array.from({ length: 6 }, (_, i) => {
268+
const name = `skill-${String(i).padStart(2, "0")}`;
269+
const skillKey = i === 0 ? "skill<zero`raw" : name;
270+
return makeEntry(makeSkill(name, "A".repeat(500)), { skillKey });
271+
});
272+
273+
const prompt = buildWorkspaceSkillsPrompt("/fake", {
274+
entries,
275+
config: {
276+
skills: {
277+
limits: {
278+
maxSkillsInPrompt: 100,
279+
maxSkillsPromptChars,
280+
},
281+
},
282+
} satisfies OpenClawConfig,
283+
});
284+
285+
expect(prompt.length).toBeLessThanOrEqual(maxSkillsPromptChars);
286+
expect(prompt).toContain("⚠️ Skills truncated");
287+
expect(prompt).not.toContain("openclaw skills check");
288+
expect(parseAdvertisedOmittedSkillKeys(prompt)).toContain("skill<zero`raw");
289+
});
290+
264291
it("compact preserves all skills where full format would drop some", () => {
265292
const skills = Array.from({ length: 50 }, (_, i) => makeSkill(`skill-${i}`, "A".repeat(200)));
266293
const compactLen = formatSkillsCompact(skills).length;

src/skills/loading/workspace.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ function resolveNativeUserHomeDir(): string | undefined {
6868
}
6969

7070
function resolveCompactHomePrefixes(): string[] {
71-
const homes = [resolveUserHomeDir(), resolveNativeUserHomeDir()].filter(
72-
(home): home is string => Boolean(home),
71+
const homes = [resolveUserHomeDir(), resolveNativeUserHomeDir()].filter((home): home is string =>
72+
Boolean(home),
7373
);
7474
const resolvedHomes = homes.map((home) => path.resolve(home));
7575
const realHomes = resolvedHomes
@@ -116,15 +116,15 @@ function resolvePromptTildeRoots(): string[] {
116116
return [];
117117
}
118118
const realNativeHome = tryRealpath(resolvedNativeHome);
119-
return uniqueStrings([
120-
resolvedNativeHome,
121-
...(realNativeHome ? [realNativeHome] : []),
122-
]);
119+
return uniqueStrings([resolvedNativeHome, ...(realNativeHome ? [realNativeHome] : [])]);
123120
}
124121

125122
function isContainerStateHomeWherePromptTildeEscapes(home: string): boolean {
126123
const configDir = path.resolve(resolveConfigDir());
127-
return home === "/data" && (configDir === "/data/.openclaw" || isPathInside("/data/.openclaw", configDir));
124+
return (
125+
home === "/data" &&
126+
(configDir === "/data/.openclaw" || isPathInside("/data/.openclaw", configDir))
127+
);
128128
}
129129

130130
function shouldPreservePromptSkillPath(
@@ -1465,13 +1465,22 @@ function buildTruncationNote(params: {
14651465
const base = `${prefix}${suffix}`;
14661466
const shortBase = `⚠️ Skills truncated: included ${params.included} of ${params.total}.`;
14671467
const terse = "⚠️ Skills truncated.";
1468-
const baseNotice = formatNoticeWithinBudget([base, shortBase, terse, "⚠️"], params.maxChars);
1469-
if (!baseNotice || baseNotice !== base) {
1470-
return baseNotice;
1468+
const candidates = [
1469+
{ prefix, suffix },
1470+
{ prefix, suffix: "" },
1471+
];
1472+
for (const candidate of candidates) {
1473+
const baseNotice = `${candidate.prefix}${candidate.suffix}`;
1474+
if (baseNotice.length > params.maxChars) {
1475+
continue;
1476+
}
1477+
const segmentBudget = params.maxChars - baseNotice.length;
1478+
const omittedSegment = formatOmittedSkillKeysSegment(params.omittedSkillKeys, segmentBudget);
1479+
if (omittedSegment) {
1480+
return `${candidate.prefix}${omittedSegment}${candidate.suffix}`;
1481+
}
14711482
}
1472-
const segmentBudget = params.maxChars - base.length;
1473-
const omittedSegment = formatOmittedSkillKeysSegment(params.omittedSkillKeys, segmentBudget);
1474-
return `${prefix}${omittedSegment}${suffix}`;
1483+
return formatNoticeWithinBudget([base, shortBase, terse, "⚠️"], params.maxChars);
14751484
}
14761485

14771486
function buildCompactNote(maxChars: number): string {

0 commit comments

Comments
 (0)